admin_suite 0.2.1 → 0.2.2
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/app/assets/admin_suite.css +128 -0
- data/app/controllers/admin_suite/application_controller.rb +31 -0
- data/app/controllers/admin_suite/dashboard_controller.rb +59 -226
- data/app/helpers/admin_suite/base_helper.rb +108 -108
- data/app/helpers/admin_suite/panels_helper.rb +1 -1
- data/app/javascript/controllers/admin_suite/file_upload_controller.js +9 -9
- data/app/javascript/controllers/admin_suite/json_editor_controller.js +8 -8
- data/app/javascript/controllers/admin_suite/searchable_select_controller.js +2 -2
- data/app/javascript/controllers/admin_suite/tag_select_controller.js +1 -1
- data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +1 -1
- data/app/views/admin_suite/dashboard/index.html.erb +6 -15
- data/app/views/admin_suite/panels/_cards.html.erb +6 -6
- data/app/views/admin_suite/panels/_chart.html.erb +12 -12
- data/app/views/admin_suite/panels/_health.html.erb +14 -14
- data/app/views/admin_suite/panels/_recent.html.erb +11 -11
- data/app/views/admin_suite/panels/_stat.html.erb +24 -24
- data/app/views/admin_suite/panels/_table.html.erb +10 -10
- data/app/views/admin_suite/portals/show.html.erb +1 -1
- data/app/views/admin_suite/resources/_form.html.erb +1 -1
- data/app/views/admin_suite/resources/edit.html.erb +4 -4
- data/app/views/admin_suite/resources/index.html.erb +23 -23
- data/app/views/admin_suite/resources/new.html.erb +4 -4
- data/app/views/admin_suite/resources/show.html.erb +17 -17
- data/app/views/admin_suite/shared/_form.html.erb +8 -8
- data/app/views/admin_suite/shared/_json_editor_field.html.erb +4 -4
- data/app/views/admin_suite/shared/_sidebar.html.erb +4 -4
- data/app/views/admin_suite/shared/_topbar.html.erb +1 -1
- data/app/views/layouts/admin_suite/application.html.erb +4 -4
- data/docs/configuration.md +25 -0
- data/docs/portals.md +42 -0
- data/lib/admin_suite/configuration.rb +10 -0
- data/lib/admin_suite/engine.rb +9 -0
- data/lib/admin_suite/ui/field_renderer_registry.rb +2 -2
- data/lib/admin_suite/ui/form_field_renderer.rb +2 -2
- data/lib/admin_suite/ui/show_formatter_registry.rb +5 -5
- data/lib/admin_suite/ui/show_value_formatter.rb +1 -1
- data/lib/admin_suite/version.rb +1 -1
- data/lib/admin_suite.rb +31 -0
- data/test/dummy/log/test.log +1328 -440
- data/test/dummy/tmp/local_secret.txt +1 -1
- data/test/integration/dashboard_test.rb +57 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22d3fb1dd5649136ae88ea54aa4b948790c1e1fd7a81681ffd07707f82d6494a
|
|
4
|
+
data.tar.gz: 00c3ba3433eb3d9d59289c70d75fe9b0a3f613a043b6dc1d109b7af4a375272e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b104f41152647c491a4f318beb6d1b2ea7f69619af6c7cb9ee05fef3a32b719aef73f48200e6b0bd5efc4c2ad7fe85637db40dc3023d63a3281cd39882539b7f
|
|
7
|
+
data.tar.gz: 8d27c97e8790b50d59f769abacdc1a19158158fcb284d31b6e3785fcb8064e3020ef8577e3b1960681038314ea865e1502ba408e7db8359924ea5accc39bb99e
|
data/app/assets/admin_suite.css
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
body.admin-suite {
|
|
12
|
+
color-scheme: light;
|
|
12
13
|
--admin-suite-bg: #f8fafc; /* slate-50 */
|
|
13
14
|
--admin-suite-fg: #0f172a; /* slate-900 */
|
|
14
15
|
--admin-suite-muted: #475569; /* slate-600 */
|
|
@@ -265,6 +266,133 @@ body.admin-suite .admin-suite-sidebar .admin-suite-sidebar-muted {
|
|
|
265
266
|
color: rgba(255, 255, 255, 0.75);
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
/* Responsive layout fallbacks (engine-owned)
|
|
270
|
+
----------------------------------------
|
|
271
|
+
Tailwind v4 emits media queries like `@media (width >= 64rem)`, which can fail
|
|
272
|
+
in some embedded/older browsers. For critical layout, we provide classic
|
|
273
|
+
`min-width` media queries so the UI remains usable everywhere. */
|
|
274
|
+
|
|
275
|
+
/* Dashboard row grid (>= 1024px)
|
|
276
|
+
Used by `render_dashboard_rows` to honor panel `span`. */
|
|
277
|
+
body.admin-suite .admin-suite-dashboard-row {
|
|
278
|
+
display: grid;
|
|
279
|
+
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
280
|
+
gap: 1.5rem; /* ~ gap-6 */
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@media (min-width: 1024px) {
|
|
284
|
+
body.admin-suite .admin-suite-dashboard-row {
|
|
285
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Cards panel grids (portals/resources) */
|
|
290
|
+
body.admin-suite .admin-suite-cards-grid {
|
|
291
|
+
display: grid;
|
|
292
|
+
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
body.admin-suite .admin-suite-cards-grid--portals {
|
|
296
|
+
gap: 1rem; /* ~ gap-4 */
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
body.admin-suite .admin-suite-cards-grid--resources {
|
|
300
|
+
gap: 0.75rem; /* ~ gap-3 */
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@media (min-width: 640px) {
|
|
304
|
+
body.admin-suite .admin-suite-cards-grid--resources {
|
|
305
|
+
grid-template-columns: repeat(2, minmax(0, 1fr)); /* sm:grid-cols-2 */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@media (min-width: 768px) {
|
|
310
|
+
body.admin-suite .admin-suite-cards-grid--portals {
|
|
311
|
+
grid-template-columns: repeat(2, minmax(0, 1fr)); /* md:grid-cols-2 */
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@media (min-width: 1024px) {
|
|
316
|
+
body.admin-suite .admin-suite-cards-grid--portals {
|
|
317
|
+
grid-template-columns: repeat(4, minmax(0, 1fr)); /* lg:grid-cols-4 */
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
body.admin-suite .admin-suite-cards-grid--resources {
|
|
321
|
+
grid-template-columns: repeat(3, minmax(0, 1fr)); /* lg:grid-cols-3 */
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* Portal fallback sections grid (>= 1024px) */
|
|
326
|
+
body.admin-suite .admin-suite-portal-sections-grid {
|
|
327
|
+
display: grid;
|
|
328
|
+
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
329
|
+
gap: 1.5rem; /* ~ gap-6 */
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@media (min-width: 1024px) {
|
|
333
|
+
body.admin-suite .admin-suite-portal-sections-grid {
|
|
334
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Desktop sidebar visibility (>= 1024px) */
|
|
339
|
+
body.admin-suite .admin-suite-desktop-sidebar {
|
|
340
|
+
display: none;
|
|
341
|
+
flex-shrink: 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Prevent flex children from overflowing (required for nested tables). */
|
|
345
|
+
body.admin-suite .admin-suite-main {
|
|
346
|
+
min-width: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@media (min-width: 1024px) {
|
|
350
|
+
body.admin-suite .admin-suite-desktop-sidebar {
|
|
351
|
+
display: flex;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Ensure the mobile menu button/overlay never show on desktop */
|
|
355
|
+
body.admin-suite .admin-suite-mobile-menu-button {
|
|
356
|
+
display: none;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
body.admin-suite .admin-suite-mobile-overlay,
|
|
360
|
+
body.admin-suite .admin-suite-mobile-sidebar {
|
|
361
|
+
display: none !important;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* Two-column resource layouts */
|
|
366
|
+
body.admin-suite .admin-suite-two-col {
|
|
367
|
+
display: flex;
|
|
368
|
+
flex-direction: column;
|
|
369
|
+
gap: 1.5rem; /* ~ gap-6 */
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@media (min-width: 768px) {
|
|
373
|
+
body.admin-suite .admin-suite-two-col {
|
|
374
|
+
flex-direction: row;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
body.admin-suite .admin-suite-two-col__sidebar {
|
|
378
|
+
flex: 0 0 auto;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
body.admin-suite .admin-suite-two-col__sidebar--filters {
|
|
382
|
+
width: 16rem; /* ~ w-64 */
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
body.admin-suite .admin-suite-two-col__sidebar--details {
|
|
386
|
+
width: 20rem; /* ~ w-80 */
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Ensure the engine sidebar has a stable width even without Tailwind. */
|
|
391
|
+
body.admin-suite .admin-suite-sidebar {
|
|
392
|
+
width: 18rem; /* ~ w-72 */
|
|
393
|
+
height: 100%;
|
|
394
|
+
}
|
|
395
|
+
|
|
268
396
|
.form-group { margin-bottom: 1rem; }
|
|
269
397
|
.form-label {
|
|
270
398
|
display: block;
|
|
@@ -72,6 +72,37 @@ module AdminSuite
|
|
|
72
72
|
retry
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Loads the root dashboard definition files (safe to call per-request).
|
|
76
|
+
#
|
|
77
|
+
# Host apps typically define this in:
|
|
78
|
+
# - `config/admin_suite/dashboard.rb`
|
|
79
|
+
# - `app/admin_suite/dashboard.rb`
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
def ensure_root_dashboard_loaded!
|
|
83
|
+
if Rails.env.development?
|
|
84
|
+
globs = Array(AdminSuite.config.dashboard_globs).flat_map { |g| Dir[g] }.uniq
|
|
85
|
+
# Re-evaluate dashboard layout on each request in development.
|
|
86
|
+
# Always reset, even when no files match, so removed dashboards are cleared.
|
|
87
|
+
AdminSuite.reset_root_dashboard!
|
|
88
|
+
globs.each { |file| load file }
|
|
89
|
+
else
|
|
90
|
+
# In non-dev, load once.
|
|
91
|
+
return if AdminSuite.config.root_dashboard_loaded
|
|
92
|
+
globs = Array(AdminSuite.config.dashboard_globs).flat_map { |g| Dir[g] }.uniq
|
|
93
|
+
if globs.empty?
|
|
94
|
+
# Avoid hitting the filesystem on every request when no dashboard files exist.
|
|
95
|
+
AdminSuite.config.root_dashboard_loaded = true
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
globs.each { |file| require file }
|
|
99
|
+
AdminSuite.config.root_dashboard_loaded = true
|
|
100
|
+
end
|
|
101
|
+
rescue NameError
|
|
102
|
+
require "admin_suite"
|
|
103
|
+
retry
|
|
104
|
+
end
|
|
105
|
+
|
|
75
106
|
# Builds the navigation structure from registered resources.
|
|
76
107
|
#
|
|
77
108
|
# @return [Hash]
|
|
@@ -3,256 +3,89 @@
|
|
|
3
3
|
module AdminSuite
|
|
4
4
|
class DashboardController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
|
|
7
|
-
items = navigation_items
|
|
6
|
+
ensure_root_dashboard_loaded!
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
@
|
|
11
|
-
@recent = build_root_recent
|
|
8
|
+
items = navigation_items
|
|
9
|
+
@portal_cards = build_portal_cards(items)
|
|
12
10
|
|
|
13
|
-
@
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
description: portal[:description],
|
|
20
|
-
color: color,
|
|
21
|
-
icon: portal[:icon],
|
|
22
|
-
path: portal_path(portal: portal_key),
|
|
23
|
-
count: portal[:sections].values.sum { |s| s[:items].size }
|
|
24
|
-
}
|
|
25
|
-
end
|
|
11
|
+
@page_title = eval_setting(AdminSuite.config.root_dashboard_title, default: "Admin Suite")
|
|
12
|
+
@page_description =
|
|
13
|
+
eval_setting(
|
|
14
|
+
AdminSuite.config.root_dashboard_description,
|
|
15
|
+
default: "Admin framework for managing application resources across one or more portals."
|
|
16
|
+
)
|
|
26
17
|
|
|
27
|
-
@
|
|
18
|
+
@dashboard_rows = resolve_root_dashboard_rows(items)
|
|
28
19
|
end
|
|
29
20
|
|
|
30
21
|
private
|
|
31
22
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
when :email then "emerald"
|
|
36
|
-
when :ai then "cyan"
|
|
37
|
-
when :assistant then "violet"
|
|
38
|
-
when :payments then "emerald"
|
|
39
|
-
else "slate"
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def build_sections
|
|
44
|
-
sections = []
|
|
45
|
-
|
|
46
|
-
sections << {
|
|
47
|
-
title: "System Health",
|
|
48
|
-
subtitle: nil,
|
|
49
|
-
rows: [
|
|
50
|
-
AdminSuite::UI::RowDefinition.new(panels: [
|
|
51
|
-
AdminSuite::UI::PanelDefinition.new(type: :health, title: "Application", options: { span: 3, status: @health.dig(:app, :status), metrics: @health.dig(:app, :metrics) }),
|
|
52
|
-
AdminSuite::UI::PanelDefinition.new(type: :health, title: "Scraping Pipeline", options: { span: 3, status: @health.dig(:scraping, :status), metrics: @health.dig(:scraping, :metrics) }),
|
|
53
|
-
AdminSuite::UI::PanelDefinition.new(type: :health, title: "LLM API", options: { span: 3, status: @health.dig(:llm, :status), metrics: @health.dig(:llm, :metrics) }),
|
|
54
|
-
AdminSuite::UI::PanelDefinition.new(type: :health, title: "Assistant", options: { span: 3, status: @health.dig(:assistant, :status), metrics: @health.dig(:assistant, :metrics) })
|
|
55
|
-
])
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
sections << {
|
|
60
|
-
title: nil,
|
|
61
|
-
subtitle: nil,
|
|
62
|
-
rows: [
|
|
63
|
-
AdminSuite::UI::RowDefinition.new(panels: [
|
|
64
|
-
AdminSuite::UI::PanelDefinition.new(
|
|
65
|
-
type: :cards,
|
|
66
|
-
title: "Portals",
|
|
67
|
-
options: { span: 12, variant: :portals, resources: @portal_cards }
|
|
68
|
-
)
|
|
69
|
-
])
|
|
70
|
-
]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
sections << {
|
|
74
|
-
title: nil,
|
|
75
|
-
subtitle: nil,
|
|
76
|
-
rows: [
|
|
77
|
-
AdminSuite::UI::RowDefinition.new(panels: [
|
|
78
|
-
AdminSuite::UI::PanelDefinition.new(type: :stat, title: "Total Resources", options: { span: 3, variant: :mini, color: :slate, value: @stats[:total_resources] }),
|
|
79
|
-
AdminSuite::UI::PanelDefinition.new(type: :stat, title: "Ops Resources", options: { span: 3, variant: :mini, color: :amber, value: @stats[:ops_resources] }),
|
|
80
|
-
AdminSuite::UI::PanelDefinition.new(type: :stat, title: "Email Resources", options: { span: 2, variant: :mini, color: :emerald, value: @stats[:email_resources] }),
|
|
81
|
-
AdminSuite::UI::PanelDefinition.new(type: :stat, title: "AI Resources", options: { span: 2, variant: :mini, color: :cyan, value: @stats[:ai_resources] }),
|
|
82
|
-
AdminSuite::UI::PanelDefinition.new(type: :stat, title: "Assistant Resources", options: { span: 2, variant: :mini, color: :violet, value: @stats[:assistant_resources] })
|
|
83
|
-
])
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
sections << {
|
|
88
|
-
title: "Recent Activity",
|
|
89
|
-
subtitle: nil,
|
|
90
|
-
rows: [
|
|
91
|
-
AdminSuite::UI::RowDefinition.new(panels: [
|
|
92
|
-
AdminSuite::UI::PanelDefinition.new(type: :recent, title: "Recent Signups", options: { span: 3, scope: @recent[:recent_users], view_all_path: ->(view) { view.resources_path(portal: :ops, resource_name: "users") } }),
|
|
93
|
-
AdminSuite::UI::PanelDefinition.new(type: :recent, title: "Recent Applications", options: { span: 3, scope: @recent[:recent_applications], view_all_path: ->(view) { view.resources_path(portal: :ops, resource_name: "interview_applications") } }),
|
|
94
|
-
AdminSuite::UI::PanelDefinition.new(type: :recent, title: "Recent Assistant", options: { span: 3, scope: @recent[:recent_threads], view_all_path: ->(view) { view.resources_path(portal: :assistant, resource_name: "assistant_threads") } }),
|
|
95
|
-
AdminSuite::UI::PanelDefinition.new(type: :recent, title: "Recent Scraping", options: { span: 3, scope: @recent[:recent_scraping], view_all_path: ->(view) { view.resources_path(portal: :ops, resource_name: "scraping_attempts") } })
|
|
96
|
-
])
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
sections
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def build_root_stats(items)
|
|
104
|
-
{
|
|
105
|
-
total_resources: Admin::Base::Resource.registered_resources.count,
|
|
106
|
-
portals: items.keys.count,
|
|
107
|
-
ops_resources: Admin::Base::Resource.resources_for_portal(:ops).count,
|
|
108
|
-
email_resources: Admin::Base::Resource.resources_for_portal(:email).count,
|
|
109
|
-
ai_resources: Admin::Base::Resource.resources_for_portal(:ai).count,
|
|
110
|
-
assistant_resources: Admin::Base::Resource.resources_for_portal(:assistant).count
|
|
111
|
-
}
|
|
23
|
+
def eval_setting(value, default:)
|
|
24
|
+
evaluated = value.respond_to?(:call) ? value.call(self) : value
|
|
25
|
+
evaluated.to_s.presence || default
|
|
112
26
|
rescue StandardError
|
|
113
|
-
|
|
27
|
+
default
|
|
114
28
|
end
|
|
115
29
|
|
|
116
|
-
def
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
llm: llm_health,
|
|
130
|
-
assistant: assistant_health
|
|
131
|
-
}
|
|
30
|
+
def build_portal_cards(items)
|
|
31
|
+
items.sort_by { |(_k, v)| (v[:order] || 100).to_i }.map do |portal_key, portal|
|
|
32
|
+
color = portal[:color].presence || default_portal_color(portal_key)
|
|
33
|
+
{
|
|
34
|
+
key: portal_key,
|
|
35
|
+
label: portal[:label] || portal_key.to_s.humanize,
|
|
36
|
+
description: portal[:description],
|
|
37
|
+
color: color,
|
|
38
|
+
icon: portal[:icon],
|
|
39
|
+
path: portal_path(portal: portal_key),
|
|
40
|
+
count: (portal[:sections] || {}).values.sum { |s| Array(s[:items]).size }
|
|
41
|
+
}
|
|
42
|
+
end
|
|
132
43
|
end
|
|
133
44
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"Users" => safe_count(::User),
|
|
139
|
-
"24h signups" => safe_count(::User, ->(rel) { rel.where("created_at > ?", 24.hours.ago) }),
|
|
140
|
-
"Applications" => (defined?(::InterviewApplication) ? safe_count(::InterviewApplication) : "—"),
|
|
141
|
-
"Job listings" => (defined?(::JobListing) ? safe_count(::JobListing) : "—")
|
|
142
|
-
}
|
|
45
|
+
def resolve_root_dashboard_rows(items)
|
|
46
|
+
definition = AdminSuite.root_dashboard_definition
|
|
47
|
+
configured_rows = definition&.rows
|
|
48
|
+
return configured_rows unless configured_rows.nil?
|
|
143
49
|
|
|
144
|
-
|
|
145
|
-
rescue StandardError
|
|
146
|
-
{ status: :unknown, metrics: {} }
|
|
50
|
+
build_default_root_dashboard_rows(items)
|
|
147
51
|
end
|
|
148
52
|
|
|
149
|
-
def
|
|
150
|
-
|
|
53
|
+
def build_default_root_dashboard_rows(items)
|
|
54
|
+
portal_cards = @portal_cards
|
|
55
|
+
total_portals = items.keys.count
|
|
56
|
+
total_resources = safe_resource_count
|
|
151
57
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
successful = recent_attempts.where(status: :completed).count
|
|
155
|
-
failed = recent_attempts.where(status: :failed).count
|
|
156
|
-
stuck = recent_attempts.where(status: :processing).where("updated_at < ?", 1.hour.ago).count
|
|
58
|
+
definition = AdminSuite::UI::DashboardDefinition.new
|
|
59
|
+
dsl = AdminSuite::UI::DashboardDSL.new(definition)
|
|
157
60
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
:critical
|
|
162
|
-
elsif stuck > 0 || (total > 10 && success_rate < 80)
|
|
163
|
-
:degraded
|
|
164
|
-
else
|
|
165
|
-
:healthy
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
{
|
|
169
|
-
status: status,
|
|
170
|
-
metrics: {
|
|
171
|
-
"24h attempts" => total,
|
|
172
|
-
"success rate" => "#{success_rate}%",
|
|
173
|
-
"failed" => failed,
|
|
174
|
-
"stuck" => stuck
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
rescue StandardError
|
|
178
|
-
{ status: :unknown, metrics: {} }
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def llm_health
|
|
182
|
-
return { status: :unknown, metrics: {} } unless defined?(::Ai::LlmApiLog)
|
|
183
|
-
|
|
184
|
-
recent_logs = ::Ai::LlmApiLog.where("created_at > ?", 24.hours.ago)
|
|
185
|
-
total = recent_logs.count
|
|
186
|
-
successful = recent_logs.where(status: :success).count
|
|
187
|
-
failed = recent_logs.where(status: :failed).count
|
|
188
|
-
avg_latency = recent_logs.where(status: :success).average(:latency_ms)&.round || 0
|
|
189
|
-
total_cost_cents = recent_logs.sum(:estimated_cost_cents) || 0
|
|
190
|
-
total_cost = (total_cost_cents / 100.0).round(2)
|
|
61
|
+
dsl.row do
|
|
62
|
+
cards_panel "Portals", span: 12, variant: :portals, resources: portal_cards
|
|
63
|
+
end
|
|
191
64
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
elsif total > 10 && success_rate < 95
|
|
197
|
-
:degraded
|
|
198
|
-
else
|
|
199
|
-
:healthy
|
|
200
|
-
end
|
|
65
|
+
dsl.row do
|
|
66
|
+
stat_panel "Portals", total_portals, span: 6, variant: :mini, color: :slate
|
|
67
|
+
stat_panel "Resources", total_resources, span: 6, variant: :mini, color: :slate
|
|
68
|
+
end
|
|
201
69
|
|
|
202
|
-
|
|
203
|
-
status: status,
|
|
204
|
-
metrics: {
|
|
205
|
-
"24h calls" => total,
|
|
206
|
-
"success rate" => "#{success_rate}%",
|
|
207
|
-
"avg latency" => "#{avg_latency}ms",
|
|
208
|
-
"24h cost" => "$#{total_cost}",
|
|
209
|
-
"failed" => failed
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
rescue StandardError
|
|
213
|
-
{ status: :unknown, metrics: {} }
|
|
70
|
+
definition.rows
|
|
214
71
|
end
|
|
215
72
|
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
failed = recent_executions.where(status: :failed).count
|
|
225
|
-
pending = ::Assistant::ToolExecution.where(status: :pending_approval).count
|
|
226
|
-
|
|
227
|
-
success_rate = total_executions > 0 ? (successful.to_f / total_executions * 100).round : 100
|
|
228
|
-
status =
|
|
229
|
-
if failed > 10
|
|
230
|
-
:critical
|
|
231
|
-
elsif pending > 20 || (total_executions > 10 && success_rate < 70)
|
|
232
|
-
:degraded
|
|
233
|
-
else
|
|
234
|
-
:healthy
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
{
|
|
238
|
-
status: status,
|
|
239
|
-
metrics: {
|
|
240
|
-
"24h threads" => (recent_threads ? recent_threads.count : "—"),
|
|
241
|
-
"24h tool runs" => total_executions,
|
|
242
|
-
"success rate" => "#{success_rate}%",
|
|
243
|
-
"pending" => pending
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
rescue StandardError
|
|
247
|
-
{ status: :unknown, metrics: {} }
|
|
73
|
+
def default_portal_color(portal_key)
|
|
74
|
+
case portal_key.to_sym
|
|
75
|
+
when :ops then "amber"
|
|
76
|
+
when :email then "emerald"
|
|
77
|
+
when :ai then "cyan"
|
|
78
|
+
when :assistant then "violet"
|
|
79
|
+
else "slate"
|
|
80
|
+
end
|
|
248
81
|
end
|
|
249
82
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
83
|
+
def safe_resource_count
|
|
84
|
+
return 0 unless defined?(Admin::Base::Resource)
|
|
85
|
+
|
|
86
|
+
Admin::Base::Resource.registered_resources.count
|
|
254
87
|
rescue StandardError
|
|
255
|
-
|
|
88
|
+
0
|
|
256
89
|
end
|
|
257
90
|
end
|
|
258
91
|
end
|