diff --git a/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor
new file mode 100644
index 0000000..371e156
--- /dev/null
+++ b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor
@@ -0,0 +1,137 @@
+@page "/notifications/lists"
+@using ScadaLink.Security
+@using ScadaLink.Commons.Entities.Notifications
+@using ScadaLink.Commons.Interfaces.Repositories
+@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
+@inject INotificationRepository NotificationRepository
+@inject NavigationManager NavigationManager
+@inject IDialogService Dialog
+
+
+
+
+
+
Notification Lists
+
+
+
+ @if (_loading)
+ {
+
+ }
+ else if (_errorMessage != null)
+ {
+
@_errorMessage
+ }
+ else if (_lists.Count == 0)
+ {
+
+
+
No notification lists
+
+
+
+ }
+ else
+ {
+
+
+
+
+ | Name |
+ Recipients |
+ Actions |
+
+
+
+ @foreach (var list in _lists)
+ {
+ var recipients = _recipients.GetValueOrDefault(list.Id)
+ ?? (IReadOnlyList)Array.Empty();
+
+ | @list.Name |
+
+ @if (recipients.Count == 0)
+ {
+ No recipients
+ }
+ else
+ {
+ @foreach (var r in recipients)
+ {
+ @r.Name <@r.EmailAddress>
+ }
+ }
+ |
+
+
+
+ |
+
+ }
+
+
+
+ }
+
+
+@code {
+ private bool _loading = true;
+ private string? _errorMessage;
+ private List _lists = new();
+ private readonly Dictionary> _recipients = new();
+ private ToastNotification _toast = default!;
+
+ protected override async Task OnInitializedAsync() => await LoadAsync();
+
+ private async Task LoadAsync()
+ {
+ _loading = true;
+ _errorMessage = null;
+ try
+ {
+ _lists = (await NotificationRepository.GetAllNotificationListsAsync()).ToList();
+ _recipients.Clear();
+ foreach (var list in _lists)
+ {
+ _recipients[list.Id] = await NotificationRepository.GetRecipientsByListIdAsync(list.Id);
+ }
+ }
+ catch (Exception ex)
+ {
+ _errorMessage = ex.Message;
+ }
+ _loading = false;
+ }
+
+ private async Task DeleteList(NotificationList list)
+ {
+ if (!await Dialog.ConfirmAsync("Delete", $"Delete notification list '{list.Name}'?", danger: true))
+ {
+ return;
+ }
+ try
+ {
+ await NotificationRepository.DeleteNotificationListAsync(list.Id);
+ await NotificationRepository.SaveChangesAsync();
+ _toast.ShowSuccess("Deleted.");
+ await LoadAsync();
+ }
+ catch (Exception ex)
+ {
+ _toast.ShowError(ex.Message);
+ }
+ }
+}
diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs
new file mode 100644
index 0000000..c998130
--- /dev/null
+++ b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs
@@ -0,0 +1,76 @@
+using System.Security.Claims;
+using Bunit;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using ScadaLink.CentralUI.Components.Shared;
+using ScadaLink.Commons.Entities.Notifications;
+using ScadaLink.Commons.Interfaces.Repositories;
+using NotificationListsPage = ScadaLink.CentralUI.Components.Pages.Notifications.NotificationLists;
+
+namespace ScadaLink.CentralUI.Tests.Pages;
+
+///
+/// bUnit rendering tests for the standalone Notification Lists page (Task 7).
+///
+public class NotificationListsPageTests : BunitContext
+{
+ private void WireAuthAndDialog()
+ {
+ Services.AddSingleton(new AlwaysConfirmDialogService());
+
+ var claims = new[]
+ {
+ new Claim("Username", "tester"),
+ new Claim(ClaimTypes.Role, "Design"),
+ };
+ var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
+ Services.AddSingleton(new TestAuthStateProvider(user));
+ Services.AddAuthorizationCore();
+ }
+
+ [Fact]
+ public void RendersNotificationListRows()
+ {
+ var repo = Substitute.For();
+ repo.GetAllNotificationListsAsync()
+ .Returns(Task.FromResult>(
+ new List { new("Ops On-Call") { Id = 1 } }));
+ repo.GetRecipientsByListIdAsync(1)
+ .Returns(Task.FromResult>(
+ new List { new("Jane", "jane@example.com") }));
+ Services.AddSingleton(repo);
+ WireAuthAndDialog();
+
+ var cut = Render();
+
+ Assert.Contains("Ops On-Call", cut.Markup);
+ Assert.Contains("jane@example.com", cut.Markup);
+ }
+
+ [Fact]
+ public void ShowsEmptyState_WhenNoLists()
+ {
+ var repo = Substitute.For();
+ repo.GetAllNotificationListsAsync()
+ .Returns(Task.FromResult>(
+ new List()));
+ Services.AddSingleton(repo);
+ WireAuthAndDialog();
+
+ var cut = Render();
+
+ Assert.Contains("No notification lists", cut.Markup);
+ }
+
+ /// A dialog service that auto-confirms, so action paths run end-to-end.
+ private sealed class AlwaysConfirmDialogService : IDialogService
+ {
+ public Task ConfirmAsync(string title, string message, bool danger = false)
+ => Task.FromResult(true);
+
+ public Task PromptAsync(
+ string title, string label, string initialValue = "", string? placeholder = null)
+ => Task.FromResult(null);
+ }
+}