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 timeline — Solid Cache overview page gains two side-by-side 24-hour bar charts: **Entries written per hour** and **Bytes written per hour**; each bar has a hover tooltip via the `sparkline-tooltip` Stimulus controller; backed by a `CacheTimeline` PORO (single query, bucketed in Ruby)
- Cache size distribution stats — Solid Cache overview page gains a **Size Distribution** table (5 byte-range buckets with proportional inline bars) and a **Largest Entries** table (top 10 by byte size, each linked to its detail page); both sections are hidden when the cache is empty
- 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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
### Features

- **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)
- **Cache timeline** — two side-by-side 24-hour bar charts showing entries written per hour and bytes written per hour; each bar has a hover tooltip
- **Size distribution** — byte-range histogram (< 1 KB → > 1 MB) with entry counts and proportional inline bars; hidden when cache is empty
- **Largest entries** — top 10 entries by byte size, each linked to the detail page; 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
Expand Down
9 changes: 0 additions & 9 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das

---

## v0.5.0 — Solid Cache: Deep Monitoring

> _Move beyond a single count; give operators visibility into what's in the cache._

### Added
- **Cache timeline** — 24-hour chart of entry count and total byte size growth

---

## v0.6.0 — Solid Cable: Channel Monitoring

> _Surface what's actually flowing through Action Cable._
Expand Down
24 changes: 22 additions & 2 deletions app/assets/stylesheets/solid_stack_web/_07_dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,28 @@ a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; }
height: 40px;
}

.sqw-sparkline--sm {
height: 24px;
.sqw-sparkline--sm { height: 24px; }
.sqw-sparkline--lg { height: 64px; }

.sqw-timeline-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
margin-top: 1.5rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow);
overflow: hidden;
}

.sqw-timeline-chart {
border-top: none;
color: var(--purple);
}

.sqw-timeline-chart + .sqw-timeline-chart {
border-left: 1px solid var(--border);
}

.sqw-queue-sparkline {
Expand Down
1 change: 1 addition & 0 deletions app/controllers/solid_stack_web/cache_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def index
@total_entries = ::SolidCache::Entry.count
@total_byte_size = ::SolidCache::Entry.sum(:byte_size)
@size_stats = CacheSizeStats.new
@timeline = CacheTimeline.new
end
end
end
24 changes: 24 additions & 0 deletions app/helpers/solid_stack_web/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ def format_duration(seconds)
"#{s / 3600}h #{(s % 3600) / 60}m"
end

def cache_entries_timeline_svg(timeline)
build_sparkline_svg(
Struct.new(:buckets, :max).new(timeline.entry_buckets, timeline.entry_max),
css_class: "sqw-sparkline sqw-sparkline--lg",
aria_label: "Cache entries written over the last 24 hours"
) do |count, i|
hours_ago = CacheTimeline::HOURS - 1 - i
hours_ago.zero? ? "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} this hour" \
: "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} #{hours_ago}h ago"
end
end

def cache_bytes_timeline_svg(timeline)
build_sparkline_svg(
Struct.new(:buckets, :max).new(timeline.byte_buckets, timeline.byte_max),
css_class: "sqw-sparkline sqw-sparkline--lg",
aria_label: "Cache bytes written over the last 24 hours"
) do |bytes, i|
hours_ago = CacheTimeline::HOURS - 1 - i
size = number_to_human_size(bytes)
hours_ago.zero? ? "#{size} written this hour" : "#{size} written #{hours_ago}h ago"
end
end

def throughput_sparkline_svg(sparkline)
build_sparkline_svg(sparkline, aria_label: "Throughput over the last 12 hours") do |count, i|
hours_ago = SolidStackWeb::ThroughputSparkline::HOURS - i
Expand Down
40 changes: 40 additions & 0 deletions app/models/solid_stack_web/cache_timeline.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module SolidStackWeb
class CacheTimeline
HOURS = 24

def entry_buckets
@entry_buckets ||= build_buckets { |rows, from, to| rows.count { |t, _| t >= from && t < to } }
end

def byte_buckets
@byte_buckets ||= build_buckets { |rows, from, to| rows.sum { |t, b| t >= from && t < to ? b : 0 } }
end

def entry_max
entry_buckets.max || 0
end

def byte_max
byte_buckets.max || 0
end

private

def rows
@rows ||= begin
origin = Time.current - HOURS.hours
::SolidCache::Entry.where(created_at: origin..Time.current).pluck(:created_at, :byte_size)
end
end

def build_buckets(&block)
now = Time.current
origin = now - HOURS.hours
HOURS.times.map do |i|
from = origin + i.hours
to = origin + (i + 1).hours
block.call(rows, from, to)
end
end
end
end
38 changes: 34 additions & 4 deletions app/views/solid_stack_web/cache/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
<tr>
<td class="sqw-muted"><%=bucket[:label] %></td>
<td><%= bucket[:count] %></td>
<td class="sqw-dist-bar-cell">
<div class="sqw-dist-bar" style="width: <%= pct %>%"></div>
<span class="sqw-dist-pct sqw-muted"><%= pct %>%</span>
<td>
<div class="sqw-dist-bar-cell">
<div class="sqw-dist-bar" style="width: <%= pct %>%"></div>
<span class="sqw-dist-pct sqw-muted"><%= pct %>%</span>
</div>
</td>
</tr>
<% end %>
Expand Down Expand Up @@ -63,4 +65,32 @@
</table>
</section>
</div>
<% end %>
<% end %>

<div class="sqw-timeline-grid" data-controller="sparkline-tooltip">
<div class="sqw-sparkline-wrap sqw-timeline-chart">
<span class="sqw-sparkline-label">Entries written — last 24 hours</span>
<div class="sqw-sparkline-positioner">
<%= cache_entries_timeline_svg(@timeline) %>
<div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
</div>
<div class="sqw-sparkline-axis">
<span>24h ago</span>
<span>12h ago</span>
<span>now</span>
</div>
</div>

<div class="sqw-sparkline-wrap sqw-timeline-chart">
<span class="sqw-sparkline-label">Bytes written — last 24 hours</span>
<div class="sqw-sparkline-positioner">
<%= cache_bytes_timeline_svg(@timeline) %>
<div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
</div>
<div class="sqw-sparkline-axis">
<span>24h ago</span>
<span>12h ago</span>
<span>now</span>
</div>
</div>
</div>
12 changes: 12 additions & 0 deletions spec/requests/solid_stack_web/cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,17 @@
expect(response.body).to include("linked:key")
expect(response.body).to include("cache/entries")
end

it "renders 24-hour timeline charts" do
get "#{engine_root}/cache"
expect(response.body).to include("Entries written — last 24 hours")
expect(response.body).to include("Bytes written — last 24 hours")
end

it "shows timeline bars for entries written in the last 24 hours" do
SolidCache::Entry.write("recent:key", "v")
get "#{engine_root}/cache"
expect(response.body).to include("sqw-sparkline--lg")
end
end
end