Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
}
Expand Down
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/solid_stack_web/_07_dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down
5 changes: 3 additions & 2 deletions app/models/solid_stack_web/cache_stats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/views/solid_stack_web/cache/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
</div>

<div class="sqw-stats-grid">
<div class="sqw-stat sqw-stat--cache">
<%= link_to cache_entries_path, class: "sqw-stat sqw-stat--cache" do %>
<span class="sqw-stat__label">Total Entries</span>
<span class="sqw-stat__value"><%= @total_entries %></span>
</div>
<% end %>
<div class="sqw-stat sqw-stat--cache">
<span class="sqw-stat__label">Total Size</span>
<span class="sqw-stat__value"><%= number_to_human_size(@total_byte_size) %></span>
Expand Down
10 changes: 8 additions & 2 deletions app/views/solid_stack_web/dashboard/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,20 @@
<%= link_to "View Cache →", cache_path, class: "sqw-gem-card__link" %>
</div>
<div class="sqw-gem-card__body">
<div class="sqw-inline-stat sqw-inline-stat--cache">
<%= link_to cache_entries_path, class: "sqw-inline-stat sqw-inline-stat--cache" do %>
<span class="sqw-inline-stat__label">Entries</span>
<span class="sqw-inline-stat__value"><%= @cache_stats[:entries] %></span>
</div>
<% end %>
<div class="sqw-inline-stat sqw-inline-stat--cache">
<span class="sqw-inline-stat__label">Size</span>
<span class="sqw-inline-stat__value"><%= number_to_human_size(@cache_stats[:byte_size]) %></span>
</div>
<% if @cache_stats[:oldest_entry] %>
<div class="sqw-inline-stat sqw-inline-stat--cache">
<span class="sqw-inline-stat__label">Oldest</span>
<span class="sqw-inline-stat__value sqw-inline-stat__value--sm" title="<%= @cache_stats[:oldest_entry].strftime("%b %d, %Y %H:%M") %>"><%= time_ago_in_words(@cache_stats[:oldest_entry]) %></span>
</div>
<% end %>
</div>
</div>

Expand Down
12 changes: 12 additions & 0 deletions spec/requests/solid_stack_web/dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 15 additions & 1 deletion spec/requests/solid_stack_web/metrics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down