solid_observer 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +25 -10
- data/app/controllers/solid_observer/dashboard_controller.rb +20 -6
- data/app/controllers/solid_observer/traces_controller.rb +17 -0
- data/app/helpers/solid_observer/application_helper.rb +31 -3
- data/app/views/layouts/solid_observer/application.html.erb +16 -1
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +6 -1
- data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +6 -1
- data/app/views/solid_observer/dashboard/_component_cards.html.erb +18 -0
- data/app/views/solid_observer/dashboard/_health.html.erb +6 -0
- data/app/views/solid_observer/dashboard/_home.html.erb +10 -0
- data/app/views/solid_observer/dashboard/_queue.html.erb +138 -0
- data/app/views/solid_observer/dashboard/_unified_feed.html.erb +38 -0
- data/app/views/solid_observer/dashboard/index.html.erb +5 -138
- data/app/views/solid_observer/events/index.html.erb +6 -1
- data/app/views/solid_observer/traces/show.html.erb +80 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260630000001_add_correlation_id_to_cache_events.rb +8 -0
- data/db/migrate/20260630000002_add_correlation_id_to_cable_events.rb +8 -0
- data/lib/generators/solid_observer/templates/initializer.rb.tt +6 -2
- data/lib/solid_observer/cli/health.rb +49 -0
- data/lib/solid_observer/cli/trace.rb +106 -0
- data/lib/solid_observer/configuration.rb +1 -4
- data/lib/solid_observer/correlated.rb +23 -0
- data/lib/solid_observer/queries/trace_query.rb +151 -0
- data/lib/solid_observer/services/health_score.rb +85 -0
- data/lib/solid_observer/services/record_cable_event.rb +1 -0
- data/lib/solid_observer/services/record_cache_event.rb +1 -0
- data/lib/solid_observer/services/unified_feed.rb +101 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +6 -0
- data/lib/tasks/solid_observer.rake +15 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c363ea187ff44b2a28184384cfbdee1d570c6e8fd981abceded3844819a2338
|
|
4
|
+
data.tar.gz: ff5bb32bb53ad65ea688365a5b8666aa34552580d3d78856f8e542fcf53b6d4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbac07f62a04d4ea7ae4c8c2c1200f04c07f400f94ca5775785380853cd63f56eccdc154b1debe55438fc60482d54be55199265c299eae43577e1943b60abd1e
|
|
7
|
+
data.tar.gz: '018f433df069c72a8a12ced3434e4b05f5a26eb6209bb33af0e5c29b3aca2df1961359692e250f247769cb0e9bab322b946dc208ca265688fb4ae0316de24dd9'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [0.6.0] - 2026-08-04
|
|
2
|
+
|
|
3
|
+
Headline: **Cross-component Tracing, Health Scoring & Unified Dashboard** — trace correlation across Queue/Cache/Cable, unified home dashboard, health scoring service, and CLI health & trace commands.
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Correlation ID propagation** — automatic correlation ID generation and propagation across ActiveJob (`Correlated.stamp`), SolidCache, and SolidCable events; configurable `correlation_id_generator`.
|
|
7
|
+
- **Cross-component trace service & CLI** — `Queries::TraceQuery` for fetching cross-component events by `correlation_id`; `rails solid_observer:trace[CORRELATION_ID,LIMIT]` CLI command.
|
|
8
|
+
- **Trace Web UI** — Web interface at `/solid_observer/traces/:correlation_id` displaying component-tagged event timelines across Queue, Cache, and Cable.
|
|
9
|
+
- **Unified health scoring service** — `Services::HealthScore` auto-calculates overall host stability using worst-of component statuses (Queue, Cache, Cable) with zero configuration required.
|
|
10
|
+
- **CLI health command** — `rails solid_observer:health` CLI command displaying aggregate and per-component health status.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Combined / Unified Dashboard** — Engine root (`GET /solid_observer/`) now renders the unified Home dashboard (overall health banner, per-component status cards, and merged multi-component activity feed). Dedicated Queue overview moved to `/solid_observer/queue`.
|
|
14
|
+
|
|
15
|
+
### Upgrade & Database Migration
|
|
16
|
+
- **Required DB Migration** — Running `bin/rails solid_observer:install:migrations && bin/rails db:migrate` is required when upgrading to v0.6.0. Adds `correlation_id` columns to `solid_observer_cache_events` and `solid_observer_cable_events` tables.
|
|
17
|
+
|
|
1
18
|
## [0.5.0] - 2026-06-24
|
|
2
19
|
|
|
3
20
|
Headline: **Solid Cable observability** — optional Cable telemetry, dashboard, storage health, and guarded trim controls.
|
data/README.md
CHANGED
|
@@ -7,33 +7,35 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/bart-oz/solid_observer/releases"><img src="https://img.shields.io/badge/version-0.
|
|
10
|
+
<a href="https://github.com/bart-oz/solid_observer/releases"><img src="https://img.shields.io/badge/version-0.6.0-blue.svg" alt="Version"></a>
|
|
11
11
|
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
12
12
|
<a href="https://github.com/bart-oz/solid_observer/actions"><img src="https://img.shields.io/badge/tests-passing-brightgreen.svg" alt="Tests"></a>
|
|
13
|
-
<a href="https://github.com/bart-oz/solid_observer/actions"><img src="https://img.shields.io/badge/coverage-96
|
|
14
|
-
</p>
|
|
13
|
+
<a href="https://github.com/bart-oz/solid_observer/actions"><img src="https://img.shields.io/badge/coverage-97.96%25-brightgreen.svg" alt="Coverage"></a>
|
|
15
14
|
|
|
16
15
|
---
|
|
17
16
|
<p align="center">
|
|
18
17
|
<a href=".github/assets/dash_1.png"><img src=".github/assets/dash_1.png" alt="SolidObserver dashboard overview" width="700"></a>
|
|
19
18
|
</p>
|
|
20
19
|
|
|
21
|
-
SolidObserver is a production-grade observability solution for Rails 8's Solid Stack. v0.
|
|
20
|
+
SolidObserver is a production-grade observability solution for Rails 8's Solid Stack. v0.6.0 covers **Solid Queue**, **Solid Cache**, and **Solid Cable** with a unified Web UI dashboard, health scoring (`Services::HealthScore`), CLI tools, metrics collection, and cross-component trace correlation by `correlation_id`.
|
|
22
21
|
|
|
23
|
-
## Features (v0.
|
|
22
|
+
## Features (v0.6.0)
|
|
24
23
|
|
|
25
24
|
| | Solid Queue | Solid Cache | Solid Cable |
|
|
26
25
|
|---|---|---|---|
|
|
27
26
|
| **Web UI Dashboard** | Queue stats, jobs browser, events log | Hit rate, ops/sec, error rate, avg duration | Broadcasts, rejection rate, trends |
|
|
27
|
+
| **Unified Home** | Overall health banner, component cards, merged activity feed at `/solid_observer` |
|
|
28
|
+
| **Health Scoring** | Worst-of aggregate stability (`Services::HealthScore`), config-free |
|
|
29
|
+
| **Cross-Component Trace** | Timeline by `correlation_id` across Queue, Cache, Cable (`/solid_observer/traces/:id`) |
|
|
28
30
|
| **Storage footprint** | DB size, event counts | SolidCache table size, row counts | Message count + backlogs |
|
|
29
31
|
| **Activity trends** | Sparklines (Performed, Ready, Failed) | Activity trend sparklines | Broadcast/rejection sparklines |
|
|
30
32
|
| **Stability indicator** | Stable / Degraded / Critical badge | Stability pill badge | Stable / Degraded / Critical (hybrid) |
|
|
31
33
|
| **Operational controls** | Retry / discard failed jobs | Prune expired entries, clear all entries | Trim expired messages |
|
|
32
|
-
| **CLI tools** | status, jobs:list/show/retry/discard | cache:prune, cache:clear | cable:trim |
|
|
34
|
+
| **CLI tools** | status, health, trace, jobs:list/show/retry/discard | cache:prune, cache:clear | cable:trim |
|
|
33
35
|
| **Privacy** | Job arguments excluded from persisted events | Keys and values **never** shown | Broadcasting names hashed (SHA256) |
|
|
34
36
|
| **Operating modes** | Real-time (no DB) or persistence (full history) | Persistence mode only | Persistence mode only |
|
|
35
37
|
|
|
36
|
-
Additional: 🔗
|
|
38
|
+
Additional: 🔗 Cross-component trace lookup by correlation_id · 🩺 Config-free aggregate health score · 🏠 Unified Home dashboard · ⚡ buffered writes · 🛡️ Docker/CI/K8s safe boot
|
|
37
39
|
|
|
38
40
|
## Requirements
|
|
39
41
|
|
|
@@ -175,6 +177,14 @@ Configuration:
|
|
|
175
177
|
|
|
176
178
|
SolidObserver ships with a zero-dependency Web UI (no asset pipeline, no JS framework) at `/solid_observer`.
|
|
177
179
|
|
|
180
|
+
### Views & Routes
|
|
181
|
+
|
|
182
|
+
- **Unified Home Dashboard** (`/solid_observer/`) — Overall worst-of health score banner, per-component status cards (Queue, Cache, Cable), and a unified cross-component activity feed.
|
|
183
|
+
- **Queue Overview** (`/solid_observer/queue`) — Dedicated Queue metrics, job browser (ready/scheduled/claimed/failed), events log, time-range trends, and operational controls.
|
|
184
|
+
- **Cache Dashboard** (`/solid_observer/cache_dashboard`) — SolidCache hit rates, ops/sec, error rates, storage footprint, and prune/clear controls.
|
|
185
|
+
- **Cable Dashboard** (`/solid_observer/cable_dashboard`) — SolidCable broadcast volume, rejection rates, backlog trends, and message trim controls.
|
|
186
|
+
- **Cross-Component Trace Timeline** (`/solid_observer/traces/:correlation_id`) — Component-tagged event sequence across Queue, Cache, and Cable for a specific correlation ID.
|
|
187
|
+
|
|
178
188
|
### Mount
|
|
179
189
|
|
|
180
190
|
The install generator mounts it for you. To mount manually:
|
|
@@ -398,7 +408,10 @@ SolidObserver.configure do |config|
|
|
|
398
408
|
end
|
|
399
409
|
```
|
|
400
410
|
|
|
401
|
-
When configured, all job events will include your correlation ID
|
|
411
|
+
When configured, all job, cache, and cable events will include your correlation ID. You can query the full cross-component event timeline for any request or job:
|
|
412
|
+
|
|
413
|
+
- **CLI**: `bin/rails "solid_observer:trace[CORRELATION_ID,LIMIT]"`
|
|
414
|
+
- **Web UI**: `/solid_observer/traces/:correlation_id`
|
|
402
415
|
|
|
403
416
|
### Production hardening (recommended)
|
|
404
417
|
|
|
@@ -427,6 +440,7 @@ end
|
|
|
427
440
|
| Command | Description |
|
|
428
441
|
|---------|-------------|
|
|
429
442
|
| `solid_observer:status` | Show queue status overview |
|
|
443
|
+
| `solid_observer:health` | Display aggregate and per-component health status |
|
|
430
444
|
| `solid_observer:jobs:list[status,queue,class,limit]` | List jobs with optional filters |
|
|
431
445
|
| `solid_observer:jobs:show[ID]` | Show job details |
|
|
432
446
|
| `solid_observer:jobs:retry[ID]` | Retry a failed job |
|
|
@@ -444,6 +458,7 @@ end
|
|
|
444
458
|
| `solid_observer:cache:clear` | Clear all SolidCache entries (with confirmation) |
|
|
445
459
|
| `solid_observer:cache:prune` | Prune expired SolidCache entries |
|
|
446
460
|
| `solid_observer:cable:trim` | Trim expired Solid Cable messages |
|
|
461
|
+
| `solid_observer:trace[CORRELATION_ID,LIMIT]` | Show cross-component trace timeline |
|
|
447
462
|
|
|
448
463
|
> **Note:** Storage commands manage **SolidObserver's storage** (event logs, metrics, snapshots) — not Solid Queue's jobs. Cache commands operate on SolidCache entries in the host app's cache store.
|
|
449
464
|
|
|
@@ -534,8 +549,8 @@ gem "sqlite3", "~> 2.0"
|
|
|
534
549
|
| v0.1.1 | Real-time mode (no migrations needed) | ✅ Released |
|
|
535
550
|
| v0.3.0 | Web UI dashboard + stability hardening | ✅ Released |
|
|
536
551
|
| v0.4.0 | Solid Cache monitoring | ✅ Released |
|
|
537
|
-
| v0.5.0 | Solid Cable monitoring | ✅
|
|
538
|
-
| v0.6.0 | Cross-component correlation, health scores |
|
|
552
|
+
| v0.5.0 | Solid Cable monitoring | ✅ Released |
|
|
553
|
+
| v0.6.0 | Cross-component trace correlation, health scores, unified dashboard | ✅ Current |
|
|
539
554
|
| v0.7.0 | Alerting & notifications | 🔜 Planned |
|
|
540
555
|
| v1.0.0 | Production stable release | 🎯 Goal |
|
|
541
556
|
|
|
@@ -12,10 +12,23 @@ module SolidObserver
|
|
|
12
12
|
def index
|
|
13
13
|
@component = selected_component
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
if @component == "queue" && SolidObserver.config.solid_queue_enabled?
|
|
16
|
+
assign_range_and_stats
|
|
17
|
+
load_persistence_data if persistence_mode?
|
|
18
|
+
elsif @component == "home"
|
|
19
|
+
@health = begin
|
|
20
|
+
Services::HealthScore.call
|
|
21
|
+
rescue => error
|
|
22
|
+
Rails.logger&.warn("Home HealthScore: #{error.class}: #{error.message}")
|
|
23
|
+
{overall: :degraded, components: {}}
|
|
24
|
+
end
|
|
25
|
+
@feed = begin
|
|
26
|
+
Services::UnifiedFeed.call
|
|
27
|
+
rescue => error
|
|
28
|
+
Rails.logger&.warn("Home UnifiedFeed: #{error.class}: #{error.message}")
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
19
32
|
end
|
|
20
33
|
|
|
21
34
|
def live_poll
|
|
@@ -98,17 +111,18 @@ module SolidObserver
|
|
|
98
111
|
""
|
|
99
112
|
end
|
|
100
113
|
requested = path_component if requested.empty?
|
|
114
|
+
return "queue" if requested == "queue" && SolidObserver.config.solid_queue_enabled?
|
|
101
115
|
return "cache" if requested == "cache" && SolidObserver.config.solid_cache_enabled?
|
|
102
116
|
|
|
103
|
-
"
|
|
117
|
+
"home"
|
|
104
118
|
end
|
|
105
119
|
|
|
106
120
|
def path_component
|
|
107
121
|
return "" unless request&.respond_to?(:path)
|
|
108
122
|
|
|
109
123
|
path = request&.path.to_s
|
|
110
|
-
return "cache" if path.end_with?("/cache")
|
|
111
124
|
return "queue" if path.end_with?("/queue")
|
|
125
|
+
return "cache" if path.end_with?("/cache")
|
|
112
126
|
|
|
113
127
|
""
|
|
114
128
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../helpers/solid_observer/application_helper"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
class TracesController < ApplicationController
|
|
7
|
+
helper SolidObserver::ApplicationHelper
|
|
8
|
+
|
|
9
|
+
include RequirePersistenceMode
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
result = SolidObserver::Queries::TraceQuery.new.call(correlation_id: params[:id])
|
|
13
|
+
@rows = result.rows
|
|
14
|
+
@unavailable_components = result.unavailable_components
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -250,9 +250,37 @@ module SolidObserver
|
|
|
250
250
|
SolidObserver.config.solid_cable_enabled?
|
|
251
251
|
end
|
|
252
252
|
|
|
253
|
-
def
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
def home?
|
|
254
|
+
controller_name == "dashboard" && @component.to_s == "home"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def health_status_badge(status)
|
|
258
|
+
stability_badge_for(status.to_sym)
|
|
259
|
+
rescue KeyError
|
|
260
|
+
Rails.logger&.warn("health_status_badge: unknown status #{status.inspect}, falling back to degraded")
|
|
261
|
+
stability_badge_for(:degraded)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def unified_feed_detail(row)
|
|
265
|
+
detail = row[:detail].to_s
|
|
266
|
+
if row[:correlation_id].present?
|
|
267
|
+
link_to(detail.presence || row[:correlation_id], trace_path(row[:correlation_id]), class: "so-trace__link")
|
|
268
|
+
else
|
|
269
|
+
detail.presence || "—"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def component_overview_path(name)
|
|
274
|
+
case name.to_sym
|
|
275
|
+
when :queue
|
|
276
|
+
queue_dashboard_path
|
|
277
|
+
when :cache
|
|
278
|
+
cache_dashboard_path
|
|
279
|
+
when :cable
|
|
280
|
+
cable_dashboard_path
|
|
281
|
+
else
|
|
282
|
+
root_path
|
|
283
|
+
end
|
|
256
284
|
end
|
|
257
285
|
|
|
258
286
|
private
|
|
@@ -524,6 +524,19 @@
|
|
|
524
524
|
|
|
525
525
|
.so-card li + li { margin-top: 0.45rem; }
|
|
526
526
|
|
|
527
|
+
/* SO-098 trace page */
|
|
528
|
+
.so-trace { max-width: 1120px; }
|
|
529
|
+
.so-trace__meta { margin-top: .35rem; font-size: .875rem; line-height: 1.5; color: var(--so-text-subtle); }
|
|
530
|
+
.so-trace__correlation { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; overflow-wrap: anywhere; }
|
|
531
|
+
.so-trace__table { min-width: 760px; }
|
|
532
|
+
.so-trace__time { width: 10.75rem; white-space: nowrap; }
|
|
533
|
+
.so-trace__component { width: 7.75rem; white-space: nowrap; }
|
|
534
|
+
.so-trace__event { width: 10.5rem; white-space: nowrap; }
|
|
535
|
+
.so-trace__details { min-width: 20rem; white-space: normal; overflow-wrap: anywhere; line-height: 1.5; }
|
|
536
|
+
.so-trace__link { color: var(--so-info); text-decoration: none; }
|
|
537
|
+
.so-trace__link:hover { text-decoration: underline; }
|
|
538
|
+
.so-trace__link:focus-visible { outline: 2px solid var(--so-info); outline-offset: 2px; box-shadow: 0 0 0 3px var(--so-focus-ring); }
|
|
539
|
+
|
|
527
540
|
@media (max-width: 960px) {
|
|
528
541
|
.so-dashboard-toolbar { flex-direction: column; align-items: stretch; }
|
|
529
542
|
.so-dashboard-toolbar__left, .so-dashboard-toolbar__right { margin-left: 0; }
|
|
@@ -553,9 +566,11 @@
|
|
|
553
566
|
<aside class="so-sidebar">
|
|
554
567
|
<div class="so-sidebar__logo">SolidObserver</div>
|
|
555
568
|
<nav class="so-sidebar__nav" aria-label="SolidObserver navigation">
|
|
569
|
+
<%= link_to "Home", root_path, class: ("active" if home?), "aria-current": (home? ? "page" : nil) %>
|
|
570
|
+
|
|
556
571
|
<% if SolidObserver.config.solid_queue_enabled? %>
|
|
557
572
|
<div class="so-sidebar__section">Queue</div>
|
|
558
|
-
<%= link_to "Overview",
|
|
573
|
+
<%= link_to "Overview", queue_dashboard_path, class: ("active" if controller_name == "dashboard" && @component.to_s == "queue"), "aria-current": (controller_name == "dashboard" && @component.to_s == "queue" ? "page" : nil) %>
|
|
559
574
|
<%= link_to "Jobs", jobs_path, class: ("active" if controller_name == "jobs"), "aria-current": (controller_name == "jobs" ? "page" : nil) %>
|
|
560
575
|
<% if persistence_mode? %>
|
|
561
576
|
<%= link_to "Events", events_path, class: ("active" if controller_name == "events"), "aria-current": (controller_name == "events" ? "page" : nil) %>
|
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
<tbody>
|
|
20
20
|
<% recent_events.each do |event| %>
|
|
21
21
|
<tr>
|
|
22
|
-
<td
|
|
22
|
+
<td>
|
|
23
|
+
<%= event.event_type.to_s %>
|
|
24
|
+
<% if event.correlation_id.present? %>
|
|
25
|
+
<%= link_to "Trace →", trace_path(event.correlation_id), class: "so-trace__link" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</td>
|
|
23
28
|
<td><%= event.channel_class.presence || "—" %></td>
|
|
24
29
|
<td><span class="so-cable-dashboard__digest"><%= cable_event_digest(event.broadcasting_digest) %></span></td>
|
|
25
30
|
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
<tbody>
|
|
20
20
|
<% recent_events.each do |event| %>
|
|
21
21
|
<tr>
|
|
22
|
-
<td
|
|
22
|
+
<td>
|
|
23
|
+
<%= event.event_type.to_s.humanize %>
|
|
24
|
+
<% if event.correlation_id.present? %>
|
|
25
|
+
<%= link_to "Trace →", trace_path(event.correlation_id), class: "so-trace__link" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</td>
|
|
23
28
|
<td><span class="so-cache-dashboard__digest"><%= cache_event_digest(event.key_digest) %></span></td>
|
|
24
29
|
<td><%= cache_event_outcome_badge(event) %></td>
|
|
25
30
|
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% if health[:components].present? %>
|
|
2
|
+
<section class="so-cards-section" data-so-zone="home-cards" aria-labelledby="so-home-components-heading">
|
|
3
|
+
<h2 class="so-cards-section__title" id="so-home-components-heading">Components</h2>
|
|
4
|
+
<div class="so-stat-cards">
|
|
5
|
+
<% health[:components].each do |name, info| %>
|
|
6
|
+
<div class="so-card" aria-labelledby="so-card-<%= name %>">
|
|
7
|
+
<div class="so-card__label" id="so-card-<%= name %>"><%= name.to_s.titleize %></div>
|
|
8
|
+
<div class="so-card__value">
|
|
9
|
+
<%= health_status_badge(info[:status]) %>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="so-card__subtitle">
|
|
12
|
+
<% unless info[:available] %><span class="so-badge so-badge--default">Unavailable</span> · <% end %><%= link_to "View overview →", component_overview_path(name), class: "so-stability__link" %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
</section>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% non_stable = health[:components].count { |_n, i| i[:status].to_sym != :stable } %>
|
|
2
|
+
<section class="so-stability" data-so-zone="home-health" aria-labelledby="so-health-heading">
|
|
3
|
+
<span class="so-stability__label" id="so-health-heading">Overall Health</span>
|
|
4
|
+
<%= health_status_badge(health[:overall]) %>
|
|
5
|
+
<span class="so-stability__detail"><%= non_stable.zero? ? "All included components stable" : "#{non_stable} component#{'s' unless non_stable == 1} not stable" %></span>
|
|
6
|
+
</section>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% content_for :title, "Home" %>
|
|
2
|
+
<div class="so-dashboard" data-so-zone="home">
|
|
3
|
+
<div class="so-content__header">
|
|
4
|
+
<h1>Home</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<%= render "health", health: health %>
|
|
8
|
+
<%= render "component_cards", health: health %>
|
|
9
|
+
<%= render "unified_feed", feed: feed %>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<div class="so-dashboard">
|
|
2
|
+
<% queue_enabled = SolidObserver.config.solid_queue_enabled? %>
|
|
3
|
+
<% queue_status_class = queue_enabled ? "so-badge--success" : "so-badge--warning" %>
|
|
4
|
+
|
|
5
|
+
<div class="so-content__header">
|
|
6
|
+
<h1>Queue overview</h1>
|
|
7
|
+
<div class="so-queue-overview__intro">
|
|
8
|
+
<span class="so-badge so-badge--pill <%= queue_status_class %>">
|
|
9
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
10
|
+
<circle r="3" cx="3" cy="3" />
|
|
11
|
+
</svg>
|
|
12
|
+
<%= queue_enabled ? "Available" : "Unavailable" %>
|
|
13
|
+
</span>
|
|
14
|
+
<% if queue_enabled %>
|
|
15
|
+
<p class="so-queue-overview__hint">Selected range: <%= range_label(@range) %></p>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<% if !queue_enabled %>
|
|
21
|
+
<section class="so-card so-card--section" aria-labelledby="so-queue-unavailable-heading">
|
|
22
|
+
<h2 id="so-queue-unavailable-heading">Queue Unavailable</h2>
|
|
23
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Queue component is disabled or not installed. Enable SolidQueue to access dashboard controls.</p>
|
|
24
|
+
</section>
|
|
25
|
+
<% else %>
|
|
26
|
+
<%# Zone A: Toolbar — range selector, Refresh, Live toggle, help disclosure %>
|
|
27
|
+
<div class="so-dashboard-toolbar" data-so-live data-so-poll-url="<%= poll_data_path %>">
|
|
28
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar__left">
|
|
29
|
+
<label for="so-range" class="so-card__label so-filters__label">Range</label>
|
|
30
|
+
<select id="so-range" name="range" data-so-range-select onchange="this.form.submit()">
|
|
31
|
+
<% SolidObserver::QueueStats::RANGES.each_key do |key| %>
|
|
32
|
+
<option value="<%= key %>" <%= "selected" if key == @range %>><%= key %></option>
|
|
33
|
+
<% end %>
|
|
34
|
+
</select>
|
|
35
|
+
<button type="button" class="so-btn so-btn--refresh" data-so-refresh aria-busy="false">Refresh data</button>
|
|
36
|
+
<span class="so-toolbar-freshness" data-so-freshness aria-live="polite"></span>
|
|
37
|
+
<input type="hidden" name="live" value="<%= @live ? 'on' : '' %>">
|
|
38
|
+
</form>
|
|
39
|
+
<div class="so-dashboard-toolbar__right">
|
|
40
|
+
<label class="so-toggle so-toggle--pill <%= "so-toggle--on" if @live %>">
|
|
41
|
+
<input type="checkbox" name="live" value="on" <%= "checked" if @live %> data-so-live-toggle>
|
|
42
|
+
<span class="so-toggle__track" aria-hidden="true">
|
|
43
|
+
<span class="so-toggle__thumb"></span>
|
|
44
|
+
</span>
|
|
45
|
+
<span class="so-toggle__label">Live</span>
|
|
46
|
+
<span class="so-toggle__sep" aria-hidden="true"> · </span>
|
|
47
|
+
<span class="so-toggle__cadence" aria-live="polite"><%= @live ? "5s" : "off" %></span>
|
|
48
|
+
<span class="so-toggle__dot" aria-hidden="true"></span>
|
|
49
|
+
</label>
|
|
50
|
+
<div class="so-help-wrapper" data-so-help-wrapper>
|
|
51
|
+
<button type="button" class="so-btn so-btn--help" data-so-help-btn aria-expanded="false" aria-controls="so-help-panel">(?)
|
|
52
|
+
</button>
|
|
53
|
+
<div id="so-help-panel" class="so-help-panel" data-so-help-panel hidden>
|
|
54
|
+
<p>Live mode polls every 5 seconds for current queue state. Throughput and chart data update only on range change or manual refresh.</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<%# Zone B: Right now — live state %>
|
|
61
|
+
<section class="so-dashboard-section" aria-labelledby="so-live-state-heading">
|
|
62
|
+
<div class="so-dashboard-section__header">
|
|
63
|
+
<h2 class="so-dashboard-section__title" id="so-live-state-heading">Right now</h2>
|
|
64
|
+
<span class="so-dashboard-section__meta"><span class="so-badge so-badge--pill so-badge--success"><svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" fill="var(--so-live-marker, var(--so-success))"/></svg></span> current</span>
|
|
65
|
+
</div>
|
|
66
|
+
<%= render "live_state", stats: @stats %>
|
|
67
|
+
</section>
|
|
68
|
+
|
|
69
|
+
<%# Zone C: Throughput in selected range %>
|
|
70
|
+
<% if persistence_mode? && @stats[:available] %>
|
|
71
|
+
<section class="so-dashboard-section" aria-labelledby="so-throughput-heading">
|
|
72
|
+
<div class="so-dashboard-section__header">
|
|
73
|
+
<h2 class="so-dashboard-section__title" id="so-throughput-heading">Throughput in selected range</h2>
|
|
74
|
+
<span class="so-dashboard-section__meta"><span class="so-badge so-badge--pill so-badge--info"><svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" fill="var(--so-range-marker, var(--so-info))"/></svg></span> <span data-so-range-copy><%= range_label(@range) %></span></span>
|
|
75
|
+
</div>
|
|
76
|
+
<%= render "throughput", stats: @stats, range: @range %>
|
|
77
|
+
</section>
|
|
78
|
+
<% end %>
|
|
79
|
+
|
|
80
|
+
<%# Zone D: Charts %>
|
|
81
|
+
<% if @stats[:available] %>
|
|
82
|
+
<section class="so-dashboard-section" aria-labelledby="so-chart-heading">
|
|
83
|
+
<div class="so-dashboard-section__header">
|
|
84
|
+
<h2 class="so-dashboard-section__title" id="so-chart-heading">Charts</h2>
|
|
85
|
+
</div>
|
|
86
|
+
<%= render "chart", range: @range %>
|
|
87
|
+
</section>
|
|
88
|
+
<% end %>
|
|
89
|
+
|
|
90
|
+
<%# Zone E: Per-queue table %>
|
|
91
|
+
<%= render "queue_table", stats: @stats, range: @range %>
|
|
92
|
+
|
|
93
|
+
<%# Zone F: Stability strip %>
|
|
94
|
+
<% if @stats[:available] && persistence_mode? %>
|
|
95
|
+
<section class="so-stability" data-so-zone="stability" aria-labelledby="so-stability-heading">
|
|
96
|
+
<span class="so-stability__label" id="so-stability-heading">Stability</span>
|
|
97
|
+
<%= stability_badge(@stats) %>
|
|
98
|
+
<span class="so-stability__detail"><%= stability_detail(@stats) %></span>
|
|
99
|
+
<%= link_to "View failures →", events_path(event_type: "job_failed"), class: "so-stability__link" %>
|
|
100
|
+
</section>
|
|
101
|
+
<% end %>
|
|
102
|
+
|
|
103
|
+
<%# Zone G: Recent Events %>
|
|
104
|
+
<% if @stats[:available] && persistence_mode? %>
|
|
105
|
+
<% if defined?(@recent_events) && @recent_events.present? %>
|
|
106
|
+
<div class="so-card">
|
|
107
|
+
<div class="so-card__label">Recent Events</div>
|
|
108
|
+
<table class="so-table so-table--card">
|
|
109
|
+
<caption class="sr-only">Recent queue events</caption>
|
|
110
|
+
<thead>
|
|
111
|
+
<tr>
|
|
112
|
+
<th>Event</th>
|
|
113
|
+
<th>Job Class</th>
|
|
114
|
+
<th>Queue</th>
|
|
115
|
+
<th>Time</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
<% @recent_events.each do |event| %>
|
|
120
|
+
<tr>
|
|
121
|
+
<td><%= event.event_type.humanize %></td>
|
|
122
|
+
<td><%= event.job_class %></td>
|
|
123
|
+
<td><%= event.queue_name %></td>
|
|
124
|
+
<td><%= time_ago_in_words(event.recorded_at) %> ago</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<% end %>
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
<% else %>
|
|
131
|
+
<div class="so-card">
|
|
132
|
+
<div class="so-card__label">Recent Events</div>
|
|
133
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No recent events yet. Queue activity will appear here after jobs are enqueued, performed, or failed.</p>
|
|
134
|
+
</div>
|
|
135
|
+
<% end %>
|
|
136
|
+
<% end %>
|
|
137
|
+
<% end %>
|
|
138
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<section class="so-card" data-so-zone="home-feed" aria-labelledby="so-feed-heading">
|
|
2
|
+
<div class="so-card__label" id="so-feed-heading">Recent Activity</div>
|
|
3
|
+
<% if SolidObserver.config.realtime_mode? %>
|
|
4
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">Feed requires persistence mode. Switch to persistence mode to see recent activity across components.</p>
|
|
5
|
+
<% elsif feed.present? %>
|
|
6
|
+
<table class="so-table so-table--card">
|
|
7
|
+
<caption class="sr-only">Unified recent activity across enabled components</caption>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Component</th>
|
|
11
|
+
<th>Event</th>
|
|
12
|
+
<th>Detail</th>
|
|
13
|
+
<th>Time</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% feed.each do |row| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><%= row[:component].to_s.titleize %></td>
|
|
20
|
+
<td><%= row[:event_type].to_s.humanize %></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%= unified_feed_detail(row) %>
|
|
23
|
+
</td>
|
|
24
|
+
<td>
|
|
25
|
+
<% if row[:recorded_at] %>
|
|
26
|
+
<time datetime="<%= row[:recorded_at].iso8601 %>" title="<%= row[:recorded_at].iso8601 %>"><%= time_ago_in_words(row[:recorded_at]) %> ago</time>
|
|
27
|
+
<% else %>
|
|
28
|
+
—
|
|
29
|
+
<% end %>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
<% else %>
|
|
36
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No recent activity yet. Events will appear here after components record activity.</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</section>
|