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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Solid Cable channel browser — `GET /cable` lists all distinct channels with per-channel message count and last-message timestamp, ordered by most recent activity; a total-messages stat and channel-count stat are shown at the top; empty state shown when no messages exist; Cable subnav (Overview) added to the layout

## [0.5.0] - 2026-05-26

### Added
Expand Down
15 changes: 14 additions & 1 deletion app/controllers/solid_stack_web/cable_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@ module SolidStackWeb
class CableController < ApplicationController
def index
@total_messages = ::SolidCable::Message.count
@channels = ::SolidCable::Message.distinct.pluck(:channel).sort
@channels = channel_rows
end

private

def channel_rows
::SolidCable::Message
.group(:channel)
.order(Arel.sql("MAX(created_at) DESC"))
.pluck(:channel, Arel.sql("COUNT(*)"), Arel.sql("MAX(created_at)"))
.map do |ch, cnt, last_at|
last_at = Time.zone.parse(last_at) if last_at.is_a?(String)
{ channel: ch, message_count: cnt, last_message_at: last_at }
end
end
end
end
9 changes: 9 additions & 0 deletions app/views/layouts/solid_stack_web/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
</nav>
<% end %>

<% if current_section == :cable %>
<nav class="sqw-subnav">
<div class="sqw-subnav__inner">
<%= link_to "Overview", cable_path,
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cable"}" %>
</div>
</nav>
<% end %>

<% if current_section == :queue %>
<nav class="sqw-subnav">
<div class="sqw-subnav__inner">
Expand Down
16 changes: 14 additions & 2 deletions app/views/solid_stack_web/cable/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,24 @@
<% if @channels.any? %>
<table class="sqw-table">
<thead>
<tr><th>Channel</th></tr>
<tr>
<th>Channel</th>
<th>Messages</th>
<th>Last Message</th>
</tr>
</thead>
<tbody>
<% @channels.each do |channel| %>
<tr><td class="sqw-monospace"><%= channel %></td></tr>
<tr>
<td class="sqw-monospace"><%= channel[:channel] %></td>
<td><%= channel[:message_count] %></td>
<td class="sqw-muted"><%= channel[:last_message_at]&.strftime("%b %d %H:%M") %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<div class="sqw-empty">
<p>No cable messages.</p>
</div>
<% end %>
1 change: 1 addition & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.include ActiveSupport::Testing::TimeHelpers
end
29 changes: 28 additions & 1 deletion spec/requests/solid_stack_web/cable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,36 @@

get "#{engine_root}/cable"

# 2 distinct channels, stat value should be 2 (not 3 messages)
expect(response.body).to include("Channels")
expect(response.body).to match(/class="sqw-stat__value">\s*2\s*</)
end

it "shows message count and last message time per channel" do
SolidCable::Message.broadcast("sports:scores", "goal!")
SolidCable::Message.broadcast("sports:scores", "offside")

get "#{engine_root}/cable"

expect(response.body).to include("sports:scores")
expect(response.body).to include("Messages")
expect(response.body).to include("Last Message")
end

it "orders channels by most recent message first" do
SolidCable::Message.broadcast("older:channel", "first")
travel 1.second do
SolidCable::Message.broadcast("newer:channel", "second")
end

get "#{engine_root}/cable"

expect(response.body.index("newer:channel")).to be < response.body.index("older:channel")
end

it "shows empty state when no messages exist" do
SolidCable::Message.delete_all
get "#{engine_root}/cable"
expect(response.body).to include("No cable messages")
end
end
end