-
-
-
+
diff --git a/src/components/charts/TimeLineChart.vue b/src/components/charts/TimeLineChart.vue
index 3a7fefd54..b5f52e0c1 100644
--- a/src/components/charts/TimeLineChart.vue
+++ b/src/components/charts/TimeLineChart.vue
@@ -125,11 +125,16 @@ const defaultOptions: any = {
}
const allOptions = computed(() => {
- return merge(typeof props.options === 'object' ? props.options : {}, defaultOptions)
+ const customOptions = props.options && typeof props.options === 'object' ? props.options : {}
+ return merge({}, defaultOptions, customOptions)
})
const chartData: any = computed(() => {
- return { labels: props.labels, datasets: props.datasets }
+ // Deep-clone to strip Vue readonly proxies before handing data to Chart.js.
+ // Chart.js internally mutates dataset objects (attaches _meta, controllers, etc.)
+ // and Vue will block those mutations with "target is readonly" warnings if the
+ // objects are still wrapped in a shallowReadonly prop proxy.
+ return JSON.parse(JSON.stringify({ labels: props.labels, datasets: props.datasets }))
})
const chartStyle = computed(() => {
diff --git a/src/components/standalone/SideMenu.vue b/src/components/standalone/SideMenu.vue
index 98584f1b7..0f05a14fb 100644
--- a/src/components/standalone/SideMenu.vue
+++ b/src/components/standalone/SideMenu.vue
@@ -51,6 +51,10 @@ const navigation: Ref
-
+
{{ rule.name }}
@@ -393,24 +399,39 @@ function searchStringInRule(rule: FirewallRule, queryText: string) {
-
-
-
-
-
-
-
-
-
- {{ t('standalone.firewall_rules.tags') }}:
- {{ rule.ns_tag?.join(', ') || '-' }}
-
-
- {{ t('standalone.firewall_rules.logging') }}:
- {{ rule.log ? t('common.enabled') : t('common.disabled') }}
-
-
-
+
+
+
+
+
+
+
+
+
+ {{ t('standalone.firewall_rules.logging_enabled') }}
+
+
+
+
+
+
+
+
+
+
+ {{ rule.ns_tag?.join(', ') }}
+
+
+
|
diff --git a/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue b/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue
index bc22431ac..73bbfc1b9 100644
--- a/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue
+++ b/src/components/standalone/ipsec_tunnel/CreateOrEditTunnelDrawer.vue
@@ -59,7 +59,7 @@ type CreateEditIpsecTunnelPayload = {
}
ipcomp: string
closeaction?: string
- dpdaction: 'restart' | 'none'
+ dpdaction: TunnelDpdAction
remote_subnet: string[]
local_subnet: string[]
ns_name: string
@@ -72,6 +72,9 @@ type CreateEditIpsecTunnelPayload = {
pre_shared_key: string
}
+type DpdAction = 'clear' | 'trap' | 'restart'
+type TunnelDpdAction = DpdAction | 'none'
+
const { t } = useI18n()
const emit = defineEmits(['close', 'add-edit-tunnel'])
@@ -104,7 +107,7 @@ const remoteIdentifier = ref('')
// Step 2 fields
const presharedKeyMode = ref<'generate' | 'import'>('generate')
const presharedKey = ref('')
-const dpd = ref(false)
+const dpdAction = ref('clear')
const enableCompression = ref(false)
const closeAction = ref('none')
@@ -152,6 +155,11 @@ const closeActionOptions: NeComboboxOption[] = [
{ id: 'trap', label: 'trap' },
{ id: 'start', label: 'start' }
]
+const dpdActionOptions: NeComboboxOption[] = [
+ { id: 'clear', label: 'clear' },
+ { id: 'trap', label: 'trap' },
+ { id: 'restart', label: 'restart' }
+]
const encryptionOptions = ref([])
const integrityOptions = ref([])
const diffieHellmanOptions = ref([])
@@ -253,7 +261,7 @@ async function resetForm() {
wanIpAddress.value = tunnelData?.local_ip ?? ''
remoteIpAddress.value = tunnelData?.gateway ?? ''
remoteNetworks.value = tunnelData?.remote_subnet ?? ['']
- dpd.value = tunnelData ? tunnelData.dpdaction == 'restart' : false
+ dpdAction.value = normalizeDpdAction(tunnelData?.dpdaction)
enableCompression.value = tunnelData ? tunnelData.ipcomp === 'true' : false
closeAction.value = tunnelData?.closeaction ?? 'none'
ikeVersion.value = tunnelData?.keyexchange ?? ikeVersionOptions[0].id
@@ -403,6 +411,18 @@ function handlePreviousStep() {
}
}
+function normalizeDpdAction(action?: string | null): DpdAction {
+ switch (action) {
+ case 'trap':
+ case 'restart':
+ case 'clear':
+ return action
+ case 'none':
+ default:
+ return 'clear'
+ }
+}
+
async function createOrEditTunnel() {
error.value.notificationTitle = ''
error.value.notificationDescription = ''
@@ -425,7 +445,7 @@ async function createOrEditTunnel() {
ipcomp: enableCompression.value ? 'true' : 'false',
closeaction: closeAction.value,
enabled: enabled.value ? '1' : '0',
- dpdaction: dpd.value ? 'restart' : 'none',
+ dpdaction: dpdAction.value,
keyexchange: ikeVersion.value,
remote_subnet: remoteNetworks.value.filter((x) => x != ''),
local_subnet: localNetworks.value
@@ -621,17 +641,25 @@ watch(
-
- {{
- t('standalone.ipsec_tunnel.dpd_dead_peer_detection')
- }}
-
-
+
+
+
+
+ {{ t('standalone.ipsec_tunnel.dpd_dead_peer_detection_tooltip') }}
+
+
+
+
{{ t('standalone.ipsec_tunnel.compression') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue b/src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue
new file mode 100644
index 000000000..6edbcaa5a
--- /dev/null
+++ b/src/components/standalone/monitoring/metrics/MetricsAlertsSection.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('standalone.metrics.severity') }}
+ {{ t('standalone.metrics.alert') }}
+ {{ t('standalone.metrics.status') }}
+ {{ t('standalone.metrics.triggered_by') }}
+ {{ t('standalone.metrics.started_at') }}
+
+
+
+
+
+ {{ getSeverityLabel(alert) }}
+
+
+
+
+
+ {{ getLocalizedAnnotation(alert, 'summary') || alert.name }}
+
+
+ {{ getLocalizedAnnotation(alert, 'description') }}
+
+
+
+
+
+ {{ getStateLabel(alert) }}
+
+
+
+
+
+ {{ alert.name }}
+
+
+ {{ t('standalone.metrics.service') }}: {{ alert.labels.service ?? '-' }}
+ -
+ {{ t('standalone.metrics.group') }}: {{ alert.labels.alertgroup ?? '-' }}
+
+
+
+
+ {{ formatActiveAt(alert.activeAt) }}
+
+
+
+
+
+
diff --git a/src/components/standalone/monitoring/metrics/MetricsChartsSection.vue b/src/components/standalone/monitoring/metrics/MetricsChartsSection.vue
new file mode 100644
index 000000000..e9f349005
--- /dev/null
+++ b/src/components/standalone/monitoring/metrics/MetricsChartsSection.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('standalone.metrics.system_section') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('standalone.metrics.network_section') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useAlerts.ts b/src/composables/useAlerts.ts
new file mode 100644
index 000000000..3d14dd45b
--- /dev/null
+++ b/src/composables/useAlerts.ts
@@ -0,0 +1,81 @@
+import { ubusCall } from '@/lib/standalone/ubus'
+import { useQuery } from '@tanstack/vue-query'
+import type { AxiosResponse } from 'axios'
+import type { NeNotificationV2 } from '@nethesis/vue-components'
+import { computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+export type Alert = {
+ id?: string
+ activeAt: string
+ annotations: Record
+ labels: Record
+ name: string
+ source?: string
+ state: string
+}
+
+type ListAlertsResponse = {
+ alerts: Alert[]
+}
+
+function getSeverityBadgeKind(severity: string | undefined): NeNotificationV2['kind'] {
+ switch (severity) {
+ case 'critical':
+ return 'error'
+ case 'warning':
+ return 'warning'
+ default:
+ return 'info'
+ }
+}
+
+export function useAlerts() {
+ const { locale, t } = useI18n()
+
+ const { data, error, status, isPending, isError, dataUpdatedAt } = useQuery({
+ queryKey: ['metrics', 'alerts'],
+ queryFn: async () =>
+ await ubusCall>('ns.telegraf', 'list-alerts'),
+ select: (response) => response.data.alerts,
+ refetchInterval: 5000
+ })
+
+ const notifications = computed(() => {
+ if (!data.value) {
+ return []
+ }
+
+ return data.value.map((alert) => {
+ const summary =
+ alert.annotations[`summary_${locale.value}`] ||
+ alert.annotations['summary_en'] ||
+ alert.name
+ const description =
+ alert.annotations[`description_${locale.value}`] ||
+ alert.annotations['description_en'] ||
+ ''
+
+ return {
+ id: alert.id ?? `${alert.name}-${alert.activeAt}`,
+ kind: getSeverityBadgeKind(alert.labels.severity),
+ title: summary,
+ description: description,
+ timestamp: new Date(alert.activeAt),
+ firstButtonLabel: t('standalone.metrics.go_to_alerts'),
+ firstButtonAction: 'goto_alerts',
+ isShown: true
+ }
+ })
+ })
+
+ return {
+ data,
+ notifications,
+ error,
+ status,
+ isPending,
+ isError,
+ dataUpdatedAt
+ }
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0f68f40e5..1ac83b093 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -212,6 +212,7 @@
"cannot_retrieve_backup_info": "Cannot retrieve backup information",
"cannot_retrieve_flashstart_configuration": "Cannot retrieve configuration",
"cannot_retrieve_netdata_configuration": "Cannot retrieve configuration",
+ "cannot_retrieve_telegraf_configuration": "Cannot retrieve Telegraf configuration",
"cannot_retrieve_dpi_rules": "Cannot retrieve DPI rules",
"cannot_retrieve_dpi_exceptions": "Cannot retrieve DPI exceptions",
"cannot_create_dpi_exception": "Cannot create DPI exception",
@@ -882,8 +883,6 @@
"edit_scheduled_date": "Edit scheduled date",
"update": "Update",
"automatic_updates": "Automatic updates",
- "automatic_updates_tooltip": "Enable to activate automatic updates. Available only with subscription.",
- "go_to_subscription": "Go to subscription",
"automatic_updates_enabled_message": "Automatic updates successfully enabled",
"automatic_updates_disabled_message": "Automatic updates successfully disabled",
"schedule_update": "Schedule update",
@@ -1506,7 +1505,8 @@
"zone_dmz_color": "Orange",
"preset_active_title": "Preset selected",
"preset_active_description": "When you create this zone, the system will automatically add firewall rules to allow access to essential services. You can view or modify them anytime under Firewall > Rules > Input Rules.",
- "choose_one_or_more_zones": "Choose one or more zones"
+ "choose_one_or_more_zones": "Choose one or more zones",
+ "logging_enabled": "Logging enabled"
},
"port_forward": {
"title": "Port forward",
@@ -1538,6 +1538,8 @@
"log": "Log",
"hairpin_nat": "Hairpin NAT",
"hairpin_nat_zones": "Hairpin NAT enabled on following zones",
+ "hairpin_nat_enabled": "Hairpin NAT enabled on {zones}",
+ "logging_enabled": "Logging enabled",
"choose_zone": "Choose zone",
"choose_protocol": "Choose protocol",
"protocol_helper": "Leave unselected for any source protocol",
@@ -1643,7 +1645,8 @@
"destination_object": "Destination object",
"inactive": "Inactive",
"zone_no_longer_exists": "This rule is inactive because it uses a zone that no longer exists",
- "disabled_rule": "This rule has been manually disabled, but can be re-enabled"
+ "disabled_rule": "This rule has been manually disabled, but can be re-enabled",
+ "logging_enabled": "Logging enabled"
},
"firewall": {
"title": "Firewall"
@@ -2186,7 +2189,8 @@
"pre_shared_key_invalid_chars_tooltip": "The characters '\\{' and '\\}' are not allowed in the pre-shared key",
"use_generated_key": "Use generated key",
"use_custom_key": "Use custom key",
- "dpd_dead_peer_detection": "DPD (dead peer detection)",
+ "dpd_dead_peer_detection": "DPD action",
+ "dpd_dead_peer_detection_tooltip": "Action to perform for this CHILD_SA on DPD (Dead Peer Detection) timeout. The default action is 'clear' which closes the CHILD_SA and does not take further action. 'trap' installs a trap policy, which catches matching traffic and tries to re-negotiate the tunnel on-demand. 'restart' immediately tries to re-negotiate the CHILD_SA under a fresh IKE_SA.",
"pfs_perfect_forward_secrecy_disabled": "PFS (Perfect Forward Secrecy) disabled",
"compression": "Compression",
"close_action": "Close action",
@@ -2271,7 +2275,6 @@
"unchanged": "Unchanged",
"remote_database_added": "Remote database added",
"remote_database_added_description": "{name} added successfully",
- "add_remote_database_tooltip": "Remote users databases are available only with a subscription plan",
"administrator": "Administrator user",
"administrator_tooltip": "A user promoted to administrator can access the administration web interface and configure this unit.",
"remove_admin": "Remove from administrator users",
@@ -2298,9 +2301,7 @@
"real_time_monitor": {
"title": "Real-time monitor",
"description": "In-depth analysis of system and application performance, detailed metrics and visualizations. Monitoring data are stored in RAM and reset upon unit reboot. If the unit is linked to a remote controller, metrics are saved on the controller too and persist through unit reboots.",
- "open_report": "Open Netdata",
"cannot_open_grafana_message": "Grafana monitoring is available only after connecting the unit to a controller",
- "cannot_open_report_from_controller": "Cannot open report from a controlled unit. You can open the report by accessing the unit UI directly, or by accessing the controller UI, going to 'Unit manager' page, clicking the three dots menu of the unit and selecting 'Open metrics'.",
"traffic": "Traffic",
"daily_traffic": "Daily traffic",
"connectivity": "WAN uplinks",
@@ -2444,6 +2445,59 @@
"raw_data": "Raw data",
"table_aria_label": "Table of active flows"
},
+ "metrics": {
+ "title": "Metrics",
+ "time_range": "Time range",
+ "time_range_minutes": "Last minute | Last {count} minutes",
+ "time_range_hours": "Last hour | Last {count} hours",
+ "time_range_days": "Last day | Last {count} days",
+ "refresh_interval": "Auto-refresh",
+ "system_section": "System",
+ "network_section": "Network",
+ "connections": "Connections (conntrack)",
+ "traffic": "Network interface traffic",
+ "latency": "Latency",
+ "packet_delivery": "Packet delivery",
+ "cpu": "CPU usage",
+ "load": "System load",
+ "diskio": "Disk I/O",
+ "disk": "Disk usage (%)",
+ "processes": "Total processes",
+ "memory": "RAM usage",
+ "packets": "Network packets",
+ "no_data": "No data available for the selected time range",
+ "cannot_retrieve_metrics": "Cannot retrieve metrics",
+ "tabs": {
+ "charts": "Charts",
+ "alerts": "Alerts"
+ },
+ "alerts_description": "Current pending and firing alerts evaluated by vmalert.",
+ "alerts_table_aria_label": "Table of current monitoring alerts",
+ "alert": "Alert",
+ "status": "Status",
+ "severity": "Severity",
+ "triggered_by": "Triggered by",
+ "started_at": "Started at",
+ "service": "Service",
+ "group": "Group",
+ "no_alerts_yet": "No alerts yet",
+ "no_alerts_description": "System is fully operational.",
+ "cannot_retrieve_alerts": "Cannot retrieve alerts",
+ "firing": "Firing",
+ "pending": "Pending",
+ "critical": "Critical",
+ "warning": "Warning",
+ "info": "Info",
+ "unknown": "Unknown",
+ "go_to_alerts": "Go to Alerts",
+ "active_alerts": "@:standalone.metrics.no_alerts_description | active alert | active alerts",
+ "no_alerts": "No alerts"
+ },
+ "metrics_monitor": {
+ "description": "Historical view of key system metrics collected by Telegraf and stored in VictoriaMetrics. Select a time range to adjust the chart window.",
+ "refresh": "Refresh",
+ "vm_unreachable": "VictoriaMetrics is not reachable. Make sure the service is running."
+ },
"ping_latency_monitor": {
"title": "Ping latency monitor",
"description": "Measure round-trip time and packet delivery rates by pinging network hosts. Add one or more hosts, including VPN IPs to monitor tunnel quality. Latency and packet delivery charts are available under Monitoring > Real-time monitor > WAN uplinks.",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index ba07f7ebe..d1da194b2 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -189,6 +189,7 @@
"invalid_future_date": "La data non può essere impostata nel passato",
"cannot_set_passphrase": "Impossibile impostare passphrase",
"cannot_retrieve_netdata_configuration": "Impossibile recuperare la configurazione Netdata",
+ "cannot_retrieve_telegraf_configuration": "Impossibile recuperare la configurazione Telegraf",
"cannot_retrieve_tunnel_options": "Impossibile recuperare le opzioni del tunnel",
"invalid_negative_integer": "Inserire un numero intero maggiore o uguale a 0",
"cannot_cancel_update_schedule": "Impossibile annullare la programmazione dell'aggiornamento",
@@ -1052,7 +1053,8 @@
"zone_dmz_color": "Orange",
"preset_active_title": "Preset selezionato",
"preset_active_description": "Quando si crea questa zona, il sistema aggiungerà automaticamente le regole firewall per consentire l'accesso ai servizi essenziali. È possibile visualizzarli o modificarli in qualsiasi momento sotto Firewall > Regole > Regole di input.",
- "choose_one_or_more_zones": "Scegliere una o più zone"
+ "choose_one_or_more_zones": "Scegliere una o più zone",
+ "logging_enabled": "Logging abilitato"
},
"port_forward": {
"disable": "Disabilita",
@@ -1079,6 +1081,8 @@
"status": "Stato",
"title": "Port forward",
"hairpin_nat_zones": "Hairpin NAT abilitato sulle seguenti zone",
+ "hairpin_nat_enabled": "Hairpin NAT abilitato su {zones}",
+ "logging_enabled": "Logging abilitato",
"log": "Log",
"wan_ip": "IP WAN",
"destination_port": "Porta di destinazione",
@@ -1559,7 +1563,8 @@
"no_tunnel_found": "Nessun tunnel trovato",
"connected": "Connesso",
"use_generated_key": "Utilizza la chiave generata",
- "dpd_dead_peer_detection": "DPD (dead peer detection)",
+ "dpd_dead_peer_detection": "DPD action",
+ "dpd_dead_peer_detection_tooltip": "Azione da eseguire per questo CHILD_SA al timeout DPD (Dead Peer Detection). Il valore predefinito 'clear' chiude il CHILD_SA e non esegue altre azioni. 'trap' installa una trap policy, che intercetta il traffico corrispondente e prova a rinegoziare il tunnel on-demand. 'restart' prova immediatamente a rinegoziare il CHILD_SA con un nuovo IKE_SA.",
"choose_wan": "Scegli WAN",
"wan_ip_address": "Indirizzo IP WAN",
"enable": "Abilita",
@@ -1621,8 +1626,6 @@
"automatic_updates": "Aggiornamenti automatici",
"automatic_updates_enabled_message": "Aggiornamenti automatici abilitati con successo",
"automatic_updates_disabled_message": "Aggiornamenti automatici disattivati con successo",
- "automatic_updates_tooltip": "Abilita per attivare gli aggiornamenti automatici. Disponibile solo con subscription.",
- "go_to_subscription": "Vai a subscription",
"schedule_update": "Pianifica aggiornamento",
"no_updates_available": "Nessun aggiornamento disponibilw",
"new_release_available": "Nuova versione disponibile",
@@ -1740,7 +1743,6 @@
"set_admin": "Impostare come utente amministratore",
"remove_admin": "Rimuovi dagli utenti amministratori",
"administrator_table_tooltip": "Utente amministratore",
- "add_remote_database_tooltip": "I database degli utenti remoti sono disponibili solo con subscription",
"administrator_tooltip": "Un utente promosso ad amministratore può accedere all'interfaccia web di amministrazione e configurare questa unità.",
"user_bind_dn": "User bind DN personalizzato",
"user_bind_dn_tooltip": "Distinguished Name (DN) utilizzato per collegarsi al server LDAP. Questo campo sovrascrive il bind DN calcolato. Utilizzare quando ci si autenticata ad un server Active Directory. Può contenere una variabile '%u' che verrà espansa con il nome dell'utente. Esempio: '%u{'@'}secretexample.com', 'SECRETEXAMPLE\\%u'",
@@ -2002,7 +2004,8 @@
"destination_object": "Oggetto di destinazione",
"inactive": "Inattiva",
"zone_no_longer_exists": "Questa regola è stata disattivata perché utilizza una zona che non esiste più",
- "disabled_rule": "Questa regola è stata disabilitata manualmente, ma può essere riattivata"
+ "disabled_rule": "Questa regola è stata disabilitata manualmente, ma può essere riattivata",
+ "logging_enabled": "Logging abilitato"
},
"certificates": {
"title": "Certificati",
@@ -2408,10 +2411,8 @@
"account": "Account",
"threat_shield_disabled_message": "Non sono disponibili statistiche perché Threat Shield IP è disabilitato.",
"title": "Monitor in tempo reale",
- "open_report": "Apri Netdata",
"cannot_open_grafana_message": "Il monitoraggio con Grafana è disponibile solo dopo aver collegato l'unità a un controller",
"description": "Analisi approfondita delle prestazioni del sistema e delle applicazioni, metriche dettagliate e visualizzazioni. I dati di monitoraggio vengono memorizzati in RAM e ripristinati al riavvio dell'unità. Se l'unità è collegata a un controller remoto, le metriche vengono salvate anche sul controller e persistono al riavvio dell'unità .",
- "cannot_open_report_from_controller": "Impossibile aprire il report per l'unità controllata. È possibile aprire il report accedendo direttamente all'interfaccia dell unità, o accedendo all'interfaccia del controller, andando alla pagina 'Gestione unità, facendo clic sul menu tre punti dell'unità e selezionando 'Apri metriche'.",
"times_blocked": "Numero blocchi",
"timestamp": "Data e ora",
"daily_total_traffic": "Traffico totale giornaliero",
@@ -2457,6 +2458,63 @@
"monitoring": {
"title": "Monitoraggio"
},
+ "metrics": {
+ "title": "Metriche",
+ "time_range": "Intervallo di tempo",
+ "time_range_minutes": "Ultimo {num} minuto | Ultimi {num} minuti",
+ "time_range_hours": "Ultima {num} ora | Ultime {num} ore",
+ "time_range_days": "Ultimo {num} giorno | Ultimi {num} giorni",
+ "refresh_interval": "Aggiornamento automatico",
+ "refresh_off": "Disattivato",
+ "refresh_30s": "Ogni 30 secondi",
+ "refresh_60s": "Ogni 60 secondi",
+ "system_section": "Sistema",
+ "network_section": "Rete",
+ "connections": "Connessioni (conntrack)",
+ "traffic": "Traffico delle interfacce di rete",
+ "latency": "Latenza",
+ "packet_delivery": "Consegna pacchetti",
+ "cpu": "Utilizzo CPU",
+ "load": "Carico di sistema",
+ "diskio": "I/O disco",
+ "disk": "Utilizzo disco (%)",
+ "processes": "Processi totali",
+ "memory": "Utilizzo RAM",
+ "packets": "Pacchetti di rete",
+ "no_data": "Nessun dato disponibile per l'intervallo selezionato",
+ "cannot_retrieve_metrics": "Impossibile recuperare le metriche",
+ "tabs": {
+ "charts": "Grafici",
+ "alerts": "Avvisi"
+ },
+ "alerts_description": "Elenco corrente degli avvisi in stato pending o firing valutati da vmalert.",
+ "alerts_table_aria_label": "Tabella degli avvisi di monitoraggio correnti",
+ "alert": "Avviso",
+ "status": "Stato",
+ "severity": "Gravita",
+ "triggered_by": "Rilevato da",
+ "started_at": "Avviato il",
+ "service": "Servizio",
+ "group": "Gruppo",
+ "no_alerts_yet": "Nessun avviso",
+ "no_alerts_description": "Il sistema funziona correttamente.",
+ "cannot_retrieve_alerts": "Impossibile recuperare gli avvisi",
+ "firing": "Attivo",
+ "pending": "In attesa",
+ "critical": "Critico",
+ "warning": "Avviso",
+ "info": "Info",
+ "unknown": "Sconosciuto",
+ "go_to_alerts": "Vai agli avvisi",
+ "active_alerts": "@:standalone.metrics.no_alerts_description | avviso attivo | avvisi attivi",
+ "system_degraded": "Sistema degradato",
+ "no_alerts": "Nessun avviso"
+ },
+ "metrics_monitor": {
+ "description": "Vista storica delle metriche principali del sistema raccolte da Telegraf e archiviate in VictoriaMetrics. Seleziona un intervallo di tempo per regolare la finestra dei grafici.",
+ "refresh": "Aggiorna",
+ "vm_unreachable": "VictoriaMetrics non e raggiungibile. Verifica che il servizio sia in esecuzione."
+ },
"netify_informatics": {
"title": "Netify Informatics",
"netify_informatics_disable_description": "L'invio di metadati è stato disattivato",
diff --git a/src/i18n/ta.json b/src/i18n/ta.json
index 881499d06..63843c99c 100644
--- a/src/i18n/ta.json
+++ b/src/i18n/ta.json
@@ -133,6 +133,7 @@
"cannot_retrieve_backup_info": "காப்புப் பிரதி தகவலை மீட்டெடுக்க முடியாது",
"cannot_retrieve_flashstart_configuration": "உள்ளமைவை மீட்டெடுக்க முடியவில்லை",
"cannot_retrieve_netdata_configuration": "உள்ளமைவை மீட்டெடுக்க முடியவில்லை",
+ "cannot_retrieve_telegraf_configuration": "Telegraf உள்ளமைவை மீட்டெடுக்க முடியவில்லை",
"cannot_retrieve_dpi_rules": "DPI விதிகளை மீட்டெடுக்க முடியாது",
"cannot_retrieve_dpi_exceptions": "DPI விதிவிலக்குகளை மீட்டெடுக்க முடியாது",
"cannot_create_dpi_exception": "DPI விதிவிலக்கை உருவாக்க முடியாது",
@@ -859,8 +860,6 @@
"edit_scheduled_date": "திட்டமிடப்பட்ட தேதியைத் திருத்தவும்",
"update": "புதுப்பிப்பு",
"automatic_updates": "தானியங்கி புதுப்பிப்புகள்",
- "automatic_updates_tooltip": "தானியங்கி புதுப்பிப்புகளை செயல்படுத்த இயக்கவும். சந்தாவுடன் மட்டுமே கிடைக்கும்.",
- "go_to_subscription": "சந்தாவிற்கு செல்க",
"automatic_updates_enabled_message": "தானியங்கி புதுப்பிப்புகள் வெற்றிகரமாக இயக்கப்பட்டன",
"automatic_updates_disabled_message": "தானியங்கி புதுப்பிப்புகள் வெற்றிகரமாக முடக்கப்பட்டன",
"schedule_update": "அட்டவணை புதுப்பிப்பு",
@@ -2184,7 +2183,6 @@
"unchanged": "மாறாதது",
"remote_database_added": "தொலைநிலை தரவுத்தளம் சேர்க்கப்பட்டது",
"remote_database_added_description": "{name} வெற்றிகரமாக சேர்க்கப்பட்டது",
- "add_remote_database_tooltip": "தொலைநிலை பயனர்களின் தரவுத்தளங்கள் சந்தா திட்டத்துடன் மட்டுமே கிடைக்கும்",
"administrator": "நிர்வாகி பயனர்",
"administrator_tooltip": "நிர்வாகியாக பதவி உயர்வு பெற்ற ஒரு பயனர், நிர்வாக இணைய இடைமுகத்தை அணுகி இந்த அலகை உள்ளமைக்க முடியும்.",
"remove_admin": "நிர்வாகி பயனர்களிடமிருந்து அகற்று",
diff --git a/src/lib/standalone/metricsCharts.ts b/src/lib/standalone/metricsCharts.ts
new file mode 100644
index 000000000..55203c81b
--- /dev/null
+++ b/src/lib/standalone/metricsCharts.ts
@@ -0,0 +1,308 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { byteFormat1024 } from '@nethesis/vue-components'
+import {
+ AMBER_500,
+ AMBER_600,
+ CYAN_500,
+ CYAN_600,
+ EMERALD_500,
+ EMERALD_600,
+ FUCHSIA_500,
+ FUCHSIA_600,
+ INDIGO_500,
+ INDIGO_600,
+ ROSE_500,
+ ROSE_600,
+ VIOLET_500,
+ VIOLET_600
+} from '@/lib/color'
+
+// ── Types ────────────────────────────────────────────────────────────────────
+
+export interface MetricDataset {
+ label: string
+ data: number[]
+}
+
+export interface MetricSeries {
+ labels: number[]
+ datasets: MetricDataset[]
+}
+
+export interface TrafficInterface extends MetricSeries {
+ zone?: string
+ device?: string
+}
+
+export interface PingHost {
+ latency: MetricSeries
+ quality: MetricSeries
+}
+
+export interface MetricsData {
+ connections: MetricSeries
+ traffic: Record
+ cpu: MetricSeries
+ load: MetricSeries
+ diskio: MetricSeries
+ disk: MetricSeries
+ processes: MetricSeries
+ memory: MetricSeries
+ packets: MetricSeries
+ latency_quality?: Record
+}
+
+export interface ChartData {
+ labels: number[]
+ datasets: object[]
+}
+
+// ── Color palette ─────────────────────────────────────────────────────────────
+
+const PALETTE_LIGHT = [
+ CYAN_600,
+ INDIGO_600,
+ EMERALD_600,
+ ROSE_600,
+ AMBER_600,
+ VIOLET_600,
+ FUCHSIA_600
+]
+const PALETTE_DARK = [
+ CYAN_500,
+ INDIGO_500,
+ EMERALD_500,
+ ROSE_500,
+ AMBER_500,
+ VIOLET_500,
+ FUCHSIA_500
+]
+
+// ── Helper functions ──────────────────────────────────────────────────────────
+
+function toMs(labels: number[]): number[] {
+ return labels.map((s) => s * 1000)
+}
+
+function makeDataset(ds: MetricDataset, color: string) {
+ return {
+ label: ds.label,
+ data: ds.data,
+ borderColor: color,
+ backgroundColor: color,
+ borderWidth: 1.5,
+ radius: 0,
+ tension: 0
+ }
+}
+
+function buildDatasets(
+ series: MetricSeries,
+ colorsLight: string[],
+ colorsDark: string[],
+ isLight: boolean
+): object[] {
+ const palette = isLight ? colorsLight : colorsDark
+ return series.datasets.map((ds, i) => makeDataset(ds, palette[i % palette.length]!))
+}
+
+function buildTrafficDatasets(series: MetricSeries, isLight: boolean): object[] {
+ return buildDatasets(series, [CYAN_600, INDIGO_600], [CYAN_500, INDIGO_500], isLight)
+}
+
+function buildLatencyDatasets(series: MetricSeries, isLight: boolean): object[] {
+ return buildDatasets(
+ series,
+ [EMERALD_600, AMBER_600, ROSE_600],
+ [EMERALD_500, AMBER_500, ROSE_500],
+ isLight
+ )
+}
+
+function buildQualityDatasets(series: MetricSeries, isLight: boolean): object[] {
+ return buildDatasets(series, [INDIGO_600], [INDIGO_500], isLight)
+}
+
+// ── Chart options (plain objects – they never change) ─────────────────────────
+
+export const byteAxisOptions = {
+ scales: {
+ y: { ticks: { callback: (value: number) => byteFormat1024(Math.abs(value)) } }
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => {
+ const label = ctx.dataset.label ? `${ctx.dataset.label}: ` : ''
+ return label + byteFormat1024(Math.abs(ctx.parsed.y))
+ }
+ }
+ }
+ }
+}
+
+export const latencyOptions = {
+ scales: {
+ y: { ticks: { callback: (value: number) => `${Math.round(value)} ms` } }
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => {
+ const label = ctx.dataset.label ? `${ctx.dataset.label}: ` : ''
+ return label + `${Math.round(ctx.parsed.y)} ms`
+ }
+ }
+ }
+ }
+}
+
+export const qualityOptions = {
+ scales: {
+ y: {
+ min: 0,
+ max: 100,
+ ticks: { callback: (value: number) => `${Math.round(value)}%` }
+ }
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => {
+ const label = ctx.dataset.label ? `${ctx.dataset.label}: ` : ''
+ return label + `${Math.round(ctx.parsed.y)}%`
+ }
+ }
+ }
+ }
+}
+
+// ── Chart builders ────────────────────────────────────────────────────────────
+
+export function buildCpuChart(metrics: MetricsData | null, isLight: boolean): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.cpu
+ return { labels: toMs(s.labels), datasets: buildDatasets(s, [AMBER_600], [AMBER_500], isLight) }
+}
+
+export function buildLoadChart(metrics: MetricsData | null, isLight: boolean): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.load
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(
+ s,
+ [EMERALD_600, CYAN_600, VIOLET_600],
+ [EMERALD_500, CYAN_500, VIOLET_500],
+ isLight
+ )
+ }
+}
+
+export function buildDiskioChart(metrics: MetricsData | null, isLight: boolean): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.diskio
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(s, [CYAN_600, ROSE_600], [CYAN_500, ROSE_500], isLight)
+ }
+}
+
+export function buildDiskChart(metrics: MetricsData | null, isLight: boolean): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.disk
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(s, PALETTE_LIGHT, PALETTE_DARK, isLight)
+ }
+}
+
+export function buildProcessesChart(
+ metrics: MetricsData | null,
+ isLight: boolean
+): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.processes
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(s, [FUCHSIA_600], [FUCHSIA_500], isLight)
+ }
+}
+
+export function buildMemoryChart(metrics: MetricsData | null, isLight: boolean): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.memory
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(s, [INDIGO_600, CYAN_600], [INDIGO_500, CYAN_500], isLight)
+ }
+}
+
+export function buildPacketsChart(metrics: MetricsData | null, isLight: boolean): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.packets
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(s, [CYAN_600, INDIGO_600], [CYAN_500, INDIGO_500], isLight)
+ }
+}
+
+export function buildConnectionsChart(
+ metrics: MetricsData | null,
+ isLight: boolean
+): ChartData | null {
+ if (!metrics) return null
+ const s = metrics.connections
+ return {
+ labels: toMs(s.labels),
+ datasets: buildDatasets(s, [EMERALD_600], [EMERALD_500], isLight)
+ }
+}
+
+export function buildTrafficChart(
+ iface: string,
+ metrics: MetricsData | null,
+ isLight: boolean
+): ChartData | null {
+ if (!metrics?.traffic[iface]) return null
+ const s = metrics.traffic[iface]
+ return { labels: toMs(s.labels), datasets: buildTrafficDatasets(s, isLight) }
+}
+
+export function buildLatencyChart(hostData: PingHost, isLight: boolean): ChartData | null {
+ if (!hostData.latency?.labels?.length) return null
+ return {
+ labels: toMs(hostData.latency.labels),
+ datasets: buildLatencyDatasets(hostData.latency, isLight)
+ }
+}
+
+export function buildQualityChart(hostData: PingHost, isLight: boolean): ChartData | null {
+ if (!hostData.quality?.labels?.length) return null
+ return {
+ labels: toMs(hostData.quality.labels),
+ datasets: buildQualityDatasets(hostData.quality, isLight)
+ }
+}
+
+// ── Utility helpers ───────────────────────────────────────────────────────────
+
+export function getTrafficInterfaces(metrics: MetricsData | null): string[] {
+ if (!metrics) return []
+ return Object.keys(metrics.traffic).sort()
+}
+
+export function getInterfaceDisplayName(iface: string, metrics: MetricsData | null): string {
+ if (!metrics?.traffic[iface]) return iface
+ const data = metrics.traffic[iface]
+ if (data.zone && data.device) {
+ return `${data.zone} (${data.device})`
+ }
+ return iface
+}
+
+export function getLatencyQuality(metrics: MetricsData | null): Record {
+ return metrics?.latency_quality ?? {}
+}
diff --git a/src/router/index.ts b/src/router/index.ts
index d774594ef..6587d01dc 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -196,6 +196,11 @@ const standaloneRoutes = [
name: 'PingLatencyMonitor',
component: () => import('../views/standalone/monitoring/PingLatencyMonitorView.vue')
},
+ {
+ path: 'monitoring/metrics',
+ name: 'MetricsMonitor',
+ component: () => import('../views/standalone/monitoring/MetricsView.vue')
+ },
{
path: 'account',
name: 'Account',
diff --git a/src/stores/standalone/firewall.ts b/src/stores/standalone/firewall.ts
index 0b1745214..218830809 100644
--- a/src/stores/standalone/firewall.ts
+++ b/src/stores/standalone/firewall.ts
@@ -50,7 +50,7 @@ interface ForwardingResponse {
ipset?: string
}
-export type FirewallRuleAction = 'DROP' | 'REJECT' | 'ACCEPT'
+export type FirewallRuleAction = 'DROP' | 'REJECT' | 'ACCEPT' | 'NOTRACK'
export type NatRuleAction = 'SNAT' | 'MASQUERADE' | 'ACCEPT'
export interface FirewallRule {
diff --git a/src/stores/standalone/threatShield.ts b/src/stores/standalone/threatShield.ts
index fe8a9e574..f717cf32c 100644
--- a/src/stores/standalone/threatShield.ts
+++ b/src/stores/standalone/threatShield.ts
@@ -7,7 +7,6 @@ import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
import { getAxiosErrorMessage, sortByProperty } from '@nethesis/vue-components'
import { defineStore } from 'pinia'
import { useUciPendingChangesStore } from './uciPendingChanges'
-import { useNotificationsStore } from '../notifications'
export type Blocklist = {
name: string
@@ -36,7 +35,6 @@ export type DnsAllowedDomain = {
export const useThreatShieldStore = defineStore('threatShield', () => {
const { t, te } = useI18n()
const uciChangesStore = useUciPendingChangesStore()
- const notificationsStore = useNotificationsStore()
const dnsBlocklists = ref([])
const dnsSettings = ref()
const dnsZones = ref([])
@@ -190,17 +188,7 @@ export const useThreatShieldStore = defineStore('threatShield', () => {
try {
await ubusCall('ns.threatshield', method, domain)
-
- if (!isEditing) {
- // applied instantly, show notification (only if creating)
- notificationsStore.createNotification({
- kind: 'success',
- title: t('standalone.threat_shield_dns.blocked_domain_added_title'),
- description: t('standalone.threat_shield_dns.blocked_domain_added_description', {
- domain: domain.address
- })
- })
- }
+ uciChangesStore.getChanges()
} catch (err: any) {
console.error(err)
@@ -223,17 +211,7 @@ export const useThreatShieldStore = defineStore('threatShield', () => {
try {
await ubusCall('ns.threatshield', method, domain)
-
- if (!isEditing) {
- // applied instantly, show notification (only if creating)
- notificationsStore.createNotification({
- kind: 'success',
- title: t('standalone.threat_shield_dns.allowed_domain_added_title'),
- description: t('standalone.threat_shield_dns.allowed_domain_added_description', {
- domain: domain.address
- })
- })
- }
+ uciChangesStore.getChanges()
} catch (err: any) {
console.error(err)
@@ -278,14 +256,7 @@ export const useThreatShieldStore = defineStore('threatShield', () => {
await ubusCall('ns.threatshield', 'dns-delete-blocked', {
address: domain
})
- // applied instantly, show notification
- notificationsStore.createNotification({
- kind: 'success',
- title: t('standalone.threat_shield_dns.blocked_domain_deleted_title'),
- description: t('standalone.threat_shield_dns.blocked_domain_deleted_description', {
- domain
- })
- })
+ uciChangesStore.getChanges()
} catch (err: any) {
console.error(err)
errorDeleteDnsBlockedDomain.value = t(getAxiosErrorMessage(err))
@@ -310,14 +281,7 @@ export const useThreatShieldStore = defineStore('threatShield', () => {
await ubusCall('ns.threatshield', 'dns-delete-allowed', {
address: domain
})
- // applied instantly, show notification
- notificationsStore.createNotification({
- kind: 'success',
- title: t('standalone.threat_shield_dns.allowed_domain_deleted_title'),
- description: t('standalone.threat_shield_dns.allowed_domain_deleted_description', {
- domain
- })
- })
+ uciChangesStore.getChanges()
} catch (err: any) {
console.error(err)
errorDeleteDnsAllowedDomain.value = t(getAxiosErrorMessage(err))
diff --git a/src/views/standalone/StandaloneDashboardView.vue b/src/views/standalone/StandaloneDashboardView.vue
index 231ca388c..2d91d2593 100644
--- a/src/views/standalone/StandaloneDashboardView.vue
+++ b/src/views/standalone/StandaloneDashboardView.vue
@@ -19,6 +19,7 @@ import MacBindingStatusCard from '@/components/standalone/dashboard/MacBindingSt
import BackupStatusCard from '@/components/standalone/dashboard/BackupStatusCard.vue'
import HaStatusCard from '@/components/standalone/dashboard/HaStatusCard.vue'
import WireguardCard from '@/components/standalone/dashboard/WireguardCard.vue'
+import AlertsCard from '@/components/standalone/dashboard/AlertsCard.vue'
const { t } = useI18n()
const route = useRoute()
@@ -49,13 +50,8 @@ function goTo(path: string) {
-
-
+
+
-
+
@@ -123,7 +130,6 @@ function editZone(zone: Zone) {
t('standalone.zones_and_policies.traffic_to_same_zone')
}}
{{ t('standalone.zones_and_policies.interfaces') }}
- {{ t('standalone.zones_and_policies.logging') }}
@@ -140,14 +146,33 @@ function editZone(zone: Zone) {
-
-
-
+
+
+
+
+
+
+
{{ item.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('standalone.zones_and_policies.logging_enabled') }}
+
+
-
{{ item.name }}
@@ -171,24 +196,38 @@ function editZone(zone: Zone) {
-
-
+
ACCEPT
-
+
REJECT
-
+
{{ item.input.toUpperCase() }}
-
+
{{ item.forward.toUpperCase() }}
@@ -198,16 +237,6 @@ function editZone(zone: Zone) {
-
-
-
-
- {{ item.logging ? t('common.enabled') : t('common.disabled') }}
-
-
+
+
+
+
+
+
+
{{ t('standalone.metrics.title') }}
+
+
+
+
+
+
+ {{ pageDescription }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/standalone/monitoring/PingLatencyMonitorView.vue b/src/views/standalone/monitoring/PingLatencyMonitorView.vue
index 00810df3f..0f42325d7 100644
--- a/src/views/standalone/monitoring/PingLatencyMonitorView.vue
+++ b/src/views/standalone/monitoring/PingLatencyMonitorView.vue
@@ -3,10 +3,10 @@
SPDX-License-Identifier: GPL-3.0-or-later
-->
-
@@ -112,54 +78,43 @@ function save() {
{{ t('standalone.ping_latency_monitor.description') }}
-
+
-
- {{ errorConfiguration.notificationDetails }}
-
-
-
-
+ :title="t('error.cannot_retrieve_telegraf_configuration')"
+ :description="t(getAxiosErrorMessage(error))"
+ />
+
+
diff --git a/src/views/standalone/monitoring/RealTimeMonitoringView.vue b/src/views/standalone/monitoring/RealTimeMonitoringView.vue
index 85208ba70..214cbffbe 100644
--- a/src/views/standalone/monitoring/RealTimeMonitoringView.vue
+++ b/src/views/standalone/monitoring/RealTimeMonitoringView.vue
@@ -17,7 +17,6 @@ import TrafficMonitor from '@/components/standalone/monitoring/TrafficMonitor.vu
import ConnectivityMonitor from '@/components/standalone/monitoring/ConnectivityMonitor.vue'
import VpnMonitor from '@/components/standalone/monitoring/VpnMonitor.vue'
import SecurityMonitor from '@/components/standalone/monitoring/SecurityMonitor.vue'
-import { isStandaloneMode } from '@/lib/config'
import { onMounted, ref } from 'vue'
import { ubusCall } from '@/lib/standalone/ubus'
import InstantTrafficMonitor from '@/components/standalone/monitoring/InstantTrafficMonitor.vue'
@@ -91,10 +90,6 @@ async function getControllerSettings() {
}
}
-function openNetdata() {
- window.open('http://' + window.location.hostname + ':19999')
-}
-
function openGrafana() {
window.open(controllerUrl.value + '/grafana')
}
@@ -106,27 +101,6 @@ function openGrafana() {
{{ t('standalone.real_time_monitor.title') }}
-
-
-
-
-
- {{ t('standalone.real_time_monitor.open_report') }}
-
-
-
-
-
-
-
-
- {{ t('standalone.real_time_monitor.open_report') }}
-
-
-
- {{ t('standalone.real_time_monitor.cannot_open_report_from_controller') }}
-
-
{
-
-
-
-
- {{ t('standalone.update.automatic_updates_tooltip') }}
-
- {
- router.push(`${getStandaloneRoutePrefix()}/system/subscription`)
- }
- "
- >{{ t('standalone.update.go_to_subscription') }}
-
-
-
-
+ />
diff --git a/src/views/standalone/users_objects/UsersDatabaseView.vue b/src/views/standalone/users_objects/UsersDatabaseView.vue
index dd4547543..def523519 100644
--- a/src/views/standalone/users_objects/UsersDatabaseView.vue
+++ b/src/views/standalone/users_objects/UsersDatabaseView.vue
@@ -7,8 +7,7 @@ import {
NeSkeleton,
NeInlineNotification,
getAxiosErrorMessage,
- NeTabs,
- NeTooltip
+ NeTabs
} from '@nethesis/vue-components'
import { onMounted } from 'vue'
import { ref } from 'vue'
@@ -31,10 +30,7 @@ const { t } = useI18n()
const uciChangesStore = useUciPendingChangesStore()
const notificationsStore = useNotificationsStore()
-const loading = ref({
- listDatabases: true,
- getSubscriptionInfo: true
-})
+const loadingDatabases = ref(true)
const databases = ref([])
const error = ref({
listDatabases: '',
@@ -43,13 +39,12 @@ const error = ref({
getSubscriptionInfoDetails: ''
})
const showCreateDrawer = ref(false)
-const activeSubscription = ref(false)
const { tabs, selectedTab } = useTabs([])
async function fetchDatabases(resetSelectedTab: boolean = false) {
try {
- loading.value.listDatabases = true
+ loadingDatabases.value = true
if (resetSelectedTab) {
selectedTab.value = ''
}
@@ -65,7 +60,7 @@ async function fetchDatabases(resetSelectedTab: boolean = false) {
error.value.listDatabases = t(getAxiosErrorMessage(err))
error.value.listDatabasesDetails = err.toString()
} finally {
- loading.value.listDatabases = false
+ loadingDatabases.value = false
}
}
@@ -76,30 +71,14 @@ async function reloadDatabases(resetSelectedTab: boolean = false) {
onMounted(() => {
fetchDatabases()
- fetchSubscriptionInfo()
})
-
-async function fetchSubscriptionInfo() {
- loading.value.getSubscriptionInfo = true
-
- try {
- const res = await ubusCall('ns.subscription', 'info')
-
- activeSubscription.value = (res.data?.systemd_id && res.data?.active) || false
- } catch (err: any) {
- error.value.getSubscriptionInfo = t(getAxiosErrorMessage(err))
- error.value.getSubscriptionInfoDetails = err.toString()
- } finally {
- loading.value.getSubscriptionInfo = false
- }
-}
{{ t('standalone.users_database.title') }}
-
-
+
{{ t('standalone.users_database.add_remote_database') }}
-
-
-
-
-
-
-
- {{ t('standalone.users_database.add_remote_database') }}
-
-
-
- {{ t('standalone.users_database.add_remote_database_tooltip') }}
-
-
-
+