dbwatcher 1.1.1 → 1.1.3

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/app/assets/config/dbwatcher_manifest.js +1 -0
  4. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
  5. data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
  6. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  7. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  8. data/app/assets/stylesheets/dbwatcher/application.css +691 -41
  9. data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
  10. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
  11. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
  12. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
  13. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
  14. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
  15. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  16. data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
  17. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
  18. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
  19. data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
  20. data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
  21. data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
  22. data/app/helpers/dbwatcher/application_helper.rb +43 -11
  23. data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
  24. data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
  25. data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
  26. data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
  27. data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
  28. data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
  29. data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
  30. data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
  31. data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
  32. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  33. data/app/views/dbwatcher/sessions/index.html.erb +107 -87
  34. data/app/views/dbwatcher/sessions/show.html.erb +12 -4
  35. data/app/views/dbwatcher/tables/index.html.erb +32 -40
  36. data/app/views/layouts/dbwatcher/application.html.erb +101 -48
  37. data/config/routes.rb +25 -7
  38. data/lib/dbwatcher/configuration.rb +18 -1
  39. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  40. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  41. data/lib/dbwatcher/services/base_service.rb +2 -0
  42. data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
  43. data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
  44. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
  45. data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
  46. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  47. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  48. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  49. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  50. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  51. data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
  52. data/lib/dbwatcher/storage/session.rb +5 -0
  53. data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
  54. data/lib/dbwatcher/storage.rb +12 -0
  55. data/lib/dbwatcher/version.rb +1 -1
  56. data/lib/dbwatcher.rb +16 -2
  57. metadata +28 -16
  58. data/app/helpers/dbwatcher/component_helper.rb +0 -29
  59. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
  60. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
  61. data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
  62. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
  63. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
  64. data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
  65. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
  66. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
  67. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
  68. data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
  69. /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
@@ -0,0 +1,170 @@
1
+ <!-- Changes Content - Hybrid Tabulator Implementation -->
2
+ <div class="h-full"
3
+ x-data="changesTableHybrid({ sessionId: '<%= @session.id %>' })">
4
+
5
+ <!-- Loading State -->
6
+ <div x-show="loading" class="flex items-center justify-center h-64">
7
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
8
+ <span class="ml-2 text-gray-600">Loading changes...</span>
9
+ </div>
10
+
11
+ <!-- Error State -->
12
+ <div x-show="error" class="p-4 bg-red-50 border border-red-200 rounded">
13
+ <p class="text-red-700" x-text="error"></p>
14
+ <button @click="loadChangesData()" class="mt-2 text-red-600 underline">Retry</button>
15
+ </div>
16
+
17
+ <!-- No Data State -->
18
+ <div x-show="!loading && !error && Object.keys(tableData).length === 0"
19
+ class="flex flex-col items-center justify-center h-64">
20
+ <svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
22
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
23
+ </svg>
24
+ <p class="mt-2 text-gray-500">No changes data available</p>
25
+ </div>
26
+
27
+ <!-- Multiple Tables - Enhanced UI Structure -->
28
+ <template x-if="!loading && !error && Object.keys(tableData).length > 0">
29
+ <div class="h-full flex flex-col bg-gray-50">
30
+ <!-- Filter Header -->
31
+ <div class="p-3 border-b border-gray-300 bg-gray-50">
32
+ <div class="flex items-center justify-between gap-4">
33
+ <h3 class="text-sm font-medium text-gray-900">Tables View</h3>
34
+ <div class="flex items-center gap-3 text-xs">
35
+ <!-- Search Filter -->
36
+ <input type="text"
37
+ x-model="filters.search"
38
+ @input="applyFilters()"
39
+ placeholder="Search..."
40
+ class="px-2 py-1 border border-gray-300 rounded text-xs w-32 focus:outline-none focus:ring-1 focus:ring-blue-medium">
41
+
42
+ <!-- Operation Filter -->
43
+ <select x-model="filters.operation"
44
+ @change="applyFilters()"
45
+ class="px-2 py-1 border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-medium">
46
+ <option value="">All Operations</option>
47
+ <template x-for="operation in getAvailableOperations()" :key="operation">
48
+ <option :value="operation" x-text="operation"></option>
49
+ </template>
50
+ </select>
51
+
52
+ <!-- Multi-Table Filter -->
53
+ <div class="relative" x-data="{ showTableFilter: false }">
54
+ <button @click="showTableFilter = !showTableFilter"
55
+ class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50 flex items-center gap-1">
56
+ <span x-text="filters.selectedTables.length === 0 ? 'All Tables' : `${filters.selectedTables.length} Tables`"></span>
57
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
59
+ </svg>
60
+ </button>
61
+
62
+ <div x-show="showTableFilter"
63
+ x-transition
64
+ @click.away="showTableFilter = false"
65
+ class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-50 min-w-48 max-h-48 overflow-auto">
66
+ <div class="p-2 border-b border-gray-200 flex gap-2">
67
+ <button @click="selectAllTables(); showTableFilter = false"
68
+ class="text-xs text-blue-medium hover:text-navy-dark underline">
69
+ All
70
+ </button>
71
+ <button @click="clearTableFilters()"
72
+ class="text-xs text-blue-medium hover:text-navy-dark underline">
73
+ None
74
+ </button>
75
+ </div>
76
+ <div class="p-1">
77
+ <template x-for="tableName in getAvailableTables()" :key="tableName">
78
+ <label class="flex items-center gap-2 p-1 rounded hover:bg-gray-100 cursor-pointer">
79
+ <input type="checkbox"
80
+ :value="tableName"
81
+ x-model="filters.selectedTables"
82
+ @change="applyFilters()"
83
+ class="form-checkbox h-3 w-3 text-blue-medium">
84
+ <span class="text-xs text-gray-700" x-text="tableName"></span>
85
+ </label>
86
+ </template>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Clear Filters -->
92
+ <button @click="clearAllFilters()"
93
+ class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50">
94
+ Clear
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Tables Content -->
101
+ <div class="flex-1 overflow-auto p-2">
102
+ <template x-for="[tableName, tableInfo] in Object.entries(tableData)" :key="tableName">
103
+ <div x-show="filters.selectedTables.length === 0 || filters.selectedTables.includes(tableName)"
104
+ class="mb-4 bg-white border border-gray-200 rounded shadow-sm" x-data="{ expanded: true }">
105
+ <!-- Table Header with Column Controls -->
106
+ <div class="bg-gray-100 px-3 py-2 flex items-center cursor-pointer border-b border-gray-200"
107
+ @click="expanded = !expanded"
108
+ :id="`table-${tableName}`">
109
+ <svg class="w-4 h-4 mr-2 transition-transform text-gray-600"
110
+ :class="{ 'rotate-90': expanded }"
111
+ fill="currentColor" viewBox="0 0 20 20">
112
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
113
+ </svg>
114
+ <div class="flex-1">
115
+ <h3 class="text-sm font-medium text-gray-900" x-text="tableName"></h3>
116
+ <div class="text-xs text-gray-500 mt-1" x-show="tableInfo.model_class">
117
+ <span x-text="tableInfo.model_class"></span>
118
+ </div>
119
+ </div>
120
+ <div class="flex gap-2 mr-4">
121
+ <template x-for="[op, count] in Object.entries(tableInfo.operations || {})" :key="op">
122
+ <span x-show="count > 0" class="badge changes-table-badge" :class="`badge-${op.toLowerCase()}`" x-text="count"></span>
123
+ </template>
124
+ </div>
125
+
126
+ <!-- Column Visibility Button -->
127
+ <button @click.stop="toggleColumnSelector(tableName)"
128
+ class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50 relative flex items-center">
129
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path>
131
+ </svg>
132
+ Columns
133
+ </button>
134
+ </div>
135
+
136
+ <!-- Column Selector Dropdown -->
137
+ <div x-show="showColumnSelector === tableName"
138
+ x-transition
139
+ @click.away="showColumnSelector = null"
140
+ class="absolute z-50 bg-white border border-gray-300 rounded shadow-lg p-3 max-h-64 overflow-auto column-selector">
141
+ <div class="text-xs font-medium mb-2">Select Visible Columns:</div>
142
+ <div class="space-y-1 min-w-48">
143
+ <template x-for="column in tableInfo.columns" :key="column">
144
+ <label class="flex items-center text-xs hover:bg-gray-50 p-1 rounded">
145
+ <input type="checkbox"
146
+ :checked="isColumnVisible(tableName, column)"
147
+ @change="toggleColumnVisibility(tableName, column)"
148
+ class="mr-2">
149
+ <span class="flex-1" x-text="column"></span>
150
+ </label>
151
+ </template>
152
+ </div>
153
+ <div class="mt-2 pt-2 border-t border-gray-200 flex gap-1">
154
+ <button @click="selectAllColumns(tableName)"
155
+ class="text-xs bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700">All</button>
156
+ <button @click="selectNoneColumns(tableName)"
157
+ class="text-xs bg-gray-600 text-white px-2 py-1 rounded hover:bg-gray-700">None</button>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- Tabulator Container for This Table -->
162
+ <div x-show="expanded" x-collapse>
163
+ <div :id="`changes-table-${tableName}`" class="table-container"></div>
164
+ </div>
165
+ </div>
166
+ </template>
167
+ </div>
168
+ </div>
169
+ </template>
170
+ </div>
@@ -0,0 +1,260 @@
1
+ <%# Timeline View - Simple chronological list of changes %>
2
+ <div class="h-full"
3
+ x-data="DBWatcher.getComponent('timeline', { sessionId: '<%= @session.id %>' })"
4
+ x-init="init()">
5
+
6
+ <!-- Timeline Header -->
7
+ <div class="p-3 border-b border-gray-300 bg-gray-50">
8
+ <div class="flex items-center justify-between">
9
+ <h3 class="text-sm font-medium text-gray-900">Timeline View</h3>
10
+ <div class="flex items-center gap-3 text-xs">
11
+ <template x-if="!loading && filteredData.length > 0">
12
+ <span class="text-gray-600">
13
+ <span x-text="filteredData.length"></span> operations
14
+ <template x-if="filteredData.length !== timelineData.length">
15
+ <span x-text="`of ${timelineData.length} total`"></span>
16
+ </template>
17
+ </span>
18
+ </template>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <!-- Loading State -->
24
+ <div x-show="loading" class="flex items-center justify-center h-64">
25
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-medium"></div>
26
+ <span class="ml-2 text-gray-600">Loading timeline...</span>
27
+ </div>
28
+
29
+ <!-- Error State -->
30
+ <div x-show="error" class="p-4 bg-red-50 border border-red-200 rounded m-4">
31
+ <p class="text-red-700" x-text="error"></p>
32
+ <button @click="loadTimelineData()" class="mt-2 text-red-600 hover:text-red-800 underline">Retry</button>
33
+ </div>
34
+
35
+ <!-- Timeline Content -->
36
+ <div x-show="!loading && !error" class="h-full flex">
37
+ <!-- Sidebar Filters -->
38
+ <div class="w-64 bg-gray-50 border-r border-gray-200 p-3 overflow-auto">
39
+ <!-- Table Filter -->
40
+ <div class="mb-4">
41
+ <h4 class="text-sm font-medium text-gray-900 mb-2">Filter by Tables</h4>
42
+
43
+ <!-- Select All / Clear All -->
44
+ <div class="mb-2 flex gap-2">
45
+ <button @click="filters.tables = getAvailableTables(); applyFilters()"
46
+ class="text-xs text-blue-medium hover:text-navy-dark underline"
47
+ :disabled="loading">
48
+ Select All
49
+ </button>
50
+ <button @click="clearFilters()"
51
+ class="text-xs text-blue-medium hover:text-navy-dark underline"
52
+ :disabled="loading">
53
+ Clear All
54
+ </button>
55
+ </div>
56
+
57
+ <!-- Search tables -->
58
+ <input type="text"
59
+ x-model="tableSearch"
60
+ placeholder="Search tables..."
61
+ class="w-full px-2 py-1 mb-2 border border-gray-300 rounded text-xs">
62
+
63
+ <!-- Tables list with max height and scrolling -->
64
+ <div class="max-h-32 overflow-y-auto space-y-1 border border-gray-200 rounded p-2 bg-white">
65
+ <template x-for="table in getAvailableTables().filter(t => !tableSearch || t.toLowerCase().includes(tableSearch.toLowerCase()))" :key="table">
66
+ <label class="flex items-center gap-2 p-1 rounded hover:bg-gray-100 cursor-pointer">
67
+ <input type="checkbox"
68
+ :value="table"
69
+ x-model="filters.tables"
70
+ @change="applyFilters()"
71
+ class="form-checkbox h-3 w-3 text-blue-medium">
72
+ <span class="text-xs text-gray-700" x-text="table"></span>
73
+ </label>
74
+ </template>
75
+ </div>
76
+
77
+ <!-- Selected count -->
78
+ <div class="mt-2 text-xs text-gray-500" x-show="filters.tables.length > 0">
79
+ <span x-text="filters.tables.length"></span> table(s) selected
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Search -->
84
+ <div class="mb-4">
85
+ <h4 class="text-sm font-medium text-gray-900 mb-2">Search</h4>
86
+ <input type="text"
87
+ x-model="filters.searchText"
88
+ @input="applyFilters()"
89
+ placeholder="Table, operation, or record ID"
90
+ class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
91
+ </div>
92
+
93
+ <!-- Analytics Summary -->
94
+ <template x-if="!loading && timelineData.length > 0">
95
+ <div class="bg-white border border-gray-200 rounded p-3">
96
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Session Analytics</h4>
97
+ <div class="space-y-2 text-xs">
98
+ <div class="flex justify-between">
99
+ <span class="text-gray-600">Operations:</span>
100
+ <span class="font-medium" x-text="`${filteredData.length}/${timelineData.length}`"></span>
101
+ </div>
102
+ <div class="flex justify-between">
103
+ <span class="text-gray-600">Tables:</span>
104
+ <span class="font-medium" x-text="getAvailableTables().length"></span>
105
+ </div>
106
+ <div class="flex justify-between">
107
+ <span class="text-gray-600">Duration:</span>
108
+ <span class="font-medium" x-text="metadata.session_duration || 'N/A'"></span>
109
+ </div>
110
+
111
+ <!-- Operation Counts -->
112
+ <div class="mt-3 space-y-1">
113
+ <template x-for="[operation, count] in Object.entries(metadata.operation_counts || {})" :key="operation">
114
+ <div class="flex justify-between items-center">
115
+ <span class="badge badge-sm" :class="`badge-${operation.toLowerCase()}`" x-text="operation"></span>
116
+ <span class="text-xs font-medium" x-text="count"></span>
117
+ </div>
118
+ </template>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+ </div>
124
+
125
+ <!-- Main Timeline Content -->
126
+ <div class="flex-1 overflow-auto">
127
+ <!-- Empty State -->
128
+ <div x-show="filteredData.length === 0" class="flex flex-col items-center justify-center h-64">
129
+ <svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
+ <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"/>
131
+ </svg>
132
+ <p class="mt-2 text-gray-500">No operations found for the current filters</p>
133
+ <button @click="clearFilters()" class="mt-2 text-blue-medium hover:text-navy-dark underline">Clear filters</button>
134
+ </div>
135
+
136
+ <!-- Operations List -->
137
+ <template x-if="filteredData.length > 0">
138
+ <div class="divide-y divide-gray-200">
139
+ <template x-for="operation in filteredData" :key="operation.id">
140
+ <div class="transition-colors" x-data="{ expanded: false }">
141
+ <!-- Main Operation Row (Clickable) -->
142
+ <div class="p-4 hover:bg-gray-50 cursor-pointer" @click="expanded = !expanded">
143
+ <div class="flex items-start justify-between">
144
+ <!-- Operation Info -->
145
+ <div class="flex items-start gap-3 flex-1">
146
+ <div class="flex-1 min-w-0">
147
+ <div class="flex items-center gap-2 mb-1 flex-wrap">
148
+ <span class="badge badge-sm flex-shrink-0" :class="`badge-${operation.operation.toLowerCase()}`" x-text="operation.operation"></span>
149
+ <span class="text-sm font-medium text-gray-900 truncate" x-text="operation.table_name"></span>
150
+ <span x-show="operation.record_id" class="text-xs text-gray-500 flex-shrink-0">
151
+ ID: <span x-text="operation.record_id"></span>
152
+ </span>
153
+ </div>
154
+ <div class="text-xs text-gray-500">
155
+ <span x-text="formatTimestamp(operation.timestamp)"></span>
156
+ <span class="ml-2">•</span>
157
+ <span class="ml-2" x-text="formatRelativeTime(operation)"></span>
158
+ </div>
159
+
160
+ <!-- Changes Preview (if any) -->
161
+ <template x-if="operation.changes && Object.keys(operation.changes).length > 0 && !expanded">
162
+ <div class="mt-2 text-xs">
163
+ <span class="text-gray-400">Changes:</span>
164
+ <span class="text-gray-600" x-text="Object.keys(operation.changes).slice(0, 3).join(', ') + (Object.keys(operation.changes).length > 3 ? '...' : '')"></span>
165
+ </div>
166
+ </template>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Expand/Collapse Icon and Sequence -->
171
+ <div class="flex items-center gap-2 ml-4 flex-shrink-0">
172
+ <div class="text-xs text-gray-400">
173
+ #<span x-text="operation.sequence + 1"></span>
174
+ </div>
175
+ <svg class="w-4 h-4 text-gray-400 transition-transform" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
176
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
177
+ </svg>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Expanded Details -->
183
+ <div x-show="expanded" x-collapse class="bg-gray-50 border-t border-gray-200">
184
+ <div class="p-4 pl-10">
185
+ <!-- Operation Details -->
186
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm mb-4">
187
+ <div>
188
+ <span class="font-medium text-gray-700">Table:</span>
189
+ <span class="ml-2 text-gray-900" x-text="operation.table_name"></span>
190
+ </div>
191
+ <div>
192
+ <span class="font-medium text-gray-700">Operation:</span>
193
+ <span class="ml-2 badge badge-sm" :class="`badge-${operation.operation.toLowerCase()}`" x-text="operation.operation"></span>
194
+ </div>
195
+ <div x-show="operation.record_id">
196
+ <span class="font-medium text-gray-700">Record ID:</span>
197
+ <span class="ml-2 text-gray-900" x-text="operation.record_id"></span>
198
+ </div>
199
+ <div>
200
+ <span class="font-medium text-gray-700">Timestamp:</span>
201
+ <span class="ml-2 text-gray-900" x-text="formatTimestamp(operation.timestamp)"></span>
202
+ </div>
203
+ <div>
204
+ <span class="font-medium text-gray-700">Relative Time:</span>
205
+ <span class="ml-2 text-gray-900" x-text="formatRelativeTime(operation)"></span>
206
+ </div>
207
+ <div>
208
+ <span class="font-medium text-gray-700">Sequence:</span>
209
+ <span class="ml-2 text-gray-900" x-text="operation.sequence + 1"></span>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Changes Details -->
214
+ <template x-if="operation.changes && Object.keys(operation.changes).length > 0">
215
+ <div class="mt-4">
216
+ <h5 class="font-medium text-gray-700 mb-2">Changes:</h5>
217
+ <div class="bg-white border border-gray-200 rounded p-3 text-xs overflow-auto max-h-32">
218
+ <template x-for="[field, change] in Object.entries(operation.changes)" :key="field">
219
+ <div class="mb-2 last:mb-0">
220
+ <span class="font-medium text-gray-700" x-text="field"></span>:
221
+ <template x-if="change.from !== undefined">
222
+ <span>
223
+ <span class="text-red-600" x-text="change.from"></span>
224
+
225
+ <span class="text-green-600" x-text="change.to"></span>
226
+ </span>
227
+ </template>
228
+ <template x-if="change.from === undefined">
229
+ <span class="text-green-600" x-text="change.to || change"></span>
230
+ </template>
231
+ </div>
232
+ </template>
233
+ </div>
234
+ </div>
235
+ </template>
236
+
237
+ <!-- Metadata -->
238
+ <template x-if="operation.metadata && Object.keys(operation.metadata).length > 0">
239
+ <div class="mt-4">
240
+ <h5 class="font-medium text-gray-700 mb-2">Metadata:</h5>
241
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
242
+ <template x-for="[key, value] in Object.entries(operation.metadata).filter(([k, v]) => v !== null && v !== undefined)" :key="key">
243
+ <div>
244
+ <span class="font-medium text-gray-600 capitalize" x-text="key.replace('_', ' ')"></span>:
245
+ <span class="ml-1 text-gray-900" x-text="value"></span>
246
+ </div>
247
+ </template>
248
+ </div>
249
+ </div>
250
+ </template>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </template>
255
+ </div>
256
+ </template>
257
+ </div>
258
+ </div>
259
+
260
+ </div>
@@ -1,31 +1,30 @@
1
1
  <%# Sessions Index Page %>
2
- <div class="h-full flex flex-col">
2
+ <div class="h-full flex flex-col" x-data="{
3
+ filterText: '',
4
+ filterSessions(session, filterText) {
5
+ if (!filterText) return true;
6
+ const searchText = filterText.toLowerCase();
7
+ const sessionId = session.querySelector('td:first-child').textContent.toLowerCase();
8
+ const sessionName = session.querySelector('td:nth-child(2)').textContent.toLowerCase();
9
+ return sessionId.includes(searchText) || sessionName.includes(searchText);
10
+ }
11
+ }">
3
12
  <%= render 'dbwatcher/shared/header', title: 'Tracking Sessions', subtitle: "#{@sessions.count} sessions" %>
4
13
 
5
14
  <%= render 'dbwatcher/shared/tab_bar', tabs: [
6
- { name: 'All Sessions', active: true },
7
- { name: 'Active', active: false },
8
- { name: 'Recent', active: false }
15
+ { name: 'All Sessions', active: true }
9
16
  ] %>
10
17
 
11
18
  <!-- Toolbar -->
12
19
  <div class="h-8 bg-gray-100 border-b border-gray-300 flex items-center px-4 gap-2">
13
20
  <input type="text" placeholder="Filter sessions..."
14
- class="compact-input flex-1 max-w-xs">
15
- <select class="compact-select">
16
- <option>Last 24 hours</option>
17
- <option>Last week</option>
18
- <option>All time</option>
19
- </select>
21
+ class="compact-input flex-1 max-w-xs"
22
+ x-model="filterText"
23
+ @input="document.querySelectorAll('.session-row').forEach(row => {
24
+ row.classList.toggle('hidden', !filterSessions(row, filterText));
25
+ })">
20
26
 
21
27
  <div class="ml-auto flex items-center gap-2">
22
- <button class="compact-button bg-blue-medium text-white hover:bg-blue-700">
23
- <svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
24
- <path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
25
- </svg>
26
- Refresh
27
- </button>
28
-
29
28
  <%= button_to clear_sessions_path,
30
29
  method: :delete,
31
30
  class: "compact-button bg-red-600 text-white hover:bg-red-700",
@@ -56,79 +55,100 @@
56
55
  </div>
57
56
  </div>
58
57
  <% else %>
59
- <table class="compact-table w-full">
60
- <thead>
61
- <tr>
62
- <th class="text-left" style="min-width:180px; max-width:260px; width:18%">Session ID</th>
63
- <th class="text-left" style="min-width:160px; max-width:260px; width:22%">Name</th>
64
- <th class="text-center">Status</th>
65
- <th class="text-center">Changes</th>
66
- <th class="text-right">Started</th>
67
- <th class="text-right">Duration</th>
68
- <th class="text-center">Actions</th>
69
- </tr>
70
- </thead>
71
- <tbody>
72
- <% @sessions.each do |session| %>
73
- <tr class="hover:bg-blue-50">
74
- <td class="font-mono text-xs" style="min-width:180px; max-width:260px; width:18%">
75
- <span class="inline-block whitespace-nowrap overflow-x-auto" style="max-width:260px;">
76
- <%= safe_value(session, :id) %>
77
- </span>
78
- </td>
79
- <td style="min-width:160px; max-width:260px; width:22%" title="<%= safe_value(session, :name) %>">
80
- <%= link_to display_session_name(safe_value(session, :name)),
81
- session_path(safe_value(session, :id)),
82
- class: "text-navy-dark hover:text-blue-medium whitespace-normal break-words inline-block",
83
- style: "max-width:260px; overflow-x:auto; display:inline-block;" %>
84
- </td>
85
- <td class="text-center">
86
- <%= render 'dbwatcher/shared/badge',
87
- content: (session_active?(session) ? 'Active' : 'Completed'),
88
- badge_class: (session_active?(session) ? 'badge-success' : 'badge-primary') %>
89
- </td>
90
- <td class="text-center">
91
- <% change_count = session_change_count(session) %>
92
- <%= render 'dbwatcher/shared/badge',
93
- content: change_count > 99 ? "#{change_count}" : change_count,
94
- badge_class: 'bg-gray-600 text-white whitespace-nowrap' %>
95
- </td>
96
- <td class="text-right text-xs whitespace-nowrap">
97
- <%= format_timestamp(safe_value(session, :started_at)) %>
98
- </td>
99
- <td class="text-right text-xs whitespace-nowrap">
100
- <% if session_active?(session) %>
101
- <span class="text-blue-600">Active</span>
102
- <% else %>
103
- <%= distance_of_time_in_words(
104
- Time.parse(safe_value(session, :started_at)),
105
- Time.parse(safe_value(session, :ended_at))
106
- ) rescue 'N/A' %>
107
- <% end %>
108
- </td>
109
- <td class="text-center">
110
- <div class="flex gap-1 justify-end">
111
- <%= link_to session_path(safe_value(session, :id)),
112
- class: "compact-button bg-navy-dark text-white hover:bg-blue-medium" do %>
113
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
115
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
116
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
117
- d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
118
- </svg>
119
- <% end %>
120
- </div>
121
- </td>
58
+ <div class="bg-white border border-gray-300 rounded shadow-sm">
59
+ <table class="compact-table sessions-table w-full">
60
+ <thead>
61
+ <tr>
62
+ <th class="text-left" style="min-width:180px; max-width:260px; width:18%">Session ID</th>
63
+ <th class="text-left" style="min-width:160px; max-width:260px; width:22%">Name</th>
64
+ <th class="text-center" style="width:100px">Status</th>
65
+ <th class="text-center" style="width:100px">Changes</th>
66
+ <th class="text-right" style="width:120px">Started</th>
67
+ <th class="text-right" style="width:120px">Duration</th>
68
+ <th class="text-center" style="width:80px">Actions</th>
122
69
  </tr>
123
- <% end %>
124
- </tbody>
125
- </table>
70
+ </thead>
71
+ <tbody>
72
+ <% @sessions.each do |session| %>
73
+ <tr class="session-row">
74
+ <td style="min-width:180px; max-width:260px; width:18%">
75
+ <span class="font-mono text-xs">
76
+ <%= safe_value(session, :id) %>
77
+ </span>
78
+ </td>
79
+ <td style="min-width:160px; max-width:260px; width:22%" title="<%= safe_value(session, :name) %>">
80
+ <%= link_to display_session_name(safe_value(session, :name)),
81
+ session_path(safe_value(session, :id)),
82
+ class: "text-navy-dark hover:text-blue-medium whitespace-normal break-words inline-block",
83
+ style: "max-width:260px; overflow-x:auto; display:inline-block;" %>
84
+ </td>
85
+ <td class="text-center">
86
+ <%= render 'dbwatcher/shared/badge',
87
+ content: (session_active?(session) ? 'Active' : 'Completed'),
88
+ badge_class: (session_active?(session) ? 'badge-success' : 'badge-primary') %>
89
+ </td>
90
+ <td class="text-center">
91
+ <% change_count = session_change_count(session) %>
92
+ <%= render 'dbwatcher/shared/badge',
93
+ content: change_count > 99 ? "#{change_count}" : change_count,
94
+ badge_class: 'bg-gray-600 text-white whitespace-nowrap' %>
95
+ </td>
96
+ <td class="text-right text-xs whitespace-nowrap">
97
+ <%= format_timestamp(safe_value(session, :started_at)) %>
98
+ </td>
99
+ <td class="text-right text-xs whitespace-nowrap">
100
+ <% if session_active?(session) %>
101
+ <span class="text-blue-600 font-medium">Active</span>
102
+ <% else %>
103
+ <%= distance_of_time_in_words(
104
+ Time.parse(safe_value(session, :started_at)),
105
+ Time.parse(safe_value(session, :ended_at))
106
+ ) rescue 'N/A' %>
107
+ <% end %>
108
+ </td>
109
+ <td class="text-center actions-cell">
110
+ <div class="flex gap-1 justify-end">
111
+ <%= link_to session_path(safe_value(session, :id)),
112
+ class: "compact-button bg-navy-dark text-white hover:bg-blue-medium",
113
+ title: "View session details" do %>
114
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
115
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
116
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
117
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
118
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
119
+ </svg>
120
+ <% end %>
121
+ </div>
122
+ </td>
123
+ </tr>
124
+ <% end %>
125
+ </tbody>
126
+ </table>
127
+ </div>
128
+
129
+ <!-- No Results Message -->
130
+ <div
131
+ x-show="filterText && document.querySelectorAll('.session-row:not(.hidden)').length === 0"
132
+ x-cloak
133
+ class="mt-4 text-center py-8 bg-gray-50 border border-gray-200 rounded">
134
+ <p class="text-gray-500">No sessions match your filter criteria</p>
135
+ <button
136
+ @click="filterText = ''"
137
+ class="mt-2 text-blue-medium hover:text-blue-700 text-sm">
138
+ Clear filter
139
+ </button>
140
+ </div>
126
141
  <% end %>
127
142
 
128
143
  <!-- Status Bar -->
129
- <div class="h-6 bg-gray-100 border-t border-gray-300 flex items-center px-4 text-xs text-gray-600">
130
- <%= @sessions.count %> sessions total •
131
- <%= @sessions.count { |s| session_active?(s) } %> active
132
- Last updated: <%= Time.current.strftime("%H:%M:%S") %>
144
+ <div class="h-6 bg-gray-100 border-t border-gray-300 flex items-center px-4 text-xs text-gray-600 mt-4">
145
+ <span x-show="!filterText"><%= @sessions.count %> sessions total •
146
+ <%= @sessions.count { |s| session_active?(s) } %> active</span>
147
+ <span x-show="filterText" x-text="`${document.querySelectorAll('.session-row:not(.hidden)').length} of ${<%= @sessions.count %>} sessions shown`"></span>
148
+ <span class="ml-auto">Last updated: <%= Time.current.strftime("%H:%M:%S") %></span>
133
149
  </div>
134
150
  </div>
151
+
152
+ <style>
153
+ [x-cloak] { display: none !important; }
154
+ </style>