dbwatcher 1.1.1 → 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.
- checksums.yaml +4 -4
- data/README.md +24 -2
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +12 -22
- data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
- data/app/assets/stylesheets/dbwatcher/application.css +394 -41
- data/app/assets/stylesheets/dbwatcher/application.scss +4 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
- data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
- data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
- data/app/helpers/dbwatcher/application_helper.rb +43 -11
- data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
- data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
- data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
- data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
- data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
- data/app/views/dbwatcher/sessions/_changes.html.erb +91 -0
- data/app/views/dbwatcher/sessions/_layout.html.erb +23 -0
- data/app/views/dbwatcher/sessions/index.html.erb +107 -87
- data/app/views/dbwatcher/sessions/show.html.erb +10 -4
- data/app/views/dbwatcher/tables/index.html.erb +32 -40
- data/app/views/layouts/dbwatcher/application.html.erb +100 -48
- data/config/routes.rb +23 -6
- data/lib/dbwatcher/configuration.rb +18 -1
- data/lib/dbwatcher/services/base_service.rb +2 -0
- data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
- data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
- data/lib/dbwatcher/storage/session.rb +5 -0
- data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
- data/lib/dbwatcher/storage.rb +12 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +15 -1
- metadata +20 -15
- data/app/helpers/dbwatcher/component_helper.rb +0 -29
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
- data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
- data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
- data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
- /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
- /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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08e110f9429a5d0a67afbea8155a063fcfb012228626cf2d7e0a42c00831db17'
|
4
|
+
data.tar.gz: 5f2f2db2a7c6c2bbaeeb3a57dafb2e0d4cd841542cd05a351473555591bc3078
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e51c681692cc08e1af7d8663e2f1b036ff4c309a62c7b4264b769b6d7e549d915d98fd4834ba7e0d5615f03913595e03fa6e99d43d2e2618ede24f3f46278f92
|
7
|
+
data.tar.gz: d5265b28f063458470804315fbb9e4480e8669443799e345c420cc7e4c26eab1d6823464c5289f707c0c936549c926d7f850598486fb14d9817127e4892edd4d
|
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# DBWatcher
|
2
2
|
|
3
|
+
[](https://github.com/patrick204nqh/dbwatcher/actions/workflows/ci.yml)
|
4
|
+
[](https://github.com/patrick204nqh/dbwatcher/actions/workflows/release.yml)
|
3
5
|
[](https://badge.fury.io/rb/dbwatcher)
|
6
|
+
[](https://qlty.sh/gh/patrick204nqh/projects/dbwatcher)
|
7
|
+
[](https://qlty.sh/gh/patrick204nqh/projects/dbwatcher)
|
4
8
|
[](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
|
-

|
22
26
|
|
23
27
|
### Session View
|
24
28
|
|
25
|
-

|
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
|
178
|
-
index: '
|
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 (
|
212
|
-
const
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
+
})();
|