diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json
index 1f319255d..12f7d596a 100644
--- a/frontend/src/i18n/en/translation.json
+++ b/frontend/src/i18n/en/translation.json
@@ -783,45 +783,40 @@
},
"backups": {
"title": "Backups",
- "page_description": "Configuration snapshots uploaded by this system. Each file is end-to-end encrypted on the appliance — only the system that created it can decrypt its contents.",
- "uploaded_at": "Uploaded at",
+ "page_description": "Configuration snapshots uploaded by this system, end-to-end encrypted so only the system that created them can decrypt the contents. At most 10 backups are retained, with up to 500 MB total storage; older entries are automatically deleted when these limits are reached.",
+ "date": "Date",
"filename": "Filename",
"size": "Size",
- "sha256": "SHA-256",
"actions": "Actions",
"download": "Download",
"delete": "Delete",
"no_backups": "No backups yet",
"no_backups_description": "This system has not uploaded any configuration backup. Backups are created automatically by the appliance on a daily schedule.",
"cannot_retrieve_backups": "Cannot retrieve backups",
- "retention_policy": "Retention policy",
- "retention_policy_description": "The ingest side keeps at most {slots} backups per system, up to {size} of total storage. Beyond any of those caps the oldest entry is pruned automatically at upload time.",
"storage_usage": "Storage used",
- "slots_usage": "Backups kept",
- "slots_used_of_max": "{used} of {max}",
"delete_backup": "Delete backup",
- "delete_backup_confirmation": "Are you sure you want to delete {filename}, uploaded on {date}? This action cannot be undone.",
- "refresh": "Refresh",
+ "delete_backup_confirmation": "Backup snapshot {filename} uploaded on {date} will be deleted. This action cannot be undone.",
+ "reload_backups": "Reload backups",
"cannot_delete_backup": "Cannot delete backup",
"backup_deleted": "Backup deleted",
- "backup_deleted_description": "{filename} has been removed.",
+ "backup_deleted_description": "Backup snapshot {filename} has been deleted.",
"cannot_download_backup": "Cannot download backup"
},
- "alerting": {
+ "alerts": {
"title": "Alerts",
- "config_title": "Alerting Configuration",
+ "config_title": "Alerts Configuration",
"alpha_notice": "ALPHA \u2013 This section has no final design. UI may change.",
"cannot_retrieve_alerts": "Cannot retrieve alerts",
"no_alerts": "No alerts found",
"no_alerts_description": "There are currently no active alerts for this company",
- "no_alerts_found": "No alerts match the current filters",
+ "no_alerts_found": "No alerts found",
"active_alerts": "Active Alerts",
"alert_history": "Alert History",
- "alertname": "Alert name",
+ "alertname": "Alert",
"severity": "Severity",
"state": "State",
"system_key": "System key",
- "starts_at": "Started at",
+ "started": "Started",
"ends_at": "Ended at",
"summary": "Summary",
"description": "Description",
@@ -835,15 +830,21 @@
"state_active": "Active",
"state_suppressed": "Suppressed",
"state_unprocessed": "Unprocessed",
- "severity_critical": "Critical",
- "severity_warning": "Warning",
- "severity_info": "Info",
+ "severity_high": "High",
+ "severity_medium": "Medium",
+ "severity_low": "Low",
+ "severity_critical": "High",
+ "severity_warning": "Medium",
+ "severity_info": "Low",
+ "high_severity": "High severity",
+ "medium_severity": "Medium severity",
+ "low_severity": "Low severity",
"cannot_retrieve_alert_history": "Cannot retrieve alert history",
"no_alert_history": "No alert history",
"no_alert_history_description": "No resolved alerts found for this system",
"no_alert_history_found": "No alert history matches the current filters",
- "config_page_description": "View and manage the alerting configuration for your company",
- "cannot_retrieve_config": "Cannot retrieve alerting configuration",
+ "config_page_description": "View and manage the alerts configuration for your company",
+ "cannot_retrieve_config": "Cannot retrieve alerts configuration",
"config_json": "Configuration (JSON)",
"config_yaml": "Configuration (YAML)",
"mail_enabled": "Email notifications",
@@ -856,16 +857,16 @@
"per_severity_overrides": "Per-severity overrides",
"per_system_overrides": "Per-system overrides",
"no_config": "No configuration found",
- "no_config_description": "Alerting is not configured for this company",
+ "no_config_description": "Alerts is not configured for this company",
"edit_config": "Edit configuration",
"save_config": "Save configuration",
"disable_alerts": "Disable all alerts",
"disable_alerts_confirmation": "Are you sure you want to disable all alerts? This will send a blackhole configuration to Alertmanager.",
"config_saved": "Configuration saved",
- "config_saved_description": "Alerting configuration has been updated successfully",
+ "config_saved_description": "Alerts configuration has been updated successfully",
"alerts_disabled": "Alerts disabled",
"alerts_disabled_description": "All alerts have been disabled successfully",
- "cannot_save_config": "Cannot save alerting configuration",
+ "cannot_save_config": "Cannot save alerts configuration",
"cannot_disable_alerts": "Cannot disable alerts",
"raw_yaml": "Raw YAML",
"structured": "Structured",
@@ -878,7 +879,7 @@
"active_alerts_count": "{num} active alert | {num} active alerts",
"silenced_alerts_count": "{num} silenced | {num} silenced",
"no_active_alerts": "No active alerts",
- "no_active_alerts_description": "This system has no active alerts",
+ "no_active_alerts_description": "All systems are operational",
"silences": "Silences",
"silences_count": "{num} silence | {num} silences",
"silence_alert": "Silence alert",
@@ -909,10 +910,39 @@
"history_tab_description": "History of resolved alerts for this system",
"cannot_retrieve_system_alerts": "Cannot retrieve active alerts for this system",
"cannot_retrieve_organizations": "Cannot retrieve companies",
- "alerting_title": "Alerting",
- "alerting_page_description": "View active alerts and manage alerting configuration for any company",
- "view_alert_details": "View alert details",
+ "alerts_title": "Alerts",
+ "alerts_page_description": "View active alerts and manage alerts configuration for any company",
+ "view_details": "View details",
"select_organization": "Company",
- "no_organizations_description": "There are no companies available for alerting"
+ "no_organizations_description": "There are no companies available for alerts",
+ "active_alerts_tab": "Active alerts",
+ "notifications_tab": "Notifications",
+ "total_alerts": "Total alerts",
+ "muted": "Muted",
+ "system": "System",
+ "organization": "Company",
+ "reload_alerts": "Reload alerts",
+ "create_silence": "Create silence",
+ "status_active": "Active",
+ "status_suppressed": "Suppressed",
+ "status_resolved": "Resolved",
+ "filter_severity": "Severity",
+ "filter_alert": "Alert",
+ "filter_system": "System",
+ "filter_organization": "Company",
+ "mute_alert": "Mute alert",
+ "mute_alert_title": "Mute {alertname} alert",
+ "mute_until_date": "Mute the alert until this date",
+ "mute_notes": "Notes",
+ "mute_notes_placeholder": "Add a note for this alarm",
+ "mute_until_date_required": "Please choose a date and time",
+ "mute_until_date_future": "The date must be in the future",
+ "alert_muted": "Alert muted",
+ "alert_muted_description": "A silence has been created for {name}.",
+ "cannot_mute_alert": "Cannot mute alert"
+ },
+ "ne_tabs": {
+ "tabs": "Tabs",
+ "select_a_tab": "Select a tab"
}
}
diff --git a/frontend/src/i18n/it/translation.json b/frontend/src/i18n/it/translation.json
index cc305ce90..c3d48e980 100644
--- a/frontend/src/i18n/it/translation.json
+++ b/frontend/src/i18n/it/translation.json
@@ -644,113 +644,5 @@
"backup_deleted": "Backup eliminato",
"backup_deleted_description": "{filename} è stato rimosso.",
"cannot_download_backup": "Impossibile scaricare il backup"
- },
- "alerting": {
- "title": "Avvisi",
- "config_title": "Configurazione avvisi",
- "alpha_notice": "ALPHA – Questa sezione non ha un design definitivo. L'interfaccia potrebbe cambiare.",
- "cannot_retrieve_alerts": "Impossibile recuperare gli avvisi",
- "no_alerts": "Nessun avviso trovato",
- "no_alerts_description": "Non ci sono avvisi attivi per questa organizzazione",
- "no_alerts_found": "Nessun avviso corrisponde ai filtri attuali",
- "active_alerts": "Avvisi attivi",
- "alert_history": "Storico avvisi",
- "alertname": "Nome avviso",
- "severity": "Gravità",
- "state": "Stato",
- "system_key": "Chiave sistema",
- "starts_at": "Iniziato il",
- "ends_at": "Terminato il",
- "summary": "Sommario",
- "description": "Descrizione",
- "labels": "Etichette",
- "annotations": "Annotazioni",
- "fingerprint": "Impronta",
- "receiver": "Destinatario",
- "filter_by_state": "Filtra per stato",
- "filter_by_severity": "Filtra per gravità",
- "filter_by_system_key": "Filtra per chiave sistema",
- "state_active": "Attivo",
- "state_suppressed": "Soppresso",
- "state_unprocessed": "Non elaborato",
- "severity_critical": "Critico",
- "severity_warning": "Avviso",
- "severity_info": "Info",
- "cannot_retrieve_alert_history": "Impossibile recuperare lo storico avvisi",
- "no_alert_history": "Nessuno storico avvisi",
- "no_alert_history_description": "Nessun avviso risolto trovato per questo sistema",
- "no_alert_history_found": "Nessuno storico corrisponde ai filtri attuali",
- "config_page_description": "Visualizza e gestisci la configurazione degli avvisi per la tua organizzazione",
- "cannot_retrieve_config": "Impossibile recuperare la configurazione degli avvisi",
- "config_json": "Configurazione (JSON)",
- "config_yaml": "Configurazione (YAML)",
- "mail_enabled": "Notifiche email",
- "webhook_enabled": "Notifiche webhook",
- "telegram_enabled": "Notifiche Telegram",
- "mail_addresses": "Indirizzi email",
- "webhook_receivers": "Destinatari webhook",
- "telegram_receivers": "Destinatari Telegram",
- "email_template_lang": "Lingua template (email e Telegram)",
- "per_severity_overrides": "Override per gravità",
- "per_system_overrides": "Override per sistema",
- "no_config": "Nessuna configurazione trovata",
- "no_config_description": "Gli avvisi non sono configurati per questa organizzazione",
- "edit_config": "Modifica configurazione",
- "save_config": "Salva configurazione",
- "disable_alerts": "Disabilita tutti gli avvisi",
- "disable_alerts_confirmation": "Sei sicuro di voler disabilitare tutti gli avvisi? Verrà inviata una configurazione blackhole ad Alertmanager.",
- "config_saved": "Configurazione salvata",
- "config_saved_description": "La configurazione degli avvisi è stata aggiornata con successo",
- "alerts_disabled": "Avvisi disabilitati",
- "alerts_disabled_description": "Tutti gli avvisi sono stati disabilitati con successo",
- "cannot_save_config": "Impossibile salvare la configurazione degli avvisi",
- "cannot_disable_alerts": "Impossibile disabilitare gli avvisi",
- "raw_yaml": "YAML grezzo",
- "structured": "Strutturato",
- "view_mode": "Modalità visualizzazione",
- "copy_yaml": "Copia YAML",
- "yaml_copied": "YAML copiato!",
- "config_editor": "Editor configurazione",
- "paste_json_config": "Incolla configurazione JSON",
- "active_alerts_card_title": "Avvisi attivi",
- "active_alerts_count": "{num} avviso attivo | {num} avvisi attivi",
- "silenced_alerts_count": "{num} silenziato | {num} silenziati",
- "no_active_alerts": "Nessun avviso attivo",
- "no_active_alerts_description": "Questo sistema non ha avvisi attivi",
- "silences": "Silenziamenti",
- "silences_count": "{num} silenziamento | {num} silenziamenti",
- "silence_alert": "Silenzia avviso",
- "silence_alert_confirmation": "Creare una silenziazione per l'avviso attivo {name}?",
- "silence_end_at": "Data e ora di fine",
- "silence_end_at_helper": "Il silenziamento scadrà a questa data e ora.",
- "silence_comment": "Commento",
- "silence_comment_helper": "Aggiungi una nota facoltativa per la silenziazione.",
- "cannot_silence_alert": "Impossibile silenziare l'avviso",
- "alert_silenced": "Avviso silenziato",
- "alert_silenced_description": "E' stata creata una silenziazione per {name}.",
- "silences_card_title": "Silenziamenti",
- "no_silences": "Nessun silenziamento",
- "no_silences_description": "Nessun silenziamento attivo o in attesa per questo sistema",
- "silence_created_by": "Creato da",
- "silence_status_active": "Attivo",
- "silence_status_pending": "In attesa",
- "cannot_retrieve_silences": "Impossibile recuperare i silenziamenti",
- "silence_deleted": "Silenziamento eliminato",
- "silence_deleted_description": "Il silenziamento è stato eliminato con successo.",
- "cannot_delete_silence": "Impossibile eliminare il silenziamento",
- "disable_silence": "Disabilita silenziamento",
- "disable_silence_confirmation": "Disabilitare il silenziamento per l'avviso {name}?",
- "disable_silence_notice": "Questa azione rimuove {count} che stanno sopprimendo questo avviso.",
- "cannot_disable_silence": "Impossibile disabilitare il silenziamento",
- "silence_disabled": "Silenziamento disabilitato",
- "silence_disabled_description": "Il silenziamento è stato disabilitato per {name}.",
- "history_tab_description": "Storico degli avvisi risolti per questo sistema",
- "cannot_retrieve_system_alerts": "Impossibile recuperare gli avvisi attivi per questo sistema",
- "cannot_retrieve_organizations": "Impossibile recuperare le organizzazioni",
- "alerting_title": "Alerting",
- "alerting_page_description": "Visualizza gli avvisi attivi e gestisci la configurazione degli avvisi per qualsiasi organizzazione",
- "view_alert_details": "Visualizza i dettagli dell'avviso",
- "select_organization": "Organizzazione",
- "no_organizations_description": "Non ci sono organizzazioni disponibili per l'alerting"
}
}
diff --git a/frontend/src/lib/alerting.ts b/frontend/src/lib/alerting.ts
deleted file mode 100644
index fc5c52277..000000000
--- a/frontend/src/lib/alerting.ts
+++ /dev/null
@@ -1,329 +0,0 @@
-// Copyright (C) 2026 Nethesis S.r.l.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import axios from 'axios'
-import { API_URL } from './config'
-import { useLoginStore } from '@/stores/login'
-import { type Pagination } from './common'
-
-export const ALERTING_CONFIG_KEY = 'alertingConfig'
-export const ALERTING_ALERTS_KEY = 'alertingAlerts'
-export const ALERT_HISTORY_KEY = 'alertHistory'
-export const ALERT_HISTORY_TABLE_ID = 'alertHistoryTable'
-export const SYSTEM_ALERT_SILENCES_KEY = 'systemAlertSilences'
-
-// ── Types ─────────────────────────────────────────────────────────────────────
-
-export interface WebhookReceiver {
- name: string
- url: string
-}
-
-export interface TelegramReceiver {
- bot_token: string
- chat_id: number
-}
-
-export interface SeverityOverride {
- severity: 'critical' | 'warning' | 'info'
- mail_enabled?: boolean
- webhook_enabled?: boolean
- telegram_enabled?: boolean
- mail_addresses?: string[]
- webhook_receivers?: WebhookReceiver[]
- telegram_receivers?: TelegramReceiver[]
-}
-
-export interface SystemOverride {
- system_key: string
- mail_enabled?: boolean
- webhook_enabled?: boolean
- telegram_enabled?: boolean
- mail_addresses?: string[]
- webhook_receivers?: WebhookReceiver[]
- telegram_receivers?: TelegramReceiver[]
-}
-
-export interface AlertingConfig {
- mail_enabled: boolean
- webhook_enabled: boolean
- telegram_enabled: boolean
- mail_addresses: string[]
- webhook_receivers: WebhookReceiver[]
- telegram_receivers: TelegramReceiver[]
- severities?: SeverityOverride[]
- systems?: SystemOverride[]
- email_template_lang?: string
-}
-
-export interface AlertStatus {
- state: string
- silencedBy: string[]
- inhibitedBy: string[]
-}
-
-export interface Alert {
- labels: Record
- annotations: Record
- status: AlertStatus
- startsAt: string
- endsAt: string
- fingerprint: string
- generatorURL?: string
- receivers?: { name: string }[]
-}
-
-export interface AlertmanagerSilenceStatus {
- state: 'active' | 'expired' | 'pending'
-}
-
-export interface AlertmanagerMatcher {
- name: string
- value: string
- isRegex: boolean
-}
-
-export interface AlertmanagerSilence {
- id: string
- matchers: AlertmanagerMatcher[]
- startsAt: string
- endsAt: string
- updatedAt: string
- createdBy: string
- comment: string
- status: AlertmanagerSilenceStatus
-}
-
-export interface AlertHistoryRecord {
- id: number
- system_key: string
- alertname: string
- severity: string | null
- status: string
- fingerprint: string
- starts_at: string
- ends_at: string | null
- summary: string | null
- labels: Record
- annotations: Record
- receiver: string | null
- created_at: string
-}
-
-// ── API functions ─────────────────────────────────────────────────────────────
-
-interface AlertingConfigResponse {
- code: number
- message: string
- data: {
- config: AlertingConfig | string
- }
-}
-
-interface AlertsResponse {
- code: number
- message: string
- data: {
- alerts: Alert[]
- }
-}
-
-interface AlertHistoryResponse {
- code: number
- message: string
- data: {
- alerts: AlertHistoryRecord[]
- pagination: Pagination
- }
-}
-
-interface CreateSystemAlertSilenceResponse {
- code: number
- message: string
- data: {
- silence_id: string
- }
-}
-
-interface SystemAlertSilencesResponse {
- code: number
- message: string
- data: {
- silences: AlertmanagerSilence[]
- }
-}
-
-export const getAlertingConfig = (organizationId: string, format?: 'yaml') => {
- const loginStore = useLoginStore()
- const params = new URLSearchParams({ organization_id: organizationId })
- if (format) {
- params.append('format', format)
- }
-
- return axios
- .get(`${API_URL}/alerts/config?${params}`, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
- .then((res) => res.data.data.config)
-}
-
-export const postAlertingConfig = (organizationId: string, config: AlertingConfig) => {
- const loginStore = useLoginStore()
-
- return axios.post(`${API_URL}/alerts/config?organization_id=${organizationId}`, config, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
-}
-
-export const deleteAlertingConfig = (organizationId: string) => {
- const loginStore = useLoginStore()
-
- return axios.delete(`${API_URL}/alerts/config?organization_id=${organizationId}`, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
-}
-
-export const getAlerts = (
- organizationId: string,
- state?: string,
- severity?: string,
- systemKey?: string,
-) => {
- const loginStore = useLoginStore()
- const params = new URLSearchParams({ organization_id: organizationId })
- if (state) params.append('state', state)
- if (severity) params.append('severity', severity)
- if (systemKey) params.append('system_key', systemKey)
-
- return axios
- .get(`${API_URL}/alerts?${params}`, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
- .then((res) => res.data.data.alerts)
-}
-
-export const getSystemAlertHistory = (
- systemId: string,
- page: number,
- pageSize: number,
- sortBy: string,
- sortDescending: boolean,
-) => {
- const loginStore = useLoginStore()
- const params = new URLSearchParams({
- page: page.toString(),
- page_size: pageSize.toString(),
- sort_by: sortBy,
- sort_direction: sortDescending ? 'desc' : 'asc',
- })
-
- return axios
- .get(`${API_URL}/systems/${systemId}/alerts/history?${params}`, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
- .then((res) => res.data.data)
-}
-
-// Get active alerts for a specific system via the dedicated endpoint
-export const getSystemActiveAlerts = (systemId: string) => {
- const loginStore = useLoginStore()
- return axios
- .get(`${API_URL}/systems/${systemId}/alerts`, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
- .then((res) => res.data.data.alerts)
-}
-
-export const getSystemAlertSilences = (systemId: string) => {
- const loginStore = useLoginStore()
- return axios
- .get(`${API_URL}/systems/${systemId}/alerts/silences`, {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- })
- .then((res) => res.data.data.silences)
-}
-
-export const createSystemAlertSilence = (
- systemId: string,
- fingerprint: string,
- comment?: string,
- endAt?: string,
-) => {
- const loginStore = useLoginStore()
- const payload: Record = { fingerprint }
- if (comment?.trim()) {
- payload.comment = comment.trim()
- }
- if (endAt) {
- payload.end_at = endAt
- }
-
- return axios
- .post(
- `${API_URL}/systems/${systemId}/alerts/silences`,
- payload,
- {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- },
- )
- .then((res) => res.data.data)
-}
-
-export const deleteSystemAlertSilence = (systemId: string, silenceId: string) => {
- const loginStore = useLoginStore()
-
- return axios.delete(
- `${API_URL}/systems/${systemId}/alerts/silences/${encodeURIComponent(silenceId)}`,
- {
- headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
- },
- )
-}
-
-type AlertAnnotationKey = 'summary' | 'description'
-type AlertWithAnnotations = {
- annotations?: Record
-}
-
-const DEFAULT_ALERT_LOCALE = 'en'
-
-function getAlertAnnotation(
- alert: AlertWithAnnotations,
- annotationKey: AlertAnnotationKey,
- locale: string,
-) {
- const annotations = alert.annotations ?? {}
- const normalizedLocale = locale.split('-')[0].toLowerCase() || DEFAULT_ALERT_LOCALE
- const candidateKeys = Array.from(
- new Set([
- `${annotationKey}_${normalizedLocale}`,
- annotationKey,
- `${annotationKey}_${DEFAULT_ALERT_LOCALE}`,
- ]),
- )
-
- for (const key of candidateKeys) {
- const value = annotations[key]
- if (typeof value === 'string' && value.trim()) {
- return value.trim()
- }
- }
-
- return ''
-}
-
-export const getAlertSummary = (alert: AlertWithAnnotations, locale: string) => {
- return getAlertAnnotation(alert, 'summary', locale)
-}
-
-export const getAlertDescription = (alert: AlertWithAnnotations, locale: string) => {
- return getAlertAnnotation(alert, 'description', locale)
-}
-
-export const getAlertSilenceIds = (alert: Alert) => {
- return Array.from(new Set((alert.status?.silencedBy || []).filter((silenceId) => !!silenceId)))
-}
-
-export const isAlertSilenced = (alert: Alert) => {
- return getAlertSilenceIds(alert).length > 0
-}
diff --git a/frontend/src/lib/alerting.test.ts b/frontend/src/lib/alerts.test.ts
similarity index 99%
rename from frontend/src/lib/alerting.test.ts
rename to frontend/src/lib/alerts.test.ts
index d971bc99d..817b0f233 100644
--- a/frontend/src/lib/alerting.test.ts
+++ b/frontend/src/lib/alerts.test.ts
@@ -8,7 +8,7 @@ import {
getAlertSummary,
isAlertSilenced,
type Alert,
-} from './alerting'
+} from './alerts'
const baseAlert: Alert = {
labels: {},
diff --git a/frontend/src/lib/alerts.ts b/frontend/src/lib/alerts.ts
new file mode 100644
index 000000000..0a4c52553
--- /dev/null
+++ b/frontend/src/lib/alerts.ts
@@ -0,0 +1,735 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import axios from 'axios'
+import { API_URL } from './config'
+import { useLoginStore } from '@/stores/login'
+import { type Pagination } from './common'
+
+export const ALERTS_CONFIG_KEY = 'alertsConfig'
+export const ALERTS_ALERTS_KEY = 'alertsAlerts'
+export const ALERTS_TOTALS_KEY = 'alertsTotals'
+export const ALERTS_TREND_KEY = 'alertsTrend'
+export const ALERTS_STATS_KEY = 'alertsStats'
+export const ALERTS_HISTORY_KEY = 'alertsHistory'
+export const ALERT_HISTORY_KEY = 'alertHistory'
+export const ALERT_ACTIVITY_KEY = 'alertActivity'
+export const ALERTS_SILENCES_KEY = 'alertsSilences'
+export const ALERT_SILENCES_KEY = 'alertSilences'
+export const ALERT_HISTORY_TABLE_ID = 'alertHistoryTable'
+export const SYSTEM_ALERT_SILENCES_KEY = 'systemAlertSilences'
+
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+export interface ChannelToggles {
+ email?: boolean | null
+ webhook?: boolean | null
+ telegram?: boolean | null
+}
+
+export interface EmailRecipient {
+ address: string
+ severities?: string[]
+ language?: 'en' | 'it'
+ format?: 'html' | 'plain'
+}
+
+export interface WebhookRecipient {
+ name: string
+ url: string
+ severities?: string[]
+}
+
+export interface TelegramRecipient {
+ bot_token: string
+ chat_id: number
+ severities?: string[]
+}
+
+export interface AlertingConfigLayer {
+ enabled: ChannelToggles
+ email_recipients: EmailRecipient[]
+ webhook_recipients: WebhookRecipient[]
+ telegram_recipients: TelegramRecipient[]
+}
+
+export type AlertState = 'active' | 'suppressed' | 'unprocessed'
+
+export interface AlertStatus {
+ state: AlertState
+ silencedBy: string[]
+ inhibitedBy: string[]
+}
+
+export interface ActiveAlert {
+ fingerprint: string
+ labels: Record
+ annotations: Record
+ status: AlertStatus
+ startsAt: string
+ endsAt: string
+ generatorURL?: string
+}
+
+export type Alert = ActiveAlert
+
+export interface AlertmanagerSilenceStatus {
+ state: 'active' | 'expired' | 'pending'
+}
+
+export interface AlertmanagerMatcher {
+ name: string
+ value: string
+ isRegex: boolean
+}
+
+export interface AlertmanagerSilence {
+ id: string
+ matchers: AlertmanagerMatcher[]
+ startsAt: string
+ endsAt: string
+ updatedAt: string
+ createdBy: string
+ comment: string
+ status: AlertmanagerSilenceStatus
+}
+
+export interface AlertHistoryRecord {
+ id: number
+ system_key: string
+ alertname: string
+ severity: string | null
+ status: string
+ fingerprint: string
+ starts_at: string
+ ends_at: string | null
+ summary: string | null
+ labels: Record
+ annotations: Record
+ receiver: string | null
+ created_at: string
+}
+
+// ── API functions ─────────────────────────────────────────────────────────────
+
+interface AlertsConfigResponse {
+ code: number
+ message: string
+ data: AlertingConfigLayer & {
+ updated_by_name?: string | null
+ updated_at?: string | null
+ }
+}
+
+interface AlertsResponse {
+ code: number
+ message: string
+ data: {
+ alerts: Alert[]
+ pagination?: Pagination
+ warnings?: string[]
+ }
+}
+
+interface AlertsTotalsResponse {
+ code: number
+ message: string
+ data: {
+ active: number
+ critical: number
+ warning: number
+ info: number
+ muted: number
+ history: number
+ warnings?: string[]
+ }
+}
+
+interface AlertNameCount {
+ alertname: string
+ count: number
+}
+
+interface SystemKeyCount {
+ system_key: string
+ count: number
+}
+
+interface AlertStats {
+ total: number
+ by_severity: Record
+ top_alertnames: AlertNameCount[]
+ top_systems: SystemKeyCount[]
+ mttr_seconds?: number
+ mtbf_seconds?: number
+}
+
+interface AlertsStatsResponse {
+ code: number
+ message: string
+ data: AlertStats
+}
+
+interface TrendDataPoint {
+ date: string
+ count: number
+}
+
+interface TrendResponse {
+ period: number
+ period_label: string
+ current_total: number
+ previous_total: number
+ delta: number
+ delta_percentage: number
+ trend: 'up' | 'down' | 'stable'
+ data_points: TrendDataPoint[]
+}
+
+interface AlertsTrendResponse {
+ code: number
+ message: string
+ data: TrendResponse
+}
+
+interface AlertHistoryResponse {
+ code: number
+ message: string
+ data: {
+ alerts: AlertHistoryRecord[]
+ pagination: Pagination
+ }
+}
+
+interface AlertActivityEntry {
+ id: number
+ organization_id: string
+ fingerprint: string
+ action: 'silenced' | 'silence_updated' | 'unsilenced'
+ actor_user_id?: string
+ actor_name?: string
+ silence_id?: string
+ details: Record
+ created_at: string
+}
+
+interface AlertActivityResponse {
+ code: number
+ message: string
+ data: {
+ events: AlertActivityEntry[]
+ }
+}
+
+interface CreateSystemAlertSilenceResponse {
+ code: number
+ message: string
+ data: {
+ silence_id: string
+ }
+}
+
+interface SystemAlertSilencesResponse {
+ code: number
+ message: string
+ data: {
+ silences: AlertmanagerSilence[]
+ }
+}
+
+export const getAlertsConfig = (format?: 'yaml') => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+ if (format) {
+ params.append('format', format)
+ }
+
+ return axios
+ .get(`${API_URL}/alerts/config?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getAlertsTotals = (organizationIds?: string | string[], include?: 'descendants') => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ if (organizationIds) {
+ const orgIds = Array.isArray(organizationIds) ? organizationIds : [organizationIds]
+ orgIds.forEach((id) => params.append('organization_id', id))
+ }
+
+ if (include === 'descendants') {
+ params.append('include', 'descendants')
+ }
+
+ return axios
+ .get(`${API_URL}/alerts/totals?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getAlertsTrend = (
+ organizationIds?: string | string[],
+ include?: 'descendants',
+ period: 7 | 30 | 180 | 365 = 7,
+) => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ if (organizationIds) {
+ const orgIds = Array.isArray(organizationIds) ? organizationIds : [organizationIds]
+ orgIds.forEach((id) => params.append('organization_id', id))
+ }
+
+ if (include === 'descendants') {
+ params.append('include', 'descendants')
+ }
+
+ params.append('period', period.toString())
+
+ return axios
+ .get(`${API_URL}/alerts/trend?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getAlertsStats = (organizationIds?: string | string[], include?: 'descendants') => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ if (organizationIds) {
+ const orgIds = Array.isArray(organizationIds) ? organizationIds : [organizationIds]
+ orgIds.forEach((id) => params.append('organization_id', id))
+ }
+
+ if (include === 'descendants') {
+ params.append('include', 'descendants')
+ }
+
+ return axios
+ .get(`${API_URL}/alerts/stats?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getAlertsHistory = (
+ organizationIds?: string | string[],
+ page: number = 1,
+ pageSize: number = 50,
+ sortBy: string = 'starts_at',
+ sortDirection: 'asc' | 'desc' = 'desc',
+ include?: 'descendants',
+) => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ if (organizationIds) {
+ const orgIds = Array.isArray(organizationIds) ? organizationIds : [organizationIds]
+ orgIds.forEach((id) => params.append('organization_id', id))
+ }
+
+ if (include === 'descendants') {
+ params.append('include', 'descendants')
+ }
+
+ params.append('page', page.toString())
+ params.append('page_size', Math.min(pageSize, 100).toString())
+ params.append('sort_by', sortBy)
+ params.append('sort_direction', sortDirection)
+
+ return axios
+ .get(`${API_URL}/alerts/history?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getAlertActivity = (fingerprint: string, page: number = 1, pageSize: number = 50) => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ params.append('page', page.toString())
+ params.append('page_size', Math.min(pageSize, 100).toString())
+
+ return axios
+ .get(
+ `${API_URL}/alerts/activity/${encodeURIComponent(fingerprint)}?${params}`,
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+ .then((res) => res.data.data)
+}
+
+export const getAlertsSilences = (
+ organizationIds?: string | string[],
+ page: number = 1,
+ pageSize: number = 50,
+ include?: 'descendants',
+) => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ if (organizationIds) {
+ const orgIds = Array.isArray(organizationIds) ? organizationIds : [organizationIds]
+ orgIds.forEach((id) => params.append('organization_id', id))
+ }
+
+ if (include === 'descendants') {
+ params.append('include', 'descendants')
+ }
+
+ params.append('page', page.toString())
+ params.append('page_size', Math.min(pageSize, 100).toString())
+
+ return axios
+ .get(`${API_URL}/alerts/silences?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const createAlertSilence = (
+ fingerprint: string,
+ organizationId?: string,
+ systemId?: string,
+ comment?: string,
+ endAt?: string,
+) => {
+ const loginStore = useLoginStore()
+ const payload: Record = { fingerprint }
+
+ if (comment?.trim()) {
+ payload.comment = comment.trim()
+ }
+ if (endAt) {
+ payload.end_at = endAt
+ }
+ if (organizationId) {
+ payload.organization_id = organizationId
+ }
+ if (systemId) {
+ payload.system_id = systemId
+ }
+
+ return axios
+ .post(`${API_URL}/alerts/silences`, payload, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getAlertSilence = (silenceId: string) => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get<{
+ code: number
+ message: string
+ data: { silence: AlertmanagerSilence }
+ }>(`${API_URL}/alerts/silences/${encodeURIComponent(silenceId)}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data.silence)
+}
+
+export const updateAlertSilence = (silenceId: string, comment?: string, endAt?: string) => {
+ const loginStore = useLoginStore()
+ const payload: Record = {}
+
+ if (comment !== undefined) {
+ payload.comment = comment
+ }
+ if (endAt) {
+ payload.end_at = endAt
+ }
+
+ return axios.put(`${API_URL}/alerts/silences/${encodeURIComponent(silenceId)}`, payload, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+}
+
+export const deleteAlertSilence = (silenceId: string) => {
+ const loginStore = useLoginStore()
+
+ return axios.delete(`${API_URL}/alerts/silences/${encodeURIComponent(silenceId)}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+}
+
+export const postAlertsConfig = (config: AlertingConfigLayer) => {
+ const loginStore = useLoginStore()
+
+ return axios.post(`${API_URL}/alerts/config`, config, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+}
+
+export const deleteAlertsConfig = () => {
+ const loginStore = useLoginStore()
+
+ return axios.delete(`${API_URL}/alerts/config`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+}
+
+export const getAlerts = (
+ organizationIds?: string | string[],
+ page: number = 1,
+ pageSize: number = 50,
+ sortBy: 'starts_at' | 'severity' | 'alertname' | 'status' = 'starts_at',
+ sortDirection: 'asc' | 'desc' = 'desc',
+ statusFilters?: string | string[],
+ severityFilters?: string | string[],
+ systemKeyFilters?: string | string[],
+ alertnameFilters?: string | string[],
+ include?: 'descendants',
+) => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams()
+
+ // Add organization_id(s)
+ if (organizationIds) {
+ const orgIds = Array.isArray(organizationIds) ? organizationIds : [organizationIds]
+ orgIds.forEach((id) => params.append('organization_id', id))
+ }
+
+ // Add include parameter
+ if (include === 'descendants') {
+ params.append('include', 'descendants')
+ }
+
+ // Add pagination
+ params.append('page', page.toString())
+ params.append('page_size', Math.min(pageSize, 100).toString())
+
+ // Add sorting
+ params.append('sort_by', sortBy)
+ params.append('sort_direction', sortDirection)
+
+ // Add status filters (renamed from state)
+ if (statusFilters) {
+ const statuses = Array.isArray(statusFilters) ? statusFilters : [statusFilters]
+ statuses.forEach((status) => params.append('status', status))
+ }
+
+ // Add severity filters
+ if (severityFilters) {
+ const severities = Array.isArray(severityFilters) ? severityFilters : [severityFilters]
+ severities.forEach((severity) => params.append('severity', severity))
+ }
+
+ // Add system_key filters
+ if (systemKeyFilters) {
+ const keys = Array.isArray(systemKeyFilters) ? systemKeyFilters : [systemKeyFilters]
+ keys.forEach((key) => params.append('system_key', key))
+ }
+
+ // Add alertname filters
+ if (alertnameFilters) {
+ const names = Array.isArray(alertnameFilters) ? alertnameFilters : [alertnameFilters]
+ names.forEach((name) => params.append('alertname', name))
+ }
+
+ return axios
+ .get(`${API_URL}/alerts?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getSystemAlertHistory = (
+ systemId: string,
+ page: number = 1,
+ pageSize: number = 50,
+ sortBy: string = 'starts_at',
+ sortDescending: boolean = true,
+) => {
+ const loginStore = useLoginStore()
+ const params = new URLSearchParams({
+ page: page.toString(),
+ page_size: Math.min(pageSize, 100).toString(),
+ sort_by: sortBy,
+ sort_direction: sortDescending ? 'desc' : 'asc',
+ })
+
+ return axios
+ .get(`${API_URL}/systems/${systemId}/alerts/history?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+// Get active alerts for a specific system via the dedicated endpoint
+export const getSystemActiveAlerts = (systemId: string) => {
+ const loginStore = useLoginStore()
+ return axios
+ .get(`${API_URL}/systems/${systemId}/alerts`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const getSystemAlertSilences = (systemId: string) => {
+ const loginStore = useLoginStore()
+ return axios
+ .get(`${API_URL}/systems/${systemId}/alerts/silences`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data.silences)
+}
+
+export const getSystemAlertSilence = (systemId: string, silenceId: string) => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get<{
+ code: number
+ message: string
+ data: { silence: AlertmanagerSilence }
+ }>(`${API_URL}/systems/${systemId}/alerts/silences/${encodeURIComponent(silenceId)}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data.silence)
+}
+
+export const createSystemAlertSilence = (
+ systemId: string,
+ fingerprint: string,
+ comment?: string,
+ endAt?: string,
+) => {
+ const loginStore = useLoginStore()
+ const payload: Record = { fingerprint }
+ if (comment?.trim()) {
+ payload.comment = comment.trim()
+ }
+ if (endAt) {
+ payload.end_at = endAt
+ }
+
+ return axios
+ .post(
+ `${API_URL}/systems/${systemId}/alerts/silences`,
+ payload,
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+ .then((res) => res.data.data)
+}
+
+export const updateSystemAlertSilence = (
+ systemId: string,
+ silenceId: string,
+ comment?: string,
+ endAt?: string,
+) => {
+ const loginStore = useLoginStore()
+ const payload: Record = {}
+
+ if (comment !== undefined) {
+ payload.comment = comment
+ }
+ if (endAt) {
+ payload.end_at = endAt
+ }
+
+ return axios.put(
+ `${API_URL}/systems/${systemId}/alerts/silences/${encodeURIComponent(silenceId)}`,
+ payload,
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+}
+
+export const deleteSystemAlertSilence = (systemId: string, silenceId: string) => {
+ const loginStore = useLoginStore()
+
+ return axios.delete(
+ `${API_URL}/systems/${systemId}/alerts/silences/${encodeURIComponent(silenceId)}`,
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+}
+
+type AlertAnnotationKey = 'summary' | 'description'
+type AlertWithAnnotations = {
+ annotations?: Record
+}
+
+const DEFAULT_ALERT_LOCALE = 'en'
+
+function getAlertAnnotation(
+ alert: AlertWithAnnotations,
+ annotationKey: AlertAnnotationKey,
+ locale: string,
+) {
+ const annotations = alert.annotations ?? {}
+ const normalizedLocale = locale.split('-')[0].toLowerCase() || DEFAULT_ALERT_LOCALE
+ const candidateKeys = Array.from(
+ new Set([
+ `${annotationKey}_${normalizedLocale}`,
+ annotationKey,
+ `${annotationKey}_${DEFAULT_ALERT_LOCALE}`,
+ ]),
+ )
+
+ for (const key of candidateKeys) {
+ const value = annotations[key]
+ if (typeof value === 'string' && value.trim()) {
+ return value.trim()
+ }
+ }
+
+ return ''
+}
+
+export const getAlertSummary = (alert: AlertWithAnnotations, locale: string) => {
+ return getAlertAnnotation(alert, 'summary', locale)
+}
+
+export const getAlertDescription = (alert: AlertWithAnnotations, locale: string) => {
+ return getAlertAnnotation(alert, 'description', locale)
+}
+
+export const getAlertSilenceIds = (alert: Alert) => {
+ return Array.from(new Set((alert.status?.silencedBy || []).filter((silenceId) => !!silenceId)))
+}
+
+export const isAlertSilenced = (alert: Alert) => {
+ return getAlertSilenceIds(alert).length > 0
+}
+
+export const getStatusBadgeColor = (status?: string) => {
+ switch (status?.toLowerCase()) {
+ case 'active':
+ return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
+ case 'suppressed':
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
+ case 'resolved':
+ return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
+ default:
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
+ }
+}
+
+export const getSeverityBadgeColor = (severity?: string) => {
+ switch (severity?.toLowerCase()) {
+ case 'critical':
+ return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
+ case 'warning':
+ return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200'
+ case 'info':
+ return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
+ default:
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
+ }
+}
diff --git a/frontend/src/lib/backups.test.ts b/frontend/src/lib/backups.test.ts
deleted file mode 100644
index 7159e916a..000000000
--- a/frontend/src/lib/backups.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2026 Nethesis S.r.l.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import { describe, expect, it } from 'vitest'
-import { formatBackupSize } from './backups'
-
-describe('formatBackupSize', () => {
- it('returns bytes as-is below 1 KiB', () => {
- expect(formatBackupSize(0)).toBe('0 B')
- expect(formatBackupSize(1)).toBe('1 B')
- expect(formatBackupSize(512)).toBe('512 B')
- expect(formatBackupSize(1023)).toBe('1023 B')
- })
-
- it('switches to KB at 1024 bytes', () => {
- expect(formatBackupSize(1024)).toBe('1.00 KB')
- expect(formatBackupSize(1536)).toBe('1.50 KB')
- })
-
- it('formats MB, GB and TB tiers', () => {
- expect(formatBackupSize(1024 * 1024)).toBe('1.00 MB')
- expect(formatBackupSize(500 * 1024 * 1024)).toBe('500 MB')
- expect(formatBackupSize(1024 * 1024 * 1024)).toBe('1.00 GB')
- expect(formatBackupSize(1024 * 1024 * 1024 * 1024)).toBe('1.00 TB')
- })
-
- it('picks decimal precision based on magnitude', () => {
- // < 10 → 2 decimals
- expect(formatBackupSize(1.23 * 1024 * 1024)).toBe('1.23 MB')
- // 10..99 → 1 decimal
- expect(formatBackupSize(12.34 * 1024 * 1024)).toBe('12.3 MB')
- // >= 100 → no decimals
- expect(formatBackupSize(123.45 * 1024 * 1024)).toBe('123 MB')
- })
-
- it('caps the unit at TB for very large values', () => {
- const tenPetabytes = 10 * 1024 * 1024 * 1024 * 1024 * 1024
- expect(formatBackupSize(tenPetabytes)).toMatch(/TB$/)
- })
-
- it('returns "-" for invalid inputs', () => {
- expect(formatBackupSize(NaN)).toBe('-')
- expect(formatBackupSize(-1)).toBe('-')
- expect(formatBackupSize(-0.5)).toBe('-')
- expect(formatBackupSize(Infinity)).toBe('-')
- })
-})
diff --git a/frontend/src/lib/backups.ts b/frontend/src/lib/backups.ts
index 0dce2dc49..449fe82fb 100644
--- a/frontend/src/lib/backups.ts
+++ b/frontend/src/lib/backups.ts
@@ -46,7 +46,6 @@ export interface BackupDownloadResponse {
// does not currently surface these values so we render them from
// constants and update them here if the server-side defaults shift. ──
-export const BACKUP_MAX_SLOTS_PER_SYSTEM = 10
export const BACKUP_MAX_SIZE_PER_SYSTEM = 500 * 1024 * 1024
// ── API ───────────────────────────────────────────────────────────────────────
@@ -81,18 +80,3 @@ export const deleteBackup = (systemId: string, backupId: string) => {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
-
-// ── Formatting helpers ────────────────────────────────────────────────────────
-
-export function formatBackupSize(bytes: number): string {
- if (!Number.isFinite(bytes) || bytes < 0) return '-'
- if (bytes < 1024) return `${bytes} B`
- const units = ['KB', 'MB', 'GB', 'TB']
- let value = bytes / 1024
- let unit = 0
- while (value >= 1024 && unit < units.length - 1) {
- value /= 1024
- unit++
- }
- return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unit]}`
-}
diff --git a/frontend/src/lib/permissions.ts b/frontend/src/lib/permissions.ts
index afb463fbc..8e480df93 100644
--- a/frontend/src/lib/permissions.ts
+++ b/frontend/src/lib/permissions.ts
@@ -21,7 +21,8 @@ const DESTROY_RESELLERS = 'destroy:resellers'
const DESTROY_CUSTOMERS = 'destroy:customers'
const DESTROY_USERS = 'destroy:users'
const DESTROY_SYSTEMS = 'destroy:systems'
-const MANAGE_ALERTING = 'manage:systems'
+const READ_ALERTS = 'read:alerts'
+const MANAGE_ALERTS = 'manage:alerts'
export const canReadDistributors = () => {
const loginStore = useLoginStore()
@@ -113,7 +114,12 @@ export const canDestroySystems = () => {
return loginStore.permissions.includes(DESTROY_SYSTEMS)
}
-export const canManageAlerting = () => {
+export const canManageAlerts = () => {
const loginStore = useLoginStore()
- return loginStore.permissions.includes(MANAGE_ALERTING)
+ return loginStore.permissions.includes(MANAGE_ALERTS)
+}
+
+export const canReadAlerts = () => {
+ const loginStore = useLoginStore()
+ return loginStore.permissions.includes(READ_ALERTS)
}
diff --git a/frontend/src/queries/alerting/alertingConfig.ts b/frontend/src/queries/alerting/alertingConfig.ts
deleted file mode 100644
index 4e580197c..000000000
--- a/frontend/src/queries/alerting/alertingConfig.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2026 Nethesis S.r.l.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import { getAlertingConfig, ALERTING_CONFIG_KEY } from '@/lib/alerting'
-import { useLoginStore } from '@/stores/login'
-import { defineQuery, useQuery } from '@pinia/colada'
-import { ref } from 'vue'
-
-export const useAlertingConfig = defineQuery(() => {
- const loginStore = useLoginStore()
- const organizationId = ref(loginStore.userInfo?.organization_id || '')
- const format = ref<'yaml' | undefined>(undefined)
-
- const { state, asyncStatus, ...rest } = useQuery({
- key: () => [ALERTING_CONFIG_KEY, organizationId.value, format.value ?? 'json'],
- enabled: () => !!loginStore.jwtToken && !!organizationId.value,
- query: () => getAlertingConfig(organizationId.value, format.value),
- })
-
- return {
- ...rest,
- state,
- asyncStatus,
- organizationId,
- format,
- }
-})
diff --git a/frontend/src/queries/alerting/alerts.ts b/frontend/src/queries/alerting/alerts.ts
deleted file mode 100644
index 5edd8cfad..000000000
--- a/frontend/src/queries/alerting/alerts.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2026 Nethesis S.r.l.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import { getAlerts, ALERTING_ALERTS_KEY } from '@/lib/alerting'
-import { useLoginStore } from '@/stores/login'
-import { defineQuery, useQuery } from '@pinia/colada'
-import { ref } from 'vue'
-
-export const useAlerts = defineQuery(() => {
- const loginStore = useLoginStore()
- const organizationId = ref('')
- const stateFilter = ref([])
- const severityFilter = ref([])
- const systemKeyFilter = ref('')
-
- const { state, asyncStatus, ...rest } = useQuery({
- key: () => [
- ALERTING_ALERTS_KEY,
- organizationId.value,
- stateFilter.value.join(','),
- severityFilter.value.join(','),
- systemKeyFilter.value,
- ],
- enabled: () => !!loginStore.jwtToken && !!organizationId.value,
- query: () =>
- getAlerts(
- organizationId.value,
- stateFilter.value[0] || undefined,
- severityFilter.value[0] || undefined,
- systemKeyFilter.value || undefined,
- ),
- })
-
- const resetFilters = () => {
- stateFilter.value = []
- severityFilter.value = []
- systemKeyFilter.value = ''
- }
-
- const areDefaultFiltersApplied = () => {
- return !stateFilter.value.length && !severityFilter.value.length && !systemKeyFilter.value
- }
-
- return {
- ...rest,
- state,
- asyncStatus,
- organizationId,
- stateFilter,
- severityFilter,
- systemKeyFilter,
- resetFilters,
- areDefaultFiltersApplied,
- }
-})
diff --git a/frontend/src/queries/alerts/alertActivity.ts b/frontend/src/queries/alerts/alertActivity.ts
new file mode 100644
index 000000000..5427270ed
--- /dev/null
+++ b/frontend/src/queries/alerts/alertActivity.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertActivity, ALERT_ACTIVITY_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertActivity = (fingerprint: string | undefined) => {
+ const loginStore = useLoginStore()
+ const pageNum = ref(1)
+ const pageSize = ref(50)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [ALERT_ACTIVITY_KEY, fingerprint as string, pageNum.value, pageSize.value],
+ enabled: () => !!loginStore.jwtToken && !!fingerprint,
+ query: () => getAlertActivity(fingerprint!, pageNum.value, pageSize.value),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ pageNum,
+ pageSize,
+ }
+}
diff --git a/frontend/src/queries/alerts/alerts.ts b/frontend/src/queries/alerts/alerts.ts
new file mode 100644
index 000000000..4c50947a5
--- /dev/null
+++ b/frontend/src/queries/alerts/alerts.ts
@@ -0,0 +1,126 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlerts, ALERTS_ALERTS_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlerts = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const organizationIds = ref([])
+ const pageNum = ref(1)
+ const pageSize = ref(50)
+ const sortBy = ref<'starts_at' | 'severity' | 'alertname' | 'status'>('starts_at')
+ const sortDirection = ref<'asc' | 'desc'>('desc')
+ const statusFilters = ref([])
+ const severityFilters = ref([])
+ const systemKeyFilters = ref([])
+ const alertnameFilters = ref([])
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [
+ ALERTS_ALERTS_KEY,
+ organizationIds.value.join(','),
+ pageNum.value,
+ pageSize.value,
+ sortBy.value,
+ sortDirection.value,
+ statusFilters.value.join(','),
+ severityFilters.value.join(','),
+ systemKeyFilters.value.join(','),
+ alertnameFilters.value.join(','),
+ ],
+ enabled: () => !!loginStore.jwtToken,
+ query: () =>
+ getAlerts(
+ organizationIds.value.length > 0 ? organizationIds.value : undefined,
+ pageNum.value,
+ pageSize.value,
+ sortBy.value,
+ sortDirection.value,
+ statusFilters.value.length > 0 ? statusFilters.value : undefined,
+ severityFilters.value.length > 0 ? severityFilters.value : undefined,
+ systemKeyFilters.value.length > 0 ? systemKeyFilters.value : undefined,
+ alertnameFilters.value.length > 0 ? alertnameFilters.value : undefined,
+ ),
+ })
+
+ const resetFilters = () => {
+ statusFilters.value = []
+ severityFilters.value = []
+ systemKeyFilters.value = []
+ alertnameFilters.value = []
+ pageNum.value = 1
+ }
+
+ const areDefaultFiltersApplied = () => {
+ return (
+ !statusFilters.value.length &&
+ !severityFilters.value.length &&
+ !systemKeyFilters.value.length &&
+ !alertnameFilters.value.length
+ )
+ }
+
+ const toggleStatusFilter = (status: string) => {
+ const idx = statusFilters.value.indexOf(status)
+ if (idx >= 0) {
+ statusFilters.value.splice(idx, 1)
+ } else {
+ statusFilters.value.push(status)
+ }
+ pageNum.value = 1
+ }
+
+ const toggleSeverityFilter = (severity: string) => {
+ const idx = severityFilters.value.indexOf(severity)
+ if (idx >= 0) {
+ severityFilters.value.splice(idx, 1)
+ } else {
+ severityFilters.value.push(severity)
+ }
+ pageNum.value = 1
+ }
+
+ const toggleSystemKeyFilter = (systemKey: string) => {
+ const idx = systemKeyFilters.value.indexOf(systemKey)
+ if (idx >= 0) {
+ systemKeyFilters.value.splice(idx, 1)
+ } else {
+ systemKeyFilters.value.push(systemKey)
+ }
+ pageNum.value = 1
+ }
+
+ const toggleAlertNameFilter = (alertname: string) => {
+ const idx = alertnameFilters.value.indexOf(alertname)
+ if (idx >= 0) {
+ alertnameFilters.value.splice(idx, 1)
+ } else {
+ alertnameFilters.value.push(alertname)
+ }
+ pageNum.value = 1
+ }
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ organizationIds,
+ pageNum,
+ pageSize,
+ sortBy,
+ sortDirection,
+ statusFilters,
+ severityFilters,
+ systemKeyFilters,
+ alertnameFilters,
+ resetFilters,
+ areDefaultFiltersApplied,
+ toggleStatusFilter,
+ toggleSeverityFilter,
+ toggleSystemKeyFilter,
+ toggleAlertNameFilter,
+ }
+})
diff --git a/frontend/src/queries/alerts/alertsConfig.ts b/frontend/src/queries/alerts/alertsConfig.ts
new file mode 100644
index 000000000..f8ad85539
--- /dev/null
+++ b/frontend/src/queries/alerts/alertsConfig.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertsConfig, ALERTS_CONFIG_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertsConfig = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const format = ref<'yaml' | undefined>(undefined)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [ALERTS_CONFIG_KEY, format.value ?? 'json'],
+ enabled: () => !!loginStore.jwtToken,
+ query: () => getAlertsConfig(format.value),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ format,
+ }
+})
diff --git a/frontend/src/queries/alerts/alertsHistory.ts b/frontend/src/queries/alerts/alertsHistory.ts
new file mode 100644
index 000000000..5b7b69b53
--- /dev/null
+++ b/frontend/src/queries/alerts/alertsHistory.ts
@@ -0,0 +1,51 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertsHistory, ALERTS_HISTORY_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertsHistory = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const organizationIds = ref([])
+ const pageNum = ref(1)
+ const pageSize = ref(50)
+ const sortBy = ref('starts_at')
+ const sortDirection = ref<'asc' | 'desc'>('desc')
+ const includeDescendants = ref(false)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [
+ ALERTS_HISTORY_KEY,
+ organizationIds.value.join(','),
+ pageNum.value,
+ pageSize.value,
+ sortBy.value,
+ sortDirection.value,
+ includeDescendants.value,
+ ],
+ enabled: () => !!loginStore.jwtToken,
+ query: () =>
+ getAlertsHistory(
+ organizationIds.value.length > 0 ? organizationIds.value : undefined,
+ pageNum.value,
+ pageSize.value,
+ sortBy.value,
+ sortDirection.value,
+ includeDescendants.value ? 'descendants' : undefined,
+ ),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ organizationIds,
+ pageNum,
+ pageSize,
+ sortBy,
+ sortDirection,
+ includeDescendants,
+ }
+})
diff --git a/frontend/src/queries/alerts/alertsSilences.ts b/frontend/src/queries/alerts/alertsSilences.ts
new file mode 100644
index 000000000..25021ddf9
--- /dev/null
+++ b/frontend/src/queries/alerts/alertsSilences.ts
@@ -0,0 +1,43 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertsSilences, ALERTS_SILENCES_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertsSilences = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const organizationIds = ref([])
+ const pageNum = ref(1)
+ const pageSize = ref(50)
+ const includeDescendants = ref(false)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [
+ ALERTS_SILENCES_KEY,
+ organizationIds.value.join(','),
+ pageNum.value,
+ pageSize.value,
+ includeDescendants.value,
+ ],
+ enabled: () => !!loginStore.jwtToken,
+ query: () =>
+ getAlertsSilences(
+ organizationIds.value.length > 0 ? organizationIds.value : undefined,
+ pageNum.value,
+ pageSize.value,
+ includeDescendants.value ? 'descendants' : undefined,
+ ),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ organizationIds,
+ pageNum,
+ pageSize,
+ includeDescendants,
+ }
+})
diff --git a/frontend/src/queries/alerts/alertsStats.ts b/frontend/src/queries/alerts/alertsStats.ts
new file mode 100644
index 000000000..269970550
--- /dev/null
+++ b/frontend/src/queries/alerts/alertsStats.ts
@@ -0,0 +1,31 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertsStats, ALERTS_STATS_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertsStats = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const organizationIds = ref([])
+ const includeDescendants = ref(false)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [ALERTS_STATS_KEY, organizationIds.value.join(','), includeDescendants.value],
+ enabled: () => !!loginStore.jwtToken,
+ query: () =>
+ getAlertsStats(
+ organizationIds.value.length > 0 ? organizationIds.value : undefined,
+ includeDescendants.value ? 'descendants' : undefined,
+ ),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ organizationIds,
+ includeDescendants,
+ }
+})
diff --git a/frontend/src/queries/alerts/alertsTotals.ts b/frontend/src/queries/alerts/alertsTotals.ts
new file mode 100644
index 000000000..3b6aca0ab
--- /dev/null
+++ b/frontend/src/queries/alerts/alertsTotals.ts
@@ -0,0 +1,31 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertsTotals, ALERTS_TOTALS_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertsTotals = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const organizationIds = ref([])
+ const includeDescendants = ref(false)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [ALERTS_TOTALS_KEY, organizationIds.value.join(','), includeDescendants.value],
+ enabled: () => !!loginStore.jwtToken,
+ query: () =>
+ getAlertsTotals(
+ organizationIds.value.length > 0 ? organizationIds.value : undefined,
+ includeDescendants.value ? 'descendants' : undefined,
+ ),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ organizationIds,
+ includeDescendants,
+ }
+})
diff --git a/frontend/src/queries/alerts/alertsTrend.ts b/frontend/src/queries/alerts/alertsTrend.ts
new file mode 100644
index 000000000..3cef1ae59
--- /dev/null
+++ b/frontend/src/queries/alerts/alertsTrend.ts
@@ -0,0 +1,39 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { getAlertsTrend, ALERTS_TREND_KEY } from '@/lib/alerts'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { ref } from 'vue'
+
+export const useAlertsTrend = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const organizationIds = ref([])
+ const includeDescendants = ref(false)
+ const period = ref<7 | 30 | 180 | 365>(7)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [
+ ALERTS_TREND_KEY,
+ organizationIds.value.join(','),
+ includeDescendants.value,
+ period.value,
+ ],
+ enabled: () => !!loginStore.jwtToken,
+ query: () =>
+ getAlertsTrend(
+ organizationIds.value.length > 0 ? organizationIds.value : undefined,
+ includeDescendants.value ? 'descendants' : undefined,
+ period.value,
+ ),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ organizationIds,
+ includeDescendants,
+ period,
+ }
+})
diff --git a/frontend/src/queries/systems/activeAlerts.ts b/frontend/src/queries/systems/activeAlerts.ts
index bc1ab3681..29f7d50b9 100644
--- a/frontend/src/queries/systems/activeAlerts.ts
+++ b/frontend/src/queries/systems/activeAlerts.ts
@@ -1,7 +1,7 @@
// Copyright (C) 2026 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later
-import { getSystemActiveAlerts } from '@/lib/alerting'
+import { getSystemActiveAlerts } from '@/lib/alerts'
import { useLoginStore } from '@/stores/login'
import { defineQuery, useQuery } from '@pinia/colada'
import { useRoute } from 'vue-router'
diff --git a/frontend/src/queries/systems/alertHistory.ts b/frontend/src/queries/systems/alertHistory.ts
index 5cf21fad3..e50687031 100644
--- a/frontend/src/queries/systems/alertHistory.ts
+++ b/frontend/src/queries/systems/alertHistory.ts
@@ -1,10 +1,10 @@
// Copyright (C) 2026 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later
-import { getSystemAlertHistory, ALERT_HISTORY_KEY } from '@/lib/alerting'
+import { getSystemAlertHistory, ALERT_HISTORY_KEY } from '@/lib/alerts'
import { DEFAULT_PAGE_SIZE, loadPageSizeFromStorage } from '@/lib/tablePageSize'
import { useLoginStore } from '@/stores/login'
-import { ALERT_HISTORY_TABLE_ID } from '@/lib/alerting'
+import { ALERT_HISTORY_TABLE_ID } from '@/lib/alerts'
import { defineQuery, useQuery } from '@pinia/colada'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 455414ad0..d44a8521e 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -81,9 +81,9 @@ const router = createRouter({
component: () => import('../views/ApplicationDetailView.vue'),
},
{
- path: '/alerting',
- name: 'alerting',
- component: () => import('../views/AlertingView.vue'),
+ path: '/alerts',
+ name: 'alerts',
+ component: () => import('../views/AlertsView.vue'),
},
{
path: '/distributors/:companyId',
diff --git a/frontend/src/views/AlertingView.vue b/frontend/src/views/AlertingView.vue
deleted file mode 100644
index 1b6ef6c61..000000000
--- a/frontend/src/views/AlertingView.vue
+++ /dev/null
@@ -1,878 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('alerting.alerting_title') }}
- ALPHA
-
-
- {{ $t('alerting.alerting_page_description') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ activeAlertsCountLabel }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('common.search') }}
-
-
-
- {{ $t('common.reset_filters') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('alerting.alertname') }}
- {{ $t('alerting.severity') }}
- {{ $t('alerting.state') }}
- {{ $t('alerting.system_key') }}
- {{ $t('alerting.summary') }}
- {{ $t('alerting.description') }}
- {{ $t('alerting.starts_at') }}
-
-
-
-
-
-
- {{ alert.labels.alertname || '-' }}
-
-
-
-
- {{ alert.labels.severity }}
-
- -
-
-
-
- {{ alert.status?.state || '-' }}
-
-
-
- {{ alert.labels.system_key || '-' }}
-
-
-
- {{ getAlertSummaryText(alert) || '-' }}
-
-
-
-
- {{ getAlertDescriptionText(alert) || '-' }}
-
-
-
- {{
- alert.startsAt ? formatDateTimeNoSeconds(new Date(alert.startsAt), locale) : '-'
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('alerting.config_editor') }}
-
-
-
-
-
-
-
- {{ $t('alerting.save_config') }}
-
-
-
-
-
- {{ $t('common.cancel') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('alerting.config_json') }}
-
-
-
-
-
-
- {{ $t('alerting.edit_config') }}
-
-
-
-
-
- {{ $t('alerting.mail_enabled') }}
-
-
- {{ config.mail_enabled ? $t('common.enabled') : $t('common.disabled') }}
-
-
-
-
- {{ $t('alerting.mail_addresses') }}
- {{ formatMailAddresses(config.mail_addresses) }}
-
-
- {{ $t('alerting.webhook_enabled') }}
-
-
- {{
- config.webhook_enabled ? $t('common.enabled') : $t('common.disabled')
- }}
-
-
-
-
- {{ $t('alerting.webhook_receivers') }}
-
-
-
- {{ recv.name }}: {{ recv.url }}
-
-
-
-
-
- {{ $t('alerting.telegram_enabled') }}
-
-
- {{
- config.telegram_enabled ? $t('common.enabled') : $t('common.disabled')
- }}
-
-
-
-
- {{ $t('alerting.telegram_receivers') }}
-
-
-
- chat_id: {{ recv.chat_id }}
-
-
-
-
-
- {{ $t('alerting.email_template_lang') }}
- {{ config.email_template_lang }}
-
-
-
-
-
- {{ $t('alerting.per_severity_overrides') }}
-
-
-
-
- {{ sv.severity }}
-
-
-
-
{{ $t('alerting.mail_enabled') }}: {{ sv.mail_enabled ?? '-' }}
-
- {{ $t('alerting.webhook_enabled') }}: {{ sv.webhook_enabled ?? '-' }}
-
-
- {{ $t('alerting.telegram_enabled') }}: {{ sv.telegram_enabled ?? '-' }}
-
-
- {{ $t('alerting.mail_addresses') }}: {{ sv.mail_addresses.join(', ') }}
-
-
-
-
-
-
-
- {{ $t('alerting.per_system_overrides') }}
-
-
-
{{ sys.system_key }}
-
-
{{ $t('alerting.mail_enabled') }}: {{ sys.mail_enabled ?? '-' }}
-
- {{ $t('alerting.webhook_enabled') }}: {{ sys.webhook_enabled ?? '-' }}
-
-
- {{ $t('alerting.telegram_enabled') }}: {{ sys.telegram_enabled ?? '-' }}
-
-
- {{ $t('alerting.mail_addresses') }}: {{ sys.mail_addresses.join(', ') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('alerting.disable_alerts') }}
-
-
- {{ $t('alerting.disable_alerts_confirmation') }}
-
-
-
-
-
-
- {{ $t('alerting.disable_alerts') }}
-
-
-
-
-
-
-
-
- {{ $t('common.delete') }}
-
-
- {{ $t('common.cancel') }}
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('alerting.raw_yaml') }}
-
-
-
-
- {{ yamlCopied ? $t('alerting.yaml_copied') : $t('alerting.copy_yaml') }}
-
-
- {{ rawYaml || $t('alerting.no_config') }}
-
-
-
-
-
-
-
-
-
{{ $t('alerting.no_config') }}
-
{{ $t('alerting.no_config_description') }}
-
{
- isEditMode = true
- selectedConfigTab = 'structured'
- }
- "
- >
-
-
-
- {{ $t('alerting.edit_config') }}
-
-
-
-
-
-
-
-
diff --git a/frontend/src/views/AlertsView.vue b/frontend/src/views/AlertsView.vue
new file mode 100644
index 000000000..f7fb15813
--- /dev/null
+++ b/frontend/src/views/AlertsView.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
{{ $t('alerts.title') }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/SystemDetailView.vue b/frontend/src/views/SystemDetailView.vue
index cee1fed5d..b0f4506aa 100644
--- a/frontend/src/views/SystemDetailView.vue
+++ b/frontend/src/views/SystemDetailView.vue
@@ -20,7 +20,6 @@ import { useTabs } from '@/composables/useTabs'
import { useI18n } from 'vue-i18n'
import SystemOverviewPanel from '@/components/systems/SystemOverviewPanel.vue'
import SystemChangeHistoryPanel from '@/components/systems/SystemChangeHistoryPanel.vue'
-import SystemAlertHistoryPanel from '@/components/systems/SystemAlertHistoryPanel.vue'
import SystemBackupsPanel from '@/components/systems/SystemBackupsPanel.vue'
import { useLatestInventory } from '@/queries/systems/latestInventory'
import { useSystemReachability } from '@/queries/systems/systemReachability'
@@ -33,7 +32,7 @@ const { state: reachabilityState, asyncStatus: reachabilityAsyncStatus } = useSy
const { tabs, selectedTab } = useTabs([
{ name: 'overview', label: t('system_detail.overview') },
{ name: 'change_history', label: t('system_detail.change_history') },
- { name: 'alert_history', label: t('alerting.title') },
+ { name: 'alert_history', label: t('alerts.title') },
{ name: 'backups', label: t('backups.title') },
])
@@ -131,7 +130,7 @@ const openSystem = () => {
/>
-
+