From 3b3835fcaa4975f9261d3b6c64bd67a540d96008 Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Tue, 12 May 2026 17:38:11 +0200 Subject: [PATCH 1/3] refactor: system backups page --- .../components/systems/SystemBackupsPanel.vue | 269 ++++++++---------- frontend/src/i18n/en/translation.json | 15 +- frontend/src/lib/backups.test.ts | 47 --- frontend/src/lib/backups.ts | 16 -- 4 files changed, 120 insertions(+), 227 deletions(-) delete mode 100644 frontend/src/lib/backups.test.ts diff --git a/frontend/src/components/systems/SystemBackupsPanel.vue b/frontend/src/components/systems/SystemBackupsPanel.vue index fd0c6f805..0949d46dd 100644 --- a/frontend/src/components/systems/SystemBackupsPanel.vue +++ b/frontend/src/components/systems/SystemBackupsPanel.vue @@ -6,20 +6,18 @@ diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 1f319255d..14df895b9 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -783,28 +783,23 @@ }, "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": { 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]}` -} From b07b5dd8ac91f9404b08dd3eb54ee0aa1344817b Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Fri, 15 May 2026 18:00:53 +0200 Subject: [PATCH 2/3] chore(agents): improve Frontend & Accessibility agent --- .../agents/frontend-accessibility.agent.md | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/agents/frontend-accessibility.agent.md b/.github/agents/frontend-accessibility.agent.md index c568da277..89e161f10 100644 --- a/.github/agents/frontend-accessibility.agent.md +++ b/.github/agents/frontend-accessibility.agent.md @@ -1,12 +1,14 @@ --- description: "Use when working on Vue 3 frontend code with a focus on accessibility, WCAG compliance, ARIA attributes, keyboard navigation, screen reader support, semantic HTML, color contrast, UX patterns, user flows, interaction design, form usability, empty states, loading states, error states, Tailwind CSS, design systems, @nethesis/vue-components, Pinia, Pinia Colada, defineQuery, useMutation, valibot schemas, or auditing UI components for a11y or UX issues. Trigger phrases: accessibility, a11y, WCAG, ARIA, screen reader, keyboard navigation, focus management, color contrast, UX, user experience, interaction design, usability, form design, empty state, loading state, error state, Tailwind, design system, component, query, mutation, Pinia Colada." name: "Frontend & Accessibility Specialist" -tools: [read, edit, search, todo, execute, web] +tools: [read, edit, search, todo, execute, web, mcp_figma_get_design_context, mcp_figma_get_screenshot, mcp_figma_search_design_system, mcp_figma_get_metadata, mcp_figma_get_code_connect_suggestions, mcp_figma_add_code_connect_map] commands: - name: a11y-fix description: Audit and fix WCAG accessibility issues in a Vue component or view - name: design-check description: Verify a Vue component or view aligns with the design system conventions + - name: design-review + description: Compare a Vue component against its Figma design and audit for design system alignment and accessibility compliance --- You are a senior frontend engineer and UX/design-system specialist with deep expertise in Vue 3, TypeScript, Tailwind CSS v4, Pinia Colada, and the `@nethesis/vue-components` library. You also hold strong accessibility knowledge (WCAG 2.1/2.2 AA, ARIA patterns, keyboard navigation). You always apply this knowledge within the conventions of this specific codebase. @@ -254,3 +256,26 @@ Checklist: - [ ] All user-visible strings use `$t()` / `t()` — no hardcoded text - [ ] New keys added only to `src/i18n/en/translation.json`, `snake_case`, correct domain namespace - [ ] License header present; ` + + diff --git a/frontend/src/components/alerts/AlertNotificationsPanel.vue b/frontend/src/components/alerts/AlertNotificationsPanel.vue new file mode 100644 index 000000000..69b435443 --- /dev/null +++ b/frontend/src/components/alerts/AlertNotificationsPanel.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/frontend/src/components/alerts/AlertsTable.vue b/frontend/src/components/alerts/AlertsTable.vue new file mode 100644 index 000000000..2dabaccc8 --- /dev/null +++ b/frontend/src/components/alerts/AlertsTable.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/frontend/src/components/alerts/MuteAlertDrawer.vue b/frontend/src/components/alerts/MuteAlertDrawer.vue new file mode 100644 index 000000000..71a126b34 --- /dev/null +++ b/frontend/src/components/alerts/MuteAlertDrawer.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/frontend/src/components/shell/SideMenu.vue b/frontend/src/components/shell/SideMenu.vue index 9bf6b7c7e..599731309 100644 --- a/frontend/src/components/shell/SideMenu.vue +++ b/frontend/src/components/shell/SideMenu.vue @@ -21,7 +21,7 @@ import { faBuilding as fasBuilding, faUserGroup as fasUserGroup, faServer as fasServer, - faBell, + faWarning, } from '@fortawesome/free-solid-svg-icons' import { faGridOne as fasGridOne } from '@nethesis/nethesis-solid-svg-icons' import { @@ -74,6 +74,15 @@ const navigation = computed(() => { }) } + if (loginStore.isOwner) { + menuItems.push({ + name: 'alerts.alerts_title', + to: 'alerts', + solidIcon: faWarning, + lightIcon: faWarning, + }) + } + if (canReadApplications()) { menuItems.push({ name: 'applications.title', @@ -119,15 +128,6 @@ const navigation = computed(() => { }) } - if (loginStore.isOwner) { - menuItems.push({ - name: 'alerting.alerting_title', - to: 'alerting', - solidIcon: faBell, - lightIcon: faBell, - }) - } - return menuItems }) diff --git a/frontend/src/components/systems/DisableSystemAlertSilenceModal.vue b/frontend/src/components/systems/DisableSystemAlertSilenceModal.vue deleted file mode 100644 index 4e7fb928d..000000000 --- a/frontend/src/components/systems/DisableSystemAlertSilenceModal.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - - - diff --git a/frontend/src/components/systems/SilenceSystemAlertModal.vue b/frontend/src/components/systems/SilenceSystemAlertModal.vue deleted file mode 100644 index 79bf486c5..000000000 --- a/frontend/src/components/systems/SilenceSystemAlertModal.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - - - diff --git a/frontend/src/components/systems/SystemActiveAlertsCard.vue b/frontend/src/components/systems/SystemActiveAlertsCard.vue deleted file mode 100644 index b2528cdb5..000000000 --- a/frontend/src/components/systems/SystemActiveAlertsCard.vue +++ /dev/null @@ -1,429 +0,0 @@ - - - - - diff --git a/frontend/src/components/systems/SystemAlertHistoryPanel.vue b/frontend/src/components/systems/SystemAlertHistoryPanel.vue deleted file mode 100644 index cbedf88b6..000000000 --- a/frontend/src/components/systems/SystemAlertHistoryPanel.vue +++ /dev/null @@ -1,189 +0,0 @@ - - - - - diff --git a/frontend/src/components/systems/SystemAlertSilencesCard.vue b/frontend/src/components/systems/SystemAlertSilencesCard.vue deleted file mode 100644 index 3f29bba42..000000000 --- a/frontend/src/components/systems/SystemAlertSilencesCard.vue +++ /dev/null @@ -1,259 +0,0 @@ - - - - - diff --git a/frontend/src/components/systems/SystemStatusCard.vue b/frontend/src/components/systems/SystemStatusCard.vue index 5a1e44121..0e4412ec3 100644 --- a/frontend/src/components/systems/SystemStatusCard.vue +++ b/frontend/src/components/systems/SystemStatusCard.vue @@ -35,7 +35,7 @@ const { state: latestInventory } = useLatestInventory() const { state: activeAlerts } = useSystemActiveAlerts() const { state: systemBackups } = useSystemBackups() -const activeAlertsCount = computed(() => activeAlerts.value.data?.length ?? 0) +const activeAlertsCount = computed(() => activeAlerts.value?.data?.alerts?.length ?? 0) const backupsCount = computed(() => systemBackups.value.data?.backups?.length ?? 0) const hasActiveAlerts = computed(() => activeAlertsCount.value > 0) const hasBackups = computed(() => backupsCount.value > 0) @@ -193,7 +193,7 @@ const timezone = computed(() => {