diff --git a/CHANGELOG.md b/CHANGELOG.md index 6edfa01..c5549a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 60e5a22..317ce8f 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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 --- diff --git a/ROADMAP.md b/ROADMAP.md index 25c064a..5a79172 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/app/assets/stylesheets/solid_stack_web/_09_detail.css b/app/assets/stylesheets/solid_stack_web/_09_detail.css index dda42a3..f2463b4 100644 --- a/app/assets/stylesheets/solid_stack_web/_09_detail.css +++ b/app/assets/stylesheets/solid_stack_web/_09_detail.css @@ -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; diff --git a/app/controllers/solid_stack_web/cache_entries_controller.rb b/app/controllers/solid_stack_web/cache_entries_controller.rb index f15ecde..ad976ba 100644 --- a/app/controllers/solid_stack_web/cache_entries_controller.rb +++ b/app/controllers/solid_stack_web/cache_entries_controller.rb @@ -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]), diff --git a/app/helpers/solid_stack_web/application_helper.rb b/app/helpers/solid_stack_web/application_helper.rb index b17288d..73adc1d 100644 --- a/app/helpers/solid_stack_web/application_helper.rb +++ b/app/helpers/solid_stack_web/application_helper.rb @@ -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 diff --git a/app/views/solid_stack_web/cache_entries/index.html.erb b/app/views/solid_stack_web/cache_entries/index.html.erb index cac1967..25998e9 100644 --- a/app/views/solid_stack_web/cache_entries/index.html.erb +++ b/app/views/solid_stack_web/cache_entries/index.html.erb @@ -42,7 +42,9 @@
<% @entries.each do |entry| %><%= truncated ? content[0, 4096] : content %>+ <% if truncated %> +
Showing first 4 KB of <%= number_to_human_size(@entry.byte_size) %> total.
+ <% end %> + <% else %> +Value preview is disabled. Set config.allow_value_preview = true in your initializer to enable.