diff --git a/src/components/standalone/dashboard/HaStatusCard.vue b/src/components/standalone/dashboard/HaStatusCard.vue
new file mode 100644
index 000000000..a400722a5
--- /dev/null
+++ b/src/components/standalone/dashboard/HaStatusCard.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+ {{ t('standalone.ha.sidebar_title') }}
+
+
+
+
+
+ -
+ {{ t('standalone.ha.role') }}{{ t('standalone.ha.' + ha.role) }}
+
+
+ -
+ {{ t('standalone.ha.state') }}
+ {{ t('standalone.ha.' + ha.state) }}
+
+
+ {{ t('standalone.ha.last_sync_status') }}{{ t('standalone.ha.' + ha.lastSyncStatus) }}
+ {{ t('standalone.ha.last_sync_time') }}
+ {{ formatDateLoc(new Date(ha.lastSyncTime * 1000), 'PPpp') }}
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json
index 2f795b384..81c4b79e5 100644
--- a/src/i18n/en/translation.json
+++ b/src/i18n/en/translation.json
@@ -2409,6 +2409,24 @@
"max_detect_info": "Policy 'max-detect' is enabled.",
"max_detect_description": "Please refer to the manual for more information.",
"events_today": "event today | events today"
+ },
+ "ha": {
+ "sidebar_title": "High Availability",
+ "state": "State",
+ "role": "Role",
+ "primary": "Primary",
+ "backup": "Backup",
+ "last_sync_status": "Last sync status",
+ "last_sync_time": "Last sync time",
+ "ssh_connection_failed": "SSH connection failed",
+ "rsync_detection_failed": "Rsync detection failed",
+ "rsync_transfer_failed": "Rsync transfer failed",
+ "successful": "Successful",
+ "up_to_date": "Up to date",
+ "master": "Active",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "backup_warning": "Read-only node"
}
},
"controller": {
diff --git a/src/stores/standalone/haStatus.ts b/src/stores/standalone/haStatus.ts
new file mode 100644
index 000000000..e8b07801c
--- /dev/null
+++ b/src/stores/standalone/haStatus.ts
@@ -0,0 +1,55 @@
+import { defineStore } from 'pinia'
+import { onMounted, ref } from 'vue'
+import { ubusCall } from '@/lib/standalone/ubus'
+import type { AxiosResponse } from 'axios'
+
+type HaStatus = AxiosResponse<{
+ state: string
+ role: string
+ status: string
+ last_sync_status: string
+ last_sync_time: number
+}>
+
+export const useHaStatusStore = defineStore('haStatus', () => {
+ const state = ref('')
+ const role = ref('')
+ const status = ref('')
+ const lastSyncStatus = ref('')
+ const lastSyncTime = ref(0)
+
+ const loading = ref(true)
+ const error = ref()
+
+ function fetchStatus() {
+ ubusCall('ns.ha', 'status', {})
+ .then((response: HaStatus) => {
+ state.value = response.data.state
+ role.value = response.data.role
+ status.value = response.data.status
+ lastSyncStatus.value = response.data.last_sync_status.toLowerCase().replace(/ /g, '_')
+ lastSyncTime.value = response.data.last_sync_time
+ })
+ .catch((reason: Error) => {
+ error.value = reason
+ })
+ .finally(() => {
+ loading.value = false
+ })
+ }
+
+ onMounted(() => {
+ fetchStatus()
+ })
+
+ return {
+ state,
+ role,
+ status,
+ lastSyncStatus,
+ lastSyncTime,
+ loading,
+ error,
+ fetchStatus
+ }
+})
diff --git a/src/views/standalone/StandaloneDashboardView.vue b/src/views/standalone/StandaloneDashboardView.vue
index 3fd45954b..8757c5e98 100644
--- a/src/views/standalone/StandaloneDashboardView.vue
+++ b/src/views/standalone/StandaloneDashboardView.vue
@@ -17,6 +17,7 @@ import ThreatShieldIpCard from '@/components/standalone/dashboard/ThreatShieldIp
import IpsServiceCard from '@/components/standalone/dashboard/IpsServiceCard.vue'
import MacBindingStatusCard from '@/components/standalone/dashboard/MacBindingStatusCard.vue'
import BackupStatusCard from '@/components/standalone/dashboard/BackupStatusCard.vue'
+import HaStatusCard from '@/components/standalone/dashboard/HaStatusCard.vue'
const { t } = useI18n()
const route = useRoute()
@@ -119,5 +120,6 @@ function goTo(path: string) {
:icon="['fas', 'circle-info']"
/>
+