dbwatcher 1.1.0 → 1.1.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +12 -22
  4. data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
  5. data/app/assets/stylesheets/dbwatcher/application.css +394 -41
  6. data/app/assets/stylesheets/dbwatcher/application.scss +4 -0
  7. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
  8. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
  9. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
  10. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
  11. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
  12. data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
  13. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
  14. data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
  15. data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
  16. data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
  17. data/app/helpers/dbwatcher/application_helper.rb +43 -11
  18. data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
  19. data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
  20. data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
  21. data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
  22. data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
  23. data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
  24. data/app/views/dbwatcher/sessions/_changes.html.erb +91 -0
  25. data/app/views/dbwatcher/sessions/_layout.html.erb +23 -0
  26. data/app/views/dbwatcher/sessions/index.html.erb +107 -87
  27. data/app/views/dbwatcher/sessions/show.html.erb +10 -4
  28. data/app/views/dbwatcher/tables/index.html.erb +32 -40
  29. data/app/views/layouts/dbwatcher/application.html.erb +100 -48
  30. data/config/routes.rb +23 -6
  31. data/lib/dbwatcher/configuration.rb +18 -1
  32. data/lib/dbwatcher/services/base_service.rb +2 -0
  33. data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +177 -138
  34. data/lib/dbwatcher/services/diagram_data/dataset.rb +2 -0
  35. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +13 -9
  36. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +3 -1
  37. data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +17 -1
  38. data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
  39. data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
  40. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
  41. data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
  42. data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
  43. data/lib/dbwatcher/storage/session.rb +5 -0
  44. data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
  45. data/lib/dbwatcher/storage.rb +12 -0
  46. data/lib/dbwatcher/version.rb +1 -1
  47. data/lib/dbwatcher.rb +15 -1
  48. metadata +20 -15
  49. data/app/helpers/dbwatcher/component_helper.rb +0 -29
  50. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
  51. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
  52. data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
  53. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
  54. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
  55. data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
  56. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
  57. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
  58. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
  59. data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
  60. /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
  61. /data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +0 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Dashboard
5
+ class SystemInfoController < BaseController
6
+ def refresh
7
+ system_info_storage.refresh_info
8
+
9
+ respond_to do |format|
10
+ format.json do
11
+ render json: refresh_success_response
12
+ end
13
+ end
14
+ rescue StandardError => e
15
+ respond_to do |format|
16
+ format.json do
17
+ render json: { success: false, error: e.message }, status: :internal_server_error
18
+ end
19
+ end
20
+ end
21
+
22
+ def clear_cache
23
+ system_info_storage.clear_cache
24
+
25
+ respond_to do |format|
26
+ format.json do
27
+ render json: clear_cache_success_response
28
+ end
29
+ end
30
+ rescue StandardError => e
31
+ respond_to do |format|
32
+ format.json do
33
+ render json: { success: false, error: e.message }, status: :internal_server_error
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # Get system info storage instance
41
+ #
42
+ # @return [Storage::SystemInfoStorage] storage instance
43
+ def system_info_storage
44
+ @system_info_storage ||= Storage::SystemInfoStorage.new
45
+ end
46
+
47
+ def refresh_success_response
48
+ {
49
+ success: true,
50
+ message: "System information refreshed successfully",
51
+ data: system_info_storage.cached_info,
52
+ summary: system_info_storage.summary
53
+ }
54
+ end
55
+
56
+ def clear_cache_success_response
57
+ {
58
+ success: true,
59
+ message: "System information cache cleared successfully"
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -7,6 +7,14 @@ module Dbwatcher
7
7
  @recent_sessions = dashboard_data[:recent_sessions]
8
8
  @active_tables = dashboard_data[:active_tables]
9
9
  @query_stats = dashboard_data[:query_stats]
10
+ @active_tab = params[:tab] || "overview"
11
+
12
+ # Add system information if enabled
13
+ return unless Dbwatcher.configuration.collect_system_info
14
+
15
+ @system_info_summary = system_info_storage.summary
16
+ @system_info = system_info_storage.cached_info
17
+ @info_age = system_info_storage.info_age
10
18
  end
11
19
 
12
20
  def clear_all
@@ -16,5 +24,14 @@ module Dbwatcher
16
24
  root_path
17
25
  )
18
26
  end
27
+
28
+ private
29
+
30
+ # Get system info storage instance
31
+ #
32
+ # @return [Storage::SystemInfoStorage] storage instance
33
+ def system_info_storage
34
+ @system_info_storage ||= Storage::SystemInfoStorage.new
35
+ end
19
36
  end
20
37
  end
@@ -9,22 +9,9 @@ module Dbwatcher
9
9
  end
10
10
 
11
11
  def show
12
- redirect_to changes_session_path(@session.id)
13
- end
14
-
15
- def changes
16
- Rails.logger.info "SessionsController#changes: Loading changes for session #{@session.id}"
17
- # No server-side data processing - API-first architecture
18
- end
19
-
20
- def summary
21
- Rails.logger.info "SessionsController#summary: Loading summary for session #{@session.id}"
22
- # No server-side data processing - API-first architecture
23
- end
24
-
25
- def diagrams
26
- Rails.logger.info "SessionsController#diagrams: Loading diagrams for session #{@session.id}"
27
- # No server-side data processing - API-first architecture
12
+ @active_tab = params[:tab] || "changes"
13
+ # Debug logging
14
+ Rails.logger.info "SessionsController#show: Session ID: #{@session.id.inspect}, Class: #{@session.class}"
28
15
  end
29
16
 
30
17
  def clear
@@ -41,8 +28,5 @@ module Dbwatcher
41
28
  @session = Storage.sessions.find(params[:id])
42
29
  handle_not_found("Session", sessions_path) unless @session
43
30
  end
44
-
45
- # No longer needed with API-first architecture
46
- # All data processing happens in API services and is loaded via JavaScript
47
31
  end
48
32
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dbwatcher
4
+ # rubocop:disable Metrics/ModuleLength
4
5
  module ApplicationHelper
5
6
  include FormattingHelper
6
7
  include SessionHelper
@@ -21,7 +22,11 @@ module Dbwatcher
21
22
  sessions: sessions_icon_svg,
22
23
  tables: tables_icon_svg,
23
24
  queries: queries_icon_svg,
24
- performance: performance_icon_svg
25
+ performance: performance_icon_svg,
26
+ system: system_icon_svg,
27
+ memory: memory_icon_svg,
28
+ database: database_icon_svg,
29
+ runtime: runtime_icon_svg
25
30
  }
26
31
  end
27
32
 
@@ -60,19 +65,45 @@ module Dbwatcher
60
65
  SVG
61
66
  end
62
67
 
63
- public
68
+ def system_icon_svg
69
+ <<~SVG
70
+ <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
71
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
72
+ d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
73
+ </svg>
74
+ SVG
75
+ end
64
76
 
65
- # Generate operation badges for tables
66
- def operation_badges
67
- content_tag(:div, class: "flex gap-1 justify-center") do
68
- [
69
- content_tag(:span, "I", class: "badge badge-insert", title: "Inserts"),
70
- content_tag(:span, "U", class: "badge badge-update", title: "Updates"),
71
- content_tag(:span, "D", class: "badge badge-delete", title: "Deletes")
72
- ].join.html_safe
73
- end
77
+ def memory_icon_svg
78
+ <<~SVG
79
+ <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
80
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
81
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
82
+ </svg>
83
+ SVG
74
84
  end
75
85
 
86
+ def database_icon_svg
87
+ <<~SVG
88
+ <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
90
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
91
+ </svg>
92
+ SVG
93
+ end
94
+
95
+ def runtime_icon_svg
96
+ <<~SVG
97
+ <svg class="w-4 h-4 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
98
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
99
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
100
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
101
+ </svg>
102
+ SVG
103
+ end
104
+
105
+ public
106
+
76
107
  # Create action buttons with consistent styling
77
108
  def action_button(text, path, color: "blue")
78
109
  link_to text, path, class: "px-3 py-1 text-xs bg-#{color}-600 text-white rounded hover:bg-#{color}-700"
@@ -100,4 +131,5 @@ module Dbwatcher
100
131
  end
101
132
  end
102
133
  end
134
+ # rubocop:enable Metrics/ModuleLength
103
135
  end
@@ -2,39 +2,6 @@
2
2
 
3
3
  module Dbwatcher
4
4
  module DiagramHelper
5
- # Generate diagram configuration for Alpine.js
6
- def diagram_config(session, active_tab)
7
- {
8
- auto_generate: active_tab == "diagrams",
9
- default_type: "database_tables",
10
- endpoint: diagram_data_api_v1_session_path(session),
11
- container_id: "diagram-container"
12
- }.to_json
13
- end
14
-
15
- # Generate diagram type options dynamically from registry
16
- def diagram_type_options
17
- registry = Dbwatcher::Services::DiagramTypeRegistry.new
18
- options = registry.available_types_with_metadata.map do |type, metadata|
19
- [metadata[:display_name], type]
20
- end
21
-
22
- options_for_select(options)
23
- end
24
-
25
- # Generate CSS variables for diagram container height calculation
26
- def diagram_container_css_variables
27
- {
28
- "--header-height": "64px",
29
- "--tab-bar-height": "40px",
30
- "--toolbar-height": "72px",
31
- "--footer-height": "0px",
32
- "--diagram-height": "calc(100vh - var(--header-height) - var(--tab-bar-height) - " \
33
- "var(--toolbar-height) - var(--footer-height) - 2rem)",
34
- "--diagram-min-height": "500px"
35
- }.map { |key, value| "#{key}: #{value}" }.join("; ")
36
- end
37
-
38
5
  # Generate button classes for diagram controls
39
6
  def diagram_button_classes(type = :default)
40
7
  base_classes = "compact-button text-xs rounded"
@@ -51,60 +18,5 @@ module Dbwatcher
51
18
  style = button_styles[type] || button_styles[:primary]
52
19
  "#{base_classes} #{style}"
53
20
  end
54
-
55
- # Generate a code view with syntax highlighting for Mermaid code
56
- def diagram_code_view(content)
57
- content_tag(:div, class: "diagram-code-view") do
58
- content_tag(:pre,
59
- class: "text-xs font-mono p-4 bg-gray-50 rounded border border-gray-200 " \
60
- "overflow-x-auto whitespace-pre-wrap",
61
- style: "max-height: calc(100vh - 220px); overflow-y: auto;") do
62
- content_tag(:code, content)
63
- end
64
- end
65
- end
66
-
67
- # Generate copy to clipboard button
68
- def copy_to_clipboard_button(target_id)
69
- button_tag(
70
- type: "button",
71
- class: diagram_button_classes(:icon),
72
- "x-on:click": "copyToClipboard('#{target_id}')",
73
- title: "Copy to clipboard"
74
- ) do
75
- copy_icon_svg
76
- end
77
- end
78
-
79
- private
80
-
81
- # Generate copy icon SVG
82
- def copy_icon_svg
83
- content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
84
- tag.path(
85
- stroke_linecap: "round",
86
- stroke_linejoin: "round",
87
- stroke_width: "2",
88
- d: "M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2" \
89
- "M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
90
- )
91
- end
92
- end
93
-
94
- # Generate toggle view button
95
- def toggle_view_button
96
- button_tag(
97
- type: "button",
98
- class: diagram_button_classes(:toggle),
99
- "x-on:click": "toggleViewMode()",
100
- title: "Toggle between code and preview"
101
- ) do
102
- concat(content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
103
- tag.path(stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2",
104
- d: "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4")
105
- end)
106
- concat(content_tag(:span, "x-text": "viewMode === 'preview' ? 'View Code' : 'View Preview'"))
107
- end
108
- end
109
21
  end
110
22
  end
@@ -0,0 +1,27 @@
1
+ <%# Dashboard Layout - Common structure for all dashboard pages %>
2
+ <div class="h-full flex flex-col">
3
+ <!-- Compact Header -->
4
+ <div class="h-10 bg-navy-dark text-white flex items-center px-4">
5
+ <h1 class="text-sm font-medium">Dashboard</h1>
6
+ <span class="ml-auto text-xs text-blue-light">
7
+ <%= Time.current.strftime("%Y-%m-%d %H:%M:%S") %>
8
+ </span>
9
+ </div>
10
+
11
+ <!-- Tab Bar -->
12
+ <div class="tab-bar">
13
+ <%= link_to root_path, class: "tab-item #{active_tab == 'overview' ? 'active' : ''}" do %>
14
+ Overview
15
+ <% end %>
16
+ <% if @system_info_summary && !@system_info_summary.empty? %>
17
+ <%= link_to root_path(tab: 'system_info'), class: "tab-item #{active_tab == 'system_info' ? 'active' : ''}" do %>
18
+ System Info
19
+ <% end %>
20
+ <% end %>
21
+ </div>
22
+
23
+ <!-- Content Area -->
24
+ <div class="flex-1 overflow-auto p-4">
25
+ <%= yield %>
26
+ </div>
27
+ </div>
@@ -0,0 +1,188 @@
1
+ <%# Dashboard Overview Tab Content %>
2
+ <!-- Overview Tab Content -->
3
+ <div class="tab-content active" data-tab-content="overview">
4
+ <!-- Compact Stats Grid -->
5
+ <div class="grid grid-cols-4 gap-3 mb-4">
6
+ <!-- Sessions Card -->
7
+ <%= render 'dbwatcher/shared/stats_card',
8
+ label: 'Active Sessions',
9
+ value: @recent_sessions&.count || 0,
10
+ description: 'Last 24 hours',
11
+ icon_html: stats_icon(:sessions) %>
12
+
13
+ <!-- Tables Card -->
14
+ <%= render 'dbwatcher/shared/stats_card',
15
+ label: 'Modified Tables',
16
+ value: @active_tables&.count || 0,
17
+ description: 'With changes',
18
+ icon_html: stats_icon(:tables) %>
19
+
20
+ <!-- Queries Card -->
21
+ <%= render 'dbwatcher/shared/stats_card',
22
+ label: 'SQL Queries',
23
+ value: @query_stats&.dig(:total) || 0,
24
+ description: 'Today',
25
+ icon_html: stats_icon(:queries) %>
26
+
27
+ <!-- Performance Card -->
28
+ <% slow_queries = @query_stats&.dig(:slow_queries) || 0 %>
29
+ <%= render 'dbwatcher/shared/stats_card',
30
+ label: 'Slow Queries',
31
+ value: slow_queries,
32
+ value_class: (slow_queries > 0 ? 'text-red-600' : 'text-navy-dark'),
33
+ description: '> 100ms',
34
+ icon_html: stats_icon(:performance) %>
35
+ </div>
36
+
37
+ <!-- System Information Summary -->
38
+ <% if @system_info_summary && !@system_info_summary.empty? %>
39
+ <div class="grid grid-cols-4 gap-3 mb-4">
40
+ <!-- System Load Card -->
41
+ <%= render 'dbwatcher/shared/stats_card',
42
+ label: 'System Load',
43
+ value: @system_info_summary[:cpu_load] ? "#{@system_info_summary[:cpu_load]}" : 'N/A',
44
+ description: '1min avg',
45
+ icon_html: stats_icon(:system) %>
46
+
47
+ <!-- Memory Usage Card -->
48
+ <%= render 'dbwatcher/shared/stats_card',
49
+ label: 'Memory Usage',
50
+ value: @system_info_summary[:memory_usage] ? "#{@system_info_summary[:memory_usage]}%" : 'N/A',
51
+ value_class: (@system_info_summary[:memory_usage] && @system_info_summary[:memory_usage] > 80) ? 'text-red-600' : 'text-navy-dark',
52
+ description: 'of total',
53
+ icon_html: stats_icon(:memory) %>
54
+
55
+ <!-- DB Connections Card -->
56
+ <%= render 'dbwatcher/shared/stats_card',
57
+ label: 'DB Connections',
58
+ value: @system_info_summary[:active_connections] || 'N/A',
59
+ description: 'active',
60
+ icon_html: stats_icon(:database) %>
61
+
62
+ <!-- Runtime Info Card -->
63
+ <%= render 'dbwatcher/shared/stats_card',
64
+ label: 'Ruby/Rails',
65
+ value: @system_info_summary[:ruby_version] || 'N/A',
66
+ description: @system_info_summary[:rails_version] ? "Rails #{@system_info_summary[:rails_version]}" : 'Ruby only',
67
+ icon_html: stats_icon(:runtime) %>
68
+ </div>
69
+ <% end %>
70
+
71
+ <!-- Two Column Layout -->
72
+ <div class="grid grid-cols-2 gap-4">
73
+ <!-- Recent Sessions -->
74
+ <div class="border border-gray-200 rounded-lg shadow-sm">
75
+ <div class="bg-gradient-to-r from-blue-50 to-indigo-50 px-4 py-3 border-b border-gray-200 rounded-t-lg">
76
+ <div class="flex items-center">
77
+ <svg class="w-4 h-4 text-blue-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
79
+ </svg>
80
+ <h3 class="text-sm font-semibold text-gray-800">Recent Sessions</h3>
81
+ </div>
82
+ </div>
83
+ <div class="max-h-64 overflow-auto">
84
+ <% if @recent_sessions&.any? %>
85
+ <div class="divide-y divide-gray-100">
86
+ <% @recent_sessions.each_with_index do |session, index| %>
87
+ <div class="px-4 py-3 hover:bg-gray-50 transition-colors duration-150">
88
+ <div class="flex items-center justify-between">
89
+ <div class="flex-1 min-w-0">
90
+ <% session_id = session[:id] || session['id'] %>
91
+ <% session_name = (session[:name] || session['name']).to_s.gsub(/^HTTP \w+ /, '') %>
92
+ <%= link_to session_path(session_id), class: "block" do %>
93
+ <div class="flex items-center">
94
+ <div class="h-2 w-2 bg-blue-400 rounded-full mr-3"></div>
95
+ <p class="text-sm font-medium text-gray-900 truncate" title="<%= session_name %>">
96
+ <%= session_name %>
97
+ </p>
98
+ </div>
99
+ <% end %>
100
+ <div class="flex items-center mt-1">
101
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
102
+ <%= session[:change_count] || session['change_count'] || 0 %> changes
103
+ </span>
104
+ <span class="ml-2 text-xs text-gray-500">
105
+ <% started_at = session[:started_at] || session['started_at'] %>
106
+ <%= Time.parse(started_at).strftime("%H:%M:%S") rescue 'N/A' %>
107
+ </span>
108
+ </div>
109
+ </div>
110
+ <div class="ml-4 flex-shrink-0">
111
+ <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
113
+ </svg>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ <% end %>
118
+ </div>
119
+ <% else %>
120
+ <div class="p-8 text-center text-gray-500">
121
+ <svg class="w-8 h-8 mx-auto text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
122
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
123
+ </svg>
124
+ <p class="text-sm">No recent sessions</p>
125
+ </div>
126
+ <% end %>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Active Tables -->
131
+ <div class="border border-gray-200 rounded-lg shadow-sm">
132
+ <div class="bg-gradient-to-r from-green-50 to-emerald-50 px-4 py-3 border-b border-gray-200 rounded-t-lg">
133
+ <div class="flex items-center">
134
+ <svg class="w-4 h-4 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
135
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
136
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h2a2 2 0 012 2v2H8V5z"/>
137
+ </svg>
138
+ <h3 class="text-sm font-semibold text-gray-800">Most Active Tables</h3>
139
+ </div>
140
+ </div>
141
+ <div class="max-h-64 overflow-auto">
142
+ <% if @active_tables&.any? %>
143
+ <div class="divide-y divide-gray-100">
144
+ <% @active_tables.first(10).each_with_index do |(table_name, count), index| %>
145
+ <div class="px-4 py-3 hover:bg-gray-50 transition-colors duration-150">
146
+ <div class="flex items-center justify-between">
147
+ <div class="flex-1 min-w-0">
148
+ <%= link_to table_path(table_name), class: "block" do %>
149
+ <div class="flex items-center">
150
+ <div class="h-2 w-2 bg-green-400 rounded-full mr-3"></div>
151
+ <p class="text-sm font-medium text-gray-900 truncate" title="<%= table_name %>">
152
+ <%= table_name %>
153
+ </p>
154
+ </div>
155
+ <% end %>
156
+ <div class="flex items-center mt-1">
157
+ <div class="flex gap-1">
158
+ <span class="badge badge-insert text-xs" title="Inserts">I</span>
159
+ <span class="badge badge-update text-xs" title="Updates">U</span>
160
+ <span class="badge badge-delete text-xs" title="Deletes">D</span>
161
+ </div>
162
+ <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
163
+ <%= count %> changes
164
+ </span>
165
+ </div>
166
+ </div>
167
+ <div class="ml-4 flex-shrink-0">
168
+ <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
169
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
170
+ </svg>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ <% end %>
175
+ </div>
176
+ <% else %>
177
+ <div class="p-8 text-center text-gray-500">
178
+ <svg class="w-8 h-8 mx-auto text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
179
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"/>
180
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h2a2 2 0 012 2v2H8V5z"/>
181
+ </svg>
182
+ <p class="text-sm">No active tables</p>
183
+ </div>
184
+ <% end %>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
@@ -0,0 +1,22 @@
1
+ <div class="system-info-container">
2
+ <div class="flex items-center justify-between mb-4">
3
+ <h2 class="text-lg font-semibold text-gray-800">System Information</h2>
4
+ <div class="flex items-center gap-2">
5
+ <% if @system_info&.dig(:collected_at) %>
6
+ <span class="text-sm text-gray-500">
7
+ Last updated: <%= time_ago_in_words(Time.parse(@system_info[:collected_at])) %> ago
8
+ </span>
9
+ <% end %>
10
+ <button id="refresh-system-info" class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
11
+ Refresh
12
+ </button>
13
+ <button id="clear-cache-system-info" class="px-3 py-1 text-sm bg-red-600 text-white rounded hover:bg-red-700 transition-colors">
14
+ Clear Cache
15
+ </button>
16
+ </div>
17
+ </div>
18
+
19
+ <div id="system-info-content">
20
+ <%= render 'dbwatcher/dashboard/system_info_content' %>
21
+ </div>
22
+ </div>