solid_observer 0.3.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/CHANGELOG.md +34 -0
- data/README.md +195 -82
- data/app/assets/javascripts/solid_observer/live_poll.js +3 -1
- data/app/controllers/solid_observer/application_controller.rb +1 -0
- data/app/controllers/solid_observer/cable_dashboard_controller.rb +52 -0
- data/app/controllers/solid_observer/cable_operations_controller.rb +16 -0
- data/app/controllers/solid_observer/cache_dashboard_controller.rb +52 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +38 -1
- data/app/controllers/solid_observer/storages_controller.rb +1 -1
- data/app/helpers/solid_observer/application_helper.rb +268 -5
- data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
- data/app/models/solid_observer/cable_event.rb +13 -0
- data/app/models/solid_observer/cable_metric.rb +12 -0
- data/app/models/solid_observer/cache_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +13 -0
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/views/layouts/solid_observer/application.html.erb +157 -19
- data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
- data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
- data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
- data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
- data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
- data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +32 -5
- data/app/views/solid_observer/events/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/show.html.erb +3 -3
- data/app/views/solid_observer/storages/show.html.erb +90 -32
- data/config/routes.rb +7 -0
- data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
- data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
- data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
- data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
- data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
- data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
- data/lib/generators/solid_observer/install_generator.rb +8 -1
- data/lib/generators/solid_observer/templates/initializer.rb.tt +20 -4
- data/lib/solid_observer/base_event.rb +1 -1
- data/lib/solid_observer/base_metric.rb +1 -1
- data/lib/solid_observer/base_record.rb +8 -0
- data/lib/solid_observer/cable_event_buffer.rb +28 -0
- data/lib/solid_observer/cable_metric_buffer.rb +230 -0
- data/lib/solid_observer/cable_subscriber.rb +57 -0
- data/lib/solid_observer/cache_event_buffer.rb +28 -0
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/cli/storage.rb +16 -13
- data/lib/solid_observer/configuration.rb +67 -5
- data/lib/solid_observer/engine.rb +70 -15
- data/lib/solid_observer/event_buffer_core.rb +218 -0
- data/lib/solid_observer/queue_event_buffer.rb +9 -201
- data/lib/solid_observer/services/cable_operations.rb +74 -0
- data/lib/solid_observer/services/cable_stats.rb +385 -0
- data/lib/solid_observer/services/cache_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +346 -0
- data/lib/solid_observer/services/cleanup_storage.rb +98 -47
- data/lib/solid_observer/services/database_size.rb +13 -8
- data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
- data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
- data/lib/solid_observer/services/record_cable_event.rb +114 -0
- data/lib/solid_observer/services/record_cable_metric.rb +73 -0
- data/lib/solid_observer/services/record_cache_event.rb +165 -0
- data/lib/solid_observer/services/record_cache_metric.rb +66 -0
- data/lib/solid_observer/services/storage_info_snapshot.rb +216 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +111 -21
- metadata +47 -5
- data/bin/console +0 -11
- data/bin/quality_gate +0 -95
- data/bin/setup +0 -8
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
/* text */
|
|
16
16
|
--so-text: #171717;
|
|
17
|
-
--so-text-muted: #
|
|
18
|
-
--so-text-subtle: #
|
|
17
|
+
--so-text-muted: #707070;
|
|
18
|
+
--so-text-subtle: #707070;
|
|
19
19
|
|
|
20
20
|
/* lines */
|
|
21
21
|
--so-border: #e5e5e5;
|
|
@@ -46,6 +46,17 @@
|
|
|
46
46
|
--so-live-marker: var(--so-success);
|
|
47
47
|
--so-range-marker: var(--so-info);
|
|
48
48
|
--so-toolbar-gap: 0.75rem;
|
|
49
|
+
|
|
50
|
+
/* SO-078 badge tokens */
|
|
51
|
+
--so-badge-success-bg: #dcfce7;
|
|
52
|
+
--so-badge-success-text: #166534;
|
|
53
|
+
--so-badge-warning-bg: #fef3c7;
|
|
54
|
+
--so-badge-warning-text: #92400e;
|
|
55
|
+
--so-badge-danger-bg: #fee2e2;
|
|
56
|
+
--so-badge-danger-text: #991b1b;
|
|
57
|
+
--so-badge-info-bg: #dbeafe;
|
|
58
|
+
--so-badge-info-text: #1e40af;
|
|
59
|
+
--so-badge-neutral-text: #525252;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -93,6 +104,22 @@
|
|
|
93
104
|
.so-sidebar__nav a.active {
|
|
94
105
|
background: var(--so-sidebar-active-bg);
|
|
95
106
|
color: var(--so-sidebar-active-text);
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.so-sidebar__nav a:focus-visible {
|
|
111
|
+
outline: 2px solid var(--so-info);
|
|
112
|
+
outline-offset: 2px;
|
|
113
|
+
box-shadow: 0 0 0 3px var(--so-focus-ring);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.so-sidebar__section {
|
|
117
|
+
font-size: 0.7rem;
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
letter-spacing: 0.08em;
|
|
120
|
+
color: var(--so-text-muted);
|
|
121
|
+
padding: 0.5rem 0.75rem 0.25rem;
|
|
122
|
+
margin: 0.5rem 0.75rem 0.25rem;
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
.so-sidebar__mode {
|
|
@@ -123,6 +150,8 @@
|
|
|
123
150
|
|
|
124
151
|
/* SO-067 Metric card anatomy — separated value/suffix/range-copy */
|
|
125
152
|
.so-metric { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem; }
|
|
153
|
+
.so-metric .so-card__label,
|
|
154
|
+
.so-metric .so-card__subtitle { flex-basis: 100%; }
|
|
126
155
|
.so-metric__value { font-size: 1.625rem; font-weight: 600; color: var(--so-text); }
|
|
127
156
|
.so-metric__suffix { font-size: 0.75rem; color: var(--so-text-muted); }
|
|
128
157
|
|
|
@@ -138,6 +167,7 @@
|
|
|
138
167
|
.so-toolbar-freshness { font-size: 0.75rem; color: var(--so-text-subtle); }
|
|
139
168
|
|
|
140
169
|
/* SO-067 Focus rings */
|
|
170
|
+
.so-btn:focus-visible,
|
|
141
171
|
select:focus-visible,
|
|
142
172
|
.so-btn--refresh:focus-visible,
|
|
143
173
|
.so-toggle--pill input:focus-visible + .so-toggle__track,
|
|
@@ -239,17 +269,20 @@
|
|
|
239
269
|
.so-table tr:hover td { background: var(--so-surface-muted); }
|
|
240
270
|
|
|
241
271
|
.so-badge {
|
|
242
|
-
display: inline-
|
|
243
|
-
|
|
244
|
-
|
|
272
|
+
display: inline-flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: 0.375rem;
|
|
275
|
+
border-radius: 9999px;
|
|
245
276
|
font-size: 0.7rem;
|
|
246
|
-
|
|
277
|
+
line-height: 1rem;
|
|
278
|
+
font-weight: 500;
|
|
247
279
|
}
|
|
248
|
-
.so-badge--success { background:
|
|
249
|
-
.so-badge--warning { background:
|
|
250
|
-
.so-badge--danger { background:
|
|
251
|
-
.so-badge--info { background:
|
|
252
|
-
.so-badge--default { background: var(--so-surface-muted); color: var(--so-text
|
|
280
|
+
.so-badge--success { background: var(--so-badge-success-bg); color: var(--so-badge-success-text); }
|
|
281
|
+
.so-badge--warning { background: var(--so-badge-warning-bg); color: var(--so-badge-warning-text); }
|
|
282
|
+
.so-badge--danger { background: var(--so-badge-danger-bg); color: var(--so-badge-danger-text); }
|
|
283
|
+
.so-badge--info { background: var(--so-badge-info-bg); color: var(--so-badge-info-text); }
|
|
284
|
+
.so-badge--default { background: var(--so-surface-muted); color: var(--so-badge-neutral-text); }
|
|
285
|
+
.so-badge--recorded { background: var(--so-surface-muted); color: var(--so-text); }
|
|
253
286
|
|
|
254
287
|
.so-badge--pill {
|
|
255
288
|
display: inline-flex;
|
|
@@ -263,6 +296,15 @@
|
|
|
263
296
|
.so-badge--pill.so-badge--success .so-badge__dot { fill: var(--so-success); }
|
|
264
297
|
.so-badge--pill.so-badge--warning .so-badge__dot { fill: var(--so-warning); }
|
|
265
298
|
.so-badge--pill.so-badge--danger .so-badge__dot { fill: var(--so-danger); }
|
|
299
|
+
.so-badge--pill.so-badge--info .so-badge__dot { fill: var(--so-info); }
|
|
300
|
+
.so-badge--pill.so-badge--default .so-badge__dot { fill: var(--so-badge-neutral-text); }
|
|
301
|
+
.so-badge--pill.so-badge--recorded .so-badge__dot { fill: var(--so-text); }
|
|
302
|
+
|
|
303
|
+
a.so-badge:focus-visible, button.so-badge:focus-visible {
|
|
304
|
+
outline: 2px solid var(--so-info);
|
|
305
|
+
outline-offset: 2px;
|
|
306
|
+
box-shadow: 0 0 0 3px var(--so-focus-ring);
|
|
307
|
+
}
|
|
266
308
|
|
|
267
309
|
.so-stability {
|
|
268
310
|
display: flex;
|
|
@@ -353,6 +395,79 @@
|
|
|
353
395
|
.so-toggle__track, .so-toggle__thumb { transition: none; }
|
|
354
396
|
}
|
|
355
397
|
|
|
398
|
+
.so-cache-controls { max-width: 820px; }
|
|
399
|
+
.so-cache-dashboard__intro,
|
|
400
|
+
.so-cache-controls__intro,
|
|
401
|
+
.so-queue-overview__intro,
|
|
402
|
+
.so-cable-dashboard__intro {
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-wrap: wrap;
|
|
405
|
+
align-items: center;
|
|
406
|
+
gap: 0.75rem;
|
|
407
|
+
margin-top: 0.5rem;
|
|
408
|
+
}
|
|
409
|
+
.so-cache-dashboard__hint,
|
|
410
|
+
.so-cache-controls__hint,
|
|
411
|
+
.so-queue-overview__hint,
|
|
412
|
+
.so-cable-dashboard__hint {
|
|
413
|
+
font-size: 0.875rem;
|
|
414
|
+
color: var(--so-text-subtle);
|
|
415
|
+
line-height: 1.5;
|
|
416
|
+
}
|
|
417
|
+
.so-cache-dashboard__range-form,
|
|
418
|
+
.so-cable-dashboard__range-form {
|
|
419
|
+
align-items: center;
|
|
420
|
+
margin-bottom: 2rem;
|
|
421
|
+
}
|
|
422
|
+
.so-cache-dashboard__range-form select,
|
|
423
|
+
.so-cable-dashboard__range-form select,
|
|
424
|
+
.so-dashboard-toolbar select {
|
|
425
|
+
padding: 0.4rem 0.6rem;
|
|
426
|
+
border: 1px solid var(--so-border);
|
|
427
|
+
border-radius: var(--so-radius);
|
|
428
|
+
font-size: 0.85rem;
|
|
429
|
+
background: var(--so-card-bg);
|
|
430
|
+
color: var(--so-text);
|
|
431
|
+
}
|
|
432
|
+
.so-cache-dashboard__chart-empty,
|
|
433
|
+
.so-cable-dashboard__chart-empty {
|
|
434
|
+
max-width: 420px;
|
|
435
|
+
margin-bottom: 0;
|
|
436
|
+
}
|
|
437
|
+
.so-cache-dashboard__digest,
|
|
438
|
+
.so-cable-dashboard__digest {
|
|
439
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
440
|
+
white-space: nowrap;
|
|
441
|
+
}
|
|
442
|
+
.so-cache-control-row {
|
|
443
|
+
display: grid;
|
|
444
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
445
|
+
gap: 1rem;
|
|
446
|
+
align-items: center;
|
|
447
|
+
padding: 1rem 0;
|
|
448
|
+
}
|
|
449
|
+
.so-cache-control-row + .so-cache-control-row { border-top: 1px solid var(--so-border); }
|
|
450
|
+
.so-cache-control-row__copy { min-width: 0; }
|
|
451
|
+
.so-cache-control-row__title {
|
|
452
|
+
font-size: 0.95rem;
|
|
453
|
+
font-weight: 600;
|
|
454
|
+
color: var(--so-text);
|
|
455
|
+
}
|
|
456
|
+
.so-cache-control-row__body {
|
|
457
|
+
margin-top: 0.35rem;
|
|
458
|
+
font-size: 0.875rem;
|
|
459
|
+
line-height: 1.5;
|
|
460
|
+
color: var(--so-text-subtle);
|
|
461
|
+
}
|
|
462
|
+
.so-cache-control-row__action {
|
|
463
|
+
display: flex;
|
|
464
|
+
justify-content: flex-end;
|
|
465
|
+
}
|
|
466
|
+
.so-cache-control-row__action .so-form--inline {
|
|
467
|
+
display: flex;
|
|
468
|
+
justify-content: flex-end;
|
|
469
|
+
}
|
|
470
|
+
|
|
356
471
|
.so-empty { text-align: center; padding: 3rem 1rem; color: var(--so-text-muted); }
|
|
357
472
|
.so-empty__icon { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
358
473
|
.so-empty__message { font-size: 0.9rem; }
|
|
@@ -425,6 +540,10 @@
|
|
|
425
540
|
.so-sidebar__mode { display: inline-block; padding: 0.5rem 1rem; border-top: none; margin-top: 0; font-size: 0.7rem; }
|
|
426
541
|
.so-content { padding: 1rem; }
|
|
427
542
|
.so-stat-cards { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
|
|
543
|
+
.so-cache-control-row { grid-template-columns: 1fr; align-items: start; }
|
|
544
|
+
.so-cache-control-row__action,
|
|
545
|
+
.so-cache-control-row__action .so-form--inline { width: 100%; justify-content: stretch; }
|
|
546
|
+
.so-cache-control-row__action .so-btn { width: 100%; }
|
|
428
547
|
.so-table { display: block; overflow-x: auto; }
|
|
429
548
|
}
|
|
430
549
|
</style>
|
|
@@ -433,12 +552,31 @@
|
|
|
433
552
|
<div class="so-layout">
|
|
434
553
|
<aside class="so-sidebar">
|
|
435
554
|
<div class="so-sidebar__logo">SolidObserver</div>
|
|
436
|
-
<nav class="so-sidebar__nav">
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<%= link_to "
|
|
441
|
-
|
|
555
|
+
<nav class="so-sidebar__nav" aria-label="SolidObserver navigation">
|
|
556
|
+
<% if SolidObserver.config.solid_queue_enabled? %>
|
|
557
|
+
<div class="so-sidebar__section">Queue</div>
|
|
558
|
+
<%= link_to "Overview", root_path, class: ("active" if controller_name == "dashboard" && @component.to_s != "cache"), "aria-current": (controller_name == "dashboard" && @component.to_s != "cache" ? "page" : nil) %>
|
|
559
|
+
<%= link_to "Jobs", jobs_path, class: ("active" if controller_name == "jobs"), "aria-current": (controller_name == "jobs" ? "page" : nil) %>
|
|
560
|
+
<% if persistence_mode? %>
|
|
561
|
+
<%= link_to "Events", events_path, class: ("active" if controller_name == "events"), "aria-current": (controller_name == "events" ? "page" : nil) %>
|
|
562
|
+
<% end %>
|
|
563
|
+
<% end %>
|
|
564
|
+
|
|
565
|
+
<% if SolidObserver.config.solid_cache_enabled? %>
|
|
566
|
+
<div class="so-sidebar__section">Cache</div>
|
|
567
|
+
<%= link_to "Overview", cache_dashboard_path, class: ("active" if controller_name == "cache_dashboard"), "aria-current": (controller_name == "cache_dashboard" ? "page" : nil) %>
|
|
568
|
+
<% if SolidObserver::Services::CacheOperations.available? %>
|
|
569
|
+
<%= link_to "Controls", cache_operations_path, class: ("active" if controller_name == "cache_operations"), "aria-current": (controller_name == "cache_operations" ? "page" : nil) %>
|
|
570
|
+
<% end %>
|
|
571
|
+
<% end %>
|
|
572
|
+
|
|
573
|
+
<% if SolidObserver.config.solid_cable_enabled? %>
|
|
574
|
+
<div class="so-sidebar__section">Cable</div>
|
|
575
|
+
<%= link_to "Overview", cable_dashboard_path, class: ("active" if controller_name == "cable_dashboard"), "aria-current": (controller_name == "cable_dashboard" ? "page" : nil) %>
|
|
576
|
+
<% end %>
|
|
577
|
+
|
|
578
|
+
<% if persistence_mode? && (SolidObserver.config.solid_queue_enabled? || SolidObserver.config.solid_cache_enabled? || SolidObserver.config.solid_cable_enabled?) %>
|
|
579
|
+
<%= link_to "Storage", storage_path, class: ("active" if controller_name == "storages"), "aria-current": (controller_name == "storages" ? "page" : nil) %>
|
|
442
580
|
<% end %>
|
|
443
581
|
</nav>
|
|
444
582
|
<div class="so-sidebar__mode">
|
|
@@ -449,10 +587,10 @@
|
|
|
449
587
|
<main class="so-content" aria-labelledby="so-main-heading">
|
|
450
588
|
<h1 class="sr-only" id="so-main-heading">Dashboard</h1>
|
|
451
589
|
<% if flash[:notice] %>
|
|
452
|
-
<div class="so-flash so-flash--notice"><%= flash[:notice] %></div>
|
|
590
|
+
<div class="so-flash so-flash--notice" role="status" aria-live="polite"><%= flash[:notice] %></div>
|
|
453
591
|
<% end %>
|
|
454
592
|
<% if flash[:alert] %>
|
|
455
|
-
<div class="so-flash so-flash--alert"><%= flash[:alert] %></div>
|
|
593
|
+
<div class="so-flash so-flash--alert" role="alert"><%= flash[:alert] %></div>
|
|
456
594
|
<% end %>
|
|
457
595
|
<%= yield %>
|
|
458
596
|
</main>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<section class="so-dashboard-section" aria-labelledby="so-cable-charts-heading">
|
|
2
|
+
<header class="so-dashboard-section__header">
|
|
3
|
+
<h2 id="so-cable-charts-heading" class="so-dashboard-section__title">Activity trends</h2>
|
|
4
|
+
</header>
|
|
5
|
+
|
|
6
|
+
<div class="so-chart-strip">
|
|
7
|
+
<% if @stats[:activity_trends]&.[](:available) %>
|
|
8
|
+
<figure class="so-spark" data-so-spark="cable-broadcasts">
|
|
9
|
+
<figcaption class="so-spark__label">Broadcasts total <span data-so-range-copy><%= cable_range_label(@range) %></span></figcaption>
|
|
10
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
11
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
12
|
+
<polyline class="so-spark__line" points="<%= spark_points(@stats[:activity_trends][:broadcasts]) %>"/>
|
|
13
|
+
</svg>
|
|
14
|
+
<span class="so-spark__value"><%= number_with_delimiter(@stats[:broadcasts_count].to_i) %></span>
|
|
15
|
+
</figure>
|
|
16
|
+
|
|
17
|
+
<figure class="so-spark" data-so-spark="cable-rejections">
|
|
18
|
+
<figcaption class="so-spark__label">Rejections total <span data-so-range-copy><%= cable_range_label(@range) %></span></figcaption>
|
|
19
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
20
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
21
|
+
<polyline class="so-spark__line" points="<%= spark_points(@stats[:activity_trends][:rejections]) %>"/>
|
|
22
|
+
</svg>
|
|
23
|
+
<span class="so-spark__value"><%= number_with_delimiter(@stats[:rejections_count].to_i) %></span>
|
|
24
|
+
</figure>
|
|
25
|
+
<% else %>
|
|
26
|
+
<div class="so-card so-cable-dashboard__chart-empty">
|
|
27
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-range">No chart data in the selected range yet. Summary metrics still use bounded cable stats.</p>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<section class="so-card so-card--section" aria-labelledby="so-cable-events-heading">
|
|
2
|
+
<header class="so-dashboard-section__header">
|
|
3
|
+
<h2 id="so-cable-events-heading" class="so-dashboard-section__title">Recent Cable events</h2>
|
|
4
|
+
<span class="so-dashboard-section__meta">debug context only · broadcasting names and payloads are never shown</span>
|
|
5
|
+
</header>
|
|
6
|
+
|
|
7
|
+
<% if recent_events.present? %>
|
|
8
|
+
<table class="so-table so-table--card">
|
|
9
|
+
<caption class="sr-only">Recent Cable events without raw broadcasting names or payloads</caption>
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th>Event</th>
|
|
13
|
+
<th>Channel class</th>
|
|
14
|
+
<th>Broadcasting digest</th>
|
|
15
|
+
<th>Duration</th>
|
|
16
|
+
<th>Recorded</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% recent_events.each do |event| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td><%= event.event_type.to_s %></td>
|
|
23
|
+
<td><%= event.channel_class.presence || "—" %></td>
|
|
24
|
+
<td><span class="so-cable-dashboard__digest"><%= cable_event_digest(event.broadcasting_digest) %></span></td>
|
|
25
|
+
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
26
|
+
<td><span title="<%= event.recorded_at.utc.strftime("%Y-%m-%d %H:%M:%S UTC") %>"><%= time_ago_in_words(event.recorded_at) %> ago</span></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
<% else %>
|
|
32
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No sampled cable events in the selected range yet. Broadcasts, rejections, and errors will appear here after Cable activity is recorded.</p>
|
|
33
|
+
<% end %>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<% backlog_summary = cable_backlog_summary(stats) %>
|
|
2
|
+
<% storage_summary = cable_storage_summary(storage_components) %>
|
|
3
|
+
|
|
4
|
+
<section class="so-dashboard-section" aria-labelledby="so-cable-summary-heading">
|
|
5
|
+
<header class="so-dashboard-section__header">
|
|
6
|
+
<h2 id="so-cable-summary-heading" class="so-dashboard-section__title">Summary in selected range</h2>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<div class="so-stat-cards so-cable-dashboard__summary">
|
|
10
|
+
<article class="so-card so-metric">
|
|
11
|
+
<div class="so-card__label">Broadcasts</div>
|
|
12
|
+
<div class="so-metric__value"><%= number_with_delimiter(stats[:broadcasts_count].to_i) %></div>
|
|
13
|
+
<div class="so-card__subtitle">selected window</div>
|
|
14
|
+
</article>
|
|
15
|
+
|
|
16
|
+
<article class="so-card so-metric">
|
|
17
|
+
<div class="so-card__label">Rejection rate</div>
|
|
18
|
+
<div class="so-metric__value"><%= cable_ratio_percent(stats[:rejection_rate]) %></div>
|
|
19
|
+
<div class="so-card__subtitle">rejections / broadcasts</div>
|
|
20
|
+
</article>
|
|
21
|
+
|
|
22
|
+
<article class="so-card so-metric">
|
|
23
|
+
<div class="so-card__label">Message backlog</div>
|
|
24
|
+
<div class="so-metric__value"><%= backlog_summary[:value] %></div>
|
|
25
|
+
<div class="so-card__subtitle"><%= backlog_summary[:subtitle] %></div>
|
|
26
|
+
</article>
|
|
27
|
+
|
|
28
|
+
<article class="so-card so-metric">
|
|
29
|
+
<div class="so-card__label">Storage footprint</div>
|
|
30
|
+
<div class="so-metric__value"><%= storage_summary[:value] %></div>
|
|
31
|
+
<div class="so-card__subtitle"><%= storage_summary[:subtitle] %></div>
|
|
32
|
+
</article>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<% content_for :title, "Cable overview" %>
|
|
2
|
+
<% status_class = @cable_dashboard_available ? "so-badge--success" : "so-badge--warning" %>
|
|
3
|
+
|
|
4
|
+
<div class="so-dashboard so-cable-dashboard">
|
|
5
|
+
<span class="sr-only">Cable Dashboard</span>
|
|
6
|
+
|
|
7
|
+
<div class="so-content__header">
|
|
8
|
+
<h1>Cable overview</h1>
|
|
9
|
+
|
|
10
|
+
<% if @cable_dashboard_available %>
|
|
11
|
+
<div class="so-cable-dashboard__intro">
|
|
12
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
13
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
14
|
+
<circle r="3" cx="3" cy="3" />
|
|
15
|
+
</svg>
|
|
16
|
+
Available
|
|
17
|
+
</span>
|
|
18
|
+
<p class="so-cable-dashboard__hint">Selected range: <%= cable_range_label(@range) %> · broadcasting names and payloads are never shown.</p>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<% if @cable_dashboard_available %>
|
|
24
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar so-cable-dashboard__range-form">
|
|
25
|
+
<label for="so-cable-range" class="so-card__label so-filters__label">Range</label>
|
|
26
|
+
<select id="so-cable-range" name="range" onchange="this.form.submit()">
|
|
27
|
+
<% SolidObserver::Services::CableStats::RANGES.each_key do |key| %>
|
|
28
|
+
<option value="<%= key %>" <%= "selected" if key == @range %>><%= cable_range_label(key) %></option>
|
|
29
|
+
<% end %>
|
|
30
|
+
</select>
|
|
31
|
+
<button type="submit" class="so-btn so-btn--refresh">Refresh data</button>
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<%= render "solid_observer/cable_dashboard/summary", stats: @stats, storage_components: @storage_components %>
|
|
35
|
+
|
|
36
|
+
<section class="so-card so-card--section" aria-labelledby="so-cable-controls-heading">
|
|
37
|
+
<header class="so-dashboard-section__header">
|
|
38
|
+
<h2 id="so-cable-controls-heading" class="so-dashboard-section__title">Operational controls</h2>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<% cable_controls_available = SolidObserver::Services::CableOperations.available? && @stats&.[](:backlog_available) %>
|
|
42
|
+
<% cable_backlog_count = @stats&.[](:backlog_count).to_i %>
|
|
43
|
+
|
|
44
|
+
<% if cable_controls_available %>
|
|
45
|
+
<% if cable_backlog_count > 1000 %>
|
|
46
|
+
<div class="so-cache-control-row">
|
|
47
|
+
<div class="so-cache-control-row__copy">
|
|
48
|
+
<h3 class="so-cache-control-row__title">Use the Rake task</h3>
|
|
49
|
+
<p class="so-cache-control-row__body">More than 1,000 expired/trimmable Solid Cable messages are pending. The UI will not run this trim. Run solid_observer:cable:trim from the server instead.</p>
|
|
50
|
+
<span class="so-badge so-badge--pill so-badge--warning">
|
|
51
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
52
|
+
<circle r="3" cx="3" cy="3" />
|
|
53
|
+
</svg>
|
|
54
|
+
UI trim unavailable
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<% else %>
|
|
59
|
+
<div class="so-cache-control-row">
|
|
60
|
+
<div class="so-cache-control-row__copy">
|
|
61
|
+
<h3 class="so-cache-control-row__title">Trim expired messages</h3>
|
|
62
|
+
<p class="so-cache-control-row__body">Removes expired/trimmable Solid Cable messages only. Active messages remain available to Cable.</p>
|
|
63
|
+
<span class="so-badge so-badge--pill so-badge--success">
|
|
64
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
65
|
+
<circle r="3" cx="3" cy="3" />
|
|
66
|
+
</svg>
|
|
67
|
+
Trimmable backlog: <%= cable_backlog_count %>
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="so-cache-control-row__action">
|
|
72
|
+
<%= button_to "Trim expired messages",
|
|
73
|
+
trim_cable_operations_path,
|
|
74
|
+
method: :post,
|
|
75
|
+
class: "so-btn",
|
|
76
|
+
form: {class: "so-form--inline"} %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% else %>
|
|
81
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Cable controls are unavailable because Solid Cable support is disabled or not detected. No trim was attempted.</p>
|
|
82
|
+
<span class="so-badge so-badge--pill so-badge--warning">
|
|
83
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
84
|
+
<circle r="3" cx="3" cy="3" />
|
|
85
|
+
</svg>
|
|
86
|
+
Unavailable
|
|
87
|
+
</span>
|
|
88
|
+
<% end %>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<%= render "solid_observer/cable_dashboard/charts" %>
|
|
92
|
+
|
|
93
|
+
<% if @stats&.[](:stability)&.[](:available) %>
|
|
94
|
+
<section class="so-stability" data-so-zone="cable-stability" aria-labelledby="so-cable-stability-heading">
|
|
95
|
+
<span class="so-stability__label" id="so-cable-stability-heading">Stability</span>
|
|
96
|
+
<%= cable_stability_badge(@stats[:stability][:state]) %>
|
|
97
|
+
<span class="so-stability__detail"><%= cable_stability_detail(@stats[:stability]) %></span>
|
|
98
|
+
<a href="#so-cable-events-heading" class="so-stability__link">View recent events →</a>
|
|
99
|
+
</section>
|
|
100
|
+
<% end %>
|
|
101
|
+
|
|
102
|
+
<%= render "solid_observer/cable_dashboard/recent_events", recent_events: @recent_events %>
|
|
103
|
+
<% else %>
|
|
104
|
+
<section class="so-card so-card--section" aria-labelledby="so-cable-unavailable-heading">
|
|
105
|
+
<header class="so-dashboard-section__header">
|
|
106
|
+
<h2 id="so-cable-unavailable-heading" class="so-dashboard-section__title">Cable dashboard unavailable</h2>
|
|
107
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
108
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
109
|
+
<circle r="3" cx="3" cy="3" />
|
|
110
|
+
</svg>
|
|
111
|
+
Unavailable
|
|
112
|
+
</span>
|
|
113
|
+
</header>
|
|
114
|
+
|
|
115
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Cable dashboard is unavailable because Solid Cable support is disabled or not detected. Metrics are unavailable.</p>
|
|
116
|
+
</section>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<section class="so-dashboard-section" aria-labelledby="so-cache-charts-heading">
|
|
2
|
+
<header class="so-dashboard-section__header">
|
|
3
|
+
<h2 id="so-cache-charts-heading" class="so-dashboard-section__title">Activity trends</h2>
|
|
4
|
+
</header>
|
|
5
|
+
|
|
6
|
+
<div class="so-chart-strip">
|
|
7
|
+
<% if @activity_trends&.[](:available) %>
|
|
8
|
+
<figure class="so-spark" data-so-spark="cache-hit-rate">
|
|
9
|
+
<figcaption class="so-spark__label">Hit rate <span data-so-range-copy><%= cache_range_label(@range) %></span></figcaption>
|
|
10
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
11
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
12
|
+
<polyline class="so-spark__line" points="<%= spark_points(@activity_trends[:hit_rate]) %>"/>
|
|
13
|
+
</svg>
|
|
14
|
+
<span class="so-spark__value"><%= cache_ratio_percent(@stats[:hit_rate]) %></span>
|
|
15
|
+
</figure>
|
|
16
|
+
|
|
17
|
+
<figure class="so-spark" data-so-spark="cache-operations">
|
|
18
|
+
<figcaption class="so-spark__label">Operations total <span data-so-range-copy><%= cache_range_label(@range) %></span></figcaption>
|
|
19
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
20
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
21
|
+
<polyline class="so-spark__line" points="<%= spark_points(@activity_trends[:operations]) %>"/>
|
|
22
|
+
</svg>
|
|
23
|
+
<span class="so-spark__value"><%= number_with_delimiter(@stats[:operations_count].to_i) %></span>
|
|
24
|
+
</figure>
|
|
25
|
+
|
|
26
|
+
<figure class="so-spark" data-so-spark="cache-errors">
|
|
27
|
+
<figcaption class="so-spark__label">Errors total <span data-so-range-copy><%= cache_range_label(@range) %></span></figcaption>
|
|
28
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
29
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
30
|
+
<polyline class="so-spark__line" points="<%= spark_points(@activity_trends[:errors]) %>"/>
|
|
31
|
+
</svg>
|
|
32
|
+
<span class="so-spark__value"><%= number_with_delimiter(@stats[:errors_count].to_i) %></span>
|
|
33
|
+
</figure>
|
|
34
|
+
<% else %>
|
|
35
|
+
<div class="so-card so-cache-dashboard__chart-empty">
|
|
36
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-range">No chart data in the selected range yet. Summary metrics still use bounded cache stats.</p>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<section class="so-card so-card--section" aria-labelledby="so-cache-events-heading">
|
|
2
|
+
<header class="so-dashboard-section__header">
|
|
3
|
+
<h2 id="so-cache-events-heading" class="so-dashboard-section__title">Sampled recent events</h2>
|
|
4
|
+
<span class="so-dashboard-section__meta">debug context only · no raw keys or values</span>
|
|
5
|
+
</header>
|
|
6
|
+
|
|
7
|
+
<% if recent_events.present? %>
|
|
8
|
+
<table class="so-table so-table--card">
|
|
9
|
+
<caption class="sr-only">Sampled cache events without raw cache keys or values</caption>
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th>Event</th>
|
|
13
|
+
<th>Key digest</th>
|
|
14
|
+
<th>Outcome</th>
|
|
15
|
+
<th>Duration</th>
|
|
16
|
+
<th>Recorded</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% recent_events.each do |event| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td><%= event.event_type.to_s.humanize %></td>
|
|
23
|
+
<td><span class="so-cache-dashboard__digest"><%= cache_event_digest(event.key_digest) %></span></td>
|
|
24
|
+
<td><%= cache_event_outcome_badge(event) %></td>
|
|
25
|
+
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
26
|
+
<td><span title="<%= event.recorded_at.utc.strftime("%Y-%m-%d %H:%M:%S UTC") %>"><%= time_ago_in_words(event.recorded_at) %> ago</span></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
<% else %>
|
|
32
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No sampled cache events in the selected range yet. Slow, sampled, or errored cache operations will appear here.</p>
|
|
33
|
+
<% end %>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<% storage_summary = cache_storage_summary(storage_components) %>
|
|
2
|
+
|
|
3
|
+
<section class="so-dashboard-section" aria-labelledby="so-cache-summary-heading">
|
|
4
|
+
<header class="so-dashboard-section__header">
|
|
5
|
+
<h2 id="so-cache-summary-heading" class="so-dashboard-section__title">Summary in selected range</h2>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<div class="so-stat-cards so-cache-dashboard__summary">
|
|
9
|
+
<article class="so-card so-metric">
|
|
10
|
+
<div class="so-card__label">Hit rate</div>
|
|
11
|
+
<div class="so-metric__value"><%= cache_ratio_percent(stats[:hit_rate]) %></div>
|
|
12
|
+
<div class="so-card__subtitle">hits / read outcomes</div>
|
|
13
|
+
</article>
|
|
14
|
+
|
|
15
|
+
<article class="so-card so-metric">
|
|
16
|
+
<div class="so-card__label">Operations</div>
|
|
17
|
+
<div class="so-metric__value"><%= number_with_delimiter(stats[:operations_count].to_i) %></div>
|
|
18
|
+
<div class="so-card__subtitle">selected window</div>
|
|
19
|
+
</article>
|
|
20
|
+
|
|
21
|
+
<article class="<%= ["so-card", "so-metric", ("so-card--accent-danger" if stats[:errors_count].to_i.positive?)].compact.join(" ") %>">
|
|
22
|
+
<div class="so-card__label">Error rate</div>
|
|
23
|
+
<div class="so-metric__value"><%= cache_ratio_percent(stats[:error_rate]) %></div>
|
|
24
|
+
<div class="so-card__subtitle">errors / operations</div>
|
|
25
|
+
</article>
|
|
26
|
+
|
|
27
|
+
<article class="so-card so-metric">
|
|
28
|
+
<div class="so-card__label">Avg duration</div>
|
|
29
|
+
<div class="so-metric__value"><%= format_duration(stats[:avg_duration].to_f) %></div>
|
|
30
|
+
<div class="so-card__subtitle">operation latency</div>
|
|
31
|
+
</article>
|
|
32
|
+
|
|
33
|
+
<article class="so-card so-metric">
|
|
34
|
+
<div class="so-card__label">Storage footprint</div>
|
|
35
|
+
<div class="so-metric__value"><%= storage_summary[:value] %></div>
|
|
36
|
+
<div class="so-card__subtitle"><%= storage_summary[:subtitle] %></div>
|
|
37
|
+
</article>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<% content_for :title, "Cache overview" %>
|
|
2
|
+
<% status_class = @cache_dashboard_available ? "so-badge--success" : "so-badge--warning" %>
|
|
3
|
+
|
|
4
|
+
<div class="so-dashboard so-cache-dashboard">
|
|
5
|
+
<span class="sr-only">Cache Dashboard</span>
|
|
6
|
+
|
|
7
|
+
<div class="so-content__header">
|
|
8
|
+
<h1>Cache overview</h1>
|
|
9
|
+
|
|
10
|
+
<% if @cache_dashboard_available %>
|
|
11
|
+
<div class="so-cache-dashboard__intro">
|
|
12
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
13
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
14
|
+
<circle r="3" cx="3" cy="3" />
|
|
15
|
+
</svg>
|
|
16
|
+
Available
|
|
17
|
+
</span>
|
|
18
|
+
<p class="so-cache-dashboard__hint">Selected range: <%= cache_range_label(@range) %> · keys and values are never shown.</p>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<% if @cache_dashboard_available %>
|
|
24
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar so-cache-dashboard__range-form">
|
|
25
|
+
<label for="so-cache-range" class="so-card__label so-filters__label">Range</label>
|
|
26
|
+
<select id="so-cache-range" name="range" onchange="this.form.submit()">
|
|
27
|
+
<% SolidObserver::Services::CacheStats::RANGES.each_key do |key| %>
|
|
28
|
+
<option value="<%= key %>" <%= "selected" if key == @range %>><%= cache_range_label(key) %></option>
|
|
29
|
+
<% end %>
|
|
30
|
+
</select>
|
|
31
|
+
<button type="submit" class="so-btn so-btn--refresh">Refresh data</button>
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<%= render "solid_observer/cache_dashboard/summary", stats: @stats, storage_components: @storage_components %>
|
|
35
|
+
<%= render "solid_observer/cache_dashboard/charts" %>
|
|
36
|
+
|
|
37
|
+
<% if @stability&.[](:available) %>
|
|
38
|
+
<section class="so-stability" data-so-zone="cache-stability" aria-labelledby="so-cache-stability-heading">
|
|
39
|
+
<span class="so-stability__label" id="so-cache-stability-heading">Stability</span>
|
|
40
|
+
<%= cache_stability_badge(@stability[:state]) %>
|
|
41
|
+
<span class="so-stability__detail"><%= cache_stability_detail(@stability) %></span>
|
|
42
|
+
<a href="#so-cache-events-heading" class="so-stability__link">View sampled events →</a>
|
|
43
|
+
</section>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<%= render "solid_observer/cache_dashboard/recent_events", recent_events: @recent_events %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<section class="so-card so-card--section" aria-labelledby="so-cache-unavailable-heading">
|
|
49
|
+
<header class="so-dashboard-section__header">
|
|
50
|
+
<h2 id="so-cache-unavailable-heading" class="so-dashboard-section__title">Cache dashboard unavailable</h2>
|
|
51
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
52
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
53
|
+
<circle r="3" cx="3" cy="3" />
|
|
54
|
+
</svg>
|
|
55
|
+
Unavailable
|
|
56
|
+
</span>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Cache dashboard is unavailable because SolidCache support is disabled or not detected. Metrics are unavailable.</p>
|
|
60
|
+
</section>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|