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

- Cable timeline — 24-hour bar chart of message volume on the Solid Cable overview page; each bar represents one hour with a tooltip showing exact count; uses the `var(--info)` color token
- Cable stats dashboard card — the Solid Cable card on the overview dashboard now shows messages per hour (last 60 minutes), oldest pending message age, and a top-3 channel breakdown by message volume; Messages stat is now a link to the Cable overview
- Message purge — "Purge Old" form on the channel browser deletes all messages older than 1, 7, or 30 days; "Purge Channel" button on the per-channel message list deletes all messages for that channel; both redirect to the channel browser after purging
- Message search — channel browser (`GET /cable`) accepts `?q=` to filter the channel list by name substring; per-channel message list (`GET /cable/channels/:channel_hash`) accepts `?q=` to filter messages by payload substring; both show a contextual empty state and a Clear link when a search is active
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
### Features

- **Dashboard card** — the overview dashboard shows total messages, channels, messages per hour (last 60 minutes), oldest pending message age, and a top-3 channel breakdown by volume
- **24-hour timeline** — bar chart of message volume on the Cable overview page; each bar represents one hour with a hover tooltip showing the exact count
- **Channel browser** — `GET /cable` lists all active channels with per-channel message count and last-message timestamp, ordered by most recent activity; supports `?q=` filtering by channel name substring; empty state shown when no messages exist
- **Per-channel message list** — `GET /cable/channels/:channel_hash` shows a paginated, reverse-chronological list of that channel's `SolidCable::Message` records; each row shows the message ID, a truncated payload preview (120 chars) with the full payload on hover, and a relative sent time with the exact timestamp on hover; supports `?q=` filtering by payload substring; **Purge Channel** button deletes all messages for the channel
- **Message purge** — "Purge Old" form on the channel browser deletes all messages older than 1, 7, or 30 days; confirmation prompt before any destructive action
Expand Down
3 changes: 0 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das

> _Surface what's actually flowing through Action Cable._

### Added
- **Cable timeline** — 24-hour chart of message volume

---

## v0.7.0 — Interactivity & UX
Expand Down
9 changes: 9 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_07_dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,27 @@ a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; }
grid-template-columns: 1fr 1fr;
gap: 0;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow);
overflow: hidden;
}

.sqw-timeline-grid--single {
grid-template-columns: 1fr;
}

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

.sqw-timeline-chart--cable {
color: var(--info);
}

.sqw-timeline-chart + .sqw-timeline-chart {
border-left: 1px solid var(--border);
}
Expand Down
1 change: 1 addition & 0 deletions app/controllers/solid_stack_web/cable_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def index
@search = params[:q].presence
@total_messages = ::SolidCable::Message.count
@channels = channel_rows
@timeline = CableTimeline.new
end

private
Expand Down
12 changes: 12 additions & 0 deletions app/helpers/solid_stack_web/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ def cache_bytes_timeline_svg(timeline)
end
end

def cable_messages_timeline_svg(timeline)
build_sparkline_svg(
Struct.new(:buckets, :max).new(timeline.message_buckets, timeline.message_max),
css_class: "sqw-sparkline sqw-sparkline--lg",
aria_label: "Cable messages over the last 24 hours"
) do |count, i|
hours_ago = CableTimeline::HOURS - 1 - i
label = count == 1 ? "message" : "messages"
hours_ago.zero? ? "#{count} #{label} this hour" : "#{count} #{label} #{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
32 changes: 32 additions & 0 deletions app/models/solid_stack_web/cable_timeline.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module SolidStackWeb
class CableTimeline
HOURS = 24

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

def message_max
message_buckets.max || 0
end

private

def rows
@rows ||= begin
origin = Time.current - HOURS.hours
::SolidCable::Message.where(created_at: origin..Time.current).pluck(:created_at)
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
15 changes: 15 additions & 0 deletions app/views/solid_stack_web/cable/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@
</div>
</div>

<div class="sqw-timeline-grid sqw-timeline-grid--single" data-controller="sparkline-tooltip">
<div class="sqw-sparkline-wrap sqw-timeline-chart sqw-timeline-chart--cable">
<span class="sqw-sparkline-label">Messages — last 24 hours</span>
<div class="sqw-sparkline-positioner">
<%= cable_messages_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>

<form class="sqw-filters" action="<%= cable_path %>" method="get" data-controller="search">
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
placeholder="Filter by channel…" autocomplete="off" aria-label="Filter by channel"
Expand Down