dbwatcher 0.1.5 → 1.0.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/controllers/dbwatcher/base_controller.rb +95 -0
  4. data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
  5. data/app/controllers/dbwatcher/queries_controller.rb +24 -0
  6. data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
  7. data/app/controllers/dbwatcher/tables_controller.rb +38 -0
  8. data/app/helpers/dbwatcher/application_helper.rb +103 -0
  9. data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
  10. data/app/helpers/dbwatcher/session_helper.rb +27 -0
  11. data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
  12. data/app/views/dbwatcher/queries/index.html.erb +240 -0
  13. data/app/views/dbwatcher/sessions/index.html.erb +120 -27
  14. data/app/views/dbwatcher/sessions/show.html.erb +326 -129
  15. data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
  16. data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
  17. data/app/views/dbwatcher/shared/_header.html.erb +7 -0
  18. data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
  19. data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
  20. data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
  21. data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
  22. data/app/views/dbwatcher/tables/changes.html.erb +225 -0
  23. data/app/views/dbwatcher/tables/index.html.erb +123 -0
  24. data/app/views/dbwatcher/tables/show.html.erb +86 -0
  25. data/app/views/layouts/dbwatcher/application.html.erb +375 -26
  26. data/config/routes.rb +17 -3
  27. data/lib/dbwatcher/configuration.rb +9 -1
  28. data/lib/dbwatcher/engine.rb +12 -7
  29. data/lib/dbwatcher/logging.rb +72 -0
  30. data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -0
  31. data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
  32. data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
  33. data/lib/dbwatcher/sql_logger.rb +107 -0
  34. data/lib/dbwatcher/storage/api/base_api.rb +134 -0
  35. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +172 -0
  36. data/lib/dbwatcher/storage/api/query_api.rb +95 -0
  37. data/lib/dbwatcher/storage/api/session_api.rb +134 -0
  38. data/lib/dbwatcher/storage/api/table_api.rb +86 -0
  39. data/lib/dbwatcher/storage/base_storage.rb +113 -0
  40. data/lib/dbwatcher/storage/change_processor.rb +65 -0
  41. data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
  42. data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
  43. data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
  44. data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
  45. data/lib/dbwatcher/storage/date_helper.rb +21 -0
  46. data/lib/dbwatcher/storage/errors.rb +86 -0
  47. data/lib/dbwatcher/storage/file_manager.rb +122 -0
  48. data/lib/dbwatcher/storage/null_session.rb +39 -0
  49. data/lib/dbwatcher/storage/query_storage.rb +338 -0
  50. data/lib/dbwatcher/storage/query_validator.rb +24 -0
  51. data/lib/dbwatcher/storage/session.rb +58 -0
  52. data/lib/dbwatcher/storage/session_operations.rb +37 -0
  53. data/lib/dbwatcher/storage/session_query.rb +71 -0
  54. data/lib/dbwatcher/storage/session_storage.rb +322 -0
  55. data/lib/dbwatcher/storage/table_storage.rb +237 -0
  56. data/lib/dbwatcher/storage.rb +112 -85
  57. data/lib/dbwatcher/tracker.rb +4 -55
  58. data/lib/dbwatcher/version.rb +1 -1
  59. data/lib/dbwatcher.rb +12 -2
  60. metadata +47 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee5502341a37e588b1dcf38edd5f33fa9e0b3746f43efea7e419ec241e873150
4
- data.tar.gz: 874907249da682606c7f02df3e423ba3f0b6ef8c553ff6165561ab5cb7ad0981
3
+ metadata.gz: ea9aad773959e5c5e862792305df463112927844dd7d1473130b84c0d23583e8
4
+ data.tar.gz: 89bbc03d80d56b2a478657a9b3e24b9b6ce83439fd2342a19be4d2be4147d2e4
5
5
  SHA512:
6
- metadata.gz: 8b8d78baaa87bad96ac04968238b945d162c8acc0451c678803d7defdfa46ab66fbb9d7e96ab8bc11a4f09166713f995e641bea32090c2346ab335939faa7770
7
- data.tar.gz: 13dd481403397e95a74c04857718f878ebd35dc223ffa164dfaf871f6da4a9c0d02559555b25f5b575e5bab2cf265ce953902e400026647d3441c9bdd38a3e92
6
+ metadata.gz: 5b12cccc60ec47f58f6fb779106d38d0d915df0b49c06e10a35456abcd2c7e18874a1e5db9a0428a380a5169e62c425f4f18a092c85250858b52a237c687b9f2
7
+ data.tar.gz: a95e7b8a3674d232a99668d3f1c9051b79ffa97237412bd14563e0229761d8b51aaccb27e3ae7518e52a3375afc5c016b766b5453588a6ffc24f040ffced4254
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # DBWatcher 🔍
1
+ # DB Watcher 🔍
2
2
 
3
3
  Track and visualize database changes in your Rails application for easier debugging and development.
4
4
 
5
- DBWatcher is a powerful Rails gem that captures, stores, and visualizes all database operations in your application. Perfect for debugging complex data flows, understanding application behavior, and optimizing database performance.
5
+ DB Watcher is a powerful Rails gem that captures, stores, and visualizes all database operations in your application. Perfect for debugging complex data flows, understanding application behavior, and optimizing database performance.
6
6
 
7
7
  ## ✨ Features
8
8
 
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ # Base controller for all Dbwatcher controllers
5
+ # Provides common functionality and configuration for the entire engine
6
+ class BaseController < ActionController::Base
7
+ protect_from_forgery with: :exception
8
+ layout "dbwatcher/application"
9
+
10
+ before_action :set_current_time
11
+ before_action :log_request_info
12
+
13
+ # Common error handling
14
+ rescue_from StandardError, with: :handle_error
15
+
16
+ protected
17
+
18
+ # Set current time for consistent timestamp usage across views
19
+ def set_current_time
20
+ @current_time = Time.current
21
+ end
22
+
23
+ # Log request information for debugging
24
+ def log_request_info
25
+ return unless Rails.logger
26
+
27
+ Rails.logger.info "DBWatcher Request: #{request.method} #{request.path} - Controller: #{self.class.name}"
28
+ end
29
+
30
+ # Common error handling for all controllers
31
+ def handle_error(exception)
32
+ Rails.logger.error "DBWatcher Error in #{self.class.name}##{action_name}: #{exception.message}"
33
+ Rails.logger.error exception.backtrace.join("\n") if Rails.env.development?
34
+
35
+ respond_to do |format|
36
+ format.html do
37
+ flash[:error] = "An error occurred while processing your request."
38
+ redirect_to root_path
39
+ end
40
+ format.json do
41
+ render json: { error: "Internal server error" }, status: :internal_server_error
42
+ end
43
+ end
44
+ end
45
+
46
+ # Helper method for safely extracting data from hashes with symbol/string keys
47
+ def safe_extract(data, key)
48
+ return nil unless data.is_a?(Hash)
49
+
50
+ data[key] || data[key.to_s]
51
+ end
52
+
53
+ # Helper method for formatting timestamps consistently
54
+ def format_timestamp(timestamp_str)
55
+ return "N/A" unless timestamp_str
56
+
57
+ Time.parse(timestamp_str).strftime("%H:%M:%S")
58
+ rescue ArgumentError
59
+ "N/A"
60
+ end
61
+
62
+ # Make helper methods available to views
63
+ helper_method :format_timestamp, :safe_extract
64
+
65
+ # Helper method for rendering JSON responses with consistent structure
66
+ def render_json_response(data, status: :ok)
67
+ render json: {
68
+ status: status,
69
+ data: data,
70
+ timestamp: @current_time.iso8601
71
+ }, status: status
72
+ end
73
+
74
+ # Helper method for handling not found resources
75
+ def handle_not_found(resource_name, redirect_path)
76
+ Rails.logger.warn "#{self.class.name}##{action_name}: #{resource_name} not found for ID: #{params[:id]}"
77
+
78
+ respond_to do |format|
79
+ format.html do
80
+ redirect_to redirect_path, alert: "#{resource_name} not found"
81
+ end
82
+ format.json do
83
+ render json: { error: "#{resource_name} not found" }, status: :not_found
84
+ end
85
+ end
86
+ end
87
+
88
+ # Helper method for clearing storage with consistent messaging
89
+ def clear_storage_with_message(storage_method, resource_name, redirect_path)
90
+ cleared_count = storage_method.call
91
+ redirect_to redirect_path,
92
+ notice: "#{resource_name} cleared (#{cleared_count} files removed)"
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ class DashboardController < BaseController
5
+ def index
6
+ dashboard_data = Dbwatcher::Services::DashboardDataAggregator.call
7
+ @recent_sessions = dashboard_data[:recent_sessions]
8
+ @active_tables = dashboard_data[:active_tables]
9
+ @query_stats = dashboard_data[:query_stats]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ class QueriesController < BaseController
5
+ def index
6
+ @date = params[:date] || Date.current.strftime("%Y-%m-%d")
7
+ queries = Storage.queries.for_date(@date).all
8
+ @queries = Dbwatcher::Services::QueryFilterProcessor.call(queries, params)
9
+
10
+ respond_to do |format|
11
+ format.html
12
+ format.json { render json: @queries }
13
+ end
14
+ end
15
+
16
+ def clear
17
+ clear_storage_with_message(
18
+ -> { Storage.query_storage.clear_all },
19
+ "SQL query logs",
20
+ queries_path
21
+ )
22
+ end
23
+ end
24
+ end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dbwatcher
4
- class SessionsController < ActionController::Base
5
- protect_from_forgery with: :exception
6
- layout "dbwatcher/application"
7
-
4
+ class SessionsController < BaseController
8
5
  def index
9
- @sessions = Storage.all_sessions
6
+ @sessions = Storage.sessions.all
10
7
  end
11
8
 
12
9
  def show
13
- @session = Storage.load_session(params[:id])
10
+ Rails.logger.info "SessionsController#show: Loading session with ID: #{params[:id]}"
11
+ @session = Storage.sessions.find(params[:id])
12
+ Rails.logger.info "SessionsController#show: Loaded session: #{@session.inspect}"
13
+
14
+ return handle_not_found("Session", sessions_path) unless @session
15
+
16
+ @tables_summary = Storage.sessions.build_tables_summary(@session)
17
+ Rails.logger.info "SessionsController#show: Tables summary: #{@tables_summary.inspect}"
14
18
 
15
19
  respond_to do |format|
16
20
  format.html
@@ -18,21 +22,12 @@ module Dbwatcher
18
22
  end
19
23
  end
20
24
 
21
- def destroy_all
22
- Dbwatcher.reset!
23
- redirect_to root_path, notice: "All sessions cleared"
24
- end
25
-
26
- private
27
-
28
- # Helper method to safely get the sessions path
29
- def sessions_index_path
30
- if respond_to?(:sessions_path)
25
+ def clear
26
+ clear_storage_with_message(
27
+ -> { Storage.session_storage.clear_all },
28
+ "All sessions",
31
29
  sessions_path
32
- else
33
- "/dbwatcher"
34
- end
30
+ )
35
31
  end
36
- helper_method :sessions_index_path
37
32
  end
38
33
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ class TablesController < BaseController
5
+ def index
6
+ @tables = Dbwatcher::Services::TableStatisticsCollector.call
7
+ end
8
+
9
+ def show
10
+ @table_name = params[:id]
11
+ @changes = Storage.tables.changes_for(@table_name).all
12
+ @sessions = @changes.map { |c| c[:session_id] }.uniq
13
+
14
+ respond_to do |format|
15
+ format.html
16
+ format.json { render json: @changes }
17
+ end
18
+ end
19
+
20
+ def changes
21
+ @table_name = params[:id]
22
+ @changes = Storage.tables.changes_for(@table_name).all
23
+ @sessions = extract_session_ids(@changes)
24
+ @records = group_changes_by_record(@changes)
25
+ end
26
+
27
+ private
28
+
29
+ # These remain as they are simple data extraction helpers
30
+ def extract_session_ids(changes)
31
+ changes.map { |c| c[:session_id] }.uniq
32
+ end
33
+
34
+ def group_changes_by_record(changes)
35
+ changes.group_by { |c| c[:record_id] }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module ApplicationHelper
5
+ include FormattingHelper
6
+ include SessionHelper
7
+
8
+ # Common view helpers for templates
9
+
10
+ # Create icon HTML for stats cards
11
+ def stats_icon(type)
12
+ icon_html = icon_definitions[type.to_sym]
13
+ icon_html&.html_safe
14
+ end
15
+
16
+ private
17
+
18
+ # Icon definitions for stats cards
19
+ def icon_definitions
20
+ {
21
+ sessions: sessions_icon_svg,
22
+ tables: tables_icon_svg,
23
+ queries: queries_icon_svg,
24
+ performance: performance_icon_svg
25
+ }
26
+ end
27
+
28
+ def sessions_icon_svg
29
+ <<~SVG
30
+ <svg class="w-4 h-4 text-blue-medium" fill="currentColor" viewBox="0 0 20 20">
31
+ <path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
32
+ <path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
33
+ </svg>
34
+ SVG
35
+ end
36
+
37
+ def tables_icon_svg
38
+ <<~SVG
39
+ <svg class="w-4 h-4 text-gold-dark" fill="currentColor" viewBox="0 0 20 20">
40
+ <path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11 4a1 1 0 10-2 0v4a1 1 0 102 0V7zm-3 1a1 1 0 10-2 0v3a1 1 0 102 0V8zM8 9a1 1 0 00-2 0v2a1 1 0 102 0V9z" clip-rule="evenodd"/>
41
+ </svg>
42
+ SVG
43
+ end
44
+
45
+ def queries_icon_svg
46
+ <<~SVG
47
+ <svg class="w-4 h-4 text-blue-light" fill="currentColor" viewBox="0 0 20 20">
48
+ <path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/>
49
+ </svg>
50
+ SVG
51
+ end
52
+
53
+ def performance_icon_svg
54
+ <<~SVG
55
+ <svg class="w-4 h-4 text-gold-light" fill="currentColor" viewBox="0 0 20 20">
56
+ <path fill-rule="evenodd"
57
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
58
+ clip-rule="evenodd"/>
59
+ </svg>
60
+ SVG
61
+ end
62
+
63
+ public
64
+
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
74
+ end
75
+
76
+ # Create action buttons with consistent styling
77
+ def action_button(text, path, color: "blue")
78
+ link_to text, path, class: "px-3 py-1 text-xs bg-#{color}-600 text-white rounded hover:bg-#{color}-700"
79
+ end
80
+
81
+ # Safe value extraction with fallback
82
+ def safe_value(data, key, fallback = "N/A")
83
+ return fallback unless data.is_a?(Hash)
84
+
85
+ data[key] || data[key.to_s] || fallback
86
+ end
87
+
88
+ # Format count with proper fallback
89
+ def format_count(value)
90
+ value.to_i.to_s
91
+ end
92
+
93
+ # Create empty state message
94
+ def empty_state(message, icon: nil)
95
+ content_tag(:div, class: "text-center py-8 text-gray-500") do
96
+ content = []
97
+ content << icon if icon
98
+ content << content_tag(:p, message, class: "text-xs")
99
+ content.join.html_safe
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module FormattingHelper
5
+ # Truncate cell values for display in compact view
6
+ def truncate_cell_value(value, max_length = 50)
7
+ return "" if value.nil?
8
+
9
+ formatted_value = format_cell_value_simple(value)
10
+
11
+ if formatted_value.length > max_length
12
+ "#{formatted_value[0...max_length]}..."
13
+ else
14
+ formatted_value
15
+ end
16
+ end
17
+
18
+ # Format cell values for display
19
+ def format_cell_value(value)
20
+ return "" if value.nil?
21
+
22
+ case value
23
+ when String
24
+ format_string_value(value)
25
+ when Hash, Array
26
+ JSON.pretty_generate(value)
27
+ when Time, DateTime
28
+ format_datetime_value(value)
29
+ when Date
30
+ value.strftime("%Y-%m-%d")
31
+ else
32
+ value.to_s
33
+ end
34
+ end
35
+
36
+ # Simple formatting for truncated display
37
+ def format_cell_value_simple(value)
38
+ return "" if value.nil?
39
+
40
+ case value
41
+ when String
42
+ format_string_value_simple(value)
43
+ when Hash
44
+ format_hash_simple(value)
45
+ when Array
46
+ format_array_simple(value)
47
+ when Time, DateTime
48
+ format_datetime_value(value)
49
+ when Date
50
+ value.strftime("%Y-%m-%d")
51
+ else
52
+ value.to_s
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Format string values with JSON detection
59
+ def format_string_value(value)
60
+ return value unless json?(value)
61
+
62
+ begin
63
+ JSON.pretty_generate(JSON.parse(value))
64
+ rescue JSON::ParserError
65
+ value
66
+ end
67
+ end
68
+
69
+ def format_string_value_simple(value)
70
+ return value unless json?(value)
71
+
72
+ begin
73
+ parsed = JSON.parse(value)
74
+ format_parsed_json_simple(parsed)
75
+ rescue JSON::ParserError
76
+ value
77
+ end
78
+ end
79
+
80
+ def format_parsed_json_simple(parsed)
81
+ if parsed.is_a?(Array)
82
+ format_array_simple(parsed)
83
+ elsif parsed.is_a?(Hash)
84
+ format_hash_simple(parsed)
85
+ else
86
+ parsed.to_s
87
+ end
88
+ end
89
+
90
+ def format_array_simple(array)
91
+ "[#{array.length} items]"
92
+ end
93
+
94
+ def format_hash_simple(hash)
95
+ "{#{hash.keys.length} keys}"
96
+ end
97
+
98
+ def format_datetime_value(value)
99
+ value.strftime("%Y-%m-%d %H:%M:%S")
100
+ end
101
+
102
+ def json?(string)
103
+ return false unless string.is_a?(String)
104
+
105
+ string.strip.start_with?("{", "[")
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module SessionHelper
5
+ # Get session change count with fallback
6
+ def session_change_count(session)
7
+ safe_value(session, :change_count, 0).to_i
8
+ end
9
+
10
+ # Determine if session is active
11
+ def session_active?(session)
12
+ safe_value(session, :ended_at).blank?
13
+ end
14
+
15
+ # Format session name for display
16
+ def display_session_name(name)
17
+ name.to_s.gsub(/^HTTP \w+ /, "")
18
+ end
19
+
20
+ # Generate session ID display (truncated)
21
+ def display_session_id(id)
22
+ return "N/A" unless id
23
+
24
+ "#{id[0..7]}..."
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,177 @@
1
+ <%# Dashboard Overview Page %>
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
+ <div class="tab-item active">Overview</div>
14
+ <div class="tab-item">Statistics</div>
15
+ <div class="tab-item">Recent Activity</div>
16
+ </div>
17
+
18
+ <!-- Content Area -->
19
+ <div class="flex-1 overflow-auto p-4">
20
+ <!-- Compact Stats Grid -->
21
+ <div class="grid grid-cols-4 gap-3 mb-4">
22
+ <!-- Sessions Card -->
23
+ <%= render 'dbwatcher/shared/stats_card',
24
+ label: 'Active Sessions',
25
+ value: @recent_sessions&.count || 0,
26
+ description: 'Last 24 hours',
27
+ icon_html: stats_icon(:sessions) %>
28
+
29
+ <!-- Tables Card -->
30
+ <%= render 'dbwatcher/shared/stats_card',
31
+ label: 'Modified Tables',
32
+ value: @active_tables&.count || 0,
33
+ description: 'With changes',
34
+ icon_html: stats_icon(:tables) %>
35
+
36
+ <!-- Queries Card -->
37
+ <%= render 'dbwatcher/shared/stats_card',
38
+ label: 'SQL Queries',
39
+ value: @query_stats&.dig(:total) || 0,
40
+ description: 'Today',
41
+ icon_html: stats_icon(:queries) %>
42
+
43
+ <!-- Performance Card -->
44
+ <% slow_queries = @query_stats&.dig(:slow_queries) || 0 %>
45
+ <%= render 'dbwatcher/shared/stats_card',
46
+ label: 'Slow Queries',
47
+ value: slow_queries,
48
+ value_class: (slow_queries > 0 ? 'text-red-600' : 'text-navy-dark'),
49
+ description: '> 100ms',
50
+ icon_html: stats_icon(:performance) %>
51
+ </div>
52
+
53
+ <!-- Two Column Layout -->
54
+ <div class="grid grid-cols-2 gap-4">
55
+ <!-- Recent Sessions -->
56
+ <div class="border border-gray-300 rounded">
57
+ <div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
58
+ <h3 class="text-xs font-medium text-gray-700">Recent Sessions</h3>
59
+ </div>
60
+ <div class="max-h-64 overflow-auto">
61
+ <% if @recent_sessions&.any? %>
62
+ <table class="compact-table w-full">
63
+ <thead>
64
+ <tr>
65
+ <th class="text-left">Session</th>
66
+ <th class="text-center">Changes</th>
67
+ <th class="text-right">Time</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ <% @recent_sessions.each do |session| %>
72
+ <tr class="hover:bg-blue-50">
73
+ <td class="truncate max-w-xs" title="<%= session[:name] || session['name'] %>">
74
+ <% session_id = session[:id] || session['id'] %>
75
+ <% session_name = (session[:name] || session['name']).to_s.gsub(/^HTTP \w+ /, '') %>
76
+ <%= link_to session_name, session_path(session_id), class: "text-navy-dark hover:text-blue-medium" %>
77
+ </td>
78
+ <td class="text-center">
79
+ <span class="badge bg-gray-600 text-white">
80
+ <%= session[:change_count] || session['change_count'] || 0 %>
81
+ </span>
82
+ </td>
83
+ <td class="text-right text-gray-500">
84
+ <% started_at = session[:started_at] || session['started_at'] %>
85
+ <%= Time.parse(started_at).strftime("%H:%M:%S") rescue 'N/A' %>
86
+ </td>
87
+ </tr>
88
+ <% end %>
89
+ </tbody>
90
+ </table>
91
+ <% else %>
92
+ <div class="p-4 text-center text-gray-500 text-xs">No recent sessions</div>
93
+ <% end %>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Active Tables -->
98
+ <div class="border border-gray-300 rounded">
99
+ <div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
100
+ <h3 class="text-xs font-medium text-gray-700">Most Active Tables</h3>
101
+ </div>
102
+ <div class="max-h-64 overflow-auto">
103
+ <% if @active_tables&.any? %>
104
+ <table class="compact-table w-full">
105
+ <thead>
106
+ <tr>
107
+ <th class="text-left">Table</th>
108
+ <th class="text-center">Operations</th>
109
+ <th class="text-right">Changes</th>
110
+ </tr>
111
+ </thead>
112
+ <tbody>
113
+ <% @active_tables.first(10).each do |table_name, count| %>
114
+ <tr class="hover:bg-blue-50">
115
+ <td class="font-medium text-navy-dark">
116
+ <%= link_to table_name, table_path(table_name), class: "text-navy-dark hover:text-blue-medium" %>
117
+ </td>
118
+ <td class="text-center">
119
+ <div class="flex gap-1 justify-center">
120
+ <span class="badge badge-insert" title="Inserts">I</span>
121
+ <span class="badge badge-update" title="Updates">U</span>
122
+ <span class="badge badge-delete" title="Deletes">D</span>
123
+ </div>
124
+ </td>
125
+ <td class="text-right">
126
+ <span class="text-sm font-medium"><%= count %></span>
127
+ </td>
128
+ </tr>
129
+ <% end %>
130
+ </tbody>
131
+ </table>
132
+ <% else %>
133
+ <div class="p-4 text-center text-gray-500 text-xs">No active tables</div>
134
+ <% end %>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Query Activity Section -->
140
+ <div class="mt-4 border border-gray-300 rounded">
141
+ <div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
142
+ <h3 class="text-xs font-medium text-gray-700">Query Activity</h3>
143
+ </div>
144
+ <div class="p-4">
145
+ <% if @query_stats&.dig(:by_operation)&.any? %>
146
+ <div class="grid grid-cols-4 gap-4 text-center">
147
+ <% @query_stats[:by_operation].each do |operation, count| %>
148
+ <div class="border border-gray-200 rounded p-2">
149
+ <div class="text-lg font-bold text-navy-dark"><%= count %></div>
150
+ <div class="text-xs text-gray-500 uppercase"><%= operation %></div>
151
+ </div>
152
+ <% end %>
153
+ </div>
154
+ <% else %>
155
+ <div class="text-center text-gray-500 text-xs">No query activity</div>
156
+ <% end %>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Quick Actions -->
161
+ <div class="mt-4 border border-gray-300 rounded">
162
+ <div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
163
+ <h3 class="text-xs font-medium text-gray-700">Quick Actions</h3>
164
+ </div>
165
+ <div class="p-3">
166
+ <div class="flex gap-2">
167
+ <%= link_to "View All Sessions", sessions_path,
168
+ class: "px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700" %>
169
+ <%= link_to "Browse Tables", tables_path,
170
+ class: "px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700" %>
171
+ <%= link_to "SQL Logs", queries_path,
172
+ class: "px-3 py-1 text-xs bg-purple-600 text-white rounded hover:bg-purple-700" %>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>