From 0e5400fe4a54cec2b0dacc6b0786f21cd39ce1ef Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 26 May 2026 09:49:21 -0400 Subject: [PATCH 1/3] feat: add oldest-entry age to cache stats dashboard card (#29) CacheStats#to_h gains oldest_entry (SolidCache::Entry.minimum(:created_at)). The cache card shows a time_ago_in_words value with the exact timestamp as a title tooltip; stat is omitted entirely when no entries exist. Entries stat is now a link to the entry browser. Metrics endpoint includes oldest_entry as ISO 8601 (or null). Closes #29 Co-Authored-By: Claude Sonnet 4.6 --- .../solid_stack_web/_07_dashboard.css | 1 + app/models/solid_stack_web/cache_stats.rb | 5 +++-- .../solid_stack_web/dashboard/index.html.erb | 10 ++++++++-- spec/requests/solid_stack_web/dashboard_spec.rb | 12 ++++++++++++ spec/requests/solid_stack_web/metrics_spec.rb | 16 +++++++++++++++- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/solid_stack_web/_07_dashboard.css b/app/assets/stylesheets/solid_stack_web/_07_dashboard.css index 2b0a6e0..d561608 100644 --- a/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +++ b/app/assets/stylesheets/solid_stack_web/_07_dashboard.css @@ -69,6 +69,7 @@ a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; } } .sqw-inline-stat__value { font-size: 28px; font-weight: 700; line-height: 1; } +.sqw-inline-stat__value--sm { font-size: 16px; } .sqw-inline-stat--ready .sqw-inline-stat__value { color: var(--success); } .sqw-inline-stat--scheduled .sqw-inline-stat__value { color: var(--info); } diff --git a/app/models/solid_stack_web/cache_stats.rb b/app/models/solid_stack_web/cache_stats.rb index adf697a..226a49c 100644 --- a/app/models/solid_stack_web/cache_stats.rb +++ b/app/models/solid_stack_web/cache_stats.rb @@ -2,8 +2,9 @@ module SolidStackWeb class CacheStats def to_h { - entries: ::SolidCache::Entry.count, - byte_size: ::SolidCache::Entry.sum(:byte_size) + entries: ::SolidCache::Entry.count, + byte_size: ::SolidCache::Entry.sum(:byte_size), + oldest_entry: ::SolidCache::Entry.minimum(:created_at) } end end diff --git a/app/views/solid_stack_web/dashboard/index.html.erb b/app/views/solid_stack_web/dashboard/index.html.erb index f7d2eb6..9c3ab2b 100644 --- a/app/views/solid_stack_web/dashboard/index.html.erb +++ b/app/views/solid_stack_web/dashboard/index.html.erb @@ -76,14 +76,20 @@ <%= link_to "View Cache →", cache_path, class: "sqw-gem-card__link" %>
-
+ <%= link_to cache_entries_path, class: "sqw-inline-stat sqw-inline-stat--cache" do %> Entries <%= @cache_stats[:entries] %> -
+ <% end %>
Size <%= number_to_human_size(@cache_stats[:byte_size]) %>
+ <% if @cache_stats[:oldest_entry] %> +
+ Oldest + "><%= time_ago_in_words(@cache_stats[:oldest_entry]) %> +
+ <% end %>
diff --git a/spec/requests/solid_stack_web/dashboard_spec.rb b/spec/requests/solid_stack_web/dashboard_spec.rb index c99e4d2..b70967a 100644 --- a/spec/requests/solid_stack_web/dashboard_spec.rb +++ b/spec/requests/solid_stack_web/dashboard_spec.rb @@ -86,5 +86,17 @@ ensure SolidStackWeb.slow_job_threshold = nil end + + it "shows oldest cache entry age when entries exist" do + SolidCache::Entry.write("dashboard:key", "v") + get engine_root + expect(response.body).to include("Oldest") + end + + it "omits oldest cache entry stat when no entries exist" do + SolidCache::Entry.delete_all + get engine_root + expect(response.body).not_to include("Oldest") + end end end diff --git a/spec/requests/solid_stack_web/metrics_spec.rb b/spec/requests/solid_stack_web/metrics_spec.rb index 2a19631..3f596c8 100644 --- a/spec/requests/solid_stack_web/metrics_spec.rb +++ b/spec/requests/solid_stack_web/metrics_spec.rb @@ -26,7 +26,21 @@ it "includes cache keys" do get "#{engine_root}/metrics" cache = JSON.parse(response.body)["cache"] - expect(cache.keys).to contain_exactly("entries", "byte_size") + expect(cache.keys).to contain_exactly("entries", "byte_size", "oldest_entry") + end + + it "includes oldest_entry as ISO 8601 when entries exist" do + SolidCache::Entry.write("metrics:key", "v") + get "#{engine_root}/metrics" + cache = JSON.parse(response.body)["cache"] + expect(cache["oldest_entry"]).to match(/\d{4}-\d{2}-\d{2}T/) + end + + it "includes oldest_entry as null when no entries exist" do + SolidCache::Entry.delete_all + get "#{engine_root}/metrics" + cache = JSON.parse(response.body)["cache"] + expect(cache["oldest_entry"]).to be_nil end it "includes cable keys" do From a245285dead0954c0a03824e6086bd400f6bbd8c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 26 May 2026 09:50:59 -0400 Subject: [PATCH 2/3] docs: update CHANGELOG, README, and ROADMAP for cache stats card Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 3 ++- README.md | 4 ++-- ROADMAP.md | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5549a2..af10555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Cache entry detail page — `GET /cache/entries/:id` shows the full key, byte size, and created-at for a single entry; value body is gated behind `SolidStackWeb.allow_value_preview` (default `false`); when enabled, displays up to 4 KB of the raw serialized value with a truncation notice; entry key in the browser table is now a link to the detail page; **Delete** button on the detail page redirects back to the entry list +- Cache stats dashboard card — adds **Oldest** entry age (via `time_ago_in_words` with exact timestamp on hover) to the Solid Cache overview card; stat is hidden when the cache is empty; **Entries** count is now a link to the entry browser; `oldest_entry` added to the `/metrics` JSON payload as ISO 8601 (or `null`) +- Cache entry detail page — `GET /cache/entries/:id` shows the full key, byte size, and created-at for a single entry; value body is gated behind `SolidStackWeb.allow_value_preview` (default `false`); when enabled, displays up to 4 KB with JSON pretty-printing and a format badge (JSON / Text); entry key in the browser table is now a link to the detail page; **Delete** button on the detail page redirects back to the entry list - Solid Cache entry browser — `GET /cache/entries` lists all `SolidCache::Entry` records in a paginated, sortable table (key, byte size, created-at); sortable by any column; key-substring search filters the list; per-row **Delete** removes a single entry; **Flush All** header button deletes every entry with a confirmation prompt; Cache subnav (Overview / Entries) added to the layout ## [0.4.0] - 2026-05-26 diff --git a/README.md b/README.md index 317ce8f..826c615 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru ### Features -- **Overview dashboard card** — live entry count and total byte size +- **Overview dashboard card** — live entry count (linked to the entry browser), total byte size, and oldest-entry age (`time_ago_in_words` with exact timestamp on hover; hidden when cache is empty) - **Entry browser** — `GET /cache/entries` lists all `SolidCache::Entry` records in a paginated, sortable table; columns: key, byte size, created-at; sortable by any column; key auto-submits search after 4 characters - **Key search** — filter entries by key substring; results update automatically after 4 characters - **Entry detail page** — `GET /cache/entries/:id` shows the full key, byte size, and created-at; optionally displays the raw serialized value (see `allow_value_preview` below) @@ -133,7 +133,7 @@ _Channel monitoring coming in v0.6.0. Currently shows active message count and d "processes_stale": 0, "slow_jobs": 7 }, - "cache": { "entries": 1024, "byte_size": 2097152 }, + "cache": { "entries": 1024, "byte_size": 2097152, "oldest_entry": "2026-05-20T10:00:00Z" }, "cable": { "messages": 50, "channels": 3 }, "generated_at": "2026-05-26T10:00:00Z" } diff --git a/ROADMAP.md b/ROADMAP.md index f00ad79..73a5325 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,7 +13,6 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das ### Added - **Size distribution stats** — top-N entries by byte size; histogram bucketed by size range - **Cache timeline** — 24-hour chart of entry count and total byte size growth -- **Stats dashboard card** — expand the overview card to include hit/miss rates if `SolidCache` exposes them, plus oldest-entry age --- From bd1430c68a1e313669234d30ecb25c2255250eed Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 26 May 2026 09:55:09 -0400 Subject: [PATCH 3/3] feat: make Total Entries card on cache overview link to entry browser Co-Authored-By: Claude Sonnet 4.6 --- app/views/solid_stack_web/cache/index.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/solid_stack_web/cache/index.html.erb b/app/views/solid_stack_web/cache/index.html.erb index 91c93d6..cb4f83f 100644 --- a/app/views/solid_stack_web/cache/index.html.erb +++ b/app/views/solid_stack_web/cache/index.html.erb @@ -3,10 +3,10 @@
-
+ <%= link_to cache_entries_path, class: "sqw-stat sqw-stat--cache" do %> Total Entries <%= @total_entries %> -
+ <% end %>
Total Size <%= number_to_human_size(@total_byte_size) %>