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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11adeaf76a428fbbcff3438549b7fe8f741836f79263649dd967956704b3df1
4
- data.tar.gz: '0258691a02aa88778f0703c41c4af76ca4481c2fb61e3b0963463d1b97c7e198'
3
+ metadata.gz: '08e110f9429a5d0a67afbea8155a063fcfb012228626cf2d7e0a42c00831db17'
4
+ data.tar.gz: 5f2f2db2a7c6c2bbaeeb3a57dafb2e0d4cd841542cd05a351473555591bc3078
5
5
  SHA512:
6
- metadata.gz: da0df8d4b8f3d435bc0912bc7d9450d8299753ab8da7a3dc36749bb910b6f471bdc1238428d8ff7c4301b38fc30037de6a466e4efb4b7d7bcb5c3bf26787b152
7
- data.tar.gz: 53ad9a4c27aee0e5acebcc3c61a2b70ba82bfd21f2f3012aada133298bf03bd21e7532e2c962e1b86ed3c58815f5f131590679189ced4767c44a4d1c734423f9
6
+ metadata.gz: e51c681692cc08e1af7d8663e2f1b036ff4c309a62c7b4264b769b6d7e549d915d98fd4834ba7e0d5615f03913595e03fa6e99d43d2e2618ede24f3f46278f92
7
+ data.tar.gz: d5265b28f063458470804315fbb9e4480e8669443799e345c420cc7e4c26eab1d6823464c5289f707c0c936549c926d7f850598486fb14d9817127e4892edd4d
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # DBWatcher
2
2
 
3
+ [![CI](https://github.com/patrick204nqh/dbwatcher/actions/workflows/ci.yml/badge.svg)](https://github.com/patrick204nqh/dbwatcher/actions/workflows/ci.yml)
4
+ [![Release Gem](https://github.com/patrick204nqh/dbwatcher/actions/workflows/release.yml/badge.svg)](https://github.com/patrick204nqh/dbwatcher/actions/workflows/release.yml)
3
5
  [![Gem Version](https://badge.fury.io/rb/dbwatcher.svg)](https://badge.fury.io/rb/dbwatcher)
6
+ [![Maintainability](https://qlty.sh/gh/patrick204nqh/projects/dbwatcher/maintainability.svg)](https://qlty.sh/gh/patrick204nqh/projects/dbwatcher)
7
+ [![Code Coverage](https://qlty.sh/gh/patrick204nqh/projects/dbwatcher/coverage.svg)](https://qlty.sh/gh/patrick204nqh/projects/dbwatcher)
4
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
9
 
6
10
  A Rails gem that tracks and visualizes database operations in your application. Built for developers who need to understand complex data flows and debug database interactions.
@@ -18,11 +22,11 @@ A Rails gem that tracks and visualizes database operations in your application.
18
22
 
19
23
  ### Dashboard Interface
20
24
 
21
- ![image](https://github.com/user-attachments/assets/1ea48717-0251-4d2b-95ea-87e00e6b89c3)
25
+ ![image](https://github.com/user-attachments/assets/92c94bdc-06fd-463e-a11f-f931a8ff5346)
22
26
 
23
27
  ### Session View
24
28
 
25
- ![image](https://github.com/user-attachments/assets/411423aa-341f-4112-9d10-1dd9e0a4d409)
29
+ ![image](https://github.com/user-attachments/assets/cae4c820-d7d9-4d16-b8fa-5978e0578ff8)
26
30
 
27
31
  [View more screenshots in here →](docs/screenshots.md)
28
32
 
@@ -120,6 +124,24 @@ bundle exec rake unit # Unit tests only
120
124
  bundle exec rake acceptance # Feature tests only
121
125
  ```
122
126
 
127
+ ### Code Coverage
128
+
129
+ The project uses SimpleCov for code coverage and uploads results to Qlty. To run tests with coverage locally:
130
+
131
+ ```bash
132
+ COVERAGE=true bundle exec rake test
133
+ ```
134
+
135
+ Coverage reports will be generated in the `coverage/` directory.
136
+
137
+ ### CI Coverage Setup
138
+
139
+ To enable coverage uploads to Qlty in CI:
140
+
141
+ 1. Create an account at [Qlty.sh](https://qlty.sh)
142
+ 2. Create a new project and get your coverage token
143
+ 3. Add the token as a GitHub repository secret named `QLTY_COVERAGE_TOKEN`
144
+
123
145
  ### Local Development
124
146
 
125
147
  ```bash
@@ -174,8 +174,8 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
174
174
  responsiveLayout: false,
175
175
  height: Math.max(200, Math.min(400, (tabulatorData.length * 35) + 80)), // Minimum 200px, expand based on content
176
176
 
177
- // Force Tabulator to use our custom ID field
178
- index: 'id', // Tell Tabulator to use the 'id' field as the row identifier
177
+ // Force Tabulator to use our custom rowId field
178
+ index: 'rowId', // Tell Tabulator to use the 'rowId' field as the row identifier
179
179
 
180
180
  // Performance optimizations - disable virtual DOM to ensure all rows render
181
181
  virtualDom: false,
@@ -208,29 +208,19 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
208
208
  changes.forEach((change, index) => {
209
209
  const columnData = this.extractColumnData(change, tableInfo.columns);
210
210
 
211
- // Create truly unique row ID using table name and index only (more reliable)
212
- const uniqueId = `${tableName}_row_${index}`;
213
-
214
- // Clean column data to remove any 'id' field that might conflict
215
- const cleanColumnData = { ...columnData };
216
- delete cleanColumnData.id; // Remove any id field from column data
211
+ // Create truly unique row ID using table name and index only (for Tabulator internal use)
212
+ const uniqueRowId = `${tableName}_row_${index}`;
217
213
 
218
214
  const row = {
219
- id: uniqueId, // Force this to be the ID used by Tabulator
215
+ rowId: uniqueRowId, // Tabulator's internal row identifier
220
216
  index: index + 1, // Display index (1-based) - should maintain API order
221
217
  operation: change.operation,
222
218
  timestamp: change.timestamp,
223
219
  table_name: tableName,
224
220
  change_data: change, // Keep original change data with ID intact
225
- original_change_id: change.id, // Store original ID separately
226
- ...cleanColumnData // Use cleaned column data
221
+ ...columnData // Include all column data including actual record ID
227
222
  };
228
223
 
229
- // Ensure ID is correct
230
- if (row.id !== uniqueId) {
231
- row.id = uniqueId; // Force it back
232
- }
233
-
234
224
  rows.push(row);
235
225
  });
236
226
 
@@ -252,7 +242,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
252
242
  const rowData = cell.getRow().getData();
253
243
  return `<div class="flex items-center justify-center gap-1">
254
244
  <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
255
- data-row-id="${rowData.id}">
245
+ data-row-id="${rowData.rowId}">
256
246
  <svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
257
247
  <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 5.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
258
248
  </svg>
@@ -375,7 +365,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
375
365
  const rowData = cell.getRow().getData();
376
366
  return `<div class="flex items-center justify-center gap-1">
377
367
  <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
378
- data-row-id="${rowData.id}">
368
+ data-row-id="${rowData.rowId}">
379
369
  <svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
380
370
  <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 5.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
381
371
  </svg>
@@ -576,7 +566,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
576
566
  // Try searching through data if direct lookup fails
577
567
  try {
578
568
  const data = tabulator.getData();
579
- const matchingData = data.find(d => d.id === rowId);
569
+ const matchingData = data.find(d => d.rowId === rowId);
580
570
  if (matchingData) {
581
571
  targetRow = tabulator.getRow(rowId);
582
572
  foundInTable = tableName;
@@ -614,7 +604,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
614
604
  // Create detail row as a proper table row
615
605
  const detailRow = document.createElement('tr');
616
606
  detailRow.className = 'row-detail bg-gray-50';
617
- detailRow.setAttribute('data-parent-id', rowData.id);
607
+ detailRow.setAttribute('data-parent-id', rowData.rowId);
618
608
 
619
609
  // Create full-width cell
620
610
  const detailCell = document.createElement('td');
@@ -641,7 +631,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
641
631
  }, 50);
642
632
  }
643
633
  } catch (error) {
644
- console.error(`Error creating detail row for ${rowData.id}:`, error);
634
+ console.error(`Error creating detail row for ${rowData.rowId}:`, error);
645
635
  }
646
636
  },
647
637
 
@@ -652,7 +642,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
652
642
 
653
643
 
654
644
  // Find and remove the detail row
655
- const detailRow = element.parentNode.querySelector(`tr.row-detail[data-parent-id="${rowData.id}"]`);
645
+ const detailRow = element.parentNode.querySelector(`tr.row-detail[data-parent-id="${rowData.rowId}"]`);
656
646
  if (detailRow) {
657
647
  detailRow.remove();
658
648
  } else {
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Dashboard Component for DBWatcher
3
+ * Handles tab switching and system info refresh functionality
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // Configuration constants
10
+ const CONFIG = {
11
+ SELECTORS: {
12
+ container: '.dashboard-container',
13
+ tab: '.tab-item',
14
+ tabContent: '.tab-content',
15
+ refreshButton: '#refresh-system-info',
16
+ clearCacheButton: '#clear-cache-system-info',
17
+ systemInfoContent: '#system-info-content'
18
+ },
19
+ ENDPOINTS: {
20
+ refresh: '/dbwatcher/dashboard/system_info/refresh',
21
+ clearCache: '/dbwatcher/dashboard/system_info/clear_cache',
22
+ dashboard: '/dbwatcher',
23
+ systemInfo: '/dbwatcher/system_info'
24
+ }
25
+ };
26
+
27
+ // Dashboard Component Factory
28
+ const DashboardComponent = function(config = {}) {
29
+ // Merge configuration with defaults
30
+ const settings = {
31
+ ...CONFIG.SELECTORS,
32
+ ...config
33
+ };
34
+
35
+ // Component state
36
+ let isRefreshing = false;
37
+
38
+ // Initialize component
39
+ function init() {
40
+ console.log('Dashboard component init() called');
41
+ setupEventListeners();
42
+ console.log('Dashboard component initialized successfully');
43
+ }
44
+
45
+ // Setup event listeners
46
+ function setupEventListeners() {
47
+ // System info refresh
48
+ document.addEventListener('click', handleRefreshClick);
49
+
50
+ // Clear cache
51
+ document.addEventListener('click', handleClearCacheClick);
52
+ }
53
+
54
+
55
+ // Handle refresh button click
56
+ function handleRefreshClick(event) {
57
+ const target = event.target;
58
+
59
+ if (!target.matches(settings.refreshButton)) {
60
+ return;
61
+ }
62
+
63
+ event.preventDefault();
64
+ refreshSystemInfo();
65
+ }
66
+
67
+ // Handle clear cache button click
68
+ function handleClearCacheClick(event) {
69
+ const target = event.target;
70
+
71
+ if (!target.matches(settings.clearCacheButton)) {
72
+ return;
73
+ }
74
+
75
+ event.preventDefault();
76
+
77
+ if (confirm('Are you sure you want to clear the system information cache?')) {
78
+ clearSystemInfoCache();
79
+ }
80
+ }
81
+
82
+ // Refresh system information
83
+ async function refreshSystemInfo() {
84
+ if (isRefreshing) {
85
+ return;
86
+ }
87
+
88
+ isRefreshing = true;
89
+ const refreshButton = safeQuerySelector(settings.refreshButton);
90
+
91
+ try {
92
+ // Update button state
93
+ if (refreshButton) {
94
+ refreshButton.disabled = true;
95
+ refreshButton.textContent = 'Refreshing...';
96
+ }
97
+
98
+ // Make API call
99
+ const response = await fetch(CONFIG.ENDPOINTS.refresh, {
100
+ method: 'POST',
101
+ headers: utils.getApiHeaders()
102
+ });
103
+
104
+ const data = await utils.handleApiResponse(response);
105
+
106
+ if (data.success) {
107
+ // Update the system info content
108
+ await updateSystemInfoContent();
109
+ showNotification('System information refreshed successfully', 'success');
110
+ } else {
111
+ showNotification(data.error || 'Failed to refresh system information', 'error');
112
+ }
113
+
114
+ } catch (error) {
115
+ console.error('Error refreshing system info:', error);
116
+ showNotification('Failed to refresh system information', 'error');
117
+ } finally {
118
+ isRefreshing = false;
119
+
120
+ // Restore button state
121
+ if (refreshButton) {
122
+ refreshButton.disabled = false;
123
+ refreshButton.textContent = 'Refresh';
124
+ }
125
+ }
126
+ }
127
+
128
+ // Clear system information cache
129
+ async function clearSystemInfoCache() {
130
+ try {
131
+ const response = await fetch(CONFIG.ENDPOINTS.clearCache, {
132
+ method: 'DELETE',
133
+ headers: utils.getApiHeaders()
134
+ });
135
+
136
+ const data = await utils.handleApiResponse(response);
137
+
138
+ if (data.success) {
139
+ showNotification('System information cache cleared successfully', 'success');
140
+ } else {
141
+ showNotification(data.error || 'Failed to clear cache', 'error');
142
+ }
143
+
144
+ } catch (error) {
145
+ console.error('Error clearing cache:', error);
146
+ showNotification('Failed to clear cache', 'error');
147
+ }
148
+ }
149
+
150
+ // Update system info content
151
+ async function updateSystemInfoContent() {
152
+ const contentContainer = safeQuerySelector(settings.systemInfoContent);
153
+
154
+ if (!contentContainer) {
155
+ return;
156
+ }
157
+
158
+ try {
159
+ // Make request to get updated HTML content
160
+ const response = await fetch(CONFIG.ENDPOINTS.dashboard, {
161
+ headers: {
162
+ 'Accept': 'text/html',
163
+ 'X-Requested-With': 'XMLHttpRequest'
164
+ }
165
+ });
166
+
167
+ if (response.ok) {
168
+ const html = await response.text();
169
+ const parser = new DOMParser();
170
+ const doc = parser.parseFromString(html, 'text/html');
171
+ const newContent = doc.querySelector('#system-info-content');
172
+
173
+ if (newContent) {
174
+ contentContainer.innerHTML = newContent.innerHTML;
175
+ }
176
+ }
177
+ } catch (error) {
178
+ console.error('Error updating system info content:', error);
179
+ }
180
+ }
181
+
182
+ // Safe DOM access helper
183
+ function safeQuerySelector(selector) {
184
+ try {
185
+ return document.querySelector(selector);
186
+ } catch (e) {
187
+ console.warn('Error accessing DOM element:', selector, e);
188
+ return null;
189
+ }
190
+ }
191
+
192
+
193
+ // Utility functions
194
+ const utils = {
195
+ // Get CSRF token
196
+ getCsrfToken() {
197
+ const metaTag = safeQuerySelector('meta[name="csrf-token"]');
198
+ return metaTag?.getAttribute('content');
199
+ },
200
+
201
+ // Common headers for API requests
202
+ getApiHeaders() {
203
+ return {
204
+ 'Content-Type': 'application/json',
205
+ 'X-Requested-With': 'XMLHttpRequest',
206
+ 'X-CSRF-Token': this.getCsrfToken()
207
+ };
208
+ },
209
+
210
+ // Handle API response
211
+ async handleApiResponse(response) {
212
+ if (!response.ok) {
213
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
214
+ }
215
+ return await response.json();
216
+ }
217
+ };
218
+
219
+ // Show notification
220
+ function showNotification(message, type = 'info') {
221
+ // Create notification element
222
+ const notification = document.createElement('div');
223
+ notification.className = `notification notification-${type}`;
224
+ notification.style.cssText = `
225
+ position: fixed;
226
+ top: 20px;
227
+ right: 20px;
228
+ padding: 12px 16px;
229
+ border-radius: 4px;
230
+ color: white;
231
+ font-size: 14px;
232
+ z-index: 1000;
233
+ max-width: 300px;
234
+ word-wrap: break-word;
235
+ opacity: 0;
236
+ transform: translateY(-20px);
237
+ transition: all 0.3s ease;
238
+ `;
239
+
240
+ // Set background color based on type
241
+ switch (type) {
242
+ case 'success':
243
+ notification.style.backgroundColor = '#10b981';
244
+ break;
245
+ case 'error':
246
+ notification.style.backgroundColor = '#ef4444';
247
+ break;
248
+ default:
249
+ notification.style.backgroundColor = '#3b82f6';
250
+ }
251
+
252
+ notification.textContent = message;
253
+
254
+ // Add to page
255
+ if (document.body) {
256
+ document.body.appendChild(notification);
257
+ }
258
+
259
+ // Animate in
260
+ setTimeout(() => {
261
+ notification.style.opacity = '1';
262
+ notification.style.transform = 'translateY(0)';
263
+ }, 100);
264
+
265
+ // Remove after 5 seconds
266
+ setTimeout(() => {
267
+ notification.style.opacity = '0';
268
+ notification.style.transform = 'translateY(-20px)';
269
+ setTimeout(() => {
270
+ if (notification.parentNode) {
271
+ notification.parentNode.removeChild(notification);
272
+ }
273
+ }, 300);
274
+ }, 5000);
275
+ }
276
+
277
+ // Public API
278
+ return {
279
+ init,
280
+ refreshSystemInfo,
281
+ clearSystemInfoCache
282
+ };
283
+ };
284
+
285
+ // Register component with DBWatcher
286
+ if (window.DBWatcher && window.DBWatcher.register) {
287
+ window.DBWatcher.register('dashboard', DashboardComponent);
288
+ }
289
+
290
+ // Safe DOM access helper
291
+ function safeQuerySelector(selector) {
292
+ try {
293
+ return document.querySelector(selector);
294
+ } catch (e) {
295
+ console.warn('Error accessing DOM element:', selector, e);
296
+ return null;
297
+ }
298
+ }
299
+
300
+ // Auto-initialize when DOM is ready with better error handling
301
+ function initializeDashboard() {
302
+ try {
303
+ if (safeQuerySelector('.dashboard-container') || safeQuerySelector('.tab-bar') || safeQuerySelector('#refresh-system-info') || safeQuerySelector('#clear-cache-system-info')) {
304
+ const dashboard = DashboardComponent();
305
+ dashboard.init();
306
+ }
307
+ } catch (error) {
308
+ console.error('Error initializing dashboard:', error);
309
+ }
310
+ }
311
+
312
+ // Multiple initialization strategies
313
+ if (document.readyState === 'loading') {
314
+ document.addEventListener('DOMContentLoaded', initializeDashboard);
315
+ } else if (document.readyState === 'interactive' || document.readyState === 'complete') {
316
+ // DOM is already loaded, but wait a bit for Alpine to initialize
317
+ setTimeout(initializeDashboard, 100);
318
+ }
319
+
320
+ // Also listen for Alpine initialization
321
+ document.addEventListener('alpine:init', () => {
322
+ setTimeout(initializeDashboard, 50);
323
+ });
324
+
325
+ })();