@plutonhq/core-frontend 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist-lib/@types/backups.d.ts +26 -0
  2. package/dist-lib/@types/backups.d.ts.map +1 -1
  3. package/dist-lib/@types/devices.d.ts +7 -0
  4. package/dist-lib/@types/devices.d.ts.map +1 -1
  5. package/dist-lib/@types/plans.d.ts +21 -1
  6. package/dist-lib/@types/plans.d.ts.map +1 -1
  7. package/dist-lib/@types/restores.d.ts +2 -0
  8. package/dist-lib/@types/restores.d.ts.map +1 -1
  9. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.d.ts +3 -2
  10. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.d.ts.map +1 -1
  11. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.js +73 -85
  12. package/dist-lib/components/Device/DeviceBackups/DeviceBackups.js.map +1 -1
  13. package/dist-lib/components/Plan/BackupEvents/BackupEvents.d.ts.map +1 -1
  14. package/dist-lib/components/Plan/BackupEvents/BackupEvents.js +88 -50
  15. package/dist-lib/components/Plan/BackupEvents/BackupEvents.js.map +1 -1
  16. package/dist-lib/components/Plan/BackupEvents/BackupEvents.module.scss.js +70 -38
  17. package/dist-lib/components/Plan/BackupEvents/BackupEvents.module.scss.js.map +1 -1
  18. package/dist-lib/components/Plan/BackupProgress/BackupProgress.d.ts.map +1 -1
  19. package/dist-lib/components/Plan/BackupProgress/BackupProgress.js +166 -123
  20. package/dist-lib/components/Plan/BackupProgress/BackupProgress.js.map +1 -1
  21. package/dist-lib/components/Plan/BackupProgress/BackupProgress.module.scss.js +64 -30
  22. package/dist-lib/components/Plan/BackupProgress/BackupProgress.module.scss.js.map +1 -1
  23. package/dist-lib/components/Plan/Backups/Backups.d.ts +8 -1
  24. package/dist-lib/components/Plan/Backups/Backups.d.ts.map +1 -1
  25. package/dist-lib/components/Plan/Backups/Backups.js +154 -125
  26. package/dist-lib/components/Plan/Backups/Backups.js.map +1 -1
  27. package/dist-lib/components/Plan/EditPlan/EditPlan.d.ts.map +1 -1
  28. package/dist-lib/components/Plan/EditPlan/EditPlan.js +11 -10
  29. package/dist-lib/components/Plan/EditPlan/EditPlan.js.map +1 -1
  30. package/dist-lib/components/Plan/Mirrors/MirrorDetails.d.ts +12 -0
  31. package/dist-lib/components/Plan/Mirrors/MirrorDetails.d.ts.map +1 -0
  32. package/dist-lib/components/Plan/Mirrors/MirrorDetails.js +68 -0
  33. package/dist-lib/components/Plan/Mirrors/MirrorDetails.js.map +1 -0
  34. package/dist-lib/components/Plan/Mirrors/MirrorDetails.module.scss.js +26 -0
  35. package/dist-lib/components/Plan/Mirrors/MirrorDetails.module.scss.js.map +1 -0
  36. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.d.ts +11 -0
  37. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.d.ts.map +1 -0
  38. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.js +38 -0
  39. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.js.map +1 -0
  40. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.module.scss.js +16 -0
  41. package/dist-lib/components/Plan/Mirrors/MirrorStatusBadge.module.scss.js.map +1 -0
  42. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.d.ts +14 -0
  43. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.d.ts.map +1 -0
  44. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.js +54 -0
  45. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.js.map +1 -0
  46. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.module.scss.js +26 -0
  47. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelector.module.scss.js.map +1 -0
  48. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.d.ts +15 -0
  49. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.d.ts.map +1 -0
  50. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.js +34 -0
  51. package/dist-lib/components/Plan/Mirrors/MirrorStorageSelectorModal.js.map +1 -0
  52. package/dist-lib/components/Plan/PlanBackups/PlanBackups.d.ts.map +1 -1
  53. package/dist-lib/components/Plan/PlanBackups/PlanBackups.js +20 -17
  54. package/dist-lib/components/Plan/PlanBackups/PlanBackups.js.map +1 -1
  55. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts +2 -1
  56. package/dist-lib/components/Plan/PlanForm/PlanForm.d.ts.map +1 -1
  57. package/dist-lib/components/Plan/PlanForm/PlanForm.js +121 -94
  58. package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
  59. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts +16 -0
  60. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts.map +1 -0
  61. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js +115 -0
  62. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js.map +1 -0
  63. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js +26 -0
  64. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js.map +1 -0
  65. package/dist-lib/components/Plan/PlanItems/PlanItem.d.ts.map +1 -1
  66. package/dist-lib/components/Plan/PlanItems/PlanItem.js +58 -59
  67. package/dist-lib/components/Plan/PlanItems/PlanItem.js.map +1 -1
  68. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts +2 -2
  69. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts.map +1 -1
  70. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.js.map +1 -1
  71. package/dist-lib/components/Plan/PlanRemoveModal/PlanRemoveModal.js +8 -8
  72. package/dist-lib/components/Plan/PlanRemoveModal/PlanRemoveModal.js.map +1 -1
  73. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.d.ts +14 -0
  74. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.d.ts.map +1 -0
  75. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.js +290 -0
  76. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.js.map +1 -0
  77. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.module.scss.js +26 -0
  78. package/dist-lib/components/Plan/PlanSettings/PlanReplicationSettings.module.scss.js.map +1 -0
  79. package/dist-lib/components/Plan/PlanStats/PlanStats.d.ts.map +1 -1
  80. package/dist-lib/components/Plan/PlanStats/PlanStats.js +41 -42
  81. package/dist-lib/components/Plan/PlanStats/PlanStats.js.map +1 -1
  82. package/dist-lib/components/Plan/PlanStats/PlanStats.module.scss.js +5 -5
  83. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.d.ts +15 -0
  84. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.d.ts.map +1 -0
  85. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.js +69 -0
  86. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.js.map +1 -0
  87. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss.js +16 -0
  88. package/dist-lib/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss.js.map +1 -0
  89. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.d.ts.map +1 -1
  90. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.js +36 -34
  91. package/dist-lib/components/Restore/RestoreWizard/RestoreConfirmStep.js.map +1 -1
  92. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.d.ts.map +1 -1
  93. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.js +7 -5
  94. package/dist-lib/components/Restore/RestoreWizard/RestorePreviewStep.js.map +1 -1
  95. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.d.ts +12 -4
  96. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.d.ts.map +1 -1
  97. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.js +44 -32
  98. package/dist-lib/components/Restore/RestoreWizard/RestoreSettingsStep.js.map +1 -1
  99. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.d.ts +5 -1
  100. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.d.ts.map +1 -1
  101. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.js +48 -44
  102. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.js.map +1 -1
  103. package/dist-lib/components/Restore/RestoreWizard/RestoreWizard.module.scss.js +32 -32
  104. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts +1 -1
  105. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts.map +1 -1
  106. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js +52 -24
  107. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js.map +1 -1
  108. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.d.ts.map +1 -1
  109. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js +28 -19
  110. package/dist-lib/components/Settings/IntegrationSettings/IntegrationSettings.js.map +1 -1
  111. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts +7 -0
  112. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts.map +1 -0
  113. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js +79 -0
  114. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js.map +1 -0
  115. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js +24 -0
  116. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js.map +1 -0
  117. package/dist-lib/components/common/Icon/Icon.d.ts.map +1 -1
  118. package/dist-lib/components/common/Icon/Icon.js +11 -0
  119. package/dist-lib/components/common/Icon/Icon.js.map +1 -1
  120. package/dist-lib/components/common/PageHeader/PageHeader.module.scss.js +6 -6
  121. package/dist-lib/components/index.d.ts +6 -0
  122. package/dist-lib/components/index.d.ts.map +1 -1
  123. package/dist-lib/components.js +102 -90
  124. package/dist-lib/components.js.map +1 -1
  125. package/dist-lib/hooks/usePwaAutoUpdate.d.ts +11 -2
  126. package/dist-lib/hooks/usePwaAutoUpdate.d.ts.map +1 -1
  127. package/dist-lib/hooks/usePwaAutoUpdate.js +32 -10
  128. package/dist-lib/hooks/usePwaAutoUpdate.js.map +1 -1
  129. package/dist-lib/router.d.ts.map +1 -1
  130. package/dist-lib/router.js +46 -35
  131. package/dist-lib/router.js.map +1 -1
  132. package/dist-lib/routes/DeviceSingle/DeviceSingle.d.ts.map +1 -1
  133. package/dist-lib/routes/DeviceSingle/DeviceSingle.js +40 -40
  134. package/dist-lib/routes/DeviceSingle/DeviceSingle.js.map +1 -1
  135. package/dist-lib/routes/PlanSingle/PlanSingle.d.ts.map +1 -1
  136. package/dist-lib/routes/PlanSingle/PlanSingle.js +123 -98
  137. package/dist-lib/routes/PlanSingle/PlanSingle.js.map +1 -1
  138. package/dist-lib/services/backups.d.ts +15 -2
  139. package/dist-lib/services/backups.d.ts.map +1 -1
  140. package/dist-lib/services/backups.js +119 -100
  141. package/dist-lib/services/backups.js.map +1 -1
  142. package/dist-lib/services/plans.d.ts +20 -0
  143. package/dist-lib/services/plans.d.ts.map +1 -1
  144. package/dist-lib/services/plans.js +227 -172
  145. package/dist-lib/services/plans.js.map +1 -1
  146. package/dist-lib/services/restores.d.ts +10 -2
  147. package/dist-lib/services/restores.d.ts.map +1 -1
  148. package/dist-lib/services/restores.js +61 -57
  149. package/dist-lib/services/restores.js.map +1 -1
  150. package/dist-lib/services/settings.d.ts +16 -0
  151. package/dist-lib/services/settings.d.ts.map +1 -1
  152. package/dist-lib/services/settings.js +147 -68
  153. package/dist-lib/services/settings.js.map +1 -1
  154. package/dist-lib/services/users.d.ts.map +1 -1
  155. package/dist-lib/services/users.js +32 -32
  156. package/dist-lib/services/users.js.map +1 -1
  157. package/dist-lib/services.js +113 -101
  158. package/dist-lib/styles/core-frontend.css +1 -1
  159. package/dist-lib/utils/progressHelpers.d.ts +12 -1
  160. package/dist-lib/utils/progressHelpers.d.ts.map +1 -1
  161. package/dist-lib/utils/progressHelpers.js +121 -63
  162. package/dist-lib/utils/progressHelpers.js.map +1 -1
  163. package/dist-lib/utils.js +29 -28
  164. package/package.json +1 -1
  165. package/src/@types/backups.ts +28 -0
  166. package/src/@types/devices.ts +8 -0
  167. package/src/@types/plans.ts +23 -1
  168. package/src/@types/restores.ts +2 -0
  169. package/src/components/Device/DeviceBackups/DeviceBackups.tsx +11 -36
  170. package/src/components/Plan/BackupEvents/BackupEvents.module.scss +65 -0
  171. package/src/components/Plan/BackupEvents/BackupEvents.tsx +65 -4
  172. package/src/components/Plan/BackupProgress/BackupProgress.module.scss +121 -3
  173. package/src/components/Plan/BackupProgress/BackupProgress.tsx +149 -71
  174. package/src/components/Plan/Backups/Backups.tsx +52 -4
  175. package/src/components/Plan/EditPlan/EditPlan.tsx +1 -0
  176. package/src/components/Plan/Mirrors/MirrorDetails.module.scss +76 -0
  177. package/src/components/Plan/Mirrors/MirrorDetails.tsx +100 -0
  178. package/src/components/Plan/Mirrors/MirrorStatusBadge.module.scss +25 -0
  179. package/src/components/Plan/Mirrors/MirrorStatusBadge.tsx +65 -0
  180. package/src/components/Plan/Mirrors/MirrorStorageSelector.module.scss +97 -0
  181. package/src/components/Plan/Mirrors/MirrorStorageSelector.tsx +70 -0
  182. package/src/components/Plan/Mirrors/MirrorStorageSelectorModal.tsx +40 -0
  183. package/src/components/Plan/PlanBackups/PlanBackups.tsx +4 -1
  184. package/src/components/Plan/PlanForm/PlanForm.tsx +44 -17
  185. package/src/components/Plan/PlanIntegrity/PlanIntegrity.module.scss +110 -0
  186. package/src/components/Plan/PlanIntegrity/PlanIntegrity.tsx +187 -0
  187. package/src/components/Plan/PlanItems/PlanItem.tsx +3 -3
  188. package/src/components/Plan/PlanPendingBackup/PlanPendingBackup.tsx +2 -2
  189. package/src/components/Plan/PlanRemoveModal/PlanRemoveModal.tsx +1 -1
  190. package/src/components/Plan/PlanSettings/PlanReplicationSettings.module.scss +105 -0
  191. package/src/components/Plan/PlanSettings/PlanReplicationSettings.tsx +334 -0
  192. package/src/components/Plan/PlanStats/PlanStats.module.scss +1 -1
  193. package/src/components/Plan/PlanStats/PlanStats.tsx +8 -8
  194. package/src/components/Plan/PlanStorageInfo/PlanStorageInfo.module.scss +43 -0
  195. package/src/components/Plan/PlanStorageInfo/PlanStorageInfo.tsx +83 -0
  196. package/src/components/Restore/RestoreWizard/RestoreConfirmStep.tsx +2 -0
  197. package/src/components/Restore/RestoreWizard/RestorePreviewStep.tsx +2 -0
  198. package/src/components/Restore/RestoreWizard/RestoreSettingsStep.tsx +36 -13
  199. package/src/components/Restore/RestoreWizard/RestoreWizard.module.scss +4 -0
  200. package/src/components/Restore/RestoreWizard/RestoreWizard.tsx +9 -1
  201. package/src/components/Settings/GeneralSettings/GeneralSettings.tsx +38 -2
  202. package/src/components/Settings/IntegrationSettings/IntegrationSettings.tsx +9 -2
  203. package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss +62 -0
  204. package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.tsx +102 -0
  205. package/src/components/common/Icon/Icon.tsx +10 -1
  206. package/src/components/common/PageHeader/PageHeader.module.scss +3 -0
  207. package/src/components/index.ts +8 -0
  208. package/src/hooks/usePwaAutoUpdate.ts +51 -11
  209. package/src/router.tsx +26 -17
  210. package/src/routes/DeviceSingle/DeviceSingle.tsx +3 -3
  211. package/src/routes/PlanSingle/PlanSingle.tsx +21 -0
  212. package/src/services/backups.ts +32 -9
  213. package/src/services/plans.ts +75 -0
  214. package/src/services/restores.ts +10 -2
  215. package/src/services/settings.ts +90 -0
  216. package/src/services/users.ts +14 -5
  217. package/src/utils/progressHelpers.ts +85 -1
@@ -1,6 +1,10 @@
1
+ import { useState } from 'react';
2
+ import { ActionModal } from '../..';
1
3
  import { useTheme } from '../../../context/ThemeContext';
2
4
  import Input from '../../common/form/Input/Input';
5
+ import Toggle from '../../common/form/Toggle/Toggle';
3
6
  import Tristate from '../../common/form/Tristate/Tristate';
7
+ import TwoFactorSetup from '../TwoFactorSetup/TwoFactorSetup';
4
8
  import classes from './GeneralSettings.module.scss';
5
9
 
6
10
  interface GeneralSettingsProps {
@@ -9,8 +13,22 @@ interface GeneralSettingsProps {
9
13
  onUpdate: (settings: Record<string, any>) => void;
10
14
  }
11
15
 
12
- const GeneralSettings = ({ settings, onUpdate }: GeneralSettingsProps) => {
16
+ const GeneralSettings = ({ settings, settingsID, onUpdate }: GeneralSettingsProps) => {
13
17
  const { setTheme } = useTheme();
18
+ const [show2FASetupConfirm, setShow2FASetupConfirm] = useState(false);
19
+ const [show2FASetup, setShow2FASetup] = useState(false);
20
+
21
+ const { totp } = settings || {};
22
+ const is2FASetupComplete = totp?.secret;
23
+
24
+ const update2FASetting = (enabled: boolean) => {
25
+ if (enabled === true && !is2FASetupComplete) {
26
+ setShow2FASetupConfirm(true);
27
+ return;
28
+ } else {
29
+ onUpdate({ ...settings, totp: { ...totp, enabled } });
30
+ }
31
+ };
14
32
 
15
33
  const handleThemeChange = (newThemeValue: 'auto' | 'light' | 'dark') => {
16
34
  setTheme(newThemeValue);
@@ -55,9 +73,27 @@ const GeneralSettings = ({ settings, onUpdate }: GeneralSettingsProps) => {
55
73
  { label: 'Light', value: 'light' },
56
74
  ]}
57
75
  onUpdate={(val: string) => handleThemeChange(val as 'auto' | 'light' | 'dark')}
58
- inline={true}
76
+ inline={false}
59
77
  />
60
78
  </div>
79
+ <div className={classes.field}>
80
+ <Toggle label="Enable 2FA" fieldValue={settings?.totp?.enabled || false} onUpdate={(val) => update2FASetting(val)} inline={true} />
81
+ </div>
82
+ {show2FASetupConfirm && (
83
+ <ActionModal
84
+ title={`Enable Two-Factor Authentication (2FA)`}
85
+ message={`Are you sure you want to enable Two-Factor Authentication (2FA) to secure Pluton Login? You will be required to use an authenticator app to login if you enable this feature.`}
86
+ closeModal={() => setShow2FASetupConfirm(false)}
87
+ width="420px"
88
+ primaryAction={{
89
+ title: `Yes, Enable 2FA`,
90
+ type: 'default',
91
+ isPending: false,
92
+ action: () => setShow2FASetup(true),
93
+ }}
94
+ />
95
+ )}
96
+ {show2FASetup && <TwoFactorSetup id={settingsID} close={() => setShow2FASetup(false)} />}
61
97
  </div>
62
98
  );
63
99
  };
@@ -19,12 +19,19 @@ const IntegrationSettings = ({ settingsID, settings, onUpdate }: IntegrationSett
19
19
 
20
20
  const validationMutation = useValidateIntegration();
21
21
 
22
- console.log('smtp :', smtp);
22
+ const onIntegrationUpdate = (key: string, intSettings: Record<string, any>) => {
23
+ console.log('onIntegrationUpdate :', key, intSettings);
24
+ onUpdate({ ...integrationSettings, [key]: { ...integrationSettings[key], ...intSettings } });
25
+ };
23
26
 
24
27
  return (
25
28
  <div className={classes.integrations}>
26
29
  <div>
27
- <SMTPSettings settings={smtp} onUpdate={onUpdate} showTestModal={setShowEmailTestModal} />
30
+ <SMTPSettings
31
+ settings={smtp}
32
+ onUpdate={(iSettings) => onIntegrationUpdate('smtp', iSettings)}
33
+ showTestModal={(type) => setShowEmailTestModal(type)}
34
+ />
28
35
  </div>
29
36
  {showEmailTestModal && (
30
37
  <ActionModal
@@ -0,0 +1,62 @@
1
+ .setupContainer {
2
+ .errorMsg {
3
+ background-color: var(--error-bg-color);
4
+ color: var(--error-text-color);
5
+ padding: 6px 12px;
6
+ border-radius: 6px;
7
+ }
8
+ .instructions {
9
+ ol {
10
+ margin: 0;
11
+ padding: 12px 20px;
12
+ padding-top: 0;
13
+ }
14
+ }
15
+ .qrSection {
16
+ .qrCode {
17
+ background-color: var(--background-color);
18
+ padding: 12px;
19
+ text-align: center;
20
+ border-radius: 6px 6px 0 0;
21
+ }
22
+ .setupKey {
23
+ padding: 6px;
24
+ margin-bottom: 12px;
25
+ background: var(--primary-color-light);
26
+ text-align: center;
27
+ border-radius: 0 0 6px 6px;
28
+ }
29
+ .verifySection {
30
+ display: flex;
31
+ & > div:nth-child(1) {
32
+ width: calc(100% - 140px);
33
+ }
34
+ & > button {
35
+ margin-left: 12px;
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ .successMessage {
42
+ .successMsg {
43
+ background-color: var(--success-bg-color);
44
+ color: var(--success-text-color);
45
+ padding: 6px 12px;
46
+ border-radius: 6px;
47
+ font-weight: 600;
48
+ }
49
+ strong {
50
+ font-weight: 600;
51
+ }
52
+ code {
53
+ background-color: var(--line-color);
54
+ width: 100%;
55
+ display: block;
56
+ padding: 12px;
57
+ box-sizing: border-box;
58
+ border-radius: 6px;
59
+ white-space: pre;
60
+ font-size: 14px;
61
+ }
62
+ }
@@ -0,0 +1,102 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useSetupTwoFactorAuth, useVerifyTwoFactorAuth } from '../../../services';
3
+ import Icon from '../../common/Icon/Icon';
4
+ import Modal from '../../common/Modal/Modal';
5
+ import Input from '../../common/form/Input/Input';
6
+ import Button from '../../common/Button/Button';
7
+ import classes from './TwoFactorSetup.module.scss';
8
+
9
+ interface TwoFactorSetupProps {
10
+ id: number;
11
+ close: () => void;
12
+ }
13
+
14
+ const TwoFactorSetup = ({ close, id = 1 }: TwoFactorSetupProps) => {
15
+ const [verificationCode, setVerificationCode] = useState('');
16
+ const setupMutation = useSetupTwoFactorAuth();
17
+ const verifyMutation = useVerifyTwoFactorAuth();
18
+ const isLoading = setupMutation.isPending || verifyMutation.isPending;
19
+
20
+ const qrCodeUrl: string = setupMutation.data?.result?.qrCodeDataUrl || '';
21
+ const setupKey: string = setupMutation.data?.result?.setupKey || '';
22
+ const recoveryCodes: string[] = verifyMutation.data?.result?.recoveryCodes || [];
23
+
24
+ useEffect(() => {
25
+ setupMutation.mutate(id);
26
+ }, []);
27
+
28
+ const verify2FA = () => {
29
+ verifyMutation.mutate({ code: verificationCode, id });
30
+ };
31
+
32
+ return (
33
+ <>
34
+ {!verifyMutation.isSuccess && (
35
+ <Modal title="Two-Factor Authentication (2FA) Setup" closeModal={() => !isLoading && close()} width="500px" disableBackdropClick={true}>
36
+ <div className={classes.twoFactorSetup}>
37
+ {setupMutation.isPending && (
38
+ <p>
39
+ <Icon type="loading" /> Setting up 2FA...
40
+ </p>
41
+ )}
42
+ {setupMutation.isError && <p className={classes.error}>Error setting up 2FA. Please try again.</p>}
43
+ {setupMutation.isSuccess && (
44
+ <div className={classes.setupContainer}>
45
+ <div className={classes.instructions}>
46
+ <h4>Setup Instructions</h4>
47
+ <ol>
48
+ <li>
49
+ Download and install an authenticator app on your mobile device (e.g., Google Authenticator, Authy, Microsoft
50
+ Authenticator).
51
+ </li>
52
+ <li>Open the authenticator app and choose to add a new account.</li>
53
+ <li>Scan the QR code below using the authenticator app or manually enter the setup key provided.</li>
54
+ <li>Once added, the app will generate a 6-digit verification code that refreshes every 30 seconds.</li>
55
+ <li>Enter the current 6-digit code from the authenticator app into the field below to complete the setup.</li>
56
+ </ol>
57
+ </div>
58
+ <div className={classes.qrSection}>
59
+ <div className={classes.qrCode}>{qrCodeUrl && <img src={qrCodeUrl} alt="2FA QR Code" />}</div>
60
+ <div className={classes.setupKey}>Setup Key: {setupKey}</div>
61
+ {verifyMutation.isError && <p className={classes.errorMsg}>{verifyMutation.error?.message}</p>}
62
+ <div className={classes.verifySection}>
63
+ <Input
64
+ type="text"
65
+ placeholder="Enter 6-digit code"
66
+ fieldValue={verificationCode}
67
+ onUpdate={(val) => setVerificationCode(val)}
68
+ full={true}
69
+ />
70
+ <Button text="Verify & Enable 2FA" onClick={() => verify2FA()} variant="primary" size="sm" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ )}
75
+ </div>
76
+ </Modal>
77
+ )}
78
+
79
+ {verifyMutation.isSuccess && (
80
+ <Modal
81
+ title="Two-Factor Authentication (2FA) Setup"
82
+ closeModal={() => !isLoading && location.reload()}
83
+ width="500px"
84
+ disableBackdropClick={true}
85
+ >
86
+ <div className={classes.successMessage}>
87
+ <p className={classes.successMsg}>Two-Factor Authentication (2FA) has been successfully enabled.</p>
88
+ <p>
89
+ <strong>
90
+ Save these backup codes in a secure place. They will be required to access your account if you lose your device. They won't be
91
+ shown again.
92
+ </strong>
93
+ </p>
94
+ <code className={classes.recoveryCodes}>{recoveryCodes.join('\n')}</code>
95
+ </div>
96
+ </Modal>
97
+ )}
98
+ </>
99
+ );
100
+ };
101
+
102
+ export default TwoFactorSetup;
@@ -125,7 +125,16 @@ const Icon = ({ type, color = 'currentColor', size = 16, title = '', classes = '
125
125
  </g>
126
126
  </IconWrapper>
127
127
  )}
128
-
128
+ {type === 'mirrors' && (
129
+ <IconWrapper size={size} viewBox="0 0 24 24">
130
+ <path
131
+ fill={color}
132
+ d="M21.53 5.15a.99.99 0 0 0-.97-.04l-10 5a1 1 0 0 0-.55.89v10a1 1 0 0 0 1 1c.15 0 .31-.04.45-.11l10-5a1 1 0 0 0 .55-.89V6c0-.35-.18-.67-.47-.85ZM20 15.38l-8 4v-7.76l8-4z"
133
+ />
134
+ <path fill={color} d="m16.55 3.11l-10 5A1 1 0 0 0 6 9v10h2V9.62l9.45-4.72l-.89-1.79Z" />
135
+ <path fill={color} d="m12.55 1.11l-10 5A1 1 0 0 0 2 7v10h2V7.62l9.45-4.73l-.89-1.79Z" />
136
+ </IconWrapper>
137
+ )}
129
138
  {type === 'devices' && (
130
139
  <IconWrapper size={size} viewBox="0 0 24 24">
131
140
  <path
@@ -56,6 +56,9 @@
56
56
  }
57
57
 
58
58
  @media only screen and (max-width: 768px) {
59
+ .header {
60
+ max-height: none;
61
+ }
59
62
  .headerTitle {
60
63
  font-size: 1rem;
61
64
  }
@@ -88,6 +88,13 @@ export { default as PlanStrategySettings } from './Plan/PlanSettings/PlanStrateg
88
88
  export { default as PlanTypeSettings } from './Plan/PlanSettings/PlanTypeSettings';
89
89
  export { default as PlanStats } from './Plan/PlanStats/PlanStats';
90
90
  export { default as PlanUnlockModal } from './Plan/PlanUnlockModal/PlanUnlockModal';
91
+ export { default as PlanIntegrity } from './Plan/PlanIntegrity/PlanIntegrity';
92
+
93
+ // Mirror/Replication components
94
+ export { default as MirrorStatusBadge } from './Plan/Mirrors/MirrorStatusBadge';
95
+ export { default as MirrorStorageSelector } from './Plan/Mirrors/MirrorStorageSelector';
96
+ export { default as MirrorStorageSelectorModal } from './Plan/Mirrors/MirrorStorageSelectorModal';
97
+ export { default as PlanReplicationSettings } from './Plan/PlanSettings/PlanReplicationSettings';
91
98
 
92
99
  // Restore components
93
100
  export { default as PlanRestores } from './Plan/Restores/Restores';
@@ -105,6 +112,7 @@ export { default as AppLogs } from './Settings/AppLogs/AppLogs';
105
112
  export { default as GeneralSettings } from './Settings/GeneralSettings/GeneralSettings';
106
113
  export { default as IntegrationSettings } from './Settings/IntegrationSettings/IntegrationSettings';
107
114
  export { default as SMTPSettings } from './Settings/IntegrationSettings/SMTPSettings';
115
+ export { default as TwoFactorSetup } from './Settings/TwoFactorSetup/TwoFactorSetup';
108
116
 
109
117
  // Skeleton components
110
118
  export { default as SkeletonItems } from './Skeleton/SkeletonItems';
@@ -1,9 +1,20 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { useRegisterSW } from 'virtual:pwa-register/react';
3
3
 
4
+ const VERSION_STORAGE_KEY = 'pluton_app_version';
5
+
4
6
  /**
5
- * This hook handles the PWA auto-update logic in the background.
6
- * It's completely silent and requires no user interaction.
7
+ * Handles two PWA update scenarios:
8
+ *
9
+ * 1. **Workbox precache update** — When a new frontend build is deployed, Workbox detects
10
+ * that the precache manifest changed and sets `needRefresh`. We immediately activate
11
+ * the new service worker so the page reloads with fresh assets.
12
+ *
13
+ * 2. **Backend version mismatch** — Compares the server's `X-App-Version` header
14
+ * (stored on `window.plutonVersion` by `validateAuth`) against the last known version
15
+ * in localStorage. On mismatch (e.g. docker/binary upgrade), unregisters all service
16
+ * workers, clears all caches, and hard-reloads. This catches cases where the SW update
17
+ * check hasn't fired yet (long-lived tabs, cached sw.js, etc.).
7
18
  */
8
19
  export function usePwaAutoUpdate() {
9
20
  const {
@@ -12,22 +23,51 @@ export function usePwaAutoUpdate() {
12
23
  updateServiceWorker,
13
24
  } = useRegisterSW();
14
25
 
26
+ const versionCheckedRef = useRef(false);
27
+
28
+ // Workbox detected a new service worker with updated assets
15
29
  useEffect(() => {
16
- // This effect triggers when a new service worker is ready to take over.
17
30
  if (needRefresh) {
18
- // This immediately tells the new service worker to take control,
19
- // which will force a hard reload of the page.
20
31
  updateServiceWorker(true);
21
32
  }
22
-
23
- // This effect triggers when the app's assets are fully cached and ready for offline use.
24
- // We won't show a toast, but we'll log it for debugging purposes.
25
33
  if (offlineReady) {
26
34
  console.log('PWA is ready for offline use.');
27
- // Reset the state to prevent this log from firing on every render.
28
35
  setOfflineReady(false);
29
36
  }
30
37
  }, [needRefresh, offlineReady, setOfflineReady, updateServiceWorker]);
31
38
 
32
- // This hook doesn't need to return anything as it performs its job in the background.
39
+ // Backend version mismatch check
40
+ useEffect(() => {
41
+ if (versionCheckedRef.current) return;
42
+
43
+ const serverVersion = (window as any).plutonVersion;
44
+ if (!serverVersion || serverVersion === 'unknown') return;
45
+
46
+ const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY);
47
+
48
+ if (storedVersion && storedVersion !== serverVersion) {
49
+ versionCheckedRef.current = true;
50
+ clearCachesAndReload(serverVersion);
51
+ } else if (!storedVersion) {
52
+ localStorage.setItem(VERSION_STORAGE_KEY, serverVersion);
53
+ }
54
+ });
55
+ }
56
+
57
+ async function clearCachesAndReload(newVersion: string) {
58
+ try {
59
+ if ('serviceWorker' in navigator) {
60
+ const registrations = await navigator.serviceWorker.getRegistrations();
61
+ await Promise.all(registrations.map((r) => r.unregister()));
62
+ }
63
+ if ('caches' in window) {
64
+ const cacheNames = await caches.keys();
65
+ await Promise.all(cacheNames.map((name) => caches.delete(name)));
66
+ }
67
+ } catch (e) {
68
+ console.error('Failed to clear caches:', e);
69
+ }
70
+
71
+ localStorage.setItem(VERSION_STORAGE_KEY, newVersion);
72
+ window.location.reload();
33
73
  }
package/src/router.tsx CHANGED
@@ -3,7 +3,6 @@ import Plans from './routes/Plans/Plans';
3
3
  import Login from './routes/Login/Login';
4
4
  import Setup from './routes/Setup/Setup';
5
5
  import { useAuth } from './services/users';
6
- import { useSetupStatus } from './services/settings';
7
6
  import App from './components/App/App/App';
8
7
  import Settings from './routes/Settings/Settings';
9
8
  import Storages from './routes/Storages/Storages';
@@ -14,14 +13,10 @@ import Footer from './components/App/Footer/Footer';
14
13
  import NotFoundRoute from './routes/NotFoundRoute/NotFoundRoute';
15
14
  import Icon from './components/common/Icon/Icon';
16
15
 
17
- function ProtectedLayout() {
18
- // Check setup status FIRST - this endpoint doesn't require auth
19
- // and tells us if we're in binary mode and if setup is pending
20
- const { data: setupStatus, isLoading: setupLoading } = useSetupStatus();
21
- const { data: authData, isError: authError, isLoading: authLoading } = useAuth();
16
+ function PublicRoute({ children }: { children: React.ReactNode }) {
17
+ const { data: authData, isLoading: authLoading } = useAuth();
22
18
 
23
- // Show loading spinner while checking setup status
24
- if (setupLoading) {
19
+ if (authLoading) {
25
20
  return (
26
21
  <div className="loadingScreen">
27
22
  <Icon size={60} type="loading" />
@@ -29,16 +24,16 @@ function ProtectedLayout() {
29
24
  );
30
25
  }
31
26
 
32
- // If setup status check failed, we can still proceed with auth check
33
- // (might be a network issue or old backend without setup endpoint)
34
-
35
- // For binary installations with pending setup, redirect to setup page
36
- // This check happens BEFORE auth check so unauthenticated users can access setup
37
- if (setupStatus?.data?.isBinary && setupStatus?.data?.setupPending) {
38
- return <Navigate to="/setup" replace />;
27
+ if (authData) {
28
+ return <Navigate to="/" replace />;
39
29
  }
40
30
 
41
- // Now check auth (setup is either complete or not a binary installation)
31
+ return <>{children}</>;
32
+ }
33
+
34
+ function ProtectedLayout() {
35
+ const { data: authData, isError: authError, isLoading: authLoading } = useAuth();
36
+
42
37
  if (authLoading) {
43
38
  return (
44
39
  <div className="loadingScreen">
@@ -47,6 +42,13 @@ function ProtectedLayout() {
47
42
  );
48
43
  }
49
44
 
45
+ // Check setup-pending from auth response headers (set by versionMiddleware on all responses)
46
+ const installType = (window as any).plutonInstallType;
47
+ const setupPending = (window as any).plutonSetupPending;
48
+ if (installType === 'binary' && setupPending) {
49
+ return <Navigate to="/setup" replace />;
50
+ }
51
+
50
52
  // If auth failed, redirect to login
51
53
  if (authError) {
52
54
  return <Navigate to="/login" replace />;
@@ -74,7 +76,14 @@ export function AppRoutes() {
74
76
  <Route path={'plan/:id'} element={<PlanSingle />} />
75
77
  <Route path="*" element={<NotFoundRoute />} />
76
78
  </Route>
77
- <Route path="login" element={<Login />} />
79
+ <Route
80
+ path="login"
81
+ element={
82
+ <PublicRoute>
83
+ <Login />
84
+ </PublicRoute>
85
+ }
86
+ />
78
87
  <Route path="setup" element={<Setup />} />
79
88
  </Routes>
80
89
  );
@@ -6,10 +6,9 @@ import { useGetDevice, useGetSystemMetrics } from '../../services/devices';
6
6
  import DeviceInfo from '../../components/Device/DeviceInfo/DeviceInfo';
7
7
  import EditDevice from '../../components/Device/EditDevice/EditDevice';
8
8
  import classes from './DeviceSingle.module.scss';
9
- // import { compareVersions } from '../../utils/helpers';
10
9
  import NotFound from '../../components/common/NotFound/NotFound';
11
10
  import DeviceBackups from '../../components/Device/DeviceBackups/DeviceBackups';
12
- import { DevicePlan } from '../../@types/devices';
11
+ import { DevicePlan, DeviceStorage } from '../../@types/devices';
13
12
 
14
13
  const DeviceSingle = () => {
15
14
  const { id } = useParams();
@@ -27,6 +26,7 @@ const DeviceSingle = () => {
27
26
  const device = data?.result?.device;
28
27
  const metrics = metricsData?.result && metricsData?.result?.system ? metricsData.result : device?.metrics;
29
28
  const devicePlans: DevicePlan[] = data?.result?.plans || [];
29
+ const deviceStorages: DeviceStorage[] = data?.result?.storages || [];
30
30
  const deviceName = device?.name;
31
31
  const isPending = device?.status && device?.status === 'pending';
32
32
 
@@ -73,7 +73,7 @@ const DeviceSingle = () => {
73
73
  </div>
74
74
  ) : (
75
75
  <>
76
- <DeviceBackups plans={devicePlans} />
76
+ <DeviceBackups plans={devicePlans} storages={deviceStorages} />
77
77
  {metrics?.system && <DeviceInfo metrics={metrics} isRefetching={false} />}
78
78
  </>
79
79
  )}
@@ -14,9 +14,11 @@ import PlanUnlockModal from '../../components/Plan/PlanUnlockModal/PlanUnlockMod
14
14
  import PlanRemoveModal from '../../components/Plan/PlanRemoveModal/PlanRemoveModal';
15
15
  import PlanProgress from '../../components/Plan/PlanProgress/PlanProgress';
16
16
  import PlanBackups from '../../components/Plan/PlanBackups/PlanBackups';
17
+ import PlanIntegrity from '../../components/Plan/PlanIntegrity/PlanIntegrity';
17
18
 
18
19
  const PlanSingle = () => {
19
20
  const [showMoreOptions, setShowMoreOptions] = useState(false);
21
+ const [showIntegrityModal, setShowIntegrityModal] = useState(false);
20
22
  const { id } = useParams();
21
23
 
22
24
  const EditPlanModal = useComponentOverride('EditPlan', EditPlan);
@@ -126,6 +128,15 @@ const PlanSingle = () => {
126
128
  <Icon size={14} type="unlock" /> Unlock
127
129
  </li>
128
130
  )}
131
+ <li
132
+ className={classes.actionBtn}
133
+ onClick={() => {
134
+ setShowIntegrityModal(true);
135
+ setShowMoreOptions(false);
136
+ }}
137
+ >
138
+ <Icon size={14} type="integrity" /> Check Integrity
139
+ </li>
129
140
  <li
130
141
  className={classes.actionBtn}
131
142
  onClick={() => {
@@ -174,6 +185,16 @@ const PlanSingle = () => {
174
185
  close={() => setShowPruneModal(false)}
175
186
  />
176
187
  )}
188
+ {showIntegrityModal && !isSync && (
189
+ <PlanIntegrity
190
+ planId={plan.id}
191
+ taskPending={taskPending}
192
+ verificationData={plan.verified}
193
+ storage={plan.storage}
194
+ replicationStorages={plan.settings.replication?.enabled ? plan.settings.replication.storages : []}
195
+ onClose={() => setShowIntegrityModal(false)}
196
+ />
197
+ )}
177
198
  {showDeleteModal && (
178
199
  <PlanRemoveModal planId={id} taskPending={taskPending} actionInProgress={actionInProgress} close={() => setShowDeleteModal(false)} />
179
200
  )}
@@ -5,14 +5,12 @@ import { API_URL } from '../utils/constants';
5
5
  const notifiedBackupProgress = new Set<string>();
6
6
 
7
7
  // Generate Download
8
- export async function generateBackupDownload({ backupId }: { backupId: string; planId: string }) {
9
- // const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
10
- const res = await fetch(`${API_URL}/backups/${backupId}/action/download`, {
8
+ export async function generateBackupDownload({ backupId, replicationId }: { backupId: string; planId: string; replicationId?: string }) {
9
+ const url = `${API_URL}/backups/${backupId}/action/download?${replicationId ? `replicationId=${replicationId}` : ''}`;
10
+ const res = await fetch(url, {
11
11
  method: 'POST',
12
12
  credentials: 'include',
13
- // headers: header,
14
13
  });
15
- // Check if response is ok
16
14
  const data = await res.json();
17
15
  if (!data.success) {
18
16
  throw new Error(data.error);
@@ -241,13 +239,12 @@ export function useUpdateBackup() {
241
239
  }
242
240
 
243
241
  // Get Snapshot Files
244
- export async function getSnapshotFiles({ backupId }: { backupId: string }) {
245
- const res = await fetch(`${API_URL}/backups/${backupId}/files`, {
242
+ export async function getSnapshotFiles({ backupId, replicationId }: { backupId: string; replicationId?: string }) {
243
+ const url = replicationId ? `${API_URL}/backups/${backupId}/files?replicationId=${replicationId}` : `${API_URL}/backups/${backupId}/files`;
244
+ const res = await fetch(url, {
246
245
  method: 'GET',
247
246
  credentials: 'include',
248
- // headers: header,
249
247
  });
250
- // Check if response is ok
251
248
  const data = await res.json();
252
249
  if (!data.success) {
253
250
  throw new Error(data.error);
@@ -263,3 +260,29 @@ export function useGetSnapshotFiles(payload: { backupId: string }) {
263
260
  retry: false,
264
261
  });
265
262
  }
263
+
264
+ // Retry Failed Replications
265
+ export async function retryFailedReplications({ backupId, replicationId }: { backupId: string; planId: string; replicationId: string }) {
266
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
267
+ const res = await fetch(`${API_URL}/backups/${backupId}/action/replication-retry?replicationId=${replicationId}`, {
268
+ method: 'POST',
269
+ credentials: 'include',
270
+ headers: header,
271
+ });
272
+ const data = await res.json();
273
+ if (!data.success) {
274
+ throw new Error(data.error);
275
+ }
276
+ return data;
277
+ }
278
+
279
+ export function useRetryFailedReplications() {
280
+ const queryClient = useQueryClient();
281
+ return useMutation({
282
+ mutationFn: retryFailedReplications,
283
+ onSuccess: (res, payload) => {
284
+ console.log('res :', payload, res);
285
+ queryClient.invalidateQueries({ queryKey: ['plan', payload.planId] });
286
+ },
287
+ });
288
+ }