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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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
- 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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ SolidStackWeb.configure do |config|

# Maximum results shown by the search feature (default: 25).
config.search_results_limit = 25

# Show the raw serialized value on the cache entry detail page (default: false).
# Disable for stores that contain sensitive data.
config.allow_value_preview = true
end
```

Expand All @@ -97,9 +101,10 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
### Features

- **Overview dashboard card** — live entry count and total byte size
- **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 search** — filter entries by key substring
- **Delete entry** — per-row delete button removes a single cache entry
- **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)
- **Delete entry** — per-row delete button or detail-page button removes a single cache entry
- **Flush All** — header button deletes every cache entry with a confirmation prompt

---
Expand Down
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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
- **Entry detail** — view the serialised value of a single cache entry (with a configurable `allow_value_preview` toggle for sensitive data)
- **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
42 changes: 42 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_09_detail.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,48 @@
overflow-y: auto;
}

.sqw-detail {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 1.5rem;
font-size: 13px;
margin-bottom: 1.5rem;
}
.sqw-detail__row {
display: contents;
}
.sqw-detail__row dt { color: var(--muted); white-space: nowrap; align-self: start; padding-top: 0.15rem; }
.sqw-detail__row dd { word-break: break-all; }

.sqw-value-preview { margin-top: 1rem; }

.sqw-value-preview__header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.sqw-value-preview__header .sqw-section-title { margin-bottom: 0; }

.sqw-value-pre {
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.75rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 500px;
overflow-y: auto;
}

.sqw-value-truncated { font-size: 12px; margin-top: 0.5rem; }

.sqw-link { color: var(--primary); text-decoration: none; }
.sqw-link:hover { text-decoration: underline; }

.sqw-code-input {
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: 12px;
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/solid_stack_web/cache_entries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def index
@pagy, @entries = pagy(scope)
end

def show
@entry = ::SolidCache::Entry.find(params[:id])
end

def destroy
::SolidCache::Entry.find(params[:id]).destroy
redirect_to cache_entries_path(q: params[:q], column: params[:column], direction: params[:direction]),
Expand Down
8 changes: 8 additions & 0 deletions app/helpers/solid_stack_web/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
module SolidStackWeb
module ApplicationHelper
def format_cache_value(raw)
str = raw.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
parsed = JSON.parse(str)
{ label: "JSON", content: JSON.pretty_generate(parsed) }
rescue JSON::ParserError, JSON::GeneratorError
{ label: "Text", content: str }
end

def format_duration(seconds)
return "—" if seconds.nil?
return "#{(seconds * 1000).round}ms" if seconds < 1
Expand Down
4 changes: 3 additions & 1 deletion app/views/solid_stack_web/cache_entries/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
<tbody>
<% @entries.each do |entry| %>
<tr id="cache_entry_<%= entry.id %>">
<td class="sqw-monospace sqw-truncate" title="<%= entry.key %>"><%= entry.key %></td>
<td class="sqw-monospace sqw-truncate" title="<%= entry.key %>">
<%= link_to entry.key, cache_entry_path(entry), class: "sqw-link" %>
</td>
<td><%= number_to_human_size(entry.byte_size) %></td>
<td class="sqw-muted"><%= entry.created_at.strftime("%b %d %H:%M") %></td>
<td class="sqw-actions">
Expand Down
48 changes: 48 additions & 0 deletions app/views/solid_stack_web/cache_entries/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<div class="sqw-page-header sqw-page-header--split">
<h1 class="sqw-page-title sqw-truncate" title="<%= @entry.key %>"><%= @entry.key %></h1>
<div class="sqw-header-actions">
<%= button_to "Delete",
cache_entry_path(@entry),
method: :delete,
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
data: { turbo_confirm: "Delete this cache entry?" } %>
<%= link_to "← Entries", cache_entries_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
</div>
</div>

<dl class="sqw-detail">
<div class="sqw-detail__row">
<dt>Key</dt>
<dd class="sqw-monospace"><%= @entry.key %></dd>
</div>
<div class="sqw-detail__row">
<dt>Size</dt>
<dd><%= number_to_human_size(@entry.byte_size) %></dd>
</div>
<div class="sqw-detail__row">
<dt>Created</dt>
<dd class="sqw-muted"><%= @entry.created_at.strftime("%b %d, %Y %H:%M:%S %Z") %></dd>
</div>
</dl>

<section class="sqw-value-preview">
<div class="sqw-value-preview__header">
<h2 class="sqw-section-title">Value</h2>
<% if SolidStackWeb.allow_value_preview %>
<% formatted = format_cache_value(@entry.value) %>
<span class="sqw-badge sqw-badge--queue"><%= formatted[:label] %></span>
<% end %>
</div>
<% if SolidStackWeb.allow_value_preview %>
<% content = formatted[:content] %>
<% truncated = content.length > 4096 %>
<pre class="sqw-value-pre"><%= truncated ? content[0, 4096] : content %></pre>
<% if truncated %>
<p class="sqw-muted sqw-value-truncated">Showing first 4 KB of <%= number_to_human_size(@entry.byte_size) %> total.</p>
<% end %>
<% else %>
<div class="sqw-empty">
<p>Value preview is disabled. Set <code>config.allow_value_preview = true</code> in your initializer to enable.</p>
</div>
<% end %>
</section>
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
get "stats", to: "stats#index", as: :stats
get "history", to: "history#index", as: :history
get "cache", to: "cache#index", as: :cache
resources :cache_entries, only: [:index, :destroy], path: "cache/entries"
resources :cache_entries, only: [:index, :show, :destroy], path: "cache/entries"
resource :cache_flush, only: [:destroy], path: "cache/flush", controller: "cache/flushes"
get "cable", to: "cable#index", as: :cable
end
6 changes: 5 additions & 1 deletion lib/solid_stack_web.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class << self
:alert_webhook_url, :alert_webhook_cooldown,
:alert_failure_threshold, :alert_queue_thresholds,
:dashboard_refresh_interval, :default_refresh_interval,
:search_results_limit
:search_results_limit, :allow_value_preview

def page_size
@page_size || 25
Expand Down Expand Up @@ -49,6 +49,10 @@ def search_results_limit
@search_results_limit || 25
end

def allow_value_preview
@allow_value_preview || false
end

def configure
yield self
end
Expand Down
3 changes: 3 additions & 0 deletions spec/dummy/config/initializers/solid_stack_web.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SolidStackWeb.configure do |config|
config.allow_value_preview = true
end
25 changes: 25 additions & 0 deletions spec/requests/solid_stack_web/cache_entries_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ def create_entry(key: "mykey", value: "myval")
end
end

describe "GET /cache/entries/:id" do
it "returns 200 and shows key metadata" do
entry = create_entry(key: "detail:key", value: "hello")
get "#{engine_root}/cache/entries/#{entry.id}"
expect(response).to have_http_status(:ok)
expect(response.body).to include("detail:key")
expect(response.body).to include("Bytes") # number_to_human_size
end

it "hides value when allow_value_preview is false" do
entry = create_entry(key: "secret:key", value: "sensitive")
allow(SolidStackWeb).to receive(:allow_value_preview).and_return(false)
get "#{engine_root}/cache/entries/#{entry.id}"
expect(response.body).to include("allow_value_preview")
expect(response.body).not_to include("sensitive")
end

it "shows value when allow_value_preview is true" do
entry = create_entry(key: "visible:key", value: "my_cached_value")
allow(SolidStackWeb).to receive(:allow_value_preview).and_return(true)
get "#{engine_root}/cache/entries/#{entry.id}"
expect(response.body).to include("my_cached_value")
end
end

describe "DELETE /cache/entries/:id" do
it "deletes the entry and redirects" do
entry = create_entry
Expand Down