diff --git a/.claude/features-done/blazor-ui-overhaul.md b/.claude/features-done/blazor-ui-overhaul.md
new file mode 100644
index 0000000..0a634d4
--- /dev/null
+++ b/.claude/features-done/blazor-ui-overhaul.md
@@ -0,0 +1,25 @@
+# Feature: blazor-ui-overhaul
+
+## Goal
+Overhaul the Blazor monitoring UI: slim ListView columns, move details to a dialog showing all metadata + JSON content, show persist type per cache type, and make Clear Cache refresh the ListView.
+
+## Scope
+- `CacheTypeInfo` — add `PersistType` field
+- `CacheMonitor.Set/Track` — populate `PersistType`
+- `ListView.razor` — slim columns to Key, Expires, Size, Load Time, Stale; add persist type to the type-level grid; add chevron button per row; subscribe to monitor events for live refresh
+- `SummaryView.razor` — subscribe to monitor events for live refresh
+- New `DetailView.razor` — dialog component with all metadata + JSON content via `PeekAsync` + reflection + `CopyButton`
+
+## Acceptance Criteria
+- ListView shows slim columns with chevron to open detail dialog
+- Detail dialog shows all metadata (Created, Updated, Fresh Span, Last Accessed, Access Count) plus cached data as JSON
+- Copy JSON button works
+- Persist type column appears on the type-level grid
+- Clear Cache button refreshes the ListView without reload
+- All tests still pass
+
+## Done Condition
+User confirms the UI works as expected.
+
+## Originating Branch
+develop
diff --git a/Sample/Tharga.Cache.BlazorServer/Components/App.razor b/Sample/Tharga.Cache.BlazorServer/Components/App.razor
index 03393c5..b1e5a7a 100644
--- a/Sample/Tharga.Cache.BlazorServer/Components/App.razor
+++ b/Sample/Tharga.Cache.BlazorServer/Components/App.razor
@@ -1,4 +1,4 @@
-
+
@@ -11,14 +11,15 @@
-
-
+
+
-
+
+
diff --git a/Sample/Tharga.Cache.BlazorServer/Components/Layout/MainLayout.razor b/Sample/Tharga.Cache.BlazorServer/Components/Layout/MainLayout.razor
index 78624f3..2edd86c 100644
--- a/Sample/Tharga.Cache.BlazorServer/Components/Layout/MainLayout.razor
+++ b/Sample/Tharga.Cache.BlazorServer/Components/Layout/MainLayout.razor
@@ -21,3 +21,5 @@
Reload
🗙
+
+
diff --git a/Sample/Tharga.Cache.BlazorServer/Program.cs b/Sample/Tharga.Cache.BlazorServer/Program.cs
index b480836..d23fa36 100644
--- a/Sample/Tharga.Cache.BlazorServer/Program.cs
+++ b/Sample/Tharga.Cache.BlazorServer/Program.cs
@@ -1,3 +1,4 @@
+using Blazored.LocalStorage;
using Radzen;
using Tharga.Cache;
using Tharga.Cache.BlazorServer.Components;
@@ -13,6 +14,7 @@
.AddInteractiveServerComponents();
builder.Services.AddRadzenComponents();
+builder.Services.AddBlazoredLocalStorage();
builder.AddMongoDB();
builder.Services.AddCache(o =>
{
diff --git a/Tharga.Cache.Blazor/CacheContent.razor b/Tharga.Cache.Blazor/CacheContent.razor
new file mode 100644
index 0000000..cbf3047
--- /dev/null
+++ b/Tharga.Cache.Blazor/CacheContent.razor
@@ -0,0 +1,84 @@
+@using System.Reflection
+@using System.Text.Json
+@using Microsoft.JSInterop
+@inject IEternalCache Cache
+@inject IJSRuntime Js
+@inject NotificationService NotificationService
+
+
+
+
+
+ @if (_loading)
+ {
+
+ }
+ else
+ {
+
@_json
+ }
+
+
+@code {
+ [Parameter] public Type CacheType { get; set; }
+ [Parameter] public string CacheKey { get; set; }
+
+ private string _json;
+ private bool _loading = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadContentAsync();
+ }
+
+ private async Task LoadContentAsync()
+ {
+ _loading = true;
+ try
+ {
+ if (CacheType == null || string.IsNullOrEmpty(CacheKey))
+ {
+ _json = "(no key/type)";
+ return;
+ }
+
+ var method = typeof(ICache).GetMethod(nameof(ICache.PeekAsync), BindingFlags.Public | BindingFlags.Instance);
+ if (method == null)
+ {
+ _json = "(PeekAsync method not found)";
+ return;
+ }
+
+ var generic = method.MakeGenericMethod(CacheType);
+ Key key = CacheKey;
+ var task = (Task)generic.Invoke(Cache, [key]);
+ await task.ConfigureAwait(false);
+ var resultProperty = task.GetType().GetProperty("Result");
+ var data = resultProperty?.GetValue(task);
+
+ _json = data == null
+ ? "(no data)"
+ : JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
+ }
+ catch (Exception ex)
+ {
+ _json = $"(error: {ex.Message})";
+ }
+ finally
+ {
+ _loading = false;
+ }
+ }
+
+ private async Task CopyAction()
+ {
+ if (string.IsNullOrWhiteSpace(_json)) return;
+ await Js.InvokeVoidAsync("clipboard.copyText", _json);
+ NotificationService.Notify(new NotificationMessage
+ {
+ Summary = "Content copied.",
+ Severity = NotificationSeverity.Info
+ });
+ }
+}
diff --git a/Tharga.Cache.Blazor/DetailView.razor b/Tharga.Cache.Blazor/DetailView.razor
new file mode 100644
index 0000000..852c4fc
--- /dev/null
+++ b/Tharga.Cache.Blazor/DetailView.razor
@@ -0,0 +1,66 @@
+
+
+
+
+ Key
+ @CacheKey
+
+
+ Type
+ @CacheType?.Name
+
+
+
+
+ Created
+
+
+
+ Updated
+
+
+
+ Last Accessed
+
+
+
+
+
+ Fresh Span
+
+
+
+ Expires
+
+
+
+ Load Time
+
+
+
+
+
+ Size
+ @(Info?.Size.ToReadableByteSize() ?? "-")
+
+
+ Access Count
+ @(Info?.AccessCount.ToString() ?? "-")
+
+
+ Stale
+ @(Info?.IsStale.ToString() ?? "-")
+
+
+
+
+
+
+
+
+
+@code {
+ [Parameter] public Type CacheType { get; set; }
+ [Parameter] public string CacheKey { get; set; }
+ [Parameter] public CacheItemInfo Info { get; set; }
+}
diff --git a/Tharga.Cache.Blazor/ListView.razor b/Tharga.Cache.Blazor/ListView.razor
index ded970c..0907590 100644
--- a/Tharga.Cache.Blazor/ListView.razor
+++ b/Tharga.Cache.Blazor/ListView.razor
@@ -1,4 +1,6 @@
-@inject ICacheMonitor CacheMonitor
+@implements IDisposable
+@inject ICacheMonitor CacheMonitor
+@inject DialogService DialogService
@if (_model == null)
{
@@ -23,31 +25,11 @@ else
@item.Key
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -58,12 +40,13 @@ else
@item.Value.Size.ToReadableByteSize()
-
-
- @context.Value.AccessCount
+
+
+
+
-
@@ -73,6 +56,11 @@ else
@context.Type.Name
+
+
+ @FormatPersistType(context.PersistType)
+
+
@context.Items.Count
@@ -87,10 +75,46 @@ else
private CacheTypeInfo[] _model;
protected override Task OnInitializedAsync()
+ {
+ Refresh();
+ CacheMonitor.DataSetEvent += OnCacheChanged;
+ CacheMonitor.DataDropEvent += OnCacheChanged;
+ return base.OnInitializedAsync();
+ }
+
+ public void Dispose()
+ {
+ CacheMonitor.DataSetEvent -= OnCacheChanged;
+ CacheMonitor.DataDropEvent -= OnCacheChanged;
+ }
+
+ private void OnCacheChanged(object sender, EventArgs e)
+ {
+ Refresh();
+ InvokeAsync(StateHasChanged);
+ }
+
+ private void Refresh()
{
_model = CacheMonitor.GetInfos().ToArray();
+ }
- return base.OnInitializedAsync();
+ private static string FormatPersistType(Type persistType)
+ {
+ if (persistType == null) return "-";
+ var name = persistType.Name;
+ return name.StartsWith("I") && name.Length > 1 && char.IsUpper(name[1]) ? name.Substring(1) : name;
}
+ private async Task OpenDetailAsync(Type type, string key, CacheItemInfo info)
+ {
+ await DialogService.OpenAsync($"Cache: {key}",
+ new Dictionary
+ {
+ ["CacheType"] = type,
+ ["CacheKey"] = key,
+ ["Info"] = info
+ },
+ new DialogOptions { Width = "800px", Resizable = true, Draggable = true });
+ }
}
diff --git a/Tharga.Cache.Blazor/SummaryView.razor b/Tharga.Cache.Blazor/SummaryView.razor
index 6231147..7b6b19b 100644
--- a/Tharga.Cache.Blazor/SummaryView.razor
+++ b/Tharga.Cache.Blazor/SummaryView.razor
@@ -1,4 +1,5 @@
-@inject ICacheMonitor CacheMonitor
+@implements IDisposable
+@inject ICacheMonitor CacheMonitor
@@ -28,10 +29,23 @@
protected override Task OnInitializedAsync()
{
LoadCacheInfo();
-
+ CacheMonitor.DataSetEvent += OnCacheChanged;
+ CacheMonitor.DataDropEvent += OnCacheChanged;
return base.OnInitializedAsync();
}
+ public void Dispose()
+ {
+ CacheMonitor.DataSetEvent -= OnCacheChanged;
+ CacheMonitor.DataDropEvent -= OnCacheChanged;
+ }
+
+ private void OnCacheChanged(object sender, EventArgs e)
+ {
+ LoadCacheInfo();
+ InvokeAsync(StateHasChanged);
+ }
+
private void LoadCacheInfo()
{
_cacheCount = CacheMonitor.GetInfos()?.SelectMany(x => x.Items).Count() ?? 0;
diff --git a/Tharga.Cache.File.Tests/Tharga.Cache.File.Tests.csproj b/Tharga.Cache.File.Tests/Tharga.Cache.File.Tests.csproj
index 3136232..0cd2118 100644
--- a/Tharga.Cache.File.Tests/Tharga.Cache.File.Tests.csproj
+++ b/Tharga.Cache.File.Tests/Tharga.Cache.File.Tests.csproj
@@ -15,11 +15,11 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tharga.Cache.MongoDB.Tests/Tharga.Cache.MongoDB.Tests.csproj b/Tharga.Cache.MongoDB.Tests/Tharga.Cache.MongoDB.Tests.csproj
index 579c85f..7c3d375 100644
--- a/Tharga.Cache.MongoDB.Tests/Tharga.Cache.MongoDB.Tests.csproj
+++ b/Tharga.Cache.MongoDB.Tests/Tharga.Cache.MongoDB.Tests.csproj
@@ -15,11 +15,11 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tharga.Cache.Redis.Tests/Tharga.Cache.Redis.Tests.csproj b/Tharga.Cache.Redis.Tests/Tharga.Cache.Redis.Tests.csproj
index 0667623..112950a 100644
--- a/Tharga.Cache.Redis.Tests/Tharga.Cache.Redis.Tests.csproj
+++ b/Tharga.Cache.Redis.Tests/Tharga.Cache.Redis.Tests.csproj
@@ -15,11 +15,11 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj b/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj
index 3b768db..fac71f4 100644
--- a/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj
+++ b/Tharga.Cache.Tests/Tharga.Cache.Tests.csproj
@@ -15,11 +15,11 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tharga.Cache/CacheTypeInfo.cs b/Tharga.Cache/CacheTypeInfo.cs
index c8da8a6..1c01715 100644
--- a/Tharga.Cache/CacheTypeInfo.cs
+++ b/Tharga.Cache/CacheTypeInfo.cs
@@ -5,6 +5,7 @@ namespace Tharga.Cache;
public record CacheTypeInfo
{
public required Type Type { get; init; }
+ public required Type PersistType { get; init; }
public required bool StaleWhileRevalidate { get; init; }
public required bool ReturnDefaultOnFirstLoad { get; init; }
public required ConcurrentDictionary Items { get; init; }
diff --git a/Tharga.Cache/Core/CacheMonitor.cs b/Tharga.Cache/Core/CacheMonitor.cs
index 6f28e21..177c27a 100644
--- a/Tharga.Cache/Core/CacheMonitor.cs
+++ b/Tharga.Cache/Core/CacheMonitor.cs
@@ -23,10 +23,12 @@ public CacheMonitor(IPersistLoader persistLoader, CacheOptions cacheOptions)
public void Set(Type type, Key key, CacheItem item, bool staleWhileRevalidate, bool returnDefaultOnFirstLoad)
{
var size = item.Data.ToSize();
+ var persistType = _cacheOptions.Get().PersistType;
_caches.AddOrUpdate(type, new CacheTypeInfo
{
Type = type,
+ PersistType = persistType,
StaleWhileRevalidate = staleWhileRevalidate,
ReturnDefaultOnFirstLoad = returnDefaultOnFirstLoad,
Items = new ConcurrentDictionary(new Dictionary
@@ -64,10 +66,12 @@ public void Track(Type type, Key key, CacheItem item, bool staleWhileReval
return;
var size = item.Data.ToSize();
+ var persistType = _cacheOptions.Get().PersistType;
_caches.AddOrUpdate(type, new CacheTypeInfo
{
Type = type,
+ PersistType = persistType,
StaleWhileRevalidate = staleWhileRevalidate,
ReturnDefaultOnFirstLoad = returnDefaultOnFirstLoad,
Items = new ConcurrentDictionary(new Dictionary