dbwatcher 1.0.0 → 1.1.1

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -210
  3. data/app/assets/config/dbwatcher_manifest.js +15 -0
  4. data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
  5. data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
  6. data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
  7. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
  8. data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
  9. data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
  10. data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
  11. data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
  12. data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
  13. data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
  14. data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
  15. data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
  16. data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
  17. data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
  18. data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
  19. data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
  20. data/app/assets/stylesheets/dbwatcher/application.css +423 -0
  21. data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
  22. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
  23. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
  24. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
  25. data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
  26. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
  27. data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
  28. data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
  29. data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
  30. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
  31. data/app/controllers/dbwatcher/base_controller.rb +8 -2
  32. data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
  33. data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
  34. data/app/helpers/dbwatcher/component_helper.rb +29 -0
  35. data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
  36. data/app/helpers/dbwatcher/session_helper.rb +3 -2
  37. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
  38. data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
  39. data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
  40. data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
  41. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
  42. data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
  43. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
  44. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
  45. data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
  46. data/app/views/dbwatcher/sessions/index.html.erb +14 -10
  47. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
  48. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
  49. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
  50. data/app/views/dbwatcher/sessions/show.html.erb +3 -346
  51. data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
  52. data/app/views/layouts/dbwatcher/application.html.erb +125 -247
  53. data/bin/compile_scss +49 -0
  54. data/config/routes.rb +26 -0
  55. data/lib/dbwatcher/configuration.rb +102 -8
  56. data/lib/dbwatcher/engine.rb +17 -7
  57. data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
  58. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
  59. data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
  60. data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
  61. data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
  62. data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
  63. data/lib/dbwatcher/services/base_service.rb +64 -0
  64. data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
  65. data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
  66. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
  67. data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +603 -0
  68. data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
  69. data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -0
  70. data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
  71. data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
  72. data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
  73. data/lib/dbwatcher/services/diagram_data.rb +65 -0
  74. data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
  75. data/lib/dbwatcher/services/diagram_generator.rb +154 -0
  76. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
  77. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
  78. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
  79. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
  80. data/lib/dbwatcher/services/diagram_system.rb +69 -0
  81. data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
  82. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
  83. data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
  84. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +140 -0
  85. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -0
  86. data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
  87. data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
  88. data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +118 -0
  89. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
  90. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
  91. data/lib/dbwatcher/storage/api/session_api.rb +47 -0
  92. data/lib/dbwatcher/storage/base_storage.rb +7 -0
  93. data/lib/dbwatcher/version.rb +1 -1
  94. data/lib/dbwatcher.rb +58 -1
  95. metadata +94 -2
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Simplified Mermaid Service
3
+ *
4
+ * Modern implementation using Mermaid v10+ API and svg-pan-zoom library
5
+ * Replaces 374 lines of complex custom implementation with ~50 lines
6
+ */
7
+
8
+ const MermaidService = {
9
+ initialized: false,
10
+
11
+ // Initialize Mermaid with optimal settings
12
+ async initialize() {
13
+ if (this.initialized) return;
14
+
15
+ // Ensure Mermaid is loaded
16
+ if (!window.mermaid) {
17
+ await this.loadMermaid();
18
+ }
19
+
20
+ // Configure with modern settings
21
+ window.mermaid.initialize({
22
+ startOnLoad: false,
23
+ theme: 'neutral',
24
+ useMaxWidth: true,
25
+ responsive: true,
26
+ securityLevel: 'loose',
27
+
28
+ // Suppress warnings for better UX
29
+ logLevel: 'error',
30
+
31
+ // ER diagram settings
32
+ er: {
33
+ useMaxWidth: true,
34
+ layoutDirection: 'LR',
35
+ entityPadding: 15,
36
+ fontSize: 14
37
+ },
38
+
39
+ // Flowchart settings with better compatibility
40
+ flowchart: {
41
+ useMaxWidth: true,
42
+ htmlLabels: true,
43
+ curve: 'basis',
44
+ padding: 20,
45
+ nodeSpacing: 50,
46
+ rankSpacing: 50
47
+ },
48
+
49
+ // Suppress internal warnings
50
+ suppressErrorRendering: false,
51
+ suppressWarnings: true
52
+ });
53
+
54
+ this.initialized = true;
55
+ console.log('Mermaid service initialized');
56
+ },
57
+
58
+ // Load Mermaid library if not available
59
+ async loadMermaid() {
60
+ if (window.mermaid) return;
61
+
62
+ return new Promise((resolve, reject) => {
63
+ const script = document.createElement('script');
64
+ script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
65
+ script.onload = resolve;
66
+ script.onerror = () => reject(new Error('Failed to load Mermaid library'));
67
+ document.head.appendChild(script);
68
+ });
69
+ },
70
+
71
+ // Render diagram with svg-pan-zoom integration
72
+ async render(content, container, options = {}) {
73
+ if (!content || !container) {
74
+ throw new Error('Content and container are required');
75
+ }
76
+
77
+ await this.initialize();
78
+
79
+ try {
80
+ // Clear container
81
+ container.innerHTML = '';
82
+
83
+ // Create diagram element - ensure full container size
84
+ const diagramDiv = document.createElement('div');
85
+ diagramDiv.className = 'mermaid-diagram';
86
+
87
+ // Set explicit styles for better browser rendering
88
+ const styles = {
89
+ width: '100%',
90
+ height: '100%',
91
+ minHeight: '400px',
92
+ display: 'flex',
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ position: 'relative',
96
+ overflow: 'hidden'
97
+ };
98
+
99
+ Object.assign(diagramDiv.style, styles);
100
+
101
+ // Get container dimensions before appending
102
+ const containerWidth = container.clientWidth || 800;
103
+ const containerHeight = container.clientHeight || 600;
104
+
105
+ console.log('Container dimensions in render:', containerWidth, containerHeight);
106
+
107
+ // Set container to fill its parent
108
+ container.style.width = '100%';
109
+ container.style.height = '100%';
110
+ container.style.minHeight = '500px';
111
+ container.style.position = 'relative';
112
+ container.style.overflow = 'hidden';
113
+
114
+ container.appendChild(diagramDiv);
115
+
116
+ // Render with modern API and error handling
117
+ let renderResult;
118
+ try {
119
+ renderResult = await window.mermaid.render('diagram-' + Date.now(), content);
120
+ } catch (renderError) {
121
+ // Try to clean up content and retry once
122
+ const cleanContent = this.cleanDiagramContent(content);
123
+ renderResult = await window.mermaid.render('diagram-' + Date.now() + '-retry', cleanContent);
124
+ }
125
+
126
+ const { svg } = renderResult;
127
+ diagramDiv.innerHTML = svg;
128
+
129
+ // Post-process the SVG to ensure it has proper dimensions
130
+ const svgElement = diagramDiv.querySelector('svg');
131
+ if (svgElement) {
132
+ // Set CSS styles for SVG
133
+ Object.assign(svgElement.style, {
134
+ width: '100%',
135
+ height: '100%',
136
+ maxWidth: '100%',
137
+ maxHeight: '100%',
138
+ display: 'block'
139
+ });
140
+
141
+ // Make sure viewBox is set if not already
142
+ if (!svgElement.getAttribute('viewBox')) {
143
+ const width = svgElement.getAttribute('width') || diagramDiv.clientWidth;
144
+ const height = svgElement.getAttribute('height') || diagramDiv.clientHeight;
145
+ svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`);
146
+ }
147
+ }
148
+
149
+ // Enable pan/zoom if svg-pan-zoom is available
150
+ let panZoom = null;
151
+
152
+ if (svgElement && window.svgPanZoom) {
153
+ // enableInteractions now returns a Promise or null
154
+ panZoom = await Promise.resolve(this.enableInteractions(svgElement, options));
155
+ }
156
+
157
+ return { success: true, element: svgElement, panZoom };
158
+ } catch (error) {
159
+ console.error('Mermaid rendering failed:', error);
160
+ this.showError(container, error.message);
161
+ throw error;
162
+ }
163
+ },
164
+
165
+ // Enable SVG interactions using svg-pan-zoom library
166
+ enableInteractions(svgElement, options = {}) {
167
+ // Safety checks and set default dimensions if needed
168
+ if (!svgElement) {
169
+ console.warn('SVG element is null, cannot initialize pan-zoom');
170
+ return null;
171
+ }
172
+
173
+ // Always ensure the SVG element has dimensions before initializing pan-zoom
174
+ // Get container dimensions for reference
175
+ const containerWidth = svgElement.parentElement?.clientWidth || 800;
176
+ const containerHeight = svgElement.parentElement?.clientHeight || 600;
177
+
178
+ console.log('Container dimensions:', containerWidth, containerHeight);
179
+
180
+ // Force set dimensions regardless of existing values to ensure they're always set correctly
181
+ svgElement.setAttribute('width', containerWidth.toString());
182
+ svgElement.setAttribute('height', containerHeight.toString());
183
+
184
+ // Also set CSS dimensions for better browser rendering
185
+ svgElement.style.width = '100%';
186
+ svgElement.style.height = '100%';
187
+ svgElement.style.maxWidth = '100%';
188
+ svgElement.style.maxHeight = '100%';
189
+
190
+ // Set viewBox attribute if it doesn't exist or is invalid
191
+ const viewBox = svgElement.getAttribute('viewBox');
192
+ if (!viewBox || viewBox.split(' ').length !== 4) {
193
+ svgElement.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`);
194
+ }
195
+
196
+ // Set preserveAspectRatio for proper scaling
197
+ svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
198
+
199
+ // Double-check dimensions are now set and valid
200
+ const width = parseFloat(svgElement.getAttribute('width'));
201
+ const height = parseFloat(svgElement.getAttribute('height'));
202
+
203
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
204
+ console.warn('SVG element has invalid dimensions after setting defaults:', width, height);
205
+ // Set explicit fallback dimensions as a last resort
206
+ svgElement.setAttribute('width', '800');
207
+ svgElement.setAttribute('height', '600');
208
+ svgElement.setAttribute('viewBox', '0 0 800 600');
209
+ }
210
+
211
+ const config = {
212
+ zoomEnabled: true,
213
+ panEnabled: true,
214
+ controlIconsEnabled: false,
215
+ fit: true,
216
+ center: true,
217
+ minZoom: 0.1,
218
+ maxZoom: 5,
219
+ zoomScaleSensitivity: 0.1,
220
+ ...options
221
+ };
222
+
223
+ try {
224
+ // Ensure SVG is properly rendered in DOM before initializing pan-zoom
225
+ // This forces a reflow which can help with dimension calculations
226
+ void svgElement.getBoundingClientRect();
227
+
228
+ // Short delay to ensure SVG is fully rendered before initializing pan-zoom
229
+ return new Promise(resolve => {
230
+ setTimeout(() => {
231
+ try {
232
+ // Initialize pan-zoom with the configured options
233
+ const panZoom = window.svgPanZoom(svgElement, config);
234
+
235
+ // Add keyboard shortcuts for better UX
236
+ this.addKeyboardShortcuts(panZoom);
237
+
238
+ resolve(panZoom);
239
+ } catch (initError) {
240
+ console.warn('Failed to initialize svg-pan-zoom:', initError);
241
+ resolve(null);
242
+ }
243
+ }, 100); // Small delay for SVG rendering
244
+ });
245
+ } catch (error) {
246
+ console.warn('Failed to enable SVG interactions:', error);
247
+ return null;
248
+ }
249
+ },
250
+
251
+ // Clean diagram content to fix common issues
252
+ cleanDiagramContent(content) {
253
+ return content
254
+ // Remove any problematic characters
255
+ .replace(/[^\x00-\x7F]/g, '')
256
+ // Normalize whitespace
257
+ .replace(/\s+/g, ' ')
258
+ // Remove empty lines
259
+ .split('\n')
260
+ .filter(line => line.trim())
261
+ .join('\n');
262
+ },
263
+
264
+ // Add keyboard shortcuts for pan/zoom
265
+ addKeyboardShortcuts(panZoom) {
266
+ const handleKeydown = (e) => {
267
+ if (!e.target.closest('.mermaid-diagram')) return;
268
+
269
+ switch (e.key) {
270
+ case '+':
271
+ case '=':
272
+ e.preventDefault();
273
+ panZoom.zoomIn();
274
+ break;
275
+ case '-':
276
+ e.preventDefault();
277
+ panZoom.zoomOut();
278
+ break;
279
+ case '0':
280
+ e.preventDefault();
281
+ panZoom.resetZoom();
282
+ panZoom.center();
283
+ break;
284
+ }
285
+ };
286
+
287
+ document.addEventListener('keydown', handleKeydown);
288
+
289
+ // Return cleanup function
290
+ return () => document.removeEventListener('keydown', handleKeydown);
291
+ },
292
+
293
+ // Show error in container
294
+ showError(container, message) {
295
+ container.innerHTML = `
296
+ <div class="flex items-center justify-center h-full">
297
+ <div class="text-center p-4">
298
+ <div class="text-red-600 mb-2">
299
+ <svg class="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
300
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
301
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
302
+ </svg>
303
+ </div>
304
+ <div class="text-sm text-gray-600">
305
+ Failed to render diagram<br>
306
+ <span class="text-xs text-gray-500">${message}</span>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ `;
311
+ }
312
+ };
313
+
314
+ // Make available globally
315
+ window.MermaidService = MermaidService;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Optimized Mermaid Service
3
+ *
4
+ * Modern implementation using Mermaid v10+ API and svg-pan-zoom library
5
+ * Uses tree-shakable design pattern for minimal code footprint
6
+ */
7
+
8
+ const MermaidService = {
9
+ initialized: false,
10
+
11
+ // Initialize Mermaid with optimal settings
12
+ async initialize() {
13
+ if (this.initialized) return;
14
+
15
+ // Ensure Mermaid is loaded
16
+ if (!window.mermaid) {
17
+ console.error('Mermaid library not loaded');
18
+ throw new Error('Mermaid library not loaded');
19
+ }
20
+
21
+ // Configure with modern settings
22
+ window.mermaid.initialize({
23
+ startOnLoad: false,
24
+ theme: 'neutral',
25
+ useMaxWidth: true,
26
+ responsive: true,
27
+ securityLevel: 'loose',
28
+ logLevel: 'error',
29
+ er: {
30
+ useMaxWidth: true,
31
+ layoutDirection: 'LR',
32
+ entityPadding: 15,
33
+ fontSize: 14
34
+ },
35
+ flowchart: {
36
+ useMaxWidth: true,
37
+ htmlLabels: true,
38
+ curve: 'basis',
39
+ padding: 20,
40
+ nodeSpacing: 50,
41
+ rankSpacing: 50
42
+ },
43
+ suppressErrorRendering: false,
44
+ suppressWarnings: true
45
+ });
46
+
47
+ this.initialized = true;
48
+ console.log('Mermaid service initialized');
49
+ },
50
+
51
+ // Render diagram with svg-pan-zoom integration
52
+ async render(content, container, options = {}) {
53
+ if (!content || !container) {
54
+ console.error('Missing required parameters for rendering diagram');
55
+ return null;
56
+ }
57
+
58
+ await this.initialize();
59
+
60
+ try {
61
+ // Clean content before rendering
62
+ const cleanedContent = this.cleanDiagramContent(content);
63
+
64
+ // Use Mermaid's modern promise-based API
65
+ const { svg } = await window.mermaid.render('diagram-' + Date.now(), cleanedContent);
66
+
67
+ // Insert SVG into container
68
+ container.innerHTML = svg;
69
+
70
+ // Get the SVG element for pan-zoom initialization
71
+ const svgElement = container.querySelector('svg');
72
+ if (!svgElement) {
73
+ throw new Error('Failed to render SVG');
74
+ }
75
+
76
+ // Initialize pan-zoom
77
+ const panZoomInstance = this.enableInteractions(svgElement, options);
78
+
79
+ return {
80
+ svg: svgElement,
81
+ panZoom: panZoomInstance
82
+ };
83
+ } catch (error) {
84
+ console.error('Error rendering diagram:', error);
85
+ this.showError(container, error.message || 'Failed to render diagram');
86
+ return null;
87
+ }
88
+ },
89
+
90
+ // Enable SVG interactions using svg-pan-zoom library
91
+ enableInteractions(svgElement, options = {}) {
92
+ // Safety checks
93
+ if (!svgElement || !window.svgPanZoom) {
94
+ console.error('SVG element or svg-pan-zoom library not available');
95
+ return null;
96
+ }
97
+
98
+ // Get container dimensions for reference
99
+ const containerWidth = svgElement.parentElement?.clientWidth || 800;
100
+ const containerHeight = svgElement.parentElement?.clientHeight || 600;
101
+
102
+ // Force set dimensions for better rendering
103
+ svgElement.setAttribute('width', containerWidth.toString());
104
+ svgElement.setAttribute('height', containerHeight.toString());
105
+ svgElement.style.width = '100%';
106
+ svgElement.style.height = '100%';
107
+ svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
108
+
109
+ // Initialize pan-zoom
110
+ try {
111
+ const panZoomInstance = window.svgPanZoom(svgElement, {
112
+ zoomEnabled: true,
113
+ controlIconsEnabled: false,
114
+ fit: true,
115
+ center: true,
116
+ minZoom: 0.2,
117
+ maxZoom: 5,
118
+ zoomScaleSensitivity: 0.4
119
+ });
120
+
121
+ // Add keyboard shortcuts
122
+ this.addKeyboardShortcuts(panZoomInstance);
123
+
124
+ return panZoomInstance;
125
+ } catch (error) {
126
+ console.error('Error initializing pan-zoom:', error);
127
+ return null;
128
+ }
129
+ },
130
+
131
+ // Clean diagram content to fix common issues
132
+ cleanDiagramContent(content) {
133
+ if (!content) return '';
134
+
135
+ // Replace common problematic patterns
136
+ return content
137
+ .replace(/&nbsp;/g, ' ')
138
+ .replace(/&lt;/g, '<')
139
+ .replace(/&gt;/g, '>')
140
+ .replace(/&amp;/g, '&')
141
+ .trim();
142
+ },
143
+
144
+ // Add keyboard shortcuts for pan/zoom
145
+ addKeyboardShortcuts(panZoom) {
146
+ if (!panZoom) return;
147
+
148
+ // Add keyboard event listener
149
+ document.addEventListener('keydown', (event) => {
150
+ // Only handle events when diagram is focused
151
+ if (!event.target.closest('.diagram-container')) return;
152
+
153
+ switch (event.key) {
154
+ case '+':
155
+ case '=':
156
+ panZoom.zoomIn();
157
+ break;
158
+ case '-':
159
+ panZoom.zoomOut();
160
+ break;
161
+ case '0':
162
+ panZoom.resetZoom();
163
+ break;
164
+ case 'ArrowUp':
165
+ panZoom.panBy({x: 0, y: 50});
166
+ break;
167
+ case 'ArrowDown':
168
+ panZoom.panBy({x: 0, y: -50});
169
+ break;
170
+ case 'ArrowLeft':
171
+ panZoom.panBy({x: 50, y: 0});
172
+ break;
173
+ case 'ArrowRight':
174
+ panZoom.panBy({x: -50, y: 0});
175
+ break;
176
+ }
177
+ });
178
+ },
179
+
180
+ // Show error in container
181
+ showError(container, message) {
182
+ if (!container) return;
183
+
184
+ container.innerHTML = `
185
+ <div class="diagram-error p-4 bg-red-50 text-red-700 rounded">
186
+ <h3 class="font-semibold mb-2">Error rendering diagram</h3>
187
+ <p>${message || 'An unknown error occurred'}</p>
188
+ </div>
189
+ `;
190
+ }
191
+ };
192
+
193
+ // Register with DBWatcher if available
194
+ if (window.DBWatcher) {
195
+ window.DBWatcher.MermaidService = MermaidService;
196
+ }
197
+
198
+ // Make available globally
199
+ window.MermaidService = MermaidService;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Browser-compatible date-fns version
3
+ * Sets up window.dateFns with all date-fns methods
4
+ */
5
+ (function(global) {
6
+ // Create namespace
7
+ global.dateFns = {
8
+ // Core formatting functions
9
+ format: function(date, formatStr) {
10
+ if (!date) return '';
11
+ var d = new Date(date);
12
+ if (isNaN(d.getTime())) return '';
13
+
14
+ formatStr = formatStr || 'yyyy-MM-dd HH:mm:ss';
15
+
16
+ var year = d.getFullYear();
17
+ var month = d.getMonth() + 1;
18
+ var day = d.getDate();
19
+ var hours = d.getHours();
20
+ var minutes = d.getMinutes();
21
+ var seconds = d.getSeconds();
22
+
23
+ // Simple formatting with basic replacements
24
+ return formatStr
25
+ .replace(/yyyy/g, year)
26
+ .replace(/MM/g, month < 10 ? '0' + month : month)
27
+ .replace(/dd/g, day < 10 ? '0' + day : day)
28
+ .replace(/HH/g, hours < 10 ? '0' + hours : hours)
29
+ .replace(/mm/g, minutes < 10 ? '0' + minutes : minutes)
30
+ .replace(/ss/g, seconds < 10 ? '0' + seconds : seconds);
31
+ },
32
+
33
+ // Date manipulation
34
+ addDays: function(date, days) {
35
+ var result = new Date(date);
36
+ result.setDate(result.getDate() + days);
37
+ return result;
38
+ },
39
+
40
+ addHours: function(date, hours) {
41
+ var result = new Date(date);
42
+ result.setHours(result.getHours() + hours);
43
+ return result;
44
+ },
45
+
46
+ addMinutes: function(date, minutes) {
47
+ var result = new Date(date);
48
+ result.setMinutes(result.getMinutes() + minutes);
49
+ return result;
50
+ },
51
+
52
+ addSeconds: function(date, seconds) {
53
+ var result = new Date(date);
54
+ result.setSeconds(result.getSeconds() + seconds);
55
+ return result;
56
+ },
57
+
58
+ // Comparison
59
+ isAfter: function(date, dateToCompare) {
60
+ return new Date(date) > new Date(dateToCompare);
61
+ },
62
+
63
+ isBefore: function(date, dateToCompare) {
64
+ return new Date(date) < new Date(dateToCompare);
65
+ },
66
+
67
+ isEqual: function(date, dateToCompare) {
68
+ return new Date(date).getTime() === new Date(dateToCompare).getTime();
69
+ },
70
+
71
+ isSameDay: function(date, dateToCompare) {
72
+ var d1 = new Date(date);
73
+ var d2 = new Date(dateToCompare);
74
+ return d1.getFullYear() === d2.getFullYear() &&
75
+ d1.getMonth() === d2.getMonth() &&
76
+ d1.getDate() === d2.getDate();
77
+ },
78
+
79
+ // Utilities
80
+ parseISO: function(dateString) {
81
+ return new Date(dateString);
82
+ },
83
+
84
+ formatDistance: function(date, baseDate) {
85
+ var seconds = Math.abs(new Date(date) - new Date(baseDate)) / 1000;
86
+ var minutes = Math.floor(seconds / 60);
87
+ var hours = Math.floor(minutes / 60);
88
+ var days = Math.floor(hours / 24);
89
+
90
+ if (days > 0) return days + ' day' + (days > 1 ? 's' : '');
91
+ if (hours > 0) return hours + ' hour' + (hours > 1 ? 's' : '');
92
+ if (minutes > 0) return minutes + ' minute' + (minutes > 1 ? 's' : '');
93
+ return seconds + ' second' + (seconds !== 1 ? 's' : '');
94
+ }
95
+ };
96
+ })(window);
97
+
98
+ // Add console confirmation
99
+ console.log('Browser-compatible date-fns loaded successfully');