solid_stack_web 0.4.0 → 0.5.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/README.md +16 -2
- data/app/assets/stylesheets/solid_stack_web/_03_stats.css +27 -0
- data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +23 -2
- data/app/assets/stylesheets/solid_stack_web/_09_detail.css +42 -0
- data/app/controllers/solid_stack_web/application_controller.rb +1 -1
- data/app/controllers/solid_stack_web/cache/flushes_controller.rb +8 -0
- data/app/controllers/solid_stack_web/cache_controller.rb +2 -0
- data/app/controllers/solid_stack_web/cache_entries_controller.rb +32 -0
- data/app/helpers/solid_stack_web/application_helper.rb +32 -0
- data/app/javascript/solid_stack_web/application.js +2 -0
- data/app/javascript/solid_stack_web/search_controller.js +16 -0
- data/app/models/solid_stack_web/cache_size_stats.rb +33 -0
- data/app/models/solid_stack_web/cache_stats.rb +3 -2
- data/app/models/solid_stack_web/cache_timeline.rb +40 -0
- data/app/views/layouts/solid_stack_web/application.html.erb +11 -0
- data/app/views/solid_stack_web/cache/index.html.erb +84 -2
- data/app/views/solid_stack_web/cache_entries/index.html.erb +66 -0
- data/app/views/solid_stack_web/cache_entries/show.html.erb +48 -0
- data/app/views/solid_stack_web/dashboard/index.html.erb +8 -2
- data/app/views/solid_stack_web/history/index.html.erb +3 -3
- data/app/views/solid_stack_web/jobs/index.html.erb +7 -5
- data/config/importmap.rb +1 -0
- data/config/routes.rb +2 -0
- data/lib/solid_stack_web/version.rb +1 -1
- data/lib/solid_stack_web.rb +5 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3159278d6dd76fc0e54660ab9c11536a63037815771ff04206adca43b800c4ef
|
|
4
|
+
data.tar.gz: 05f47e20a15b2f70cbf216713cb4c18da3e3f569b64eb1a271543489fecb95c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdd04812d4cdfa1d3c48ff2a81319f8956b71afee1dded7dbce009797822248e422990f02134fbb4f3c88e6e2e2702c2c91c78d94a6d04e28ebd8d1a2b0022b9
|
|
7
|
+
data.tar.gz: 431b97295f3682b1512b09ece023c280997560d4b4f065ef742411fddc63275c8257f0d34d02f6bb0589a555c8677d0515301cabfc8b0768465fd42ff6599a54
|
data/README.md
CHANGED
|
@@ -74,6 +74,10 @@ SolidStackWeb.configure do |config|
|
|
|
74
74
|
|
|
75
75
|
# Maximum results shown by the search feature (default: 25).
|
|
76
76
|
config.search_results_limit = 25
|
|
77
|
+
|
|
78
|
+
# Show the raw serialized value on the cache entry detail page (default: false).
|
|
79
|
+
# Disable for stores that contain sensitive data.
|
|
80
|
+
config.allow_value_preview = true
|
|
77
81
|
end
|
|
78
82
|
```
|
|
79
83
|
|
|
@@ -94,7 +98,17 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
|
|
|
94
98
|
|
|
95
99
|
## Solid Cache
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
### Features
|
|
102
|
+
|
|
103
|
+
- **Overview dashboard card** — live entry count (linked to the entry browser), total byte size, and oldest-entry age (`time_ago_in_words` with exact timestamp on hover; hidden when cache is empty)
|
|
104
|
+
- **Cache timeline** — two side-by-side 24-hour bar charts showing entries written per hour and bytes written per hour; each bar has a hover tooltip
|
|
105
|
+
- **Size distribution** — byte-range histogram (< 1 KB → > 1 MB) with entry counts and proportional inline bars; hidden when cache is empty
|
|
106
|
+
- **Largest entries** — top 10 entries by byte size, each linked to the detail page; hidden when cache is empty
|
|
107
|
+
- **Entry browser** — `GET /cache/entries` lists all `SolidCache::Entry` records in a paginated, sortable table; columns: key, byte size, created-at; sortable by any column; key auto-submits search after 4 characters
|
|
108
|
+
- **Key search** — filter entries by key substring; results update automatically after 4 characters
|
|
109
|
+
- **Entry detail page** — `GET /cache/entries/:id` shows the full key, byte size, and created-at; optionally displays the raw serialized value (see `allow_value_preview` below)
|
|
110
|
+
- **Delete entry** — per-row delete button or detail-page button removes a single cache entry
|
|
111
|
+
- **Flush All** — header button deletes every cache entry with a confirmation prompt
|
|
98
112
|
|
|
99
113
|
---
|
|
100
114
|
|
|
@@ -122,7 +136,7 @@ _Channel monitoring coming in v0.6.0. Currently shows active message count and d
|
|
|
122
136
|
"processes_stale": 0,
|
|
123
137
|
"slow_jobs": 7
|
|
124
138
|
},
|
|
125
|
-
"cache": { "entries": 1024, "byte_size": 2097152 },
|
|
139
|
+
"cache": { "entries": 1024, "byte_size": 2097152, "oldest_entry": "2026-05-20T10:00:00Z" },
|
|
126
140
|
"cable": { "messages": 50, "channels": 3 },
|
|
127
141
|
"generated_at": "2026-05-26T10:00:00Z"
|
|
128
142
|
}
|
|
@@ -22,6 +22,33 @@ a.sqw-stat:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); text-decoration: none;
|
|
|
22
22
|
.sqw-stat__label { font-size: 12px; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
23
23
|
.sqw-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
24
24
|
|
|
25
|
+
.sqw-size-grid {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: 1fr 1fr;
|
|
28
|
+
gap: 1.5rem;
|
|
29
|
+
margin-top: 1.5rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sqw-size-section { }
|
|
33
|
+
|
|
34
|
+
.sqw-dist-bar-cell {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.5rem;
|
|
38
|
+
min-width: 160px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sqw-dist-bar {
|
|
42
|
+
height: 8px;
|
|
43
|
+
background: var(--purple);
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
min-width: 2px;
|
|
46
|
+
opacity: 0.7;
|
|
47
|
+
transition: width 0.3s ease;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.sqw-dist-pct { font-size: 12px; white-space: nowrap; }
|
|
51
|
+
|
|
25
52
|
.sqw-stat--ready .sqw-stat__value { color: var(--success); }
|
|
26
53
|
.sqw-stat--scheduled .sqw-stat__value { color: var(--info); }
|
|
27
54
|
.sqw-stat--claimed .sqw-stat__value { color: var(--primary); }
|
|
@@ -69,6 +69,7 @@ a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; }
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
.sqw-inline-stat__value { font-size: 28px; font-weight: 700; line-height: 1; }
|
|
72
|
+
.sqw-inline-stat__value--sm { font-size: 16px; }
|
|
72
73
|
|
|
73
74
|
.sqw-inline-stat--ready .sqw-inline-stat__value { color: var(--success); }
|
|
74
75
|
.sqw-inline-stat--scheduled .sqw-inline-stat__value { color: var(--info); }
|
|
@@ -105,8 +106,28 @@ a.sqw-inline-stat:hover { opacity: 0.7; text-decoration: none; }
|
|
|
105
106
|
height: 40px;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
.sqw-sparkline--sm {
|
|
109
|
-
|
|
109
|
+
.sqw-sparkline--sm { height: 24px; }
|
|
110
|
+
.sqw-sparkline--lg { height: 64px; }
|
|
111
|
+
|
|
112
|
+
.sqw-timeline-grid {
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-columns: 1fr 1fr;
|
|
115
|
+
gap: 0;
|
|
116
|
+
margin-top: 1.5rem;
|
|
117
|
+
border: 1px solid var(--border);
|
|
118
|
+
border-radius: var(--radius);
|
|
119
|
+
background: var(--surface);
|
|
120
|
+
box-shadow: var(--shadow);
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.sqw-timeline-chart {
|
|
125
|
+
border-top: none;
|
|
126
|
+
color: var(--purple);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.sqw-timeline-chart + .sqw-timeline-chart {
|
|
130
|
+
border-left: 1px solid var(--border);
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
.sqw-queue-sparkline {
|
|
@@ -70,6 +70,48 @@
|
|
|
70
70
|
overflow-y: auto;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
.sqw-detail {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: auto 1fr;
|
|
76
|
+
gap: 0.5rem 1.5rem;
|
|
77
|
+
font-size: 13px;
|
|
78
|
+
margin-bottom: 1.5rem;
|
|
79
|
+
}
|
|
80
|
+
.sqw-detail__row {
|
|
81
|
+
display: contents;
|
|
82
|
+
}
|
|
83
|
+
.sqw-detail__row dt { color: var(--muted); white-space: nowrap; align-self: start; padding-top: 0.15rem; }
|
|
84
|
+
.sqw-detail__row dd { word-break: break-all; }
|
|
85
|
+
|
|
86
|
+
.sqw-value-preview { margin-top: 1rem; }
|
|
87
|
+
|
|
88
|
+
.sqw-value-preview__header {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.5rem;
|
|
92
|
+
margin-bottom: 0.75rem;
|
|
93
|
+
}
|
|
94
|
+
.sqw-value-preview__header .sqw-section-title { margin-bottom: 0; }
|
|
95
|
+
|
|
96
|
+
.sqw-value-pre {
|
|
97
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
98
|
+
font-size: 12px;
|
|
99
|
+
background: var(--bg);
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
border-radius: var(--radius);
|
|
102
|
+
padding: 0.75rem;
|
|
103
|
+
overflow-x: auto;
|
|
104
|
+
white-space: pre-wrap;
|
|
105
|
+
word-break: break-word;
|
|
106
|
+
max-height: 500px;
|
|
107
|
+
overflow-y: auto;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.sqw-value-truncated { font-size: 12px; margin-top: 0.5rem; }
|
|
111
|
+
|
|
112
|
+
.sqw-link { color: var(--primary); text-decoration: none; }
|
|
113
|
+
.sqw-link:hover { text-decoration: underline; }
|
|
114
|
+
|
|
73
115
|
.sqw-code-input {
|
|
74
116
|
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
75
117
|
font-size: 12px;
|
|
@@ -16,7 +16,7 @@ module SolidStackWeb
|
|
|
16
16
|
def current_section
|
|
17
17
|
case controller_name
|
|
18
18
|
when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks" then :queue
|
|
19
|
-
when "cache" then :cache
|
|
19
|
+
when "cache", "cache_entries" then :cache
|
|
20
20
|
when "cable" then :cable
|
|
21
21
|
else :overview
|
|
22
22
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class CacheEntriesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@search = params[:q].presence
|
|
5
|
+
@sort = resolve_sort
|
|
6
|
+
|
|
7
|
+
scope = ::SolidCache::Entry.all
|
|
8
|
+
scope = scope.where("key LIKE ?", "%#{::ActiveRecord::Base.sanitize_sql_like(@search)}%") if @search
|
|
9
|
+
scope = scope.order(@sort["column"] => @sort["direction"])
|
|
10
|
+
|
|
11
|
+
@pagy, @entries = pagy(scope)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
@entry = ::SolidCache::Entry.find(params[:id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def destroy
|
|
19
|
+
::SolidCache::Entry.find(params[:id]).destroy
|
|
20
|
+
redirect_to cache_entries_path(q: params[:q], column: params[:column], direction: params[:direction]),
|
|
21
|
+
notice: "Cache entry deleted."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def resolve_sort
|
|
27
|
+
column = %w[byte_size created_at key].include?(params[:column]) ? params[:column] : "byte_size"
|
|
28
|
+
direction = %w[asc desc].include?(params[:direction]) ? params[:direction] : "desc"
|
|
29
|
+
{ "column" => column, "direction" => direction }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
def format_cache_value(raw)
|
|
4
|
+
str = raw.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
|
5
|
+
parsed = JSON.parse(str)
|
|
6
|
+
{ label: "JSON", content: JSON.pretty_generate(parsed) }
|
|
7
|
+
rescue JSON::ParserError, JSON::GeneratorError
|
|
8
|
+
{ label: "Text", content: str }
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
def format_duration(seconds)
|
|
4
12
|
return "—" if seconds.nil?
|
|
5
13
|
return "#{(seconds * 1000).round}ms" if seconds < 1
|
|
@@ -10,6 +18,30 @@ module SolidStackWeb
|
|
|
10
18
|
"#{s / 3600}h #{(s % 3600) / 60}m"
|
|
11
19
|
end
|
|
12
20
|
|
|
21
|
+
def cache_entries_timeline_svg(timeline)
|
|
22
|
+
build_sparkline_svg(
|
|
23
|
+
Struct.new(:buckets, :max).new(timeline.entry_buckets, timeline.entry_max),
|
|
24
|
+
css_class: "sqw-sparkline sqw-sparkline--lg",
|
|
25
|
+
aria_label: "Cache entries written over the last 24 hours"
|
|
26
|
+
) do |count, i|
|
|
27
|
+
hours_ago = CacheTimeline::HOURS - 1 - i
|
|
28
|
+
hours_ago.zero? ? "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} this hour" \
|
|
29
|
+
: "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} #{hours_ago}h ago"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cache_bytes_timeline_svg(timeline)
|
|
34
|
+
build_sparkline_svg(
|
|
35
|
+
Struct.new(:buckets, :max).new(timeline.byte_buckets, timeline.byte_max),
|
|
36
|
+
css_class: "sqw-sparkline sqw-sparkline--lg",
|
|
37
|
+
aria_label: "Cache bytes written over the last 24 hours"
|
|
38
|
+
) do |bytes, i|
|
|
39
|
+
hours_ago = CacheTimeline::HOURS - 1 - i
|
|
40
|
+
size = number_to_human_size(bytes)
|
|
41
|
+
hours_ago.zero? ? "#{size} written this hour" : "#{size} written #{hours_ago}h ago"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
13
45
|
def throughput_sparkline_svg(sparkline)
|
|
14
46
|
build_sparkline_svg(sparkline, aria_label: "Throughput over the last 12 hours") do |count, i|
|
|
15
47
|
hours_ago = SolidStackWeb::ThroughputSparkline::HOURS - i
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import "@hotwired/turbo"
|
|
2
2
|
import { Application } from "@hotwired/stimulus"
|
|
3
3
|
import RefreshController from "solid_stack_web/refresh_controller"
|
|
4
|
+
import SearchController from "solid_stack_web/search_controller"
|
|
4
5
|
import SelectionController from "solid_stack_web/selection_controller"
|
|
5
6
|
import SparklineTooltipController from "solid_stack_web/sparkline_tooltip_controller"
|
|
6
7
|
|
|
7
8
|
const application = Application.start()
|
|
8
9
|
application.register("refresh", RefreshController)
|
|
10
|
+
application.register("search", SearchController)
|
|
9
11
|
application.register("selection", SelectionController)
|
|
10
12
|
application.register("sparkline-tooltip", SparklineTooltipController)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
filter({ target }) {
|
|
5
|
+
clearTimeout(this._timer)
|
|
6
|
+
const len = target.value.length
|
|
7
|
+
if (len >= 4 || len === 0) {
|
|
8
|
+
this._timer = setTimeout(() => target.form.requestSubmit(), 300)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
select({ target }) {
|
|
13
|
+
clearTimeout(this._timer)
|
|
14
|
+
target.form.requestSubmit()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class CacheSizeStats
|
|
3
|
+
BUCKETS = [
|
|
4
|
+
{ label: "< 1 KB", min: 0, max: 1_024 },
|
|
5
|
+
{ label: "1 – 10 KB", min: 1_024, max: 10_240 },
|
|
6
|
+
{ label: "10 – 100 KB", min: 10_240, max: 102_400 },
|
|
7
|
+
{ label: "100 KB – 1 MB", min: 102_400, max: 1_048_576 },
|
|
8
|
+
{ label: "> 1 MB", min: 1_048_576, max: nil }
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
TOP_N = 10
|
|
12
|
+
|
|
13
|
+
def top_entries
|
|
14
|
+
@top_entries ||= ::SolidCache::Entry
|
|
15
|
+
.select(:id, :key, :byte_size)
|
|
16
|
+
.order(byte_size: :desc)
|
|
17
|
+
.limit(TOP_N)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def buckets
|
|
21
|
+
@buckets ||= BUCKETS.map do |b|
|
|
22
|
+
scope = ::SolidCache::Entry.all
|
|
23
|
+
scope = scope.where("byte_size >= ?", b[:min]) if b[:min] > 0
|
|
24
|
+
scope = scope.where("byte_size < ?", b[:max]) if b[:max]
|
|
25
|
+
{ label: b[:label], count: scope.count }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def total
|
|
30
|
+
@total ||= ::SolidCache::Entry.count
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -2,8 +2,9 @@ module SolidStackWeb
|
|
|
2
2
|
class CacheStats
|
|
3
3
|
def to_h
|
|
4
4
|
{
|
|
5
|
-
entries:
|
|
6
|
-
byte_size:
|
|
5
|
+
entries: ::SolidCache::Entry.count,
|
|
6
|
+
byte_size: ::SolidCache::Entry.sum(:byte_size),
|
|
7
|
+
oldest_entry: ::SolidCache::Entry.minimum(:created_at)
|
|
7
8
|
}
|
|
8
9
|
end
|
|
9
10
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class CacheTimeline
|
|
3
|
+
HOURS = 24
|
|
4
|
+
|
|
5
|
+
def entry_buckets
|
|
6
|
+
@entry_buckets ||= build_buckets { |rows, from, to| rows.count { |t, _| t >= from && t < to } }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def byte_buckets
|
|
10
|
+
@byte_buckets ||= build_buckets { |rows, from, to| rows.sum { |t, b| t >= from && t < to ? b : 0 } }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def entry_max
|
|
14
|
+
entry_buckets.max || 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def byte_max
|
|
18
|
+
byte_buckets.max || 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def rows
|
|
24
|
+
@rows ||= begin
|
|
25
|
+
origin = Time.current - HOURS.hours
|
|
26
|
+
::SolidCache::Entry.where(created_at: origin..Time.current).pluck(:created_at, :byte_size)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_buckets(&block)
|
|
31
|
+
now = Time.current
|
|
32
|
+
origin = now - HOURS.hours
|
|
33
|
+
HOURS.times.map do |i|
|
|
34
|
+
from = origin + i.hours
|
|
35
|
+
to = origin + (i + 1).hours
|
|
36
|
+
block.call(rows, from, to)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
</div>
|
|
25
25
|
</header>
|
|
26
26
|
|
|
27
|
+
<% if current_section == :cache %>
|
|
28
|
+
<nav class="sqw-subnav">
|
|
29
|
+
<div class="sqw-subnav__inner">
|
|
30
|
+
<%= link_to "Overview", cache_path,
|
|
31
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cache"}" %>
|
|
32
|
+
<%= link_to "Entries", cache_entries_path,
|
|
33
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cache_entries"}" %>
|
|
34
|
+
</div>
|
|
35
|
+
</nav>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
27
38
|
<% if current_section == :queue %>
|
|
28
39
|
<nav class="sqw-subnav">
|
|
29
40
|
<div class="sqw-subnav__inner">
|
|
@@ -3,12 +3,94 @@
|
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
5
|
<div class="sqw-stats-grid">
|
|
6
|
-
|
|
6
|
+
<%= link_to cache_entries_path, class: "sqw-stat sqw-stat--cache" do %>
|
|
7
7
|
<span class="sqw-stat__label">Total Entries</span>
|
|
8
8
|
<span class="sqw-stat__value"><%= @total_entries %></span>
|
|
9
|
-
|
|
9
|
+
<% end %>
|
|
10
10
|
<div class="sqw-stat sqw-stat--cache">
|
|
11
11
|
<span class="sqw-stat__label">Total Size</span>
|
|
12
12
|
<span class="sqw-stat__value"><%= number_to_human_size(@total_byte_size) %></span>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
|
+
|
|
16
|
+
<% if @total_entries > 0 %>
|
|
17
|
+
<div class="sqw-size-grid">
|
|
18
|
+
<section class="sqw-size-section">
|
|
19
|
+
<h2 class="sqw-section-title">Size Distribution</h2>
|
|
20
|
+
<table class="sqw-table">
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>Range</th>
|
|
24
|
+
<th>Entries</th>
|
|
25
|
+
<th></th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<% @size_stats.buckets.each do |bucket| %>
|
|
30
|
+
<% pct = @size_stats.total > 0 ? (bucket[:count].to_f / @size_stats.total * 100).round(1) : 0 %>
|
|
31
|
+
<tr>
|
|
32
|
+
<td class="sqw-muted"><%=bucket[:label] %></td>
|
|
33
|
+
<td><%= bucket[:count] %></td>
|
|
34
|
+
<td>
|
|
35
|
+
<div class="sqw-dist-bar-cell">
|
|
36
|
+
<div class="sqw-dist-bar" style="width: <%= pct %>%"></div>
|
|
37
|
+
<span class="sqw-dist-pct sqw-muted"><%= pct %>%</span>
|
|
38
|
+
</div>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
<% end %>
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<section class="sqw-size-section">
|
|
47
|
+
<h2 class="sqw-section-title">Largest Entries</h2>
|
|
48
|
+
<table class="sqw-table">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr>
|
|
51
|
+
<th>Key</th>
|
|
52
|
+
<th>Size</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
<% @size_stats.top_entries.each do |entry| %>
|
|
57
|
+
<tr>
|
|
58
|
+
<td class="sqw-monospace sqw-truncate" title="<%= entry.key %>">
|
|
59
|
+
<%= link_to entry.key, cache_entry_path(entry), class: "sqw-link" %>
|
|
60
|
+
</td>
|
|
61
|
+
<td><%= number_to_human_size(entry.byte_size) %></td>
|
|
62
|
+
</tr>
|
|
63
|
+
<% end %>
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
</section>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<div class="sqw-timeline-grid" data-controller="sparkline-tooltip">
|
|
71
|
+
<div class="sqw-sparkline-wrap sqw-timeline-chart">
|
|
72
|
+
<span class="sqw-sparkline-label">Entries written — last 24 hours</span>
|
|
73
|
+
<div class="sqw-sparkline-positioner">
|
|
74
|
+
<%= cache_entries_timeline_svg(@timeline) %>
|
|
75
|
+
<div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="sqw-sparkline-axis">
|
|
78
|
+
<span>24h ago</span>
|
|
79
|
+
<span>12h ago</span>
|
|
80
|
+
<span>now</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="sqw-sparkline-wrap sqw-timeline-chart">
|
|
85
|
+
<span class="sqw-sparkline-label">Bytes written — last 24 hours</span>
|
|
86
|
+
<div class="sqw-sparkline-positioner">
|
|
87
|
+
<%= cache_bytes_timeline_svg(@timeline) %>
|
|
88
|
+
<div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="sqw-sparkline-axis">
|
|
91
|
+
<span>24h ago</span>
|
|
92
|
+
<span>12h ago</span>
|
|
93
|
+
<span>now</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<h1 class="sqw-page-title">Cache Entries</h1>
|
|
3
|
+
<div class="sqw-header-actions">
|
|
4
|
+
<%= button_to "Flush All",
|
|
5
|
+
cache_flush_path,
|
|
6
|
+
method: :delete,
|
|
7
|
+
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
8
|
+
data: { turbo_confirm: "Delete all cache entries? This cannot be undone." } %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form class="sqw-filters" action="<%= cache_entries_path %>" method="get" data-controller="search">
|
|
13
|
+
<%= hidden_field_tag :column, @sort["column"] %>
|
|
14
|
+
<%= hidden_field_tag :direction, @sort["direction"] %>
|
|
15
|
+
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
16
|
+
placeholder="Filter by key…" autocomplete="off" aria-label="Filter by key"
|
|
17
|
+
data-action="input->search#filter">
|
|
18
|
+
<% if @search.present? %>
|
|
19
|
+
<%= link_to "Clear", cache_entries_path(column: @sort["column"], direction: @sort["direction"]),
|
|
20
|
+
class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</form>
|
|
23
|
+
|
|
24
|
+
<% if @entries.any? %>
|
|
25
|
+
<table class="sqw-table">
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<% [["key", "Key"], ["byte_size", "Size"], ["created_at", "Created"]].each do |col, label| %>
|
|
29
|
+
<th>
|
|
30
|
+
<% next_dir = (@sort["column"] == col && @sort["direction"] == "desc") ? "asc" : "desc" %>
|
|
31
|
+
<%= link_to cache_entries_path(q: @search, column: col, direction: next_dir) do %>
|
|
32
|
+
<%= label %>
|
|
33
|
+
<% if @sort["column"] == col %>
|
|
34
|
+
<span class="sqw-sort-indicator"><%= @sort["direction"] == "desc" ? "↓" : "↑" %></span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
</th>
|
|
38
|
+
<% end %>
|
|
39
|
+
<th></th>
|
|
40
|
+
</tr>
|
|
41
|
+
</thead>
|
|
42
|
+
<tbody>
|
|
43
|
+
<% @entries.each do |entry| %>
|
|
44
|
+
<tr id="cache_entry_<%= entry.id %>">
|
|
45
|
+
<td class="sqw-monospace sqw-truncate" title="<%= entry.key %>">
|
|
46
|
+
<%= link_to entry.key, cache_entry_path(entry), class: "sqw-link" %>
|
|
47
|
+
</td>
|
|
48
|
+
<td><%= number_to_human_size(entry.byte_size) %></td>
|
|
49
|
+
<td class="sqw-muted"><%= entry.created_at.strftime("%b %d %H:%M") %></td>
|
|
50
|
+
<td class="sqw-actions">
|
|
51
|
+
<%= button_to "Delete",
|
|
52
|
+
cache_entry_path(entry, q: @search, column: @sort["column"], direction: @sort["direction"]),
|
|
53
|
+
method: :delete,
|
|
54
|
+
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
55
|
+
data: { turbo_confirm: "Delete this cache entry?" } %>
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<% end %>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
62
|
+
<% else %>
|
|
63
|
+
<div class="sqw-empty">
|
|
64
|
+
<p><%= @search.present? ? "No entries matching “#{@search}”.".html_safe : "No cache entries." %></p>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<h1 class="sqw-page-title sqw-truncate" title="<%= @entry.key %>"><%= @entry.key %></h1>
|
|
3
|
+
<div class="sqw-header-actions">
|
|
4
|
+
<%= button_to "Delete",
|
|
5
|
+
cache_entry_path(@entry),
|
|
6
|
+
method: :delete,
|
|
7
|
+
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
8
|
+
data: { turbo_confirm: "Delete this cache entry?" } %>
|
|
9
|
+
<%= link_to "← Entries", cache_entries_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<dl class="sqw-detail">
|
|
14
|
+
<div class="sqw-detail__row">
|
|
15
|
+
<dt>Key</dt>
|
|
16
|
+
<dd class="sqw-monospace"><%= @entry.key %></dd>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="sqw-detail__row">
|
|
19
|
+
<dt>Size</dt>
|
|
20
|
+
<dd><%= number_to_human_size(@entry.byte_size) %></dd>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="sqw-detail__row">
|
|
23
|
+
<dt>Created</dt>
|
|
24
|
+
<dd class="sqw-muted"><%= @entry.created_at.strftime("%b %d, %Y %H:%M:%S %Z") %></dd>
|
|
25
|
+
</div>
|
|
26
|
+
</dl>
|
|
27
|
+
|
|
28
|
+
<section class="sqw-value-preview">
|
|
29
|
+
<div class="sqw-value-preview__header">
|
|
30
|
+
<h2 class="sqw-section-title">Value</h2>
|
|
31
|
+
<% if SolidStackWeb.allow_value_preview %>
|
|
32
|
+
<% formatted = format_cache_value(@entry.value) %>
|
|
33
|
+
<span class="sqw-badge sqw-badge--queue"><%= formatted[:label] %></span>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
<% if SolidStackWeb.allow_value_preview %>
|
|
37
|
+
<% content = formatted[:content] %>
|
|
38
|
+
<% truncated = content.length > 4096 %>
|
|
39
|
+
<pre class="sqw-value-pre"><%= truncated ? content[0, 4096] : content %></pre>
|
|
40
|
+
<% if truncated %>
|
|
41
|
+
<p class="sqw-muted sqw-value-truncated">Showing first 4 KB of <%= number_to_human_size(@entry.byte_size) %> total.</p>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% else %>
|
|
44
|
+
<div class="sqw-empty">
|
|
45
|
+
<p>Value preview is disabled. Set <code>config.allow_value_preview = true</code> in your initializer to enable.</p>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
</section>
|
|
@@ -76,14 +76,20 @@
|
|
|
76
76
|
<%= link_to "View Cache →", cache_path, class: "sqw-gem-card__link" %>
|
|
77
77
|
</div>
|
|
78
78
|
<div class="sqw-gem-card__body">
|
|
79
|
-
|
|
79
|
+
<%= link_to cache_entries_path, class: "sqw-inline-stat sqw-inline-stat--cache" do %>
|
|
80
80
|
<span class="sqw-inline-stat__label">Entries</span>
|
|
81
81
|
<span class="sqw-inline-stat__value"><%= @cache_stats[:entries] %></span>
|
|
82
|
-
|
|
82
|
+
<% end %>
|
|
83
83
|
<div class="sqw-inline-stat sqw-inline-stat--cache">
|
|
84
84
|
<span class="sqw-inline-stat__label">Size</span>
|
|
85
85
|
<span class="sqw-inline-stat__value"><%= number_to_human_size(@cache_stats[:byte_size]) %></span>
|
|
86
86
|
</div>
|
|
87
|
+
<% if @cache_stats[:oldest_entry] %>
|
|
88
|
+
<div class="sqw-inline-stat sqw-inline-stat--cache">
|
|
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>
|
|
91
|
+
</div>
|
|
92
|
+
<% end %>
|
|
87
93
|
</div>
|
|
88
94
|
</div>
|
|
89
95
|
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
<form class="sqw-filters" action="<%= history_path %>" method="get">
|
|
13
|
+
<form class="sqw-filters" action="<%= history_path %>" method="get" data-controller="search">
|
|
14
14
|
<% if @queue.present? %>
|
|
15
15
|
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
16
16
|
<% end %>
|
|
17
17
|
<input type="hidden" name="period" value="<%= @period %>">
|
|
18
18
|
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
19
|
-
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
20
|
-
|
|
19
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
20
|
+
data-action="input->search#filter">
|
|
21
21
|
<% if @search.present? %>
|
|
22
22
|
<%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
23
23
|
<% end %>
|
|
@@ -32,13 +32,15 @@
|
|
|
32
32
|
<%= turbo_frame_tag "sqw-jobs-filter",
|
|
33
33
|
data: { turbo_action: "advance", controller: "refresh",
|
|
34
34
|
refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
|
|
35
|
-
<form class="sqw-filters" action="<%= jobs_path %>" method="get">
|
|
35
|
+
<form class="sqw-filters" action="<%= jobs_path %>" method="get" data-controller="search">
|
|
36
36
|
<%= hidden_field_tag :status, @status %>
|
|
37
37
|
<%= hidden_field_tag :period, @period %>
|
|
38
38
|
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
39
|
-
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
39
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
40
|
+
data-action="input->search#filter">
|
|
40
41
|
<% if @queue_options.size > 1 %>
|
|
41
|
-
<select name="queue" class="sqw-select" aria-label="Filter by queue"
|
|
42
|
+
<select name="queue" class="sqw-select" aria-label="Filter by queue"
|
|
43
|
+
data-action="change->search#select">
|
|
42
44
|
<option value="">All queues</option>
|
|
43
45
|
<% @queue_options.each do |q| %>
|
|
44
46
|
<option value="<%= q %>" <%= "selected" if @queue == q %>><%= q %></option>
|
|
@@ -46,14 +48,14 @@
|
|
|
46
48
|
</select>
|
|
47
49
|
<% end %>
|
|
48
50
|
<% if @priority_options.size > 1 %>
|
|
49
|
-
<select name="priority" class="sqw-select" aria-label="Filter by priority"
|
|
51
|
+
<select name="priority" class="sqw-select" aria-label="Filter by priority"
|
|
52
|
+
data-action="change->search#select">
|
|
50
53
|
<option value="">All priorities</option>
|
|
51
54
|
<% @priority_options.each do |p| %>
|
|
52
55
|
<option value="<%= p %>" <%= "selected" if @priority.to_s == p.to_s %>>Priority <%= p %></option>
|
|
53
56
|
<% end %>
|
|
54
57
|
</select>
|
|
55
58
|
<% end %>
|
|
56
|
-
<button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
|
|
57
59
|
<% if @search.present? || @queue.present? || @priority.present? %>
|
|
58
60
|
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
59
61
|
<% end %>
|
data/config/importmap.rb
CHANGED
|
@@ -2,5 +2,6 @@ pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/
|
|
|
2
2
|
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
3
3
|
pin "solid_stack_web", to: "solid_stack_web/application.js"
|
|
4
4
|
pin "solid_stack_web/refresh_controller", to: "solid_stack_web/refresh_controller.js"
|
|
5
|
+
pin "solid_stack_web/search_controller", to: "solid_stack_web/search_controller.js"
|
|
5
6
|
pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js"
|
|
6
7
|
pin "solid_stack_web/sparkline_tooltip_controller", to: "solid_stack_web/sparkline_tooltip_controller.js"
|
data/config/routes.rb
CHANGED
|
@@ -35,5 +35,7 @@ SolidStackWeb::Engine.routes.draw do
|
|
|
35
35
|
get "stats", to: "stats#index", as: :stats
|
|
36
36
|
get "history", to: "history#index", as: :history
|
|
37
37
|
get "cache", to: "cache#index", as: :cache
|
|
38
|
+
resources :cache_entries, only: [:index, :show, :destroy], path: "cache/entries"
|
|
39
|
+
resource :cache_flush, only: [:destroy], path: "cache/flush", controller: "cache/flushes"
|
|
38
40
|
get "cable", to: "cable#index", as: :cable
|
|
39
41
|
end
|
data/lib/solid_stack_web.rb
CHANGED
|
@@ -7,7 +7,7 @@ module SolidStackWeb
|
|
|
7
7
|
:alert_webhook_url, :alert_webhook_cooldown,
|
|
8
8
|
:alert_failure_threshold, :alert_queue_thresholds,
|
|
9
9
|
:dashboard_refresh_interval, :default_refresh_interval,
|
|
10
|
-
:search_results_limit
|
|
10
|
+
:search_results_limit, :allow_value_preview
|
|
11
11
|
|
|
12
12
|
def page_size
|
|
13
13
|
@page_size || 25
|
|
@@ -49,6 +49,10 @@ module SolidStackWeb
|
|
|
49
49
|
@search_results_limit || 25
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def allow_value_preview
|
|
53
|
+
@allow_value_preview || false
|
|
54
|
+
end
|
|
55
|
+
|
|
52
56
|
def configure
|
|
53
57
|
yield self
|
|
54
58
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -145,7 +145,9 @@ files:
|
|
|
145
145
|
- app/assets/stylesheets/solid_stack_web/application.css
|
|
146
146
|
- app/controllers/solid_stack_web/application_controller.rb
|
|
147
147
|
- app/controllers/solid_stack_web/cable_controller.rb
|
|
148
|
+
- app/controllers/solid_stack_web/cache/flushes_controller.rb
|
|
148
149
|
- app/controllers/solid_stack_web/cache_controller.rb
|
|
150
|
+
- app/controllers/solid_stack_web/cache_entries_controller.rb
|
|
149
151
|
- app/controllers/solid_stack_web/dashboard_controller.rb
|
|
150
152
|
- app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
|
|
151
153
|
- app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
|
|
@@ -164,11 +166,14 @@ files:
|
|
|
164
166
|
- app/helpers/solid_stack_web/application_helper.rb
|
|
165
167
|
- app/javascript/solid_stack_web/application.js
|
|
166
168
|
- app/javascript/solid_stack_web/refresh_controller.js
|
|
169
|
+
- app/javascript/solid_stack_web/search_controller.js
|
|
167
170
|
- app/javascript/solid_stack_web/selection_controller.js
|
|
168
171
|
- app/javascript/solid_stack_web/sparkline_tooltip_controller.js
|
|
169
172
|
- app/models/solid_stack_web/alert_webhook.rb
|
|
170
173
|
- app/models/solid_stack_web/cable_stats.rb
|
|
174
|
+
- app/models/solid_stack_web/cache_size_stats.rb
|
|
171
175
|
- app/models/solid_stack_web/cache_stats.rb
|
|
176
|
+
- app/models/solid_stack_web/cache_timeline.rb
|
|
172
177
|
- app/models/solid_stack_web/job.rb
|
|
173
178
|
- app/models/solid_stack_web/queue_depth_sparkline.rb
|
|
174
179
|
- app/models/solid_stack_web/queue_stats.rb
|
|
@@ -176,6 +181,8 @@ files:
|
|
|
176
181
|
- app/views/layouts/solid_stack_web/application.html.erb
|
|
177
182
|
- app/views/solid_stack_web/cable/index.html.erb
|
|
178
183
|
- app/views/solid_stack_web/cache/index.html.erb
|
|
184
|
+
- app/views/solid_stack_web/cache_entries/index.html.erb
|
|
185
|
+
- app/views/solid_stack_web/cache_entries/show.html.erb
|
|
179
186
|
- app/views/solid_stack_web/dashboard/index.html.erb
|
|
180
187
|
- app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
|
|
181
188
|
- app/views/solid_stack_web/failed_jobs/index.html.erb
|
|
@@ -218,7 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
218
225
|
- !ruby/object:Gem::Version
|
|
219
226
|
version: '0'
|
|
220
227
|
requirements: []
|
|
221
|
-
rubygems_version: 4.0.
|
|
228
|
+
rubygems_version: 4.0.10
|
|
222
229
|
specification_version: 4
|
|
223
230
|
summary: A unified Rails engine dashboard for Solid Queue, Solid Cache, and Solid
|
|
224
231
|
Cable.
|