solid_ops 0.1.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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +308 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/solid_ops/application.css +1 -0
- data/app/controllers/solid_ops/application_controller.rb +127 -0
- data/app/controllers/solid_ops/cache_entries_controller.rb +38 -0
- data/app/controllers/solid_ops/channels_controller.rb +30 -0
- data/app/controllers/solid_ops/dashboard_controller.rb +80 -0
- data/app/controllers/solid_ops/events_controller.rb +37 -0
- data/app/controllers/solid_ops/jobs_controller.rb +64 -0
- data/app/controllers/solid_ops/processes_controller.rb +11 -0
- data/app/controllers/solid_ops/queues_controller.rb +75 -0
- data/app/controllers/solid_ops/recurring_tasks_controller.rb +11 -0
- data/app/helpers/solid_ops/application_helper.rb +112 -0
- data/app/jobs/solid_ops/purge_job.rb +16 -0
- data/app/models/solid_ops/event.rb +34 -0
- data/app/views/layouts/solid_ops/application.html.erb +118 -0
- data/app/views/solid_ops/cache_entries/index.html.erb +86 -0
- data/app/views/solid_ops/cache_entries/show.html.erb +153 -0
- data/app/views/solid_ops/channels/index.html.erb +81 -0
- data/app/views/solid_ops/channels/show.html.erb +66 -0
- data/app/views/solid_ops/dashboard/cable.html.erb +98 -0
- data/app/views/solid_ops/dashboard/cache.html.erb +104 -0
- data/app/views/solid_ops/dashboard/index.html.erb +169 -0
- data/app/views/solid_ops/dashboard/jobs.html.erb +108 -0
- data/app/views/solid_ops/events/index.html.erb +98 -0
- data/app/views/solid_ops/events/show.html.erb +108 -0
- data/app/views/solid_ops/jobs/failed.html.erb +89 -0
- data/app/views/solid_ops/jobs/running.html.erb +134 -0
- data/app/views/solid_ops/jobs/show.html.erb +116 -0
- data/app/views/solid_ops/processes/index.html.erb +69 -0
- data/app/views/solid_ops/queues/index.html.erb +182 -0
- data/app/views/solid_ops/queues/show.html.erb +121 -0
- data/app/views/solid_ops/recurring_tasks/index.html.erb +64 -0
- data/app/views/solid_ops/shared/_nav.html.erb +50 -0
- data/app/views/solid_ops/shared/_pagination.html.erb +31 -0
- data/app/views/solid_ops/shared/_time_window.html.erb +10 -0
- data/app/views/solid_ops/shared/component_unavailable.html.erb +63 -0
- data/config/routes.rb +49 -0
- data/db/migrate/20260224000100_create_solid_ops_events.rb +31 -0
- data/lib/generators/solid_ops/install/install_generator.rb +348 -0
- data/lib/generators/solid_ops/install/templates/create_solid_ops_events.rb +31 -0
- data/lib/generators/solid_ops/install/templates/solid_ops_initializer.rb +31 -0
- data/lib/solid_ops/configuration.rb +28 -0
- data/lib/solid_ops/context.rb +34 -0
- data/lib/solid_ops/current.rb +10 -0
- data/lib/solid_ops/engine.rb +60 -0
- data/lib/solid_ops/job_extension.rb +50 -0
- data/lib/solid_ops/middleware.rb +52 -0
- data/lib/solid_ops/subscribers.rb +215 -0
- data/lib/solid_ops/version.rb +5 -0
- data/lib/solid_ops.rb +25 -0
- data/lib/tasks/solid_ops.rake +32 -0
- data/log/test.log +2 -0
- data/sig/solid_ops.rbs +4 -0
- metadata +119 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
|
|
3
|
+
<div class="flex items-center justify-between mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Cache</p>
|
|
6
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Cache Entry</h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1 font-mono truncate max-w-lg"><%= @entry.key %></p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex items-center gap-3">
|
|
10
|
+
<%= form_tag(solid_ops.cache_entry_path(@entry.id), method: :delete, class: "inline") do %>
|
|
11
|
+
<button type="submit" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-red-700 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors ring-1 ring-inset ring-red-600/10"
|
|
12
|
+
onclick="return confirm('Delete this cache entry?')">
|
|
13
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
14
|
+
Delete
|
|
15
|
+
</button>
|
|
16
|
+
<% end %>
|
|
17
|
+
<a href="<%= solid_ops.cache_entries_path %>" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm text-gray-500 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors shadow-sm">
|
|
18
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
|
|
19
|
+
Cache
|
|
20
|
+
</a>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
25
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
26
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
27
|
+
<h3 class="text-sm font-bold text-gray-900">Entry Details</h3>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="p-6">
|
|
30
|
+
<dl class="space-y-3">
|
|
31
|
+
<% details = [
|
|
32
|
+
["Key", @entry.key],
|
|
33
|
+
["Size", format_bytes(@entry.byte_size)],
|
|
34
|
+
["Key Hash", @entry.key_hash],
|
|
35
|
+
["Created", format_datetime(@entry.created_at)],
|
|
36
|
+
]
|
|
37
|
+
details << ["Updated", format_datetime(@entry.updated_at)] if @entry.respond_to?(:updated_at)
|
|
38
|
+
details.each do |label, value| %>
|
|
39
|
+
<div class="flex items-start">
|
|
40
|
+
<dt class="w-24 flex-shrink-0 text-[11px] font-bold text-gray-400 uppercase tracking-wide pt-0.5"><%= label %></dt>
|
|
41
|
+
<dd class="font-mono text-sm text-gray-700 break-all"><%= value.presence || "—" %></dd>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</dl>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
49
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
50
|
+
<h3 class="text-sm font-bold text-gray-900">Value Preview</h3>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="p-6">
|
|
53
|
+
<%
|
|
54
|
+
raw = @entry.value.to_s
|
|
55
|
+
deserialized = nil
|
|
56
|
+
decode_method = nil
|
|
57
|
+
|
|
58
|
+
# Strategy 1: Use the cache store's internal deserializer on the raw blob
|
|
59
|
+
begin
|
|
60
|
+
if Rails.cache.respond_to?(:deserialize_entry, true)
|
|
61
|
+
entry_obj = Rails.cache.send(:deserialize_entry, raw)
|
|
62
|
+
if entry_obj.respond_to?(:value)
|
|
63
|
+
deserialized = entry_obj.value
|
|
64
|
+
decode_method = "cache deserializer"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
rescue; end
|
|
68
|
+
|
|
69
|
+
# Strategy 2: Try Marshal.load (older Rails / Marshal coder)
|
|
70
|
+
if deserialized.nil?
|
|
71
|
+
begin
|
|
72
|
+
restored = Marshal.load(raw)
|
|
73
|
+
if restored.respond_to?(:value) && restored.is_a?(ActiveSupport::Cache::Entry)
|
|
74
|
+
deserialized = restored.value
|
|
75
|
+
else
|
|
76
|
+
deserialized = restored
|
|
77
|
+
end
|
|
78
|
+
decode_method = "Marshal"
|
|
79
|
+
rescue; end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Strategy 3: Read back through the cache store by key
|
|
83
|
+
if deserialized.nil?
|
|
84
|
+
begin
|
|
85
|
+
result = Rails.cache.read(@entry.key)
|
|
86
|
+
unless result.nil?
|
|
87
|
+
deserialized = result
|
|
88
|
+
decode_method = "cache read"
|
|
89
|
+
end
|
|
90
|
+
rescue; end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Strategy 4: Try raw UTF-8 string interpretation
|
|
94
|
+
if deserialized.nil?
|
|
95
|
+
begin
|
|
96
|
+
str = raw.dup.force_encoding("UTF-8")
|
|
97
|
+
if str.valid_encoding? && str.match?(/[[:print:]]/)
|
|
98
|
+
deserialized = str
|
|
99
|
+
decode_method = "raw string"
|
|
100
|
+
end
|
|
101
|
+
rescue; end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if !deserialized.nil?
|
|
105
|
+
display_value = begin
|
|
106
|
+
case deserialized
|
|
107
|
+
when Hash, Array
|
|
108
|
+
# Convert symbol keys to strings for JSON compatibility
|
|
109
|
+
json_safe = JSON.parse(JSON.generate(deserialized))
|
|
110
|
+
JSON.pretty_generate(json_safe)
|
|
111
|
+
when String
|
|
112
|
+
# Try to parse as JSON for pretty-printing
|
|
113
|
+
begin
|
|
114
|
+
parsed = JSON.parse(deserialized)
|
|
115
|
+
JSON.pretty_generate(parsed)
|
|
116
|
+
rescue
|
|
117
|
+
deserialized
|
|
118
|
+
end
|
|
119
|
+
when Numeric, TrueClass, FalseClass
|
|
120
|
+
deserialized.to_s
|
|
121
|
+
else
|
|
122
|
+
# Use PP for complex objects — one attribute per line
|
|
123
|
+
PP.pp(deserialized, StringIO.new).string
|
|
124
|
+
end
|
|
125
|
+
rescue
|
|
126
|
+
# Last resort: pretty-print via PP or inspect
|
|
127
|
+
begin
|
|
128
|
+
PP.pp(deserialized, StringIO.new).string
|
|
129
|
+
rescue
|
|
130
|
+
deserialized.inspect
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
value_type = deserialized.class.name
|
|
134
|
+
else
|
|
135
|
+
raw_bytes = raw.bytesize
|
|
136
|
+
display_value = raw_bytes.to_s + " bytes (serialized, preview unavailable)"
|
|
137
|
+
value_type = nil
|
|
138
|
+
end
|
|
139
|
+
%>
|
|
140
|
+
<div class="flex items-center gap-2 mb-2">
|
|
141
|
+
<% if value_type %>
|
|
142
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-700/10"><%= value_type %></span>
|
|
143
|
+
<% end %>
|
|
144
|
+
<% if decode_method %>
|
|
145
|
+
<span class="text-[10px] text-gray-400">via <%= decode_method %></span>
|
|
146
|
+
<% end %>
|
|
147
|
+
</div>
|
|
148
|
+
<pre class="bg-gray-50 rounded-lg p-4 text-xs font-mono text-gray-700 max-h-80 overflow-y-auto ring-1 ring-inset ring-gray-900/5" style="white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word;"><%= display_value.to_s.truncate(10_000) %></pre>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
|
|
3
|
+
<div class="flex items-center justify-between mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Channel Management</h1>
|
|
6
|
+
<p class="text-sm text-gray-500 mt-1">Solid Cable — inspect channels and messages</p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex items-center gap-3">
|
|
9
|
+
<%= form_tag(solid_ops.trim_channels_path, method: :post, class: "inline") do %>
|
|
10
|
+
<button type="submit" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-lg hover:bg-amber-100 transition-colors ring-1 ring-inset ring-amber-600/10"
|
|
11
|
+
onclick="return confirm('Trim messages older than 1 hour?')">
|
|
12
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
13
|
+
Trim Old Messages
|
|
14
|
+
</button>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="grid grid-cols-2 gap-4 mb-8">
|
|
20
|
+
<div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
21
|
+
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-purple-400 to-purple-600"></div>
|
|
22
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Total Messages</p>
|
|
23
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-purple-600"><%= number_with_delimiter(@total_messages) %></p>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
26
|
+
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-gray-400 to-gray-600"></div>
|
|
27
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Active Channels</p>
|
|
28
|
+
<p class="text-3xl font-extrabold font-mono mt-2"><%= @channels.length %></p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
33
|
+
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
34
|
+
<h2 class="text-sm font-bold text-gray-900">Channels</h2>
|
|
35
|
+
<span class="text-xs text-gray-400"><%= @channels.length %> channels</span>
|
|
36
|
+
</div>
|
|
37
|
+
<% if @channels.any? %>
|
|
38
|
+
<div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
|
|
39
|
+
<div style="position: relative; display: inline-block;">
|
|
40
|
+
<svg style="position: absolute; left: 0.625rem; top: 50%; transform: translateY(-50%); width: 1rem; height: 1rem; color: #9ca3af; pointer-events: none;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
41
|
+
<input type="text" data-solid-search="channels-table" placeholder="Filter channels…" style="padding: 0.375rem 0.75rem 0.375rem 2rem; font-size: 0.875rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; width: 18rem; background: rgba(249,250,251,0.5);">
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<table id="channels-table" class="min-w-full divide-y divide-gray-100">
|
|
45
|
+
<thead>
|
|
46
|
+
<tr class="bg-gray-50">
|
|
47
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Channel</th>
|
|
48
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Messages</th>
|
|
49
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Last Message</th>
|
|
50
|
+
<th class="px-6 py-3 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
|
|
51
|
+
</tr>
|
|
52
|
+
</thead>
|
|
53
|
+
<tbody class="divide-y divide-gray-50">
|
|
54
|
+
<% @channels.each do |ch| %>
|
|
55
|
+
<tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.channel_path(ch.channel) %>'">
|
|
56
|
+
<td class="px-6 py-3.5 font-mono text-sm text-purple-600 font-semibold"><%= ch.channel %></td>
|
|
57
|
+
<td class="px-6 py-3.5">
|
|
58
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-purple-50 text-purple-700 ring-1 ring-inset ring-purple-700/10">
|
|
59
|
+
<%= ch.message_count %>
|
|
60
|
+
</span>
|
|
61
|
+
</td>
|
|
62
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= time_ago_short(ch.last_message_at) %></td>
|
|
63
|
+
<td class="px-6 py-3.5 text-right">
|
|
64
|
+
<svg class="w-4 h-4 text-gray-300 group-hover:text-purple-500 transition-colors inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% end %>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
<% else %>
|
|
71
|
+
<div class="py-20 text-center">
|
|
72
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
73
|
+
<svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.14 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0"/></svg>
|
|
74
|
+
</div>
|
|
75
|
+
<p class="text-sm text-gray-500 font-medium">No channels found</p>
|
|
76
|
+
<p class="text-xs text-gray-400 mt-1">Messages appear here when ActionCable broadcasts</p>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
|
|
3
|
+
<div class="flex items-center justify-between mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Channels</p>
|
|
6
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Channel: <%= @channel %></h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1"><%= @messages.size %> recent messages</p>
|
|
8
|
+
</div>
|
|
9
|
+
<a href="<%= solid_ops.channels_path %>" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm text-gray-500 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors shadow-sm">
|
|
10
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
|
|
11
|
+
Channels
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
16
|
+
<% if @messages.any? %>
|
|
17
|
+
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
18
|
+
<h2 class="text-sm font-bold text-gray-900">Messages</h2>
|
|
19
|
+
<span class="text-xs text-gray-400"><%= @messages.size %> messages</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
|
|
22
|
+
<div style="position: relative; display: inline-block;">
|
|
23
|
+
<svg style="position: absolute; left: 0.625rem; top: 50%; transform: translateY(-50%); width: 1rem; height: 1rem; color: #9ca3af; pointer-events: none;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
24
|
+
<input type="text" data-solid-search="messages-table" placeholder="Filter messages…" style="padding: 0.375rem 0.75rem 0.375rem 2rem; font-size: 0.875rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; width: 18rem; background: rgba(249,250,251,0.5);">
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<table id="messages-table" class="min-w-full divide-y divide-gray-100">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr class="bg-gray-50">
|
|
30
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">ID</th>
|
|
31
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Created</th>
|
|
32
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Payload</th>
|
|
33
|
+
</tr>
|
|
34
|
+
</thead>
|
|
35
|
+
<tbody class="divide-y divide-gray-50">
|
|
36
|
+
<% @messages.each do |msg| %>
|
|
37
|
+
<tr class="hover:bg-gray-50/80 transition-colors">
|
|
38
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= msg.id %></td>
|
|
39
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500 whitespace-nowrap"><%= format_datetime(msg.created_at) %></td>
|
|
40
|
+
<td class="px-6 py-3.5">
|
|
41
|
+
<%
|
|
42
|
+
formatted_payload = begin
|
|
43
|
+
parsed = JSON.parse(msg.payload.to_s)
|
|
44
|
+
JSON.pretty_generate(parsed)
|
|
45
|
+
rescue
|
|
46
|
+
msg.payload.to_s
|
|
47
|
+
end
|
|
48
|
+
%>
|
|
49
|
+
<pre class="text-xs font-mono text-gray-700 max-w-2xl bg-gray-50 rounded px-2 py-1 max-h-48 overflow-y-auto ring-1 ring-inset ring-gray-900/5" style="white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word;"><%= formatted_payload %></pre>
|
|
50
|
+
</td>
|
|
51
|
+
</tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
<% else %>
|
|
56
|
+
<div class="py-20 text-center">
|
|
57
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
58
|
+
<svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
|
59
|
+
</div>
|
|
60
|
+
<p class="text-sm text-gray-500 font-medium">No messages in this channel</p>
|
|
61
|
+
<p class="text-xs text-gray-400 mt-1">Messages will appear when broadcasts occur</p>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
|
|
3
|
+
<div class="flex items-center justify-between mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Dashboard</p>
|
|
6
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Cable Events</h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1">ActionCable broadcasts observed via instrumentation</p>
|
|
8
|
+
</div>
|
|
9
|
+
<%= render "solid_ops/shared/time_window" %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
|
|
13
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
14
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-purple-400 to-purple-600"></div>
|
|
15
|
+
<div class="p-5 pt-6">
|
|
16
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Broadcasts</p>
|
|
17
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-purple-600"><%= number_with_delimiter(@broadcast_count) %></p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
21
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-gray-300 to-gray-400"></div>
|
|
22
|
+
<div class="p-5 pt-6">
|
|
23
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Avg Duration</p>
|
|
24
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-gray-900"><%= format_duration(@avg_duration) %></p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
28
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-indigo-400 to-indigo-600"></div>
|
|
29
|
+
<div class="p-5 pt-6">
|
|
30
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Active Streams</p>
|
|
31
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-indigo-600"><%= @streams.length %></p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<% if @streams.any? %>
|
|
37
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden">
|
|
38
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
39
|
+
<h2 class="text-sm font-bold text-gray-900">Streams by Activity</h2>
|
|
40
|
+
</div>
|
|
41
|
+
<table class="min-w-full divide-y divide-gray-100">
|
|
42
|
+
<thead>
|
|
43
|
+
<tr class="bg-gray-50">
|
|
44
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Stream</th>
|
|
45
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Broadcasts</th>
|
|
46
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Avg Duration</th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody class="divide-y divide-gray-50">
|
|
50
|
+
<% @streams.each do |s| %>
|
|
51
|
+
<tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.events_path(event_type: 'cable.broadcast', q: s.name) %>'">
|
|
52
|
+
<td class="px-6 py-3.5 font-mono text-sm text-purple-600 font-medium"><%= s.name %></td>
|
|
53
|
+
<td class="px-6 py-3.5 font-mono text-sm font-semibold text-gray-900"><%= s.event_count %></td>
|
|
54
|
+
<td class="px-6 py-3.5 font-mono text-sm text-gray-500"><%= format_duration(s.avg_duration) %></td>
|
|
55
|
+
</tr>
|
|
56
|
+
<% end %>
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|
|
61
|
+
|
|
62
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
63
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
64
|
+
<h2 class="text-sm font-bold text-gray-900">Recent Broadcasts</h2>
|
|
65
|
+
</div>
|
|
66
|
+
<% if @recent.any? %>
|
|
67
|
+
<table class="min-w-full divide-y divide-gray-100">
|
|
68
|
+
<thead>
|
|
69
|
+
<tr class="bg-gray-50">
|
|
70
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Time</th>
|
|
71
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Stream</th>
|
|
72
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Duration</th>
|
|
73
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Correlation</th>
|
|
74
|
+
</tr>
|
|
75
|
+
</thead>
|
|
76
|
+
<tbody class="divide-y divide-gray-50">
|
|
77
|
+
<% @recent.each do |e| %>
|
|
78
|
+
<tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.event_path(e.id) %>'">
|
|
79
|
+
<td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= format_time(e.occurred_at) %></td>
|
|
80
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-700"><%= e.name %></td>
|
|
81
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_duration(e.duration_ms) %></td>
|
|
82
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-400"><%= e.correlation_id&.first(8) %></td>
|
|
83
|
+
</tr>
|
|
84
|
+
<% end %>
|
|
85
|
+
</tbody>
|
|
86
|
+
</table>
|
|
87
|
+
<% else %>
|
|
88
|
+
<div class="py-20 text-center">
|
|
89
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
90
|
+
<svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.14 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0"/></svg>
|
|
91
|
+
</div>
|
|
92
|
+
<p class="text-sm text-gray-500 font-medium">No cable events in this time window</p>
|
|
93
|
+
<p class="text-xs text-gray-400 mt-1">Try selecting a larger time range above</p>
|
|
94
|
+
</div>
|
|
95
|
+
<% end %>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
|
|
3
|
+
<div class="flex items-center justify-between mb-8">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Dashboard</p>
|
|
6
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Cache Events</h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1">Cache operations observed via instrumentation</p>
|
|
8
|
+
</div>
|
|
9
|
+
<%= render "solid_ops/shared/time_window" %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
13
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
14
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-gray-300 to-gray-400"></div>
|
|
15
|
+
<div class="p-5 pt-6">
|
|
16
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Reads</p>
|
|
17
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-gray-900"><%= number_with_delimiter(@read_count) %></p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
21
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-emerald-400 to-emerald-600"></div>
|
|
22
|
+
<div class="p-5 pt-6">
|
|
23
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Hit Rate</p>
|
|
24
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-emerald-600"><%= @hit_rate ? "#{@hit_rate}%" : "—" %></p>
|
|
25
|
+
<p class="text-xs text-gray-400 mt-1"><%= @hit_count %> hits / <%= @miss_count %> misses</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
29
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-blue-400 to-blue-600"></div>
|
|
30
|
+
<div class="p-5 pt-6">
|
|
31
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Writes</p>
|
|
32
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-blue-600"><%= number_with_delimiter(@write_count) %></p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
36
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-orange-400 to-orange-500"></div>
|
|
37
|
+
<div class="p-5 pt-6">
|
|
38
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Deletes</p>
|
|
39
|
+
<p class="text-3xl font-extrabold font-mono mt-2 text-orange-600"><%= number_with_delimiter(@delete_count) %></p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<% if @top_keys.any? %>
|
|
45
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden">
|
|
46
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
47
|
+
<h2 class="text-sm font-bold text-gray-900">Top Keys by Activity</h2>
|
|
48
|
+
</div>
|
|
49
|
+
<table class="min-w-full divide-y divide-gray-100">
|
|
50
|
+
<thead>
|
|
51
|
+
<tr class="bg-gray-50">
|
|
52
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Key</th>
|
|
53
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Operations</th>
|
|
54
|
+
</tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody class="divide-y divide-gray-50">
|
|
57
|
+
<% @top_keys.each do |k| %>
|
|
58
|
+
<tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.events_path(q: k.name) %>'">
|
|
59
|
+
<td class="px-6 py-3.5 font-mono text-sm text-blue-600 font-medium"><%= k.name %></td>
|
|
60
|
+
<td class="px-6 py-3.5 font-mono text-sm font-semibold text-gray-900"><%= k.event_count %></td>
|
|
61
|
+
</tr>
|
|
62
|
+
<% end %>
|
|
63
|
+
</tbody>
|
|
64
|
+
</table>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
69
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
70
|
+
<h2 class="text-sm font-bold text-gray-900">Recent Cache Events</h2>
|
|
71
|
+
</div>
|
|
72
|
+
<% if @recent.any? %>
|
|
73
|
+
<table class="min-w-full divide-y divide-gray-100">
|
|
74
|
+
<thead>
|
|
75
|
+
<tr class="bg-gray-50">
|
|
76
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Time</th>
|
|
77
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Type</th>
|
|
78
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Key</th>
|
|
79
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Duration</th>
|
|
80
|
+
</tr>
|
|
81
|
+
</thead>
|
|
82
|
+
<tbody class="divide-y divide-gray-50">
|
|
83
|
+
<% @recent.each do |e| %>
|
|
84
|
+
<tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.event_path(e.id) %>'">
|
|
85
|
+
<td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= format_time(e.occurred_at) %></td>
|
|
86
|
+
<td class="px-6 py-3.5"><span class="<%= event_pill_class(e.event_type) %>"><%= e.event_type %></span></td>
|
|
87
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-700"><%= e.name %></td>
|
|
88
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_duration(e.duration_ms) %></td>
|
|
89
|
+
</tr>
|
|
90
|
+
<% end %>
|
|
91
|
+
</tbody>
|
|
92
|
+
</table>
|
|
93
|
+
<% else %>
|
|
94
|
+
<div class="py-20 text-center">
|
|
95
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
96
|
+
<svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg>
|
|
97
|
+
</div>
|
|
98
|
+
<p class="text-sm text-gray-500 font-medium">No cache events in this time window</p>
|
|
99
|
+
<p class="text-xs text-gray-400 mt-1">Try selecting a larger time range above</p>
|
|
100
|
+
</div>
|
|
101
|
+
<% end %>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|