diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f592d..9f55dd7 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 +- 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 - Per-channel message list — `GET /cable/channels/:channel_hash` shows a paginated, reverse-chronological list of `SolidCable::Message` records for a specific channel; each row shows the message ID, a truncated payload preview (120 chars), and relative sent time with exact timestamp on hover; channel names in the channel browser are now links to their message list - 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 diff --git a/README.md b/README.md index 7c23216..60334bc 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,8 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru ### Features -- **Channel browser** — `GET /cable` lists all active channels with per-channel message count and last-message timestamp, ordered by most recent activity; 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 +- **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 --- diff --git a/ROADMAP.md b/ROADMAP.md index 44f9036..27fb30d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,7 +11,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 -- **Message search** — filter messages across channels by channel name or payload substring - **Message purge** — delete all messages for a channel or all messages older than N days - **Stats dashboard card** — expand the overview card to include messages per hour, oldest pending message age, and top channels by volume - **Cable timeline** — 24-hour chart of message volume diff --git a/app/controllers/solid_stack_web/cable_controller.rb b/app/controllers/solid_stack_web/cable_controller.rb index 59308a5..aefff80 100644 --- a/app/controllers/solid_stack_web/cable_controller.rb +++ b/app/controllers/solid_stack_web/cable_controller.rb @@ -1,6 +1,7 @@ module SolidStackWeb class CableController < ApplicationController def index + @search = params[:q].presence @total_messages = ::SolidCable::Message.count @channels = channel_rows end @@ -8,7 +9,11 @@ def index private def channel_rows - ::SolidCable::Message + scope = ::SolidCable::Message + if @search + scope = scope.where("channel LIKE ?", "%#{::ActiveRecord::Base.sanitize_sql_like(@search)}%") + end + scope .group(:channel, :channel_hash) .order(Arel.sql("MAX(created_at) DESC")) .pluck(:channel, :channel_hash, Arel.sql("COUNT(*)"), Arel.sql("MAX(created_at)")) diff --git a/app/controllers/solid_stack_web/cable_messages_controller.rb b/app/controllers/solid_stack_web/cable_messages_controller.rb index 36e28d5..fc6ef5e 100644 --- a/app/controllers/solid_stack_web/cable_messages_controller.rb +++ b/app/controllers/solid_stack_web/cable_messages_controller.rb @@ -1,9 +1,13 @@ module SolidStackWeb class CableMessagesController < ApplicationController def index - scope = ::SolidCable::Message.where(channel_hash: params[:channel_hash]).order(created_at: :desc) - @channel_name = scope.pick(:channel) || params[:channel_hash] - @pagy, @messages = pagy(scope) + @search = params[:q].presence + scope = ::SolidCable::Message.where(channel_hash: params[:channel_hash]) + if @search + scope = scope.where("payload LIKE ?", "%#{::ActiveRecord::Base.sanitize_sql_like(@search)}%") + end + @channel_name = ::SolidCable::Message.where(channel_hash: params[:channel_hash]).pick(:channel) || params[:channel_hash] + @pagy, @messages = pagy(scope.order(created_at: :desc)) end end end diff --git a/app/views/solid_stack_web/cable/index.html.erb b/app/views/solid_stack_web/cable/index.html.erb index 3d35578..4bd9d43 100644 --- a/app/views/solid_stack_web/cable/index.html.erb +++ b/app/views/solid_stack_web/cable/index.html.erb @@ -13,6 +13,15 @@ +
+ <% if @channels.any? %>No cable messages.
+<%= @search.present? ? "No channels matching “#{@search}”.".html_safe : "No cable messages." %>