solid_stack_web 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -2
  3. data/app/assets/stylesheets/solid_stack_web/_01_base.css +3 -2
  4. data/app/assets/stylesheets/solid_stack_web/_02_layout.css +28 -0
  5. data/app/assets/stylesheets/solid_stack_web/_04_table.css +13 -1
  6. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +49 -1
  7. data/app/assets/stylesheets/solid_stack_web/_08_filters.css +6 -0
  8. data/app/assets/stylesheets/solid_stack_web/_10_responsive.css +54 -0
  9. data/app/assets/stylesheets/solid_stack_web/_12_dark_mode.css +34 -0
  10. data/app/controllers/solid_stack_web/cable/channel_purges_controller.rb +8 -0
  11. data/app/controllers/solid_stack_web/cable/purges_controller.rb +9 -0
  12. data/app/controllers/solid_stack_web/cable_controller.rb +20 -1
  13. data/app/controllers/solid_stack_web/cable_messages_controller.rb +13 -0
  14. data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +4 -3
  15. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +2 -1
  16. data/app/controllers/solid_stack_web/jobs/selections_controller.rb +2 -2
  17. data/app/controllers/solid_stack_web/jobs_controller.rb +4 -2
  18. data/app/helpers/solid_stack_web/application_helper.rb +25 -0
  19. data/app/javascript/solid_stack_web/application.js +5 -1
  20. data/app/javascript/solid_stack_web/theme_controller.js +26 -0
  21. data/app/javascript/solid_stack_web/timestamp_controller.js +59 -0
  22. data/app/models/solid_stack_web/cable_stats.rb +15 -2
  23. data/app/models/solid_stack_web/cable_timeline.rb +32 -0
  24. data/app/views/layouts/solid_stack_web/application.html.erb +18 -4
  25. data/app/views/solid_stack_web/cable/index.html.erb +56 -3
  26. data/app/views/solid_stack_web/cable_messages/index.html.erb +56 -0
  27. data/app/views/solid_stack_web/cache_entries/index.html.erb +9 -3
  28. data/app/views/solid_stack_web/cache_entries/show.html.erb +1 -1
  29. data/app/views/solid_stack_web/dashboard/index.html.erb +25 -4
  30. data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +3 -0
  31. data/app/views/solid_stack_web/failed_jobs/index.html.erb +4 -3
  32. data/app/views/solid_stack_web/failed_jobs/show.html.erb +1 -1
  33. data/app/views/solid_stack_web/history/index.html.erb +9 -3
  34. data/app/views/solid_stack_web/jobs/_empty.html.erb +19 -2
  35. data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +3 -0
  36. data/app/views/solid_stack_web/jobs/index.html.erb +3 -3
  37. data/app/views/solid_stack_web/jobs/show.html.erb +4 -4
  38. data/app/views/solid_stack_web/processes/index.html.erb +3 -2
  39. data/app/views/solid_stack_web/queues/index.html.erb +2 -1
  40. data/app/views/solid_stack_web/queues/show.html.erb +2 -2
  41. data/app/views/solid_stack_web/recurring_tasks/index.html.erb +5 -5
  42. data/app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb +1 -1
  43. data/app/views/solid_stack_web/shared/_flash.html.erb +1 -0
  44. data/app/views/solid_stack_web/stats/index.html.erb +2 -1
  45. data/config/importmap.rb +2 -0
  46. data/config/routes.rb +4 -1
  47. data/lib/solid_stack_web/version.rb +1 -1
  48. metadata +11 -1
@@ -4,12 +4,13 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Solid Stack Dashboard</title>
7
+ <link rel="icon" href="data:,">
7
8
  <%= csrf_meta_tags %>
8
9
  <%= csp_meta_tag %>
9
10
  <%= inline_styles %>
10
11
  <%= javascript_importmap_tags "solid_stack_web" %>
11
12
  </head>
12
- <body>
13
+ <body data-controller="theme">
13
14
  <header class="sqw-header">
14
15
  <div class="sqw-header__inner">
15
16
  <%= link_to "Solid Stack", root_path, class: "sqw-header__logo" %>
@@ -21,6 +22,8 @@
21
22
  <%= link_to "Cable", cable_path,
22
23
  class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cable}" %>
23
24
  </nav>
25
+ <button class="sqw-theme-toggle" aria-label="Switch to dark mode"
26
+ data-theme-target="toggle" data-action="theme#toggle">☽</button>
24
27
  </div>
25
28
  </header>
26
29
 
@@ -35,6 +38,15 @@
35
38
  </nav>
36
39
  <% end %>
37
40
 
41
+ <% if current_section == :cable %>
42
+ <nav class="sqw-subnav">
43
+ <div class="sqw-subnav__inner">
44
+ <%= link_to "Overview", cable_path,
45
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cable"}" %>
46
+ </div>
47
+ </nav>
48
+ <% end %>
49
+
38
50
  <% if current_section == :queue %>
39
51
  <nav class="sqw-subnav">
40
52
  <div class="sqw-subnav__inner">
@@ -57,9 +69,11 @@
57
69
  <% end %>
58
70
 
59
71
  <main class="sqw-main">
60
- <% flash.each do |type, message| %>
61
- <div class="sqw-flash sqw-flash--<%= type %>"><%= message %></div>
62
- <% end %>
72
+ <div id="sqw-flash-container">
73
+ <% flash.each do |type, message| %>
74
+ <div class="sqw-flash sqw-flash--<%= type %>"><%= message %></div>
75
+ <% end %>
76
+ </div>
63
77
  <%= yield %>
64
78
  </main>
65
79
  </body>
@@ -1,5 +1,14 @@
1
- <div class="sqw-page-header">
1
+ <div class="sqw-page-header sqw-page-header--split">
2
2
  <h1 class="sqw-page-title">Solid Cable</h1>
3
+ <div class="sqw-header-actions">
4
+ <%= form_with url: cable_purge_path, method: :delete, class: "sqw-inline-form",
5
+ data: { turbo_confirm: "Purge these messages? This cannot be undone." } do |f| %>
6
+ <%= f.select :older_than,
7
+ [["Older than 1 day", 1], ["Older than 7 days", 7], ["Older than 30 days", 30]],
8
+ {}, class: "sqw-select" %>
9
+ <%= f.submit "Purge Old", class: "sqw-btn sqw-btn--danger sqw-btn--sm" %>
10
+ <% end %>
11
+ </div>
3
12
  </div>
4
13
 
5
14
  <div class="sqw-stats-grid">
@@ -13,15 +22,59 @@
13
22
  </div>
14
23
  </div>
15
24
 
25
+ <div class="sqw-timeline-grid sqw-timeline-grid--single" data-controller="sparkline-tooltip">
26
+ <div class="sqw-sparkline-wrap sqw-timeline-chart sqw-timeline-chart--cable">
27
+ <span class="sqw-sparkline-label">Messages — last 24 hours</span>
28
+ <div class="sqw-sparkline-positioner">
29
+ <%= cable_messages_timeline_svg(@timeline) %>
30
+ <div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
31
+ </div>
32
+ <div class="sqw-sparkline-axis">
33
+ <span>24h ago</span>
34
+ <span>12h ago</span>
35
+ <span>now</span>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <form class="sqw-filters" action="<%= cable_path %>" method="get" data-controller="search">
41
+ <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
42
+ placeholder="Filter by channel…" autocomplete="off" aria-label="Filter by channel"
43
+ data-action="input->search#filter">
44
+ <% if @search.present? %>
45
+ <%= link_to "Clear", cable_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
46
+ <% end %>
47
+ </form>
48
+
16
49
  <% if @channels.any? %>
17
50
  <table class="sqw-table">
18
51
  <thead>
19
- <tr><th>Channel</th></tr>
52
+ <tr>
53
+ <th>Channel</th>
54
+ <th>Messages</th>
55
+ <th>Last Message</th>
56
+ </tr>
20
57
  </thead>
21
58
  <tbody>
22
59
  <% @channels.each do |channel| %>
23
- <tr><td class="sqw-monospace"><%= channel %></td></tr>
60
+ <tr>
61
+ <td class="sqw-monospace sqw-truncate" title="<%= channel[:channel] %>">
62
+ <%= link_to channel[:channel], cable_channel_messages_path(channel[:channel_hash]), class: "sqw-link" %>
63
+ </td>
64
+ <td><%= channel[:message_count] %></td>
65
+ <td class="sqw-muted"><%= local_time(channel[:last_message_at]) %></td>
66
+ </tr>
24
67
  <% end %>
25
68
  </tbody>
26
69
  </table>
70
+ <% else %>
71
+ <div class="sqw-empty">
72
+ <% if @search.present? %>
73
+ <p class="sqw-empty__title">No channels matching &ldquo;<%= @search %>&rdquo;</p>
74
+ <p class="sqw-empty__hint"><%= link_to "Clear search", cable_path %></p>
75
+ <% else %>
76
+ <p class="sqw-empty__title">No cable messages</p>
77
+ <p class="sqw-empty__hint">Messages will appear here once clients connect and broadcast over Action Cable.</p>
78
+ <% end %>
79
+ </div>
27
80
  <% end %>
@@ -0,0 +1,56 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <h1 class="sqw-page-title sqw-truncate" title="<%= @channel_name %>"><%= @channel_name %></h1>
3
+ <div class="sqw-header-actions">
4
+ <%= button_to "Purge Channel",
5
+ cable_channel_purge_path(params[:channel_hash]),
6
+ method: :delete,
7
+ class: "sqw-btn sqw-btn--danger sqw-btn--sm",
8
+ data: { turbo_confirm: "Delete all messages for this channel? This cannot be undone." } %>
9
+ <%= link_to "← Channels", cable_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
10
+ </div>
11
+ </div>
12
+
13
+ <form class="sqw-filters" action="<%= cable_channel_messages_path(params[:channel_hash]) %>" method="get" data-controller="search">
14
+ <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
15
+ placeholder="Filter by payload…" autocomplete="off" aria-label="Filter by payload"
16
+ data-action="input->search#filter">
17
+ <% if @search.present? %>
18
+ <%= link_to "Clear", cable_channel_messages_path(params[:channel_hash]), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
19
+ <% end %>
20
+ </form>
21
+
22
+ <% if @messages.any? %>
23
+ <table class="sqw-table">
24
+ <thead>
25
+ <tr>
26
+ <th>ID</th>
27
+ <th>Payload</th>
28
+ <th>Sent</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <% @messages.each do |message| %>
33
+ <tr>
34
+ <td class="sqw-muted sqw-monospace"><%= message.id %></td>
35
+ <td class="sqw-monospace sqw-truncate" title="<%= message.payload %>">
36
+ <%= truncate(message.payload.to_s, length: 120) %>
37
+ </td>
38
+ <td class="sqw-muted">
39
+ <%= local_time(message.created_at, format: :relative) %>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+ <%== @pagy.series_nav if @pagy.pages > 1 %>
46
+ <% else %>
47
+ <div class="sqw-empty">
48
+ <% if @search.present? %>
49
+ <p class="sqw-empty__title">No messages matching &ldquo;<%= @search %>&rdquo;</p>
50
+ <p class="sqw-empty__hint"><%= link_to "Clear search", cable_channel_messages_path(params[:channel_hash]) %></p>
51
+ <% else %>
52
+ <p class="sqw-empty__title">No messages for this channel</p>
53
+ <p class="sqw-empty__hint">Messages may have been purged or the channel has gone quiet. <%= link_to "Back to channels →", cable_path %></p>
54
+ <% end %>
55
+ </div>
56
+ <% end %>
@@ -46,7 +46,7 @@
46
46
  <%= link_to entry.key, cache_entry_path(entry), class: "sqw-link" %>
47
47
  </td>
48
48
  <td><%= number_to_human_size(entry.byte_size) %></td>
49
- <td class="sqw-muted"><%= entry.created_at.strftime("%b %d %H:%M") %></td>
49
+ <td class="sqw-muted"><%= local_time(entry.created_at) %></td>
50
50
  <td class="sqw-actions">
51
51
  <%= button_to "Delete",
52
52
  cache_entry_path(entry, q: @search, column: @sort["column"], direction: @sort["direction"]),
@@ -58,9 +58,15 @@
58
58
  <% end %>
59
59
  </tbody>
60
60
  </table>
61
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
61
+ <%== @pagy.series_nav if @pagy.pages > 1 %>
62
62
  <% else %>
63
63
  <div class="sqw-empty">
64
- <p><%= @search.present? ? "No entries matching &ldquo;#{@search}&rdquo;.".html_safe : "No cache entries." %></p>
64
+ <% if @search.present? %>
65
+ <p class="sqw-empty__title">No entries matching &ldquo;<%= @search %>&rdquo;</p>
66
+ <p class="sqw-empty__hint"><%= link_to "Clear search", cache_entries_path %></p>
67
+ <% else %>
68
+ <p class="sqw-empty__title">No cache entries</p>
69
+ <p class="sqw-empty__hint">Entries will appear here once your application writes to the cache.</p>
70
+ <% end %>
65
71
  </div>
66
72
  <% end %>
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
  <div class="sqw-detail__row">
23
23
  <dt>Created</dt>
24
- <dd class="sqw-muted"><%= @entry.created_at.strftime("%b %d, %Y %H:%M:%S %Z") %></dd>
24
+ <dd class="sqw-muted"><%= local_time(@entry.created_at, format: :long) %></dd>
25
25
  </div>
26
26
  </dl>
27
27
 
@@ -87,7 +87,7 @@
87
87
  <% if @cache_stats[:oldest_entry] %>
88
88
  <div class="sqw-inline-stat sqw-inline-stat--cache">
89
89
  <span class="sqw-inline-stat__label">Oldest</span>
90
- <span class="sqw-inline-stat__value sqw-inline-stat__value--sm" title="<%= @cache_stats[:oldest_entry].strftime("%b %d, %Y %H:%M") %>"><%= time_ago_in_words(@cache_stats[:oldest_entry]) %></span>
90
+ <span class="sqw-inline-stat__value sqw-inline-stat__value--sm"><%= local_time(@cache_stats[:oldest_entry], format: :relative) %></span>
91
91
  </div>
92
92
  <% end %>
93
93
  </div>
@@ -99,15 +99,36 @@
99
99
  <%= link_to "View Cable →", cable_path, class: "sqw-gem-card__link" %>
100
100
  </div>
101
101
  <div class="sqw-gem-card__body">
102
- <div class="sqw-inline-stat sqw-inline-stat--cable">
102
+ <%= link_to cable_path, class: "sqw-inline-stat sqw-inline-stat--cable" do %>
103
103
  <span class="sqw-inline-stat__label">Messages</span>
104
104
  <span class="sqw-inline-stat__value"><%= @cable_stats[:messages] %></span>
105
- </div>
105
+ <% end %>
106
106
  <div class="sqw-inline-stat sqw-inline-stat--cable">
107
107
  <span class="sqw-inline-stat__label">Channels</span>
108
108
  <span class="sqw-inline-stat__value"><%= @cable_stats[:channels] %></span>
109
109
  </div>
110
+ <div class="sqw-inline-stat sqw-inline-stat--cable">
111
+ <span class="sqw-inline-stat__label">Msg/hr</span>
112
+ <span class="sqw-inline-stat__value"><%= @cable_stats[:messages_per_hour] %></span>
113
+ </div>
114
+ <% if @cable_stats[:oldest_message] %>
115
+ <div class="sqw-inline-stat sqw-inline-stat--cable">
116
+ <span class="sqw-inline-stat__label">Oldest</span>
117
+ <span class="sqw-inline-stat__value sqw-inline-stat__value--sm"><%= local_time(@cable_stats[:oldest_message], format: :relative) %></span>
118
+ </div>
119
+ <% end %>
110
120
  </div>
121
+ <% if @cable_stats[:top_channels].any? %>
122
+ <div class="sqw-card-list">
123
+ <span class="sqw-card-list__label">Top channels</span>
124
+ <% @cable_stats[:top_channels].each do |channel, count| %>
125
+ <div class="sqw-card-list__row">
126
+ <span class="sqw-card-list__name" title="<%= channel %>"><%= channel %></span>
127
+ <span class="sqw-card-list__count"><%= count %></span>
128
+ </div>
129
+ <% end %>
130
+ </div>
131
+ <% end %>
111
132
  </div>
112
133
  </div>
113
- <% end %>
134
+ <% end %>
@@ -1,3 +1,6 @@
1
+ <%= turbo_stream.prepend "sqw-flash-container",
2
+ partial: "solid_stack_web/shared/flash",
3
+ locals: { type: "notice", message: @notice } %>
1
4
  <% if @executions_remain %>
2
5
  <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
6
  <% else %>
@@ -50,7 +50,7 @@
50
50
  <td class="sqw-monospace"><%= link_to execution.job.class_name, failed_job_path(execution) %></td>
51
51
  <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
52
52
  <td class="sqw-muted sqw-truncate" title="<%= execution.exception_class %>: <%= execution.message %>"><%= execution.exception_class %></td>
53
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
53
+ <td class="sqw-muted"><%= local_time(execution.created_at) %></td>
54
54
  <td class="sqw-actions">
55
55
  <%= button_to "Retry", retry_failed_job_path(execution),
56
56
  method: :post, class: "sqw-btn sqw-btn--sm" %>
@@ -62,11 +62,12 @@
62
62
  <% end %>
63
63
  </tbody>
64
64
  </table>
65
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
65
+ <%== @pagy.series_nav if @pagy.pages > 1 %>
66
66
  </div>
67
67
  <% else %>
68
68
  <div class="sqw-empty">
69
- <p>No failed jobs.</p>
69
+ <p class="sqw-empty__title">No failed jobs</p>
70
+ <p class="sqw-empty__hint">All clear — your jobs are running without errors.</p>
70
71
  </div>
71
72
  <% end %>
72
73
  </div>
@@ -29,7 +29,7 @@
29
29
  <dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>
30
30
 
31
31
  <dt>Failed At</dt>
32
- <dd class="sqw-monospace"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
32
+ <dd class="sqw-monospace"><%= local_time(@execution.created_at, format: :long) %></dd>
33
33
 
34
34
  <dt>Error</dt>
35
35
  <dd class="sqw-monospace"><%= @execution.exception_class %></dd>
@@ -61,16 +61,22 @@
61
61
  class: "sqw-badge sqw-badge--queue" %>
62
62
  </td>
63
63
  <td class="sqw-monospace"><%= format_duration(job.finished_at - job.created_at) %></td>
64
- <td class="sqw-muted"><%= job.finished_at.strftime("%b %d %H:%M:%S") %></td>
64
+ <td class="sqw-muted"><%= local_time(job.finished_at) %></td>
65
65
  </tr>
66
66
  <% end %>
67
67
  </tbody>
68
68
  </table>
69
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
69
+ <%== @pagy.series_nav if @pagy.pages > 1 %>
70
70
  </div>
71
71
  <% else %>
72
72
  <div class="sqw-empty">
73
- <p>No finished jobs found.</p>
73
+ <% if @search.present? || @queue.present? || @period.present? %>
74
+ <p class="sqw-empty__title">No finished jobs match your filters</p>
75
+ <p class="sqw-empty__hint"><%= link_to "Clear filters", history_path %></p>
76
+ <% else %>
77
+ <p class="sqw-empty__title">No finished jobs yet</p>
78
+ <p class="sqw-empty__hint">Completed jobs will appear here once workers process them.</p>
79
+ <% end %>
74
80
  </div>
75
81
  <% end %>
76
82
  <% end %>
@@ -1,3 +1,20 @@
1
1
  <div id="sqw-empty" class="sqw-empty">
2
- <p>No <strong><%= @status %></strong> jobs.</p>
3
- </div>
2
+ <% if @search.present? %>
3
+ <p class="sqw-empty__title">No <strong><%= @status %></strong> jobs matching &ldquo;<%= @search %>&rdquo;</p>
4
+ <p class="sqw-empty__hint"><%= link_to "Clear search", jobs_path(status: @status) %></p>
5
+ <% else %>
6
+ <p class="sqw-empty__title">No <strong><%= @status %></strong> jobs</p>
7
+ <p class="sqw-empty__hint">
8
+ <% case @status %>
9
+ <% when "ready" %>
10
+ Jobs will appear here once they are enqueued and ready to be picked up.
11
+ <% when "scheduled" %>
12
+ No jobs are scheduled to run in the future.
13
+ <% when "claimed" %>
14
+ No jobs are currently being processed by a worker.
15
+ <% when "blocked" %>
16
+ No jobs are blocked by concurrency controls.
17
+ <% end %>
18
+ </p>
19
+ <% end %>
20
+ </div>
@@ -1,3 +1,6 @@
1
+ <%= turbo_stream.prepend "sqw-flash-container",
2
+ partial: "solid_stack_web/shared/flash",
3
+ locals: { type: "notice", message: @notice } %>
1
4
  <% if @executions_remain %>
2
5
  <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
6
  <% else %>
@@ -121,9 +121,9 @@
121
121
  <td class="sqw-monospace"><%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %></td>
122
122
  <td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
123
123
  <td><%= execution.job.priority %></td>
124
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
124
+ <td class="sqw-muted"><%= local_time(execution.created_at) %></td>
125
125
  <% if @status == "scheduled" %>
126
- <td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
126
+ <td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= local_time(execution.scheduled_at) %></td>
127
127
  <% end %>
128
128
  <td class="sqw-actions">
129
129
  <% if @status == "scheduled" %>
@@ -149,7 +149,7 @@
149
149
  <% end %>
150
150
  </tbody>
151
151
  </table>
152
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
152
+ <%== @pagy.series_nav if @pagy.pages > 1 %>
153
153
  </div>
154
154
  <% else %>
155
155
  <%= render "empty" %>
@@ -36,17 +36,17 @@
36
36
 
37
37
  <% if @status == "blocked" %>
38
38
  <dt>Blocked Until</dt>
39
- <dd class="sqw-monospace"><%= @execution.expires_at ? @execution.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
39
+ <dd class="sqw-monospace"><%= local_time(@execution.expires_at, format: :long) %></dd>
40
40
  <% end %>
41
41
 
42
42
  <dt>Enqueued At</dt>
43
- <dd class="sqw-monospace"><%= @execution.job.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
43
+ <dd class="sqw-monospace"><%= local_time(@execution.job.created_at, format: :long) %></dd>
44
44
 
45
45
  <dt>Scheduled At</dt>
46
- <dd class="sqw-monospace"><%= @execution.job.scheduled_at ? @execution.job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
46
+ <dd class="sqw-monospace"><%= local_time(@execution.job.scheduled_at, format: :long) %></dd>
47
47
 
48
48
  <dt>Finished At</dt>
49
- <dd class="sqw-monospace"><%= @execution.job.finished_at ? @execution.job.finished_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
49
+ <dd class="sqw-monospace"><%= local_time(@execution.job.finished_at, format: :long) %></dd>
50
50
  </dl>
51
51
  </div>
52
52
 
@@ -22,14 +22,15 @@
22
22
  <td class="sqw-monospace"><%= process.name %></td>
23
23
  <td class="sqw-monospace"><%= process.pid %></td>
24
24
  <td class="sqw-muted"><%= process.hostname %></td>
25
- <td class="sqw-muted"><%= process.last_heartbeat_at&.strftime("%b %d %H:%M:%S") %></td>
25
+ <td class="sqw-muted"><%= local_time(process.last_heartbeat_at) %></td>
26
26
  </tr>
27
27
  <% end %>
28
28
  </tbody>
29
29
  </table>
30
30
  <% else %>
31
31
  <div class="sqw-empty">
32
- <p>No active processes.</p>
32
+ <p class="sqw-empty__title">No active processes</p>
33
+ <p class="sqw-empty__hint">Start a Solid Queue worker to begin processing jobs.</p>
33
34
  </div>
34
35
  <% end %>
35
36
  <% end %>
@@ -44,6 +44,7 @@
44
44
  </table>
45
45
  <% else %>
46
46
  <div class="sqw-empty">
47
- <p>No queues with ready jobs.</p>
47
+ <p class="sqw-empty__title">No queues with ready jobs</p>
48
+ <p class="sqw-empty__hint">Workers are idle or all jobs are in another state. <%= link_to "Check job statuses →", jobs_path %></p>
48
49
  </div>
49
50
  <% end %>
@@ -49,7 +49,7 @@
49
49
  data: { turbo_frame: "_top" } %>
50
50
  </td>
51
51
  <td><%= execution.job.priority %></td>
52
- <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
52
+ <td class="sqw-muted"><%= local_time(execution.created_at) %></td>
53
53
  <td class="sqw-actions">
54
54
  <%= button_to "Discard", job_path(execution, status: "ready", queue: @queue_name),
55
55
  method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
@@ -59,7 +59,7 @@
59
59
  <% end %>
60
60
  </tbody>
61
61
  </table>
62
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
62
+ <%== @pagy.series_nav if @pagy.pages > 1 %>
63
63
  <% else %>
64
64
  <div class="sqw-empty">
65
65
  <p>No ready jobs in <strong><%= @queue_name %></strong>.</p>
@@ -36,12 +36,11 @@
36
36
  </td>
37
37
  <td><span class="sqw-badge sqw-badge--queue"><%= task.queue_name.presence || "default" %></span></td>
38
38
  <td class="sqw-muted sqw-monospace">
39
- <% next_run = begin; task.next_time.strftime("%b %d %H:%M"); rescue; nil; end %>
40
- <%= next_run || "—" %>
39
+ <% next_run = begin; task.next_time; rescue; nil; end %>
40
+ <%= local_time(next_run) %>
41
41
  </td>
42
42
  <td class="sqw-muted sqw-monospace">
43
- <% last_run = task.last_enqueued_time %>
44
- <%= last_run ? last_run.strftime("%b %d %H:%M") : "—" %>
43
+ <%= local_time(task.last_enqueued_time) %>
45
44
  </td>
46
45
  <td>
47
46
  <% if task.static? %>
@@ -62,6 +61,7 @@
62
61
  </table>
63
62
  <% else %>
64
63
  <div class="sqw-empty">
65
- <p>No recurring tasks configured.</p>
64
+ <p class="sqw-empty__title">No recurring tasks configured</p>
65
+ <p class="sqw-empty__hint">Define recurring tasks in your Solid Queue configuration to see them here.</p>
66
66
  </div>
67
67
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <% else %>
4
4
  <%= turbo_stream.replace "scheduled_at_#{@execution.id}" do %>
5
5
  <td id="scheduled_at_<%= @execution.id %>" class="sqw-muted">
6
- <%= @execution.scheduled_at.strftime("%b %d %H:%M") %>
6
+ <%= local_time(@execution.scheduled_at) %>
7
7
  </td>
8
8
  <% end %>
9
9
  <% end %>
@@ -0,0 +1 @@
1
+ <div class="sqw-flash sqw-flash--<%= type %>"><%= message %></div>
@@ -43,6 +43,7 @@
43
43
  </table>
44
44
  <% else %>
45
45
  <div class="sqw-empty">
46
- <p>No finished jobs yet.</p>
46
+ <p class="sqw-empty__title">No finished jobs yet</p>
47
+ <p class="sqw-empty__hint">Performance stats appear here once jobs complete. <%= link_to "View active jobs →", jobs_path %></p>
47
48
  </div>
48
49
  <% end %>
data/config/importmap.rb CHANGED
@@ -5,3 +5,5 @@ pin "solid_stack_web/refresh_controller", to: "solid_stack_web/refresh_controlle
5
5
  pin "solid_stack_web/search_controller", to: "solid_stack_web/search_controller.js"
6
6
  pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js"
7
7
  pin "solid_stack_web/sparkline_tooltip_controller", to: "solid_stack_web/sparkline_tooltip_controller.js"
8
+ pin "solid_stack_web/theme_controller", to: "solid_stack_web/theme_controller.js"
9
+ pin "solid_stack_web/timestamp_controller", to: "solid_stack_web/timestamp_controller.js"
data/config/routes.rb CHANGED
@@ -37,5 +37,8 @@ SolidStackWeb::Engine.routes.draw do
37
37
  get "cache", to: "cache#index", as: :cache
38
38
  resources :cache_entries, only: [:index, :show, :destroy], path: "cache/entries"
39
39
  resource :cache_flush, only: [:destroy], path: "cache/flush", controller: "cache/flushes"
40
- get "cable", to: "cable#index", as: :cable
40
+ get "cable", to: "cable#index", as: :cable
41
+ delete "cable/purge", to: "cable/purges#destroy", as: :cable_purge
42
+ get "cable/channels/:channel_hash", to: "cable_messages#index", as: :cable_channel_messages
43
+ delete "cable/channels/:channel_hash/purge", to: "cable/channel_purges#destroy", as: :cable_channel_purge
41
44
  end
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "0.5.0"
2
+ VERSION = "0.7.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_stack_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -142,9 +142,14 @@ files:
142
142
  - app/assets/stylesheets/solid_stack_web/_07_dashboard.css
143
143
  - app/assets/stylesheets/solid_stack_web/_08_filters.css
144
144
  - app/assets/stylesheets/solid_stack_web/_09_detail.css
145
+ - app/assets/stylesheets/solid_stack_web/_10_responsive.css
146
+ - app/assets/stylesheets/solid_stack_web/_12_dark_mode.css
145
147
  - app/assets/stylesheets/solid_stack_web/application.css
146
148
  - app/controllers/solid_stack_web/application_controller.rb
149
+ - app/controllers/solid_stack_web/cable/channel_purges_controller.rb
150
+ - app/controllers/solid_stack_web/cable/purges_controller.rb
147
151
  - app/controllers/solid_stack_web/cable_controller.rb
152
+ - app/controllers/solid_stack_web/cable_messages_controller.rb
148
153
  - app/controllers/solid_stack_web/cache/flushes_controller.rb
149
154
  - app/controllers/solid_stack_web/cache_controller.rb
150
155
  - app/controllers/solid_stack_web/cache_entries_controller.rb
@@ -169,8 +174,11 @@ files:
169
174
  - app/javascript/solid_stack_web/search_controller.js
170
175
  - app/javascript/solid_stack_web/selection_controller.js
171
176
  - app/javascript/solid_stack_web/sparkline_tooltip_controller.js
177
+ - app/javascript/solid_stack_web/theme_controller.js
178
+ - app/javascript/solid_stack_web/timestamp_controller.js
172
179
  - app/models/solid_stack_web/alert_webhook.rb
173
180
  - app/models/solid_stack_web/cable_stats.rb
181
+ - app/models/solid_stack_web/cable_timeline.rb
174
182
  - app/models/solid_stack_web/cache_size_stats.rb
175
183
  - app/models/solid_stack_web/cache_stats.rb
176
184
  - app/models/solid_stack_web/cache_timeline.rb
@@ -180,6 +188,7 @@ files:
180
188
  - app/models/solid_stack_web/throughput_sparkline.rb
181
189
  - app/views/layouts/solid_stack_web/application.html.erb
182
190
  - app/views/solid_stack_web/cable/index.html.erb
191
+ - app/views/solid_stack_web/cable_messages/index.html.erb
183
192
  - app/views/solid_stack_web/cache/index.html.erb
184
193
  - app/views/solid_stack_web/cache_entries/index.html.erb
185
194
  - app/views/solid_stack_web/cache_entries/show.html.erb
@@ -197,6 +206,7 @@ files:
197
206
  - app/views/solid_stack_web/queues/show.html.erb
198
207
  - app/views/solid_stack_web/recurring_tasks/index.html.erb
199
208
  - app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb
209
+ - app/views/solid_stack_web/shared/_flash.html.erb
200
210
  - app/views/solid_stack_web/stats/index.html.erb
201
211
  - config/importmap.rb
202
212
  - config/routes.rb