retold-data-service 2.0.16 → 2.0.18

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 (28) hide show
  1. package/.claude/launch.json +2 -2
  2. package/.quackage.json +19 -0
  3. package/package.json +13 -6
  4. package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
  5. package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
  6. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
  7. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
  8. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
  9. package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
  10. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
  11. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
  12. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
  13. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
  14. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
  15. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
  16. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
  17. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
  18. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
  19. package/source/services/data-cloner/web/data-cloner.js +7952 -0
  20. package/source/services/data-cloner/web/data-cloner.js.map +1 -0
  21. package/source/services/data-cloner/web/data-cloner.min.js +2 -0
  22. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
  23. package/source/services/data-cloner/web/index.html +17 -0
  24. package/test/DataCloner-Integration_tests.js +1205 -0
  25. package/test/DataCloner-Puppeteer_tests.js +502 -0
  26. package/test/integration-report.json +311 -0
  27. package/test/run-integration-tests.js +501 -0
  28. package/source/services/data-cloner/data-cloner-web.html +0 -2706
@@ -1,2706 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Retold Data Cloner</title>
7
- <style>
8
- * { box-sizing: border-box; margin: 0; padding: 0; }
9
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 20px; }
10
- h1 { margin-bottom: 20px; color: #1a1a1a; }
11
- h2 { margin-bottom: 12px; color: #444; font-size: 1.2em; border-bottom: 2px solid #ddd; padding-bottom: 6px; }
12
-
13
- .section { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
14
-
15
- /* Accordion layout — numbered sections */
16
- .accordion-row { display: flex; gap: 0; margin-bottom: 16px; align-items: stretch; }
17
- .accordion-number {
18
- flex: 0 0 48px; display: flex; align-items: flex-start; justify-content: center;
19
- padding-top: 16px; font-size: 1.6em; font-weight: 700; color: #4a90d9;
20
- user-select: none;
21
- }
22
- .accordion-card {
23
- flex: 1; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
24
- overflow: hidden; min-width: 0;
25
- }
26
- .accordion-header {
27
- display: flex; align-items: center; padding: 14px 20px; cursor: pointer;
28
- user-select: none; gap: 12px; transition: background 0.15s; line-height: 1.4;
29
- }
30
- .accordion-header:hover { background: #fafafa; }
31
- .accordion-title { font-weight: 600; color: #333; font-size: 1.05em; white-space: nowrap; }
32
- .accordion-preview { flex: 1; font-style: italic; color: #888; font-size: 0.9em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
33
- .accordion-toggle {
34
- flex: 0 0 20px; display: flex; align-items: center; justify-content: center;
35
- border-radius: 4px; transition: background 0.15s, transform 0.25s; font-size: 0.7em; color: #888;
36
- }
37
- .accordion-header:hover .accordion-toggle { background: #eee; color: #555; }
38
- .accordion-card.open .accordion-toggle { transform: rotate(180deg); }
39
- .accordion-body { padding: 0 20px 20px; display: none; }
40
- .accordion-card.open .accordion-body { display: block; }
41
- .accordion-card.open .accordion-header { border-bottom: 1px solid #eee; }
42
- .accordion-card.open .accordion-preview { display: none; }
43
-
44
- /* Action controls (go link + auto checkbox) shown only when collapsed */
45
- .accordion-actions { display: flex; align-items: baseline; gap: 8px; flex-shrink: 0; }
46
- .accordion-card.open .accordion-actions { display: none; }
47
- .accordion-go {
48
- font-size: 0.82em; color: #4a90d9; cursor: pointer; text-decoration: none;
49
- font-weight: 500; white-space: nowrap; padding: 2px 6px; border-radius: 3px;
50
- transition: background 0.15s;
51
- }
52
- .accordion-go:hover { background: #e8f0fe; text-decoration: underline; }
53
- .accordion-auto {
54
- font-size: 0.82em; color: #999; white-space: nowrap; cursor: pointer;
55
- }
56
- .accordion-auto .auto-label { display: none; }
57
- .accordion-auto:hover .auto-label { display: inline; }
58
- .accordion-auto input[type="checkbox"] { width: auto; margin: 0; cursor: pointer; vertical-align: middle; position: relative; top: 0px; opacity: 0.75; transition: opacity 0.15s; }
59
- .accordion-auto:hover input[type="checkbox"] { opacity: 1; }
60
- .accordion-auto:hover { color: #666; }
61
-
62
- /* Phase status indicator — sits between title and preview */
63
- .accordion-phase {
64
- flex: 0 0 auto; display: none; align-items: center; justify-content: center;
65
- font-size: 0.85em; line-height: 1;
66
- }
67
- .accordion-phase.visible { display: flex; }
68
- .accordion-phase-ok { color: #28a745; }
69
- .accordion-phase-error { color: #dc3545; }
70
- .accordion-phase-busy { color: #28a745; }
71
- .accordion-phase-busy .phase-spinner {
72
- display: inline-block; width: 14px; height: 14px;
73
- border: 2px solid #28a745; border-top-color: transparent; border-radius: 50%;
74
- animation: phase-spin 0.8s linear infinite; vertical-align: middle;
75
- }
76
- @keyframes phase-spin {
77
- to { transform: rotate(360deg); }
78
- }
79
-
80
- .accordion-controls {
81
- display: flex; gap: 8px; margin-bottom: 12px; justify-content: flex-end;
82
- }
83
- .accordion-controls button {
84
- padding: 4px 10px; font-size: 0.82em; font-weight: 500; background: none;
85
- border: 1px solid #ccc; border-radius: 4px; color: #666; cursor: pointer; margin: 0;
86
- }
87
- .accordion-controls button:hover { background: #f0f0f0; border-color: #aaa; color: #333; }
88
-
89
- label { display: block; font-weight: 600; margin-bottom: 4px; font-size: 0.9em; }
90
- input[type="text"], input[type="password"], input[type="number"] {
91
- width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;
92
- font-size: 0.95em; margin-bottom: 10px;
93
- }
94
- input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus {
95
- outline: none; border-color: #4a90d9;
96
- }
97
-
98
- button {
99
- padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
100
- font-size: 0.9em; font-weight: 600; margin-right: 8px; margin-bottom: 8px;
101
- }
102
- button.primary { background: #4a90d9; color: #fff; }
103
- button.primary:hover { background: #357abd; }
104
- button.secondary { background: #6c757d; color: #fff; }
105
- button.secondary:hover { background: #5a6268; }
106
- button.danger { background: #dc3545; color: #fff; }
107
- button.danger:hover { background: #c82333; }
108
- button.success { background: #28a745; color: #fff; }
109
- button.success:hover { background: #218838; }
110
- button:disabled { opacity: 0.5; cursor: not-allowed; }
111
-
112
- .status { padding: 8px 12px; border-radius: 4px; margin-top: 10px; font-size: 0.9em; }
113
- .status.ok { background: #d4edda; color: #155724; }
114
- .status.error { background: #f8d7da; color: #721c24; }
115
- .status.info { background: #d1ecf1; color: #0c5460; }
116
- .status.warn { background: #fff3cd; color: #856404; }
117
-
118
- .table-list { max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 8px; margin: 10px 0; }
119
- .table-item { padding: 4px 8px; display: flex; align-items: center; }
120
- .table-item:hover { background: #f0f0f0; }
121
- .table-item input[type="checkbox"] { margin-right: 8px; width: auto; }
122
- .table-item label { display: inline; font-weight: normal; margin-bottom: 0; cursor: pointer; }
123
-
124
- .progress-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
125
- .progress-table th, .progress-table td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #eee; font-size: 0.9em; }
126
- .progress-table th { background: #f8f9fa; font-weight: 600; }
127
-
128
- .progress-bar-container { width: 120px; height: 16px; background: #e9ecef; border-radius: 8px; overflow: hidden; display: inline-block; vertical-align: middle; }
129
- .progress-bar-fill { height: 100%; background: #28a745; transition: width 0.3s; }
130
-
131
- .inline-group { display: flex; gap: 8px; align-items: flex-end; margin-bottom: 10px; }
132
- .inline-group > div { flex: 1; }
133
-
134
- a { color: #4a90d9; }
135
-
136
- select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.95em; margin-bottom: 10px; }
137
-
138
- .data-table { width: 100%; border-collapse: collapse; font-size: 0.8em; font-family: monospace; }
139
- .data-table th { background: #f8f9fa; font-weight: 600; text-align: left; padding: 4px 8px; border: 1px solid #ddd; white-space: nowrap; position: sticky; top: 0; }
140
- .data-table td { padding: 4px 8px; border: 1px solid #eee; white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
141
- .data-table tr:nth-child(even) { background: #fafafa; }
142
- .data-table tr:hover { background: #f0f7ff; }
143
-
144
- .checkbox-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
145
- .checkbox-row input[type="checkbox"] { width: auto; margin: 0; }
146
- .checkbox-row label { display: inline; margin: 0; font-weight: normal; cursor: pointer; }
147
-
148
- .report-card { background: #f8f9fa; border-radius: 8px; padding: 12px 16px; min-width: 140px; text-align: center; border: 1px solid #e9ecef; }
149
- .report-card .card-label { font-size: 0.8em; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
150
- .report-card .card-value { font-size: 1.4em; font-weight: 700; }
151
- .report-card.outcome-success { border-left: 4px solid #28a745; }
152
- .report-card.outcome-partial { border-left: 4px solid #ffc107; }
153
- .report-card.outcome-error { border-left: 4px solid #dc3545; }
154
- .report-card.outcome-stopped { border-left: 4px solid #6c757d; }
155
-
156
- /* Live Status Bar */
157
- .live-status-bar {
158
- background: #fff; border-radius: 8px; padding: 14px 20px; margin-bottom: 16px;
159
- box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 14px;
160
- position: sticky; top: 0; z-index: 100; border-left: 4px solid #6c757d;
161
- }
162
- .live-status-bar.phase-idle { border-left-color: #6c757d; }
163
- .live-status-bar.phase-disconnected { border-left-color: #dc3545; }
164
- .live-status-bar.phase-ready { border-left-color: #4a90d9; }
165
- .live-status-bar.phase-syncing { border-left-color: #28a745; }
166
- .live-status-bar.phase-stopping { border-left-color: #ffc107; }
167
- .live-status-bar.phase-complete { border-left-color: #28a745; }
168
-
169
- .live-status-dot {
170
- width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
171
- background: #6c757d;
172
- }
173
- .live-status-bar.phase-idle .live-status-dot { background: #6c757d; }
174
- .live-status-bar.phase-disconnected .live-status-dot { background: #dc3545; }
175
- .live-status-bar.phase-ready .live-status-dot { background: #4a90d9; }
176
- .live-status-bar.phase-syncing .live-status-dot {
177
- background: #28a745;
178
- animation: live-pulse 1.5s ease-in-out infinite;
179
- }
180
- .live-status-bar.phase-stopping .live-status-dot {
181
- background: #ffc107;
182
- animation: live-pulse 0.8s ease-in-out infinite;
183
- }
184
- .live-status-bar.phase-complete .live-status-dot { background: #28a745; }
185
-
186
- @keyframes live-pulse {
187
- 0%, 100% { opacity: 1; transform: scale(1); }
188
- 50% { opacity: 0.4; transform: scale(0.8); }
189
- }
190
-
191
- .live-status-message { flex: 1; font-size: 0.92em; color: #333; line-height: 1.4; }
192
-
193
- .live-status-meta {
194
- display: flex; gap: 16px; flex-shrink: 0; font-size: 0.82em; color: #666;
195
- }
196
- .live-status-meta-item { white-space: nowrap; }
197
- .live-status-meta-item strong { color: #333; }
198
-
199
- .live-status-progress-bar {
200
- height: 3px; background: #e9ecef; border-radius: 2px; overflow: hidden;
201
- position: absolute; bottom: 0; left: 0; right: 0;
202
- }
203
- .live-status-progress-fill {
204
- height: 100%; background: #28a745; transition: width 1s ease;
205
- }
206
- </style>
207
- </head>
208
- <body>
209
-
210
- <h1>Retold Data Cloner</h1>
211
-
212
- <!-- ======== Live Status Bar ======== -->
213
- <div id="liveStatusBar" class="live-status-bar phase-idle" style="position:relative">
214
- <div class="live-status-dot"></div>
215
- <div class="live-status-message" id="liveStatusMessage">Idle</div>
216
- <div class="live-status-meta" id="liveStatusMeta"></div>
217
- <div class="live-status-progress-bar"><div class="live-status-progress-fill" id="liveStatusProgressFill" style="width:0%"></div></div>
218
- </div>
219
-
220
- <!-- ======== Expand / Collapse All ======== -->
221
- <div class="accordion-controls">
222
- <button onclick="expandAllSections()">Expand All</button>
223
- <button onclick="collapseAllSections()">Collapse All</button>
224
- </div>
225
-
226
- <!-- ======== Section 1: Database Connection ======== -->
227
- <div class="accordion-row">
228
- <div class="accordion-number">1</div>
229
- <div class="accordion-card" id="section1" data-section="1">
230
- <div class="accordion-header" onclick="toggleSection('section1')">
231
- <div class="accordion-title">Database Connection</div>
232
- <span class="accordion-phase" id="phase1"></span>
233
- <div class="accordion-preview" id="preview1">SQLite at data/cloned.sqlite</div>
234
- <div class="accordion-actions">
235
- <span class="accordion-go" onclick="event.stopPropagation(); goSection1()">go</span>
236
- <label class="accordion-auto" onclick="event.stopPropagation()"><input type="checkbox" id="auto1"> <span class="auto-label">auto</span></label>
237
- </div>
238
- <div class="accordion-toggle">&#9660;</div>
239
- </div>
240
- <div class="accordion-body">
241
- <p style="font-size:0.9em; color:#666; margin-bottom:10px">Configure the local database where cloned data will be stored. SQLite is connected by default.</p>
242
-
243
- <div class="inline-group">
244
- <div style="flex:0 0 200px">
245
- <label for="connProvider">Provider</label>
246
- <select id="connProvider" onchange="onProviderChange()">
247
- <option value="SQLite" selected>SQLite</option>
248
- <option value="MySQL">MySQL</option>
249
- <option value="MSSQL">MSSQL</option>
250
- <option value="PostgreSQL">PostgreSQL</option>
251
- <option value="Solr">Solr</option>
252
- <option value="MongoDB">MongoDB</option>
253
- <option value="RocksDB">RocksDB</option>
254
- <option value="Bibliograph">Bibliograph</option>
255
- </select>
256
- </div>
257
- <div style="flex:1; display:flex; align-items:flex-end; gap:8px">
258
- <button class="primary" onclick="connectProvider()">Connect</button>
259
- <button class="secondary" onclick="testConnection()">Test Connection</button>
260
- </div>
261
- </div>
262
-
263
- <!-- SQLite Config -->
264
- <div id="configSQLite">
265
- <label for="sqliteFilePath">SQLite File Path</label>
266
- <input type="text" id="sqliteFilePath" placeholder="data/cloned.sqlite" value="data/cloned.sqlite">
267
- </div>
268
-
269
- <!-- MySQL Config -->
270
- <div id="configMySQL" style="display:none">
271
- <div class="inline-group">
272
- <div style="flex:2">
273
- <label for="mysqlServer">Server</label>
274
- <input type="text" id="mysqlServer" placeholder="127.0.0.1" value="127.0.0.1">
275
- </div>
276
- <div style="flex:1">
277
- <label for="mysqlPort">Port</label>
278
- <input type="number" id="mysqlPort" placeholder="3306" value="3306">
279
- </div>
280
- </div>
281
- <div class="inline-group">
282
- <div>
283
- <label for="mysqlUser">User</label>
284
- <input type="text" id="mysqlUser" placeholder="root" value="root">
285
- </div>
286
- <div>
287
- <label for="mysqlPassword">Password</label>
288
- <input type="password" id="mysqlPassword" placeholder="password">
289
- </div>
290
- </div>
291
- <label for="mysqlDatabase">Database</label>
292
- <input type="text" id="mysqlDatabase" placeholder="meadow_clone">
293
- <div class="inline-group">
294
- <div>
295
- <label for="mysqlConnectionLimit">Connection Limit</label>
296
- <input type="number" id="mysqlConnectionLimit" placeholder="20" value="20">
297
- </div>
298
- <div></div>
299
- </div>
300
- </div>
301
-
302
- <!-- MSSQL Config -->
303
- <div id="configMSSQL" style="display:none">
304
- <div class="inline-group">
305
- <div style="flex:2">
306
- <label for="mssqlServer">Server</label>
307
- <input type="text" id="mssqlServer" placeholder="127.0.0.1" value="127.0.0.1">
308
- </div>
309
- <div style="flex:1">
310
- <label for="mssqlPort">Port</label>
311
- <input type="number" id="mssqlPort" placeholder="1433" value="1433">
312
- </div>
313
- </div>
314
- <div class="inline-group">
315
- <div>
316
- <label for="mssqlUser">User</label>
317
- <input type="text" id="mssqlUser" placeholder="sa" value="sa">
318
- </div>
319
- <div>
320
- <label for="mssqlPassword">Password</label>
321
- <input type="password" id="mssqlPassword" placeholder="password">
322
- </div>
323
- </div>
324
- <label for="mssqlDatabase">Database</label>
325
- <input type="text" id="mssqlDatabase" placeholder="meadow_clone">
326
- <div class="inline-group">
327
- <div>
328
- <label for="mssqlConnectionLimit">Connection Limit</label>
329
- <input type="number" id="mssqlConnectionLimit" placeholder="20" value="20">
330
- </div>
331
- <div></div>
332
- </div>
333
- </div>
334
-
335
- <!-- PostgreSQL Config -->
336
- <div id="configPostgreSQL" style="display:none">
337
- <div class="inline-group">
338
- <div style="flex:2">
339
- <label for="postgresqlHost">Host</label>
340
- <input type="text" id="postgresqlHost" placeholder="127.0.0.1" value="127.0.0.1">
341
- </div>
342
- <div style="flex:1">
343
- <label for="postgresqlPort">Port</label>
344
- <input type="number" id="postgresqlPort" placeholder="5432" value="5432">
345
- </div>
346
- </div>
347
- <div class="inline-group">
348
- <div>
349
- <label for="postgresqlUser">User</label>
350
- <input type="text" id="postgresqlUser" placeholder="postgres" value="postgres">
351
- </div>
352
- <div>
353
- <label for="postgresqlPassword">Password</label>
354
- <input type="password" id="postgresqlPassword" placeholder="password">
355
- </div>
356
- </div>
357
- <label for="postgresqlDatabase">Database</label>
358
- <input type="text" id="postgresqlDatabase" placeholder="meadow_clone">
359
- <div class="inline-group">
360
- <div>
361
- <label for="postgresqlConnectionLimit">Connection Pool Limit</label>
362
- <input type="number" id="postgresqlConnectionLimit" placeholder="10" value="10">
363
- </div>
364
- <div></div>
365
- </div>
366
- </div>
367
-
368
- <!-- Solr Config -->
369
- <div id="configSolr" style="display:none">
370
- <div class="inline-group">
371
- <div style="flex:2">
372
- <label for="solrHost">Host</label>
373
- <input type="text" id="solrHost" placeholder="localhost" value="localhost">
374
- </div>
375
- <div style="flex:1">
376
- <label for="solrPort">Port</label>
377
- <input type="number" id="solrPort" placeholder="8983" value="8983">
378
- </div>
379
- </div>
380
- <div class="inline-group">
381
- <div style="flex:2">
382
- <label for="solrCore">Core</label>
383
- <input type="text" id="solrCore" placeholder="default" value="default">
384
- </div>
385
- <div style="flex:1">
386
- <label for="solrPath">Path</label>
387
- <input type="text" id="solrPath" placeholder="/solr" value="/solr">
388
- </div>
389
- </div>
390
- <div class="checkbox-row">
391
- <input type="checkbox" id="solrSecure">
392
- <label for="solrSecure">Use HTTPS</label>
393
- </div>
394
- </div>
395
-
396
- <!-- MongoDB Config -->
397
- <div id="configMongoDB" style="display:none">
398
- <div class="inline-group">
399
- <div style="flex:2">
400
- <label for="mongodbHost">Host</label>
401
- <input type="text" id="mongodbHost" placeholder="127.0.0.1" value="127.0.0.1">
402
- </div>
403
- <div style="flex:1">
404
- <label for="mongodbPort">Port</label>
405
- <input type="number" id="mongodbPort" placeholder="27017" value="27017">
406
- </div>
407
- </div>
408
- <div class="inline-group">
409
- <div>
410
- <label for="mongodbUser">User</label>
411
- <input type="text" id="mongodbUser" placeholder="(optional)">
412
- </div>
413
- <div>
414
- <label for="mongodbPassword">Password</label>
415
- <input type="password" id="mongodbPassword" placeholder="(optional)">
416
- </div>
417
- </div>
418
- <label for="mongodbDatabase">Database</label>
419
- <input type="text" id="mongodbDatabase" placeholder="test" value="test">
420
- <div class="inline-group">
421
- <div>
422
- <label for="mongodbConnectionLimit">Max Pool Size</label>
423
- <input type="number" id="mongodbConnectionLimit" placeholder="10" value="10">
424
- </div>
425
- <div></div>
426
- </div>
427
- </div>
428
-
429
- <!-- RocksDB Config -->
430
- <div id="configRocksDB" style="display:none">
431
- <label for="rocksdbFolder">RocksDB Folder Path</label>
432
- <input type="text" id="rocksdbFolder" placeholder="data/rocksdb" value="data/rocksdb">
433
- </div>
434
-
435
- <!-- Bibliograph Config -->
436
- <div id="configBibliograph" style="display:none">
437
- <label for="bibliographFolder">Storage Folder Path</label>
438
- <input type="text" id="bibliographFolder" placeholder="data/bibliograph" value="data/bibliograph">
439
- </div>
440
-
441
- <div id="connectionStatus"></div>
442
- </div>
443
- </div>
444
- </div>
445
-
446
- <!-- ======== Section 2: Remote Session ======== -->
447
- <div class="accordion-row">
448
- <div class="accordion-number">2</div>
449
- <div class="accordion-card" id="section2" data-section="2">
450
- <div class="accordion-header" onclick="toggleSection('section2')">
451
- <div class="accordion-title">Remote Session</div>
452
- <span class="accordion-phase" id="phase2"></span>
453
- <div class="accordion-preview" id="preview2">Configure remote server URL and credentials</div>
454
- <div class="accordion-actions">
455
- <span class="accordion-go" onclick="event.stopPropagation(); goSection2()">go</span>
456
- <label class="accordion-auto" onclick="event.stopPropagation()"><input type="checkbox" id="auto2"> <span class="auto-label">auto</span></label>
457
- </div>
458
- <div class="accordion-toggle">&#9660;</div>
459
- </div>
460
- <div class="accordion-body">
461
- <div class="inline-group">
462
- <div style="flex:2">
463
- <label for="serverURL">Remote Server URL</label>
464
- <input type="text" id="serverURL" placeholder="http://remote-server:8086" value="">
465
- </div>
466
- <div style="flex:1">
467
- <label for="authMethod">Auth Method</label>
468
- <input type="text" id="authMethod" placeholder="get" value="get">
469
- </div>
470
- </div>
471
-
472
- <details style="margin-bottom:10px">
473
- <summary style="cursor:pointer; font-size:0.9em; color:#666">Advanced Session Options</summary>
474
- <div style="padding:10px 0">
475
- <label for="authURI">Authentication URI Template (leave blank for default)</label>
476
- <input type="text" id="authURI" placeholder="Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}">
477
- <label for="checkURI">Check Session URI Template</label>
478
- <input type="text" id="checkURI" placeholder="CheckSession">
479
- <label for="cookieName">Cookie Name</label>
480
- <input type="text" id="cookieName" placeholder="SessionID" value="SessionID">
481
- <label for="cookieValueAddr">Cookie Value Address</label>
482
- <input type="text" id="cookieValueAddr" placeholder="SessionID" value="SessionID">
483
- <label for="cookieValueTemplate">Cookie Value Template (overrides Address if set)</label>
484
- <input type="text" id="cookieValueTemplate" placeholder="{~D:Record.SessionID~}">
485
- <label for="loginMarker">Login Marker</label>
486
- <input type="text" id="loginMarker" placeholder="LoggedIn" value="LoggedIn">
487
- </div>
488
- </details>
489
-
490
- <button class="primary" onclick="configureSession()">Configure Session</button>
491
- <div id="sessionConfigStatus"></div>
492
-
493
- <hr style="margin:16px 0; border:none; border-top:1px solid #eee">
494
-
495
- <div class="inline-group">
496
- <div>
497
- <label for="userName">Username</label>
498
- <input type="text" id="userName" placeholder="username">
499
- </div>
500
- <div>
501
- <label for="password">Password</label>
502
- <input type="password" id="password" placeholder="password">
503
- </div>
504
- </div>
505
-
506
- <button class="success" onclick="authenticate()">Authenticate</button>
507
- <button class="secondary" onclick="checkSession()">Check Session</button>
508
- <button class="danger" onclick="deauthenticate()">Deauthenticate</button>
509
- <div id="sessionAuthStatus"></div>
510
- </div>
511
- </div>
512
- </div>
513
-
514
- <!-- ======== Section 3: Schema ======== -->
515
- <div class="accordion-row">
516
- <div class="accordion-number">3</div>
517
- <div class="accordion-card" id="section3" data-section="3">
518
- <div class="accordion-header" onclick="toggleSection('section3')">
519
- <div class="accordion-title">Remote Schema</div>
520
- <span class="accordion-phase" id="phase3"></span>
521
- <div class="accordion-preview" id="preview3">Fetch and select tables from the remote server</div>
522
- <div class="accordion-actions">
523
- <span class="accordion-go" onclick="event.stopPropagation(); goSection3()">go</span>
524
- <label class="accordion-auto" onclick="event.stopPropagation()"><input type="checkbox" id="auto3"> <span class="auto-label">auto</span></label>
525
- </div>
526
- <div class="accordion-toggle">&#9660;</div>
527
- </div>
528
- <div class="accordion-body">
529
- <label for="schemaURL">Schema URL (leave blank for default: /1.0/Retold/Models)</label>
530
- <input type="text" id="schemaURL" placeholder="http://remote-server:8086/1.0/Retold/Models">
531
-
532
- <button class="primary" onclick="fetchSchema()">Fetch Schema</button>
533
- <div id="schemaStatus"></div>
534
-
535
- <div id="tableSelection" style="display:none">
536
- <h3 style="margin:12px 0 8px; font-size:1em;">Select Tables</h3>
537
- <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px">
538
- <input type="text" id="tableFilter" placeholder="Filter tables..." style="flex:1; margin-bottom:0" oninput="filterTableList()">
539
- <button class="secondary" onclick="selectAllTables(true)" style="font-size:0.8em">Select All</button>
540
- <button class="secondary" onclick="selectAllTables(false)" style="font-size:0.8em">Deselect All</button>
541
- <span id="tableSelectionCount" style="font-size:0.85em; color:#666; white-space:nowrap"></span>
542
- </div>
543
- <div id="tableList" class="table-list"></div>
544
- </div>
545
- </div>
546
- </div>
547
- </div>
548
-
549
- <!-- ======== Section 4: Deploy ======== -->
550
- <div class="accordion-row">
551
- <div class="accordion-number">4</div>
552
- <div class="accordion-card" id="section4" data-section="4">
553
- <div class="accordion-header" onclick="toggleSection('section4')">
554
- <div class="accordion-title">Deploy Schema</div>
555
- <span class="accordion-phase" id="phase4"></span>
556
- <div class="accordion-preview" id="preview4">Create selected tables in the local database</div>
557
- <div class="accordion-actions">
558
- <span class="accordion-go" onclick="event.stopPropagation(); goSection4()">go</span>
559
- <label class="accordion-auto" onclick="event.stopPropagation()"><input type="checkbox" id="auto4"> <span class="auto-label">auto</span></label>
560
- </div>
561
- <div class="accordion-toggle">&#9660;</div>
562
- </div>
563
- <div class="accordion-body">
564
- <p style="font-size:0.9em; color:#666; margin-bottom:10px">Creates the selected tables in the local database and sets up CRUD endpoints (e.g. GET /1.0/Documents).</p>
565
- <button class="primary" onclick="deploySchema()">Deploy Selected Tables</button>
566
- <button class="danger" onclick="resetDatabase()">Reset Database</button>
567
- <div id="deployStatus"></div>
568
- </div>
569
- </div>
570
- </div>
571
-
572
- <!-- ======== Section 5: Sync ======== -->
573
- <div class="accordion-row">
574
- <div class="accordion-number">5</div>
575
- <div class="accordion-card" id="section5" data-section="5">
576
- <div class="accordion-header" onclick="toggleSection('section5')">
577
- <div class="accordion-title">Synchronize Data</div>
578
- <span class="accordion-phase" id="phase5"></span>
579
- <div class="accordion-preview" id="preview5">Initial sync, page size 100</div>
580
- <div class="accordion-actions">
581
- <span class="accordion-go" onclick="event.stopPropagation(); goSection5()">go</span>
582
- <label class="accordion-auto" onclick="event.stopPropagation()"><input type="checkbox" id="auto5"> <span class="auto-label">auto</span></label>
583
- </div>
584
- <div class="accordion-toggle">&#9660;</div>
585
- </div>
586
- <div class="accordion-body">
587
- <div style="display:flex; gap:8px; align-items:flex-end; margin-bottom:4px">
588
- <div style="flex:0 0 150px">
589
- <label for="pageSize">Page Size</label>
590
- <input type="number" id="pageSize" value="100" min="1" max="10000" style="margin-bottom:0">
591
- </div>
592
- <div style="flex:0 0 220px">
593
- <label for="dateTimePrecisionMS">Timestamp Precision (ms)</label>
594
- <input type="number" id="dateTimePrecisionMS" value="1000" min="0" max="60000" style="margin-bottom:0">
595
- </div>
596
- <div style="flex:0 0 auto; display:flex; gap:8px">
597
- <button class="success" style="margin:0" onclick="startSync()">Start Sync</button>
598
- <button class="danger" style="margin:0" onclick="stopSync()">Stop Sync</button>
599
- </div>
600
- </div>
601
- <div style="font-size:0.8em; color:#888; margin-bottom:10px; padding-left:158px">Cross-DB tolerance for date comparison (default: 1000ms)</div>
602
-
603
- <div style="margin-bottom:10px">
604
- <label style="margin-bottom:6px">Sync Mode</label>
605
- <div style="display:flex; gap:16px; align-items:center">
606
- <label style="font-weight:normal; margin:0; cursor:pointer">
607
- <input type="radio" name="syncMode" id="syncModeInitial" value="Initial" checked> Initial
608
- <span style="color:#888; font-size:0.85em">(full clone — download all records)</span>
609
- </label>
610
- <label style="font-weight:normal; margin:0; cursor:pointer">
611
- <input type="radio" name="syncMode" id="syncModeOngoing" value="Ongoing"> Ongoing
612
- <span style="color:#888; font-size:0.85em">(delta — only new/updated records since last sync)</span>
613
- </label>
614
- </div>
615
- </div>
616
-
617
- <div class="checkbox-row">
618
- <input type="checkbox" id="syncDeletedRecords">
619
- <label for="syncDeletedRecords">Sync deleted records (fetch records marked Deleted=1 on source and mirror locally)</label>
620
- </div>
621
-
622
- <div id="syncStatus"></div>
623
- <div id="syncProgress"></div>
624
-
625
- <!-- Sync Report (appears after sync completes) -->
626
- <div id="syncReportSection" style="display:none; margin-top:16px; padding-top:16px; border-top:2px solid #ddd">
627
- <h3 style="margin:0 0 12px; font-size:1.1em">Sync Report</h3>
628
-
629
- <!-- Summary cards -->
630
- <div id="reportSummaryCards" style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:16px"></div>
631
-
632
- <!-- Anomalies -->
633
- <div id="reportAnomalies" style="margin-bottom:16px"></div>
634
-
635
- <!-- Top tables by duration -->
636
- <div id="reportTopTables" style="margin-bottom:16px"></div>
637
-
638
- <!-- Buttons -->
639
- <div style="display:flex; gap:8px">
640
- <button class="secondary" onclick="downloadReport()">Download Report JSON</button>
641
- <button class="secondary" onclick="copyReport()">Copy Report</button>
642
- </div>
643
- <div id="reportStatus"></div>
644
- </div>
645
- </div>
646
- </div>
647
- </div>
648
-
649
- <!-- ======== Section 6: Export Config ======== -->
650
- <div class="accordion-row">
651
- <div class="accordion-number">6</div>
652
- <div class="accordion-card" id="section6" data-section="6">
653
- <div class="accordion-header" onclick="toggleSection('section6')">
654
- <div class="accordion-title">Export Configuration</div>
655
- <div class="accordion-preview" id="preview6">Generate JSON config for headless cloning</div>
656
- <div class="accordion-toggle">&#9660;</div>
657
- </div>
658
- <div class="accordion-body">
659
- <p style="font-size:0.9em; color:#666; margin-bottom:10px">Generate a JSON config file from your current settings. Use it to run headless clones from the command line.</p>
660
- <div class="inline-group">
661
- <div style="flex:0 0 200px">
662
- <label for="exportMaxRecords">Max Records per Entity</label>
663
- <input type="number" id="exportMaxRecords" value="" min="0" placeholder="0 = unlimited">
664
- </div>
665
- <div style="flex:0 0 auto; display:flex; align-items:flex-end; padding-bottom:2px">
666
- <div class="checkbox-row" style="margin-bottom:0">
667
- <input type="checkbox" id="exportLogFile" checked>
668
- <label for="exportLogFile">Write log file</label>
669
- </div>
670
- </div>
671
- </div>
672
- <div style="display:flex; gap:8px; margin-bottom:10px">
673
- <button class="primary" onclick="generateConfig()">Generate Config</button>
674
- <button class="secondary" onclick="copyConfig()">Copy to Clipboard</button>
675
- <button class="secondary" onclick="downloadConfig()">Download JSON</button>
676
- </div>
677
- <div id="configExportStatus"></div>
678
- <div id="cliCommand" style="display:none; margin-bottom:10px">
679
- <label style="margin-bottom:4px">CLI Command <span style="color:#888; font-weight:normal">(with config file)</span></label>
680
- <div style="background:#1a1a1a; color:#4fc3f7; padding:10px 14px; border-radius:4px; font-family:monospace; font-size:0.9em; word-break:break-all; cursor:pointer" onclick="copyCLI()" title="Click to copy"></div>
681
- </div>
682
- <div id="cliOneShot" style="display:none; margin-bottom:10px">
683
- <label style="margin-bottom:4px">One-liner <span style="color:#888; font-weight:normal">(no config file needed)</span></label>
684
- <div style="background:#1a1a1a; color:#4fc3f7; padding:10px 14px; border-radius:4px; font-family:monospace; font-size:0.9em; word-break:break-all; cursor:pointer; white-space:pre-wrap" onclick="copyOneShot()" title="Click to copy"></div>
685
- </div>
686
- <textarea id="configOutput" style="display:none; width:100%; min-height:300px; font-family:monospace; font-size:0.85em; padding:10px; border:1px solid #ccc; border-radius:4px; background:#fafafa; tab-size:4; resize:vertical" readonly></textarea>
687
-
688
- <div id="mdwintExport" style="display:none; margin-top:16px; padding-top:16px; border-top:1px solid #eee">
689
- <h3 style="margin:0 0 8px; font-size:1em">meadow-integration CLI <span style="color:#888; font-weight:normal; font-size:0.85em">(mdwint clone)</span></h3>
690
- <p style="font-size:0.85em; color:#666; margin-bottom:8px">Save as <code>.meadow.config.json</code> in your project root, then run the command below. Requires a local Meadow extended schema JSON file.</p>
691
- <div style="display:flex; gap:8px; margin-bottom:10px">
692
- <button class="secondary" onclick="copyMdwintConfig()">Copy Config</button>
693
- <button class="secondary" onclick="downloadMdwintConfig()">Download .meadow.config.json</button>
694
- </div>
695
- <div id="mdwintCLICommand" style="margin-bottom:10px">
696
- <label style="margin-bottom:4px">CLI Command</label>
697
- <div style="background:#1a1a1a; color:#4fc3f7; padding:10px 14px; border-radius:4px; font-family:monospace; font-size:0.9em; word-break:break-all; cursor:pointer" onclick="copyMdwintCLI()" title="Click to copy"></div>
698
- </div>
699
- <div id="mdwintConfigStatus"></div>
700
- <textarea id="mdwintConfigOutput" style="width:100%; min-height:250px; font-family:monospace; font-size:0.85em; padding:10px; border:1px solid #ccc; border-radius:4px; background:#fafafa; tab-size:4; resize:vertical" readonly></textarea>
701
- </div>
702
- </div>
703
- </div>
704
- </div>
705
-
706
- <!-- ======== Section 7: View Data ======== -->
707
- <div class="accordion-row">
708
- <div class="accordion-number">7</div>
709
- <div class="accordion-card" id="section7" data-section="7">
710
- <div class="accordion-header" onclick="toggleSection('section7')">
711
- <div class="accordion-title">View Data</div>
712
- <div class="accordion-preview" id="preview7">Browse synced table data</div>
713
- <div class="accordion-toggle">&#9660;</div>
714
- </div>
715
- <div class="accordion-body">
716
- <div class="inline-group">
717
- <div style="flex:1">
718
- <label for="viewTable">Table</label>
719
- <select id="viewTable">
720
- <option value="">— deploy tables first —</option>
721
- </select>
722
- </div>
723
- <div style="flex:0 0 120px">
724
- <label for="viewLimit">Max Rows</label>
725
- <input type="number" id="viewLimit" value="100" min="1" max="10000">
726
- </div>
727
- <div style="flex:0 0 auto; display:flex; align-items:flex-end">
728
- <button class="primary" onclick="loadTableData()">Load</button>
729
- </div>
730
- </div>
731
- <div id="viewStatus"></div>
732
- <div id="viewDataContainer" style="overflow-x:auto; margin-top:10px"></div>
733
- </div>
734
- </div>
735
- </div>
736
-
737
- <script>
738
- // ================================================================
739
- // Accordion — Toggle / Expand / Collapse / Previews
740
- // ================================================================
741
-
742
- function toggleSection(pSectionId)
743
- {
744
- var tmpCard = document.getElementById(pSectionId);
745
- if (!tmpCard) return;
746
- tmpCard.classList.toggle('open');
747
- }
748
-
749
- function expandAllSections()
750
- {
751
- var tmpCards = document.querySelectorAll('.accordion-card');
752
- for (var i = 0; i < tmpCards.length; i++)
753
- {
754
- tmpCards[i].classList.add('open');
755
- }
756
- }
757
-
758
- function collapseAllSections()
759
- {
760
- var tmpCards = document.querySelectorAll('.accordion-card');
761
- for (var i = 0; i < tmpCards.length; i++)
762
- {
763
- tmpCards[i].classList.remove('open');
764
- }
765
- }
766
-
767
- // ================================================================
768
- // Phase status indicators — show check / x / spinner on each header
769
- // ================================================================
770
-
771
- /**
772
- * Set the phase indicator for a section.
773
- * @param {number} pSection — section number (1–5)
774
- * @param {string} pState — 'ok' | 'error' | 'busy' | 'none'
775
- */
776
- function setSectionPhase(pSection, pState)
777
- {
778
- var tmpEl = document.getElementById('phase' + pSection);
779
- if (!tmpEl) return;
780
-
781
- tmpEl.className = 'accordion-phase';
782
-
783
- if (pState === 'ok')
784
- {
785
- tmpEl.innerHTML = '&#10003;';
786
- tmpEl.classList.add('visible', 'accordion-phase-ok');
787
- }
788
- else if (pState === 'error')
789
- {
790
- tmpEl.innerHTML = '&#10007;';
791
- tmpEl.classList.add('visible', 'accordion-phase-error');
792
- }
793
- else if (pState === 'busy')
794
- {
795
- tmpEl.innerHTML = '<span class="phase-spinner"></span>';
796
- tmpEl.classList.add('visible', 'accordion-phase-busy');
797
- }
798
- else
799
- {
800
- tmpEl.innerHTML = '';
801
- }
802
- }
803
-
804
- /**
805
- * Build a short italic description for each collapsed section
806
- * based on current form values.
807
- */
808
- function updateAllPreviews()
809
- {
810
- // Section 1 — Database Connection
811
- var tmpProvider = document.getElementById('connProvider').value;
812
- var tmpPreview1 = tmpProvider;
813
- if (tmpProvider === 'SQLite')
814
- {
815
- var tmpPath = document.getElementById('sqliteFilePath').value || 'data/cloned.sqlite';
816
- tmpPreview1 = 'SQLite at ' + tmpPath;
817
- }
818
- else if (tmpProvider === 'MySQL')
819
- {
820
- var tmpHost = document.getElementById('mysqlServer').value || '127.0.0.1';
821
- var tmpPort = document.getElementById('mysqlPort').value || '3306';
822
- var tmpUser = document.getElementById('mysqlUser').value || 'root';
823
- tmpPreview1 = 'MySQL on ' + tmpHost + ':' + tmpPort + ' as ' + tmpUser;
824
- }
825
- else if (tmpProvider === 'MSSQL')
826
- {
827
- var tmpHost = document.getElementById('mssqlServer').value || '127.0.0.1';
828
- var tmpPort = document.getElementById('mssqlPort').value || '1433';
829
- var tmpUser = document.getElementById('mssqlUser').value || 'sa';
830
- tmpPreview1 = 'MSSQL on ' + tmpHost + ':' + tmpPort + ' as ' + tmpUser;
831
- }
832
- else if (tmpProvider === 'PostgreSQL')
833
- {
834
- var tmpHost = document.getElementById('postgresqlHost').value || '127.0.0.1';
835
- var tmpPort = document.getElementById('postgresqlPort').value || '5432';
836
- var tmpUser = document.getElementById('postgresqlUser').value || 'postgres';
837
- tmpPreview1 = 'PostgreSQL on ' + tmpHost + ':' + tmpPort + ' as ' + tmpUser;
838
- }
839
- else if (tmpProvider === 'MongoDB')
840
- {
841
- var tmpHost = document.getElementById('mongodbHost').value || '127.0.0.1';
842
- var tmpPort = document.getElementById('mongodbPort').value || '27017';
843
- tmpPreview1 = 'MongoDB on ' + tmpHost + ':' + tmpPort;
844
- }
845
- else if (tmpProvider === 'Solr')
846
- {
847
- var tmpHost = document.getElementById('solrHost').value || '127.0.0.1';
848
- var tmpPort = document.getElementById('solrPort').value || '8983';
849
- tmpPreview1 = 'Solr on ' + tmpHost + ':' + tmpPort;
850
- }
851
- else if (tmpProvider === 'RocksDB')
852
- {
853
- var tmpFolder = document.getElementById('rocksdbFolder').value || 'data/rocksdb';
854
- tmpPreview1 = 'RocksDB at ' + tmpFolder;
855
- }
856
- else if (tmpProvider === 'Bibliograph')
857
- {
858
- var tmpFolder = document.getElementById('bibliographFolder').value || 'data/bibliograph';
859
- tmpPreview1 = 'Bibliograph at ' + tmpFolder;
860
- }
861
- document.getElementById('preview1').textContent = tmpPreview1;
862
-
863
- // Section 2 — Remote Session
864
- var tmpServerURL = document.getElementById('serverURL').value;
865
- var tmpUserName = document.getElementById('userName').value;
866
- if (tmpServerURL)
867
- {
868
- var tmpPreview2 = tmpServerURL;
869
- if (tmpUserName) tmpPreview2 += ' as ' + tmpUserName;
870
- document.getElementById('preview2').textContent = tmpPreview2;
871
- }
872
- else
873
- {
874
- document.getElementById('preview2').textContent = 'Configure remote server URL and credentials';
875
- }
876
-
877
- // Section 3 — Remote Schema
878
- var tmpSchemaURL = document.getElementById('schemaURL').value;
879
- var tmpTableChecks = document.querySelectorAll('#schemaTableList input[type="checkbox"]:checked');
880
- if (tmpTableChecks.length > 0)
881
- {
882
- document.getElementById('preview3').textContent = tmpTableChecks.length + ' table' + (tmpTableChecks.length === 1 ? '' : 's') + ' selected';
883
- }
884
- else if (tmpSchemaURL)
885
- {
886
- document.getElementById('preview3').textContent = 'Schema from ' + tmpSchemaURL;
887
- }
888
- else
889
- {
890
- document.getElementById('preview3').textContent = 'Fetch and select tables from the remote server';
891
- }
892
-
893
- // Section 4 — Deploy Schema
894
- var tmpDeployedEl = document.getElementById('deployStatus');
895
- var tmpDeployedText = tmpDeployedEl ? tmpDeployedEl.textContent : '';
896
- if (tmpDeployedText && tmpDeployedText.indexOf('deployed') !== -1)
897
- {
898
- document.getElementById('preview4').textContent = tmpDeployedText;
899
- }
900
- else
901
- {
902
- document.getElementById('preview4').textContent = 'Create selected tables in the local database';
903
- }
904
-
905
- // Section 5 — Synchronize Data
906
- var tmpSyncMode = document.querySelector('input[name="syncMode"]:checked');
907
- var tmpModeName = tmpSyncMode ? tmpSyncMode.value : 'Initial';
908
- var tmpPageSize = document.getElementById('pageSize').value || '100';
909
- var tmpSyncPreview = tmpModeName + ' sync, page size ' + tmpPageSize;
910
- var tmpDeleted = document.getElementById('syncDeletedRecords').checked;
911
- if (tmpDeleted) tmpSyncPreview += ', including deleted';
912
- document.getElementById('preview5').textContent = tmpSyncPreview;
913
-
914
- // Section 6 — Export Configuration
915
- var tmpMaxRecords = document.getElementById('exportMaxRecords').value;
916
- var tmpLogFile = document.getElementById('exportLogFile').checked;
917
- var tmpExportParts = [];
918
- if (tmpMaxRecords && parseInt(tmpMaxRecords, 10) > 0) tmpExportParts.push('max ' + tmpMaxRecords + ' records');
919
- if (tmpLogFile) tmpExportParts.push('log enabled');
920
- else tmpExportParts.push('log disabled');
921
- document.getElementById('preview6').textContent = tmpExportParts.length > 0 ? 'Export: ' + tmpExportParts.join(', ') : 'Generate JSON config for headless cloning';
922
-
923
- // Section 7 — View Data
924
- var tmpViewTable = document.getElementById('viewTable').value;
925
- if (tmpViewTable)
926
- {
927
- document.getElementById('preview7').textContent = 'Viewing ' + tmpViewTable;
928
- }
929
- else
930
- {
931
- document.getElementById('preview7').textContent = 'Browse synced table data';
932
- }
933
- }
934
-
935
- /**
936
- * Wire change listeners to all accordion-relevant fields to keep previews fresh.
937
- */
938
- function initAccordionPreviews()
939
- {
940
- var tmpPreviewFields = [
941
- 'connProvider', 'sqliteFilePath',
942
- 'mysqlServer', 'mysqlPort', 'mysqlUser',
943
- 'mssqlServer', 'mssqlPort', 'mssqlUser',
944
- 'postgresqlHost', 'postgresqlPort', 'postgresqlUser',
945
- 'mongodbHost', 'mongodbPort',
946
- 'solrHost', 'solrPort',
947
- 'rocksdbFolder', 'bibliographFolder',
948
- 'serverURL', 'userName',
949
- 'schemaURL',
950
- 'pageSize', 'dateTimePrecisionMS',
951
- 'exportMaxRecords',
952
- 'viewTable', 'viewLimit'
953
- ];
954
-
955
- for (var i = 0; i < tmpPreviewFields.length; i++)
956
- {
957
- (function(pId)
958
- {
959
- var tmpEl = document.getElementById(pId);
960
- if (tmpEl)
961
- {
962
- tmpEl.addEventListener('input', updateAllPreviews);
963
- tmpEl.addEventListener('change', updateAllPreviews);
964
- }
965
- })(tmpPreviewFields[i]);
966
- }
967
-
968
- // Checkboxes and radios
969
- var tmpCheckboxes = ['syncDeletedRecords', 'exportLogFile'];
970
- for (var i = 0; i < tmpCheckboxes.length; i++)
971
- {
972
- var tmpEl = document.getElementById(tmpCheckboxes[i]);
973
- if (tmpEl) tmpEl.addEventListener('change', updateAllPreviews);
974
- }
975
-
976
- document.querySelectorAll('input[name="syncMode"]').forEach(function(pEl)
977
- {
978
- pEl.addEventListener('change', updateAllPreviews);
979
- });
980
- }
981
-
982
- // ================================================================
983
- // Accordion "Go" shortcuts — run the phase action from collapsed bar
984
- // ================================================================
985
-
986
- function goSection1() { connectProvider(); }
987
-
988
- function goSection2()
989
- {
990
- // Two-step: configure session, then authenticate
991
- setSectionPhase(2, 'busy');
992
- configureSession();
993
- setTimeout(function() { authenticate(); }, 1500);
994
- }
995
-
996
- function goSection3() { fetchSchema(); }
997
- function goSection4() { deploySchema(); }
998
- function goSection5() { startSync(); }
999
-
1000
- // ================================================================
1001
- // Auto-Process — run pipeline phases automatically on page load
1002
- // ================================================================
1003
-
1004
- var _ServerBusyAtLoad = false;
1005
-
1006
- function initAutoProcess()
1007
- {
1008
- // Check if the server is already busy (syncing/stopping)
1009
- api('GET', '/clone/sync/live-status')
1010
- .then(function(pData)
1011
- {
1012
- if (pData.Phase === 'syncing' || pData.Phase === 'stopping')
1013
- {
1014
- _ServerBusyAtLoad = true;
1015
- setSectionPhase(5, 'busy');
1016
- startPolling();
1017
- return;
1018
- }
1019
- runAutoProcessChain();
1020
- })
1021
- .catch(function()
1022
- {
1023
- // Server unreachable — don't auto-process
1024
- });
1025
- }
1026
-
1027
- function runAutoProcessChain()
1028
- {
1029
- var tmpDelay = 0;
1030
- var tmpStepDelay = 2000;
1031
-
1032
- if (document.getElementById('auto1') && document.getElementById('auto1').checked)
1033
- {
1034
- setTimeout(function() { goSection1(); }, tmpDelay);
1035
- tmpDelay += tmpStepDelay;
1036
- }
1037
- if (document.getElementById('auto2') && document.getElementById('auto2').checked)
1038
- {
1039
- setTimeout(function() { goSection2(); }, tmpDelay);
1040
- tmpDelay += tmpStepDelay + 1500; // extra for the two-step auth
1041
- }
1042
- if (document.getElementById('auto3') && document.getElementById('auto3').checked)
1043
- {
1044
- setTimeout(function() { goSection3(); }, tmpDelay);
1045
- tmpDelay += tmpStepDelay;
1046
- }
1047
- if (document.getElementById('auto4') && document.getElementById('auto4').checked)
1048
- {
1049
- setTimeout(function() { goSection4(); }, tmpDelay);
1050
- tmpDelay += tmpStepDelay;
1051
- }
1052
- if (document.getElementById('auto5') && document.getElementById('auto5').checked)
1053
- {
1054
- setTimeout(function() { goSection5(); }, tmpDelay);
1055
- }
1056
- }
1057
-
1058
- // ================================================================
1059
- // LocalStorage Persistence
1060
- // ================================================================
1061
-
1062
- var _PersistFields = [
1063
- 'serverURL', 'authMethod', 'authURI', 'checkURI',
1064
- 'cookieName', 'cookieValueAddr', 'cookieValueTemplate', 'loginMarker',
1065
- 'userName', 'password', 'schemaURL', 'pageSize', 'dateTimePrecisionMS',
1066
- 'connProvider', 'sqliteFilePath',
1067
- 'mysqlServer', 'mysqlPort', 'mysqlUser', 'mysqlPassword', 'mysqlDatabase', 'mysqlConnectionLimit',
1068
- 'mssqlServer', 'mssqlPort', 'mssqlUser', 'mssqlPassword', 'mssqlDatabase', 'mssqlConnectionLimit',
1069
- 'postgresqlHost', 'postgresqlPort', 'postgresqlUser', 'postgresqlPassword', 'postgresqlDatabase', 'postgresqlConnectionLimit',
1070
- 'solrHost', 'solrPort', 'solrCore', 'solrPath',
1071
- 'mongodbHost', 'mongodbPort', 'mongodbUser', 'mongodbPassword', 'mongodbDatabase', 'mongodbConnectionLimit',
1072
- 'rocksdbFolder',
1073
- 'bibliographFolder'
1074
- ];
1075
-
1076
- function saveField(pFieldId)
1077
- {
1078
- var el = document.getElementById(pFieldId);
1079
- if (el)
1080
- {
1081
- localStorage.setItem('dataCloner_' + pFieldId, el.value);
1082
- }
1083
- }
1084
-
1085
- function restoreFields()
1086
- {
1087
- for (var i = 0; i < _PersistFields.length; i++)
1088
- {
1089
- var tmpId = _PersistFields[i];
1090
- var tmpSaved = localStorage.getItem('dataCloner_' + tmpId);
1091
- if (tmpSaved !== null)
1092
- {
1093
- var el = document.getElementById(tmpId);
1094
- if (el) el.value = tmpSaved;
1095
- }
1096
- }
1097
-
1098
- // Restore checkbox state
1099
- var tmpSyncDeleted = localStorage.getItem('dataCloner_syncDeletedRecords');
1100
- if (tmpSyncDeleted !== null)
1101
- {
1102
- document.getElementById('syncDeletedRecords').checked = tmpSyncDeleted === 'true';
1103
- }
1104
- // Restore sync mode
1105
- var tmpSyncMode = localStorage.getItem('dataCloner_syncMode');
1106
- if (tmpSyncMode === 'Ongoing')
1107
- {
1108
- document.getElementById('syncModeOngoing').checked = true;
1109
- }
1110
- var tmpSolrSecure = localStorage.getItem('dataCloner_solrSecure');
1111
- if (tmpSolrSecure !== null)
1112
- {
1113
- document.getElementById('solrSecure').checked = tmpSolrSecure === 'true';
1114
- }
1115
- }
1116
-
1117
- // Attach change listeners to all persisted fields
1118
- function initPersistence()
1119
- {
1120
- restoreFields();
1121
- for (var i = 0; i < _PersistFields.length; i++)
1122
- {
1123
- (function(pId)
1124
- {
1125
- var el = document.getElementById(pId);
1126
- if (el)
1127
- {
1128
- el.addEventListener('input', function() { saveField(pId); });
1129
- el.addEventListener('change', function() { saveField(pId); });
1130
- }
1131
- })(_PersistFields[i]);
1132
- }
1133
-
1134
- // Persist sync deleted checkbox
1135
- document.getElementById('syncDeletedRecords').addEventListener('change', function()
1136
- {
1137
- localStorage.setItem('dataCloner_syncDeletedRecords', this.checked);
1138
- });
1139
-
1140
- // Persist sync mode radio
1141
- document.querySelectorAll('input[name="syncMode"]').forEach(function(pEl)
1142
- {
1143
- pEl.addEventListener('change', function()
1144
- {
1145
- localStorage.setItem('dataCloner_syncMode', this.value);
1146
- });
1147
- });
1148
-
1149
- // Persist solr secure checkbox
1150
- document.getElementById('solrSecure').addEventListener('change', function()
1151
- {
1152
- localStorage.setItem('dataCloner_solrSecure', this.checked);
1153
- });
1154
-
1155
- // Persist auto-process checkboxes
1156
- var tmpAutoIds = ['auto1', 'auto2', 'auto3', 'auto4', 'auto5'];
1157
- for (var a = 0; a < tmpAutoIds.length; a++)
1158
- {
1159
- (function(pId)
1160
- {
1161
- var tmpEl = document.getElementById(pId);
1162
- if (tmpEl)
1163
- {
1164
- var tmpSaved = localStorage.getItem('dataCloner_' + pId);
1165
- if (tmpSaved !== null) tmpEl.checked = tmpSaved === 'true';
1166
- tmpEl.addEventListener('change', function()
1167
- {
1168
- localStorage.setItem('dataCloner_' + pId, this.checked);
1169
- });
1170
- }
1171
- })(tmpAutoIds[a]);
1172
- }
1173
- }
1174
-
1175
- // ================================================================
1176
- // API Helper
1177
- // ================================================================
1178
-
1179
- function api(method, path, body)
1180
- {
1181
- var opts = { method: method, headers: {} };
1182
- if (body)
1183
- {
1184
- opts.headers['Content-Type'] = 'application/json';
1185
- opts.body = JSON.stringify(body);
1186
- }
1187
- return fetch(path, opts).then(function(r) { return r.json(); });
1188
- }
1189
-
1190
- function setStatus(elementId, message, type)
1191
- {
1192
- var el = document.getElementById(elementId);
1193
- el.className = 'status ' + (type || 'info');
1194
- el.textContent = message;
1195
- el.style.display = 'block';
1196
- }
1197
-
1198
- // ================================================================
1199
- // Connection Management
1200
- // ================================================================
1201
-
1202
- function onProviderChange()
1203
- {
1204
- var provider = document.getElementById('connProvider').value;
1205
- var tmpProviders = ['SQLite', 'MySQL', 'MSSQL', 'PostgreSQL', 'Solr', 'MongoDB', 'RocksDB', 'Bibliograph'];
1206
- for (var i = 0; i < tmpProviders.length; i++)
1207
- {
1208
- var tmpEl = document.getElementById('config' + tmpProviders[i]);
1209
- if (tmpEl) tmpEl.style.display = (provider === tmpProviders[i]) ? '' : 'none';
1210
- }
1211
- saveField('connProvider');
1212
- }
1213
-
1214
- function getProviderConfig()
1215
- {
1216
- var provider = document.getElementById('connProvider').value;
1217
- var config = {};
1218
-
1219
- if (provider === 'SQLite')
1220
- {
1221
- config.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || 'data/cloned.sqlite';
1222
- }
1223
- else if (provider === 'MySQL')
1224
- {
1225
- config.host = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
1226
- config.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
1227
- config.user = document.getElementById('mysqlUser').value.trim() || 'root';
1228
- config.password = document.getElementById('mysqlPassword').value;
1229
- config.database = document.getElementById('mysqlDatabase').value.trim();
1230
- config.connectionLimit = parseInt(document.getElementById('mysqlConnectionLimit').value, 10) || 20;
1231
- }
1232
- else if (provider === 'MSSQL')
1233
- {
1234
- config.server = document.getElementById('mssqlServer').value.trim() || '127.0.0.1';
1235
- config.port = parseInt(document.getElementById('mssqlPort').value, 10) || 1433;
1236
- config.user = document.getElementById('mssqlUser').value.trim() || 'sa';
1237
- config.password = document.getElementById('mssqlPassword').value;
1238
- config.database = document.getElementById('mssqlDatabase').value.trim();
1239
- config.connectionLimit = parseInt(document.getElementById('mssqlConnectionLimit').value, 10) || 20;
1240
- }
1241
- else if (provider === 'PostgreSQL')
1242
- {
1243
- config.host = document.getElementById('postgresqlHost').value.trim() || '127.0.0.1';
1244
- config.port = parseInt(document.getElementById('postgresqlPort').value, 10) || 5432;
1245
- config.user = document.getElementById('postgresqlUser').value.trim() || 'postgres';
1246
- config.password = document.getElementById('postgresqlPassword').value;
1247
- config.database = document.getElementById('postgresqlDatabase').value.trim();
1248
- config.max = parseInt(document.getElementById('postgresqlConnectionLimit').value, 10) || 10;
1249
- }
1250
- else if (provider === 'Solr')
1251
- {
1252
- config.host = document.getElementById('solrHost').value.trim() || 'localhost';
1253
- config.port = parseInt(document.getElementById('solrPort').value, 10) || 8983;
1254
- config.core = document.getElementById('solrCore').value.trim() || 'default';
1255
- config.path = document.getElementById('solrPath').value.trim() || '/solr';
1256
- config.secure = document.getElementById('solrSecure').checked;
1257
- }
1258
- else if (provider === 'MongoDB')
1259
- {
1260
- config.host = document.getElementById('mongodbHost').value.trim() || '127.0.0.1';
1261
- config.port = parseInt(document.getElementById('mongodbPort').value, 10) || 27017;
1262
- config.user = document.getElementById('mongodbUser').value.trim();
1263
- config.password = document.getElementById('mongodbPassword').value;
1264
- config.database = document.getElementById('mongodbDatabase').value.trim() || 'test';
1265
- config.maxPoolSize = parseInt(document.getElementById('mongodbConnectionLimit').value, 10) || 10;
1266
- }
1267
- else if (provider === 'RocksDB')
1268
- {
1269
- config.RocksDBFolder = document.getElementById('rocksdbFolder').value.trim() || 'data/rocksdb';
1270
- }
1271
- else if (provider === 'Bibliograph')
1272
- {
1273
- config.StorageFolder = document.getElementById('bibliographFolder').value.trim() || 'data/bibliograph';
1274
- }
1275
-
1276
- return { Provider: provider, Config: config };
1277
- }
1278
-
1279
- function connectProvider()
1280
- {
1281
- var connInfo = getProviderConfig();
1282
-
1283
- setSectionPhase(1, 'busy');
1284
- setStatus('connectionStatus', 'Connecting to ' + connInfo.Provider + '...', 'info');
1285
-
1286
- api('POST', '/clone/connection/configure', connInfo)
1287
- .then(function(data)
1288
- {
1289
- if (data.Success)
1290
- {
1291
- setStatus('connectionStatus', data.Message, 'ok');
1292
- setSectionPhase(1, 'ok');
1293
- }
1294
- else
1295
- {
1296
- setStatus('connectionStatus', 'Connection failed: ' + (data.Error || 'Unknown error'), 'error');
1297
- setSectionPhase(1, 'error');
1298
- }
1299
- })
1300
- .catch(function(err)
1301
- {
1302
- setStatus('connectionStatus', 'Request failed: ' + err.message, 'error');
1303
- setSectionPhase(1, 'error');
1304
- });
1305
- }
1306
-
1307
- function testConnection()
1308
- {
1309
- var connInfo = getProviderConfig();
1310
-
1311
- setStatus('connectionStatus', 'Testing ' + connInfo.Provider + ' connection...', 'info');
1312
-
1313
- api('POST', '/clone/connection/test', connInfo)
1314
- .then(function(data)
1315
- {
1316
- if (data.Success)
1317
- {
1318
- setStatus('connectionStatus', data.Message, 'ok');
1319
- }
1320
- else
1321
- {
1322
- setStatus('connectionStatus', 'Test failed: ' + (data.Error || 'Unknown error'), 'error');
1323
- }
1324
- })
1325
- .catch(function(err)
1326
- {
1327
- setStatus('connectionStatus', 'Request failed: ' + err.message, 'error');
1328
- });
1329
- }
1330
-
1331
- function checkConnectionStatus()
1332
- {
1333
- api('GET', '/clone/connection/status')
1334
- .then(function(data)
1335
- {
1336
- if (data.Connected)
1337
- {
1338
- setStatus('connectionStatus', 'Connected: ' + data.Provider, 'ok');
1339
- setSectionPhase(1, 'ok');
1340
- }
1341
- })
1342
- .catch(function() { /* ignore */ });
1343
- }
1344
-
1345
- // ================================================================
1346
- // Session Management
1347
- // ================================================================
1348
-
1349
- function configureSession()
1350
- {
1351
- var serverURL = document.getElementById('serverURL').value.trim();
1352
- if (!serverURL)
1353
- {
1354
- setStatus('sessionConfigStatus', 'Server URL is required.', 'error');
1355
- return;
1356
- }
1357
-
1358
- var body = { ServerURL: serverURL.replace(/\/+$/, '') + '/1.0/' };
1359
-
1360
- var authMethod = document.getElementById('authMethod').value.trim();
1361
- if (authMethod) body.AuthenticationMethod = authMethod;
1362
-
1363
- var authURI = document.getElementById('authURI').value.trim();
1364
- if (authURI) body.AuthenticationURITemplate = authURI;
1365
-
1366
- var checkURI = document.getElementById('checkURI').value.trim();
1367
- if (checkURI) body.CheckSessionURITemplate = checkURI;
1368
-
1369
- var cookieName = document.getElementById('cookieName').value.trim();
1370
- if (cookieName) body.CookieName = cookieName;
1371
-
1372
- var cookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
1373
- if (cookieValueAddr) body.CookieValueAddress = cookieValueAddr;
1374
-
1375
- var cookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
1376
- if (cookieValueTemplate) body.CookieValueTemplate = cookieValueTemplate;
1377
-
1378
- var loginMarker = document.getElementById('loginMarker').value.trim();
1379
- if (loginMarker) body.CheckSessionLoginMarker = loginMarker;
1380
-
1381
- setStatus('sessionConfigStatus', 'Configuring session...', 'info');
1382
-
1383
- api('POST', '/clone/session/configure', body)
1384
- .then(function(data)
1385
- {
1386
- if (data.Success)
1387
- {
1388
- setStatus('sessionConfigStatus', 'Session configured for ' + data.ServerURL + ' (domain: ' + data.DomainMatch + ')', 'ok');
1389
- }
1390
- else
1391
- {
1392
- setStatus('sessionConfigStatus', 'Configuration failed: ' + (data.Error || 'Unknown error'), 'error');
1393
- }
1394
- })
1395
- .catch(function(err)
1396
- {
1397
- setStatus('sessionConfigStatus', 'Request failed: ' + err.message, 'error');
1398
- });
1399
- }
1400
-
1401
- function authenticate()
1402
- {
1403
- var userName = document.getElementById('userName').value.trim();
1404
- var password = document.getElementById('password').value.trim();
1405
-
1406
- if (!userName || !password)
1407
- {
1408
- setStatus('sessionAuthStatus', 'Username and password are required.', 'error');
1409
- setSectionPhase(2, 'error');
1410
- return;
1411
- }
1412
-
1413
- setSectionPhase(2, 'busy');
1414
- setStatus('sessionAuthStatus', 'Authenticating...', 'info');
1415
-
1416
- api('POST', '/clone/session/authenticate', { UserName: userName, Password: password })
1417
- .then(function(data)
1418
- {
1419
- if (data.Success && data.Authenticated)
1420
- {
1421
- setStatus('sessionAuthStatus', 'Authenticated successfully.', 'ok');
1422
- setSectionPhase(2, 'ok');
1423
- }
1424
- else
1425
- {
1426
- setStatus('sessionAuthStatus', 'Authentication failed: ' + (data.Error || 'Not authenticated'), 'error');
1427
- setSectionPhase(2, 'error');
1428
- }
1429
- })
1430
- .catch(function(err)
1431
- {
1432
- setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
1433
- setSectionPhase(2, 'error');
1434
- });
1435
- }
1436
-
1437
- function checkSession()
1438
- {
1439
- setStatus('sessionAuthStatus', 'Checking session...', 'info');
1440
-
1441
- api('GET', '/clone/session/check')
1442
- .then(function(data)
1443
- {
1444
- if (data.Authenticated)
1445
- {
1446
- setStatus('sessionAuthStatus', 'Session is active. Server: ' + (data.ServerURL || 'N/A'), 'ok');
1447
- }
1448
- else if (data.Configured)
1449
- {
1450
- setStatus('sessionAuthStatus', 'Session configured but not authenticated.', 'warn');
1451
- }
1452
- else
1453
- {
1454
- setStatus('sessionAuthStatus', 'No session configured.', 'warn');
1455
- }
1456
- })
1457
- .catch(function(err)
1458
- {
1459
- setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
1460
- });
1461
- }
1462
-
1463
- function deauthenticate()
1464
- {
1465
- api('POST', '/clone/session/deauthenticate')
1466
- .then(function(data)
1467
- {
1468
- setStatus('sessionAuthStatus', 'Session deauthenticated.', 'info');
1469
- })
1470
- .catch(function(err)
1471
- {
1472
- setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
1473
- });
1474
- }
1475
-
1476
- // ================================================================
1477
- // Schema Management
1478
- // ================================================================
1479
-
1480
- var _FetchedTables = [];
1481
-
1482
- function fetchSchema()
1483
- {
1484
- var schemaURL = document.getElementById('schemaURL').value.trim();
1485
- var body = {};
1486
- if (schemaURL) body.SchemaURL = schemaURL;
1487
-
1488
- setSectionPhase(3, 'busy');
1489
- setStatus('schemaStatus', 'Fetching schema...', 'info');
1490
-
1491
- api('POST', '/clone/schema/fetch', body)
1492
- .then(function(data)
1493
- {
1494
- if (data.Success)
1495
- {
1496
- _FetchedTables = data.Tables || [];
1497
- setStatus('schemaStatus', 'Fetched ' + data.TableCount + ' tables from ' + data.SchemaURL, 'ok');
1498
- setSectionPhase(3, 'ok');
1499
- renderTableList();
1500
- }
1501
- else
1502
- {
1503
- setStatus('schemaStatus', 'Fetch failed: ' + (data.Error || 'Unknown error'), 'error');
1504
- setSectionPhase(3, 'error');
1505
- }
1506
- })
1507
- .catch(function(err)
1508
- {
1509
- setStatus('schemaStatus', 'Request failed: ' + err.message, 'error');
1510
- setSectionPhase(3, 'error');
1511
- });
1512
- }
1513
-
1514
- function loadSavedSelections()
1515
- {
1516
- try
1517
- {
1518
- var tmpRaw = localStorage.getItem('dataCloner_selectedTables');
1519
- if (tmpRaw) return JSON.parse(tmpRaw);
1520
- }
1521
- catch (e) { /* ignore */ }
1522
- return null;
1523
- }
1524
-
1525
- function saveSelections()
1526
- {
1527
- var tmpSelected = getSelectedTables();
1528
- localStorage.setItem('dataCloner_selectedTables', JSON.stringify(tmpSelected));
1529
- updateSelectionCount();
1530
- updateAllPreviews();
1531
- }
1532
-
1533
- function updateSelectionCount()
1534
- {
1535
- var tmpCount = getSelectedTables().length;
1536
- var tmpEl = document.getElementById('tableSelectionCount');
1537
- if (tmpEl) tmpEl.textContent = tmpCount + ' / ' + _FetchedTables.length + ' selected';
1538
- }
1539
-
1540
- function renderTableList()
1541
- {
1542
- var container = document.getElementById('tableList');
1543
- container.innerHTML = '';
1544
-
1545
- // Load previously saved selections; if none, default to none checked
1546
- var tmpSaved = loadSavedSelections();
1547
- var tmpSavedSet = null;
1548
- if (tmpSaved)
1549
- {
1550
- tmpSavedSet = {};
1551
- for (var s = 0; s < tmpSaved.length; s++) tmpSavedSet[tmpSaved[s]] = true;
1552
- }
1553
-
1554
- for (var i = 0; i < _FetchedTables.length; i++)
1555
- {
1556
- var name = _FetchedTables[i];
1557
- var div = document.createElement('div');
1558
- div.className = 'table-item';
1559
- div.setAttribute('data-table', name.toLowerCase());
1560
-
1561
- var checkbox = document.createElement('input');
1562
- checkbox.type = 'checkbox';
1563
- checkbox.id = 'tbl_' + name;
1564
- checkbox.value = name;
1565
- // If we have saved selections, restore them; otherwise default unchecked
1566
- checkbox.checked = tmpSavedSet ? (tmpSavedSet[name] === true) : false;
1567
- checkbox.addEventListener('change', saveSelections);
1568
-
1569
- var label = document.createElement('label');
1570
- label.htmlFor = 'tbl_' + name;
1571
- label.textContent = name;
1572
-
1573
- div.appendChild(checkbox);
1574
- div.appendChild(label);
1575
- container.appendChild(div);
1576
- }
1577
-
1578
- document.getElementById('tableSelection').style.display = _FetchedTables.length > 0 ? 'block' : 'none';
1579
- document.getElementById('tableFilter').value = '';
1580
- updateSelectionCount();
1581
- }
1582
-
1583
- function filterTableList()
1584
- {
1585
- var tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
1586
- var tmpItems = document.getElementById('tableList').children;
1587
- for (var i = 0; i < tmpItems.length; i++)
1588
- {
1589
- var tmpName = tmpItems[i].getAttribute('data-table') || '';
1590
- tmpItems[i].style.display = (!tmpFilter || tmpName.indexOf(tmpFilter) >= 0) ? '' : 'none';
1591
- }
1592
- }
1593
-
1594
- function selectAllTables(pChecked)
1595
- {
1596
- // Only affect visible (non-filtered) items
1597
- var tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
1598
- for (var i = 0; i < _FetchedTables.length; i++)
1599
- {
1600
- var name = _FetchedTables[i];
1601
- if (tmpFilter && name.toLowerCase().indexOf(tmpFilter) < 0) continue;
1602
- var cb = document.getElementById('tbl_' + name);
1603
- if (cb) cb.checked = pChecked;
1604
- }
1605
- saveSelections();
1606
- }
1607
-
1608
- function getSelectedTables()
1609
- {
1610
- var selected = [];
1611
- for (var i = 0; i < _FetchedTables.length; i++)
1612
- {
1613
- var cb = document.getElementById('tbl_' + _FetchedTables[i]);
1614
- if (cb && cb.checked) selected.push(_FetchedTables[i]);
1615
- }
1616
- return selected;
1617
- }
1618
-
1619
- // ================================================================
1620
- // Deploy
1621
- // ================================================================
1622
-
1623
- function deploySchema()
1624
- {
1625
- var selectedTables = getSelectedTables();
1626
-
1627
- if (selectedTables.length === 0)
1628
- {
1629
- setStatus('deployStatus', 'No tables selected. Fetch a schema and select tables first.', 'error');
1630
- setSectionPhase(4, 'error');
1631
- return;
1632
- }
1633
-
1634
- setSectionPhase(4, 'busy');
1635
- setStatus('deployStatus', 'Deploying ' + selectedTables.length + ' tables...', 'info');
1636
-
1637
- api('POST', '/clone/schema/deploy', { Tables: selectedTables })
1638
- .then(function(data)
1639
- {
1640
- if (data.Success)
1641
- {
1642
- setStatus('deployStatus', data.Message, 'ok');
1643
- setSectionPhase(4, 'ok');
1644
- _DeployedTables = data.TablesDeployed || selectedTables;
1645
- saveDeployedTables();
1646
- populateViewTableDropdown();
1647
- updateAllPreviews();
1648
- }
1649
- else
1650
- {
1651
- setStatus('deployStatus', 'Deploy failed: ' + (data.Error || 'Unknown error'), 'error');
1652
- setSectionPhase(4, 'error');
1653
- }
1654
- })
1655
- .catch(function(err)
1656
- {
1657
- setStatus('deployStatus', 'Request failed: ' + err.message, 'error');
1658
- setSectionPhase(4, 'error');
1659
- });
1660
- }
1661
-
1662
- function resetDatabase()
1663
- {
1664
- if (!confirm('This will delete ALL data in the local SQLite database. Continue?'))
1665
- {
1666
- return;
1667
- }
1668
-
1669
- setStatus('deployStatus', 'Resetting database...', 'info');
1670
-
1671
- api('POST', '/clone/reset')
1672
- .then(function(data)
1673
- {
1674
- if (data.Success)
1675
- {
1676
- setStatus('deployStatus', data.Message, 'ok');
1677
- // Clear the sync progress display
1678
- document.getElementById('syncProgress').innerHTML = '';
1679
- }
1680
- else
1681
- {
1682
- setStatus('deployStatus', 'Reset failed: ' + (data.Error || 'Unknown error'), 'error');
1683
- }
1684
- })
1685
- .catch(function(err)
1686
- {
1687
- setStatus('deployStatus', 'Request failed: ' + err.message, 'error');
1688
- });
1689
- }
1690
-
1691
- // ================================================================
1692
- // Sync
1693
- // ================================================================
1694
-
1695
- var _SyncPollTimer = null;
1696
-
1697
- function startSync()
1698
- {
1699
- var selectedTables = getSelectedTables();
1700
- var pageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
1701
- var dateTimePrecisionMS = parseInt(document.getElementById('dateTimePrecisionMS').value, 10);
1702
- if (isNaN(dateTimePrecisionMS)) dateTimePrecisionMS = 1000;
1703
- var syncDeletedRecords = document.getElementById('syncDeletedRecords').checked;
1704
- var syncMode = document.querySelector('input[name="syncMode"]:checked').value;
1705
-
1706
- if (selectedTables.length === 0)
1707
- {
1708
- setStatus('syncStatus', 'No tables selected for sync.', 'error');
1709
- setSectionPhase(5, 'error');
1710
- return;
1711
- }
1712
-
1713
- setSectionPhase(5, 'busy');
1714
- setStatus('syncStatus', 'Starting ' + syncMode.toLowerCase() + ' sync...', 'info');
1715
-
1716
- api('POST', '/clone/sync/start', { Tables: selectedTables, PageSize: pageSize, DateTimePrecisionMS: dateTimePrecisionMS, SyncDeletedRecords: syncDeletedRecords, SyncMode: syncMode })
1717
- .then(function(data)
1718
- {
1719
- if (data.Success)
1720
- {
1721
- var msg = data.SyncMode + ' sync started for ' + data.Tables.length + ' tables.';
1722
- if (data.SyncDeletedRecords) msg += ' (including deleted records)';
1723
- setStatus('syncStatus', msg, 'ok');
1724
- startPolling();
1725
- }
1726
- else
1727
- {
1728
- setStatus('syncStatus', 'Sync start failed: ' + (data.Error || 'Unknown error'), 'error');
1729
- setSectionPhase(5, 'error');
1730
- }
1731
- })
1732
- .catch(function(err)
1733
- {
1734
- setStatus('syncStatus', 'Request failed: ' + err.message, 'error');
1735
- setSectionPhase(5, 'error');
1736
- });
1737
- }
1738
-
1739
- function stopSync()
1740
- {
1741
- api('POST', '/clone/sync/stop')
1742
- .then(function(data)
1743
- {
1744
- setStatus('syncStatus', 'Sync stop requested.', 'warn');
1745
- })
1746
- .catch(function(err)
1747
- {
1748
- setStatus('syncStatus', 'Request failed: ' + err.message, 'error');
1749
- });
1750
- }
1751
-
1752
- function startPolling()
1753
- {
1754
- if (_SyncPollTimer) clearInterval(_SyncPollTimer);
1755
- _SyncPollTimer = setInterval(pollSyncStatus, 2000);
1756
- pollSyncStatus();
1757
- }
1758
-
1759
- function stopPolling()
1760
- {
1761
- if (_SyncPollTimer)
1762
- {
1763
- clearInterval(_SyncPollTimer);
1764
- _SyncPollTimer = null;
1765
- }
1766
- }
1767
-
1768
- function pollSyncStatus()
1769
- {
1770
- api('GET', '/clone/sync/status')
1771
- .then(function(data)
1772
- {
1773
- renderSyncProgress(data);
1774
-
1775
- if (!data.Running && !data.Stopping)
1776
- {
1777
- stopPolling();
1778
- if (Object.keys(data.Tables || {}).length > 0)
1779
- {
1780
- // Check if any tables had errors or partial sync
1781
- var tables = data.Tables || {};
1782
- var hasErrors = false;
1783
- var hasPartial = false;
1784
- var names = Object.keys(tables);
1785
- for (var i = 0; i < names.length; i++)
1786
- {
1787
- if (tables[names[i]].Status === 'Error') hasErrors = true;
1788
- if (tables[names[i]].Status === 'Partial') hasPartial = true;
1789
- }
1790
-
1791
- if (hasErrors)
1792
- {
1793
- setStatus('syncStatus', 'Sync finished with errors. Check the table below for details.', 'error');
1794
- setSectionPhase(5, 'error');
1795
- }
1796
- else if (hasPartial)
1797
- {
1798
- setStatus('syncStatus', 'Sync finished. Some records were skipped (GUID conflicts or permission issues).', 'warn');
1799
- setSectionPhase(5, 'ok');
1800
- }
1801
- else
1802
- {
1803
- setStatus('syncStatus', 'Sync complete.', 'ok');
1804
- setSectionPhase(5, 'ok');
1805
- }
1806
-
1807
- // Fetch the structured report
1808
- fetchSyncReport();
1809
- }
1810
- }
1811
- })
1812
- .catch(function(err)
1813
- {
1814
- // Silently ignore poll errors
1815
- });
1816
- }
1817
-
1818
- var _LastReport = null;
1819
-
1820
- function fetchSyncReport()
1821
- {
1822
- api('GET', '/clone/sync/report')
1823
- .then(function(data)
1824
- {
1825
- if (data && data.ReportVersion)
1826
- {
1827
- _LastReport = data;
1828
- renderSyncReport(data);
1829
- }
1830
- })
1831
- .catch(function(err)
1832
- {
1833
- // Ignore report fetch errors
1834
- });
1835
- }
1836
-
1837
- function renderSyncReport(pReport)
1838
- {
1839
- var section = document.getElementById('syncReportSection');
1840
- section.style.display = '';
1841
-
1842
- // --- Summary Cards ---
1843
- var cardsContainer = document.getElementById('reportSummaryCards');
1844
- var outcomeClass = 'outcome-' + pReport.Outcome.toLowerCase();
1845
- var outcomeColor = { Success: '#28a745', Partial: '#ffc107', Error: '#dc3545', Stopped: '#6c757d' }[pReport.Outcome] || '#666';
1846
-
1847
- var durationSec = pReport.RunTimestamps.DurationSeconds || 0;
1848
- var durationStr = durationSec < 60 ? durationSec + 's' : Math.floor(durationSec / 60) + 'm ' + (durationSec % 60) + 's';
1849
-
1850
- var totalSynced = pReport.Summary.TotalSynced.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1851
- var totalRecords = pReport.Summary.TotalRecords.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1852
-
1853
- cardsContainer.innerHTML = ''
1854
- + '<div class="report-card ' + outcomeClass + '">'
1855
- + ' <div class="card-label">Outcome</div>'
1856
- + ' <div class="card-value" style="color:' + outcomeColor + '">' + pReport.Outcome + '</div>'
1857
- + '</div>'
1858
- + '<div class="report-card">'
1859
- + ' <div class="card-label">Mode</div>'
1860
- + ' <div class="card-value">' + pReport.Config.SyncMode + '</div>'
1861
- + '</div>'
1862
- + '<div class="report-card">'
1863
- + ' <div class="card-label">Duration</div>'
1864
- + ' <div class="card-value">' + durationStr + '</div>'
1865
- + '</div>'
1866
- + '<div class="report-card">'
1867
- + ' <div class="card-label">Tables</div>'
1868
- + ' <div class="card-value">' + pReport.Summary.Complete + ' / ' + pReport.Summary.TotalTables + '</div>'
1869
- + '</div>'
1870
- + '<div class="report-card">'
1871
- + ' <div class="card-label">Records</div>'
1872
- + ' <div class="card-value">' + totalSynced + '</div>'
1873
- + ' <div style="font-size:0.75em; color:#888">of ' + totalRecords + '</div>'
1874
- + '</div>';
1875
-
1876
- // --- Anomalies ---
1877
- var anomalyContainer = document.getElementById('reportAnomalies');
1878
- if (pReport.Anomalies.length === 0)
1879
- {
1880
- anomalyContainer.innerHTML = '<div style="color:#28a745; font-weight:600; font-size:0.9em">No anomalies detected.</div>';
1881
- }
1882
- else
1883
- {
1884
- var html = '<h4 style="margin:0 0 8px; color:#dc3545; font-size:0.95em">Anomalies (' + pReport.Anomalies.length + ')</h4>';
1885
- html += '<table class="progress-table">';
1886
- html += '<tr><th>Table</th><th>Type</th><th>Message</th></tr>';
1887
- for (var i = 0; i < pReport.Anomalies.length; i++)
1888
- {
1889
- var a = pReport.Anomalies[i];
1890
- var typeColor = a.Type === 'Error' ? '#dc3545' : (a.Type === 'Partial' ? '#ffc107' : '#6c757d');
1891
- html += '<tr>';
1892
- html += '<td><strong>' + escapeHtml(a.Table) + '</strong></td>';
1893
- html += '<td style="color:' + typeColor + '">' + a.Type + '</td>';
1894
- html += '<td>' + escapeHtml(a.Message) + '</td>';
1895
- html += '</tr>';
1896
- }
1897
- html += '</table>';
1898
- anomalyContainer.innerHTML = html;
1899
- }
1900
-
1901
- // --- Top Tables by Duration ---
1902
- var topContainer = document.getElementById('reportTopTables');
1903
- var topCount = Math.min(10, pReport.Tables.length);
1904
- if (topCount > 0)
1905
- {
1906
- var html = '<h4 style="margin:0 0 8px; font-size:0.95em; color:#444">Top Tables by Duration</h4>';
1907
- html += '<table class="progress-table">';
1908
- html += '<tr><th>Table</th><th>Duration</th><th>Records</th><th>Status</th></tr>';
1909
- for (var i = 0; i < topCount; i++)
1910
- {
1911
- var t = pReport.Tables[i];
1912
- var dur = t.DurationSeconds < 60 ? t.DurationSeconds + 's' : Math.floor(t.DurationSeconds / 60) + 'm ' + (t.DurationSeconds % 60) + 's';
1913
- var recs = t.Total.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1914
- var statusColor = { Complete: '#28a745', Error: '#dc3545', Partial: '#ffc107' }[t.Status] || '#666';
1915
- html += '<tr>';
1916
- html += '<td><strong>' + escapeHtml(t.Name) + '</strong></td>';
1917
- html += '<td>' + dur + '</td>';
1918
- html += '<td>' + recs + '</td>';
1919
- html += '<td style="color:' + statusColor + '">' + t.Status + '</td>';
1920
- html += '</tr>';
1921
- }
1922
- html += '</table>';
1923
- topContainer.innerHTML = html;
1924
- }
1925
- }
1926
-
1927
- function downloadReport()
1928
- {
1929
- if (!_LastReport)
1930
- {
1931
- setStatus('reportStatus', 'No report available.', 'warn');
1932
- return;
1933
- }
1934
- var json = JSON.stringify(_LastReport, null, '\t');
1935
- var blob = new Blob([json], { type: 'application/json' });
1936
- var a = document.createElement('a');
1937
- a.href = URL.createObjectURL(blob);
1938
- var ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1939
- a.download = 'DataCloner-Report-' + ts + '.json';
1940
- a.click();
1941
- URL.revokeObjectURL(a.href);
1942
- setStatus('reportStatus', 'Report downloaded.', 'ok');
1943
- }
1944
-
1945
- function copyReport()
1946
- {
1947
- if (!_LastReport)
1948
- {
1949
- setStatus('reportStatus', 'No report available.', 'warn');
1950
- return;
1951
- }
1952
- var json = JSON.stringify(_LastReport, null, '\t');
1953
- navigator.clipboard.writeText(json).then(function()
1954
- {
1955
- setStatus('reportStatus', 'Report copied to clipboard.', 'ok');
1956
- });
1957
- }
1958
-
1959
- function renderSyncProgress(data)
1960
- {
1961
- var container = document.getElementById('syncProgress');
1962
- var tables = data.Tables || {};
1963
- var tableNames = Object.keys(tables);
1964
-
1965
- if (tableNames.length === 0)
1966
- {
1967
- container.innerHTML = '';
1968
- return;
1969
- }
1970
-
1971
- var html = '<table class="progress-table">';
1972
- html += '<tr><th>Table</th><th>Status</th><th>Progress</th><th>Synced</th><th>Details</th></tr>';
1973
-
1974
- for (var i = 0; i < tableNames.length; i++)
1975
- {
1976
- var name = tableNames[i];
1977
- var t = tables[name];
1978
-
1979
- // Calculate percentage: if total is 0, show 100% (nothing to sync)
1980
- var pct = 0;
1981
- if (t.Total === 0 && (t.Status === 'Complete' || t.Status === 'Error'))
1982
- {
1983
- pct = 100;
1984
- }
1985
- else if (t.Total > 0)
1986
- {
1987
- pct = Math.round((t.Synced / t.Total) * 100);
1988
- }
1989
-
1990
- // Color the progress bar based on status
1991
- var barColor = '#28a745'; // green
1992
- if (t.Status === 'Error') barColor = '#dc3545'; // red
1993
- else if (t.Status === 'Partial') barColor = '#ffc107'; // yellow
1994
- else if (t.Status === 'Syncing') barColor = '#4a90d9'; // blue
1995
-
1996
- // Status badge
1997
- var statusBadge = t.Status;
1998
- if (t.Status === 'Complete' && t.Total === 0) statusBadge = 'Complete (empty)';
1999
- if (t.Status === 'Partial') statusBadge = 'Partial \u26A0';
2000
- if (t.Status === 'Error') statusBadge = 'Error \u2716';
2001
-
2002
- // Details column
2003
- var details = '';
2004
- if (t.ErrorMessage) details = t.ErrorMessage;
2005
- else if (t.Skipped > 0) details = t.Skipped + ' record(s) skipped';
2006
- else if ((t.Errors || 0) > 0) details = t.Errors + ' error(s)';
2007
- else if (t.Status === 'Complete' && t.Total === 0) details = 'No records on server';
2008
- else if (t.Status === 'Complete') details = '\u2714 OK';
2009
-
2010
- html += '<tr>';
2011
- html += '<td><strong>' + name + '</strong></td>';
2012
- html += '<td>' + statusBadge + '</td>';
2013
- html += '<td>';
2014
- html += '<div class="progress-bar-container"><div class="progress-bar-fill" style="width:' + pct + '%; background:' + barColor + '"></div></div>';
2015
- html += ' ' + pct + '%';
2016
- html += '</td>';
2017
- html += '<td>' + t.Synced + ' / ' + t.Total + '</td>';
2018
- html += '<td>' + details + '</td>';
2019
- html += '</tr>';
2020
- }
2021
-
2022
- html += '</table>';
2023
- container.innerHTML = html;
2024
- }
2025
-
2026
- // ================================================================
2027
- // View Data
2028
- // ================================================================
2029
-
2030
- var _DeployedTables = [];
2031
-
2032
- function populateViewTableDropdown()
2033
- {
2034
- var tmpSelect = document.getElementById('viewTable');
2035
- var tmpCurrentValue = tmpSelect.value;
2036
-
2037
- tmpSelect.innerHTML = '';
2038
-
2039
- if (_DeployedTables.length === 0)
2040
- {
2041
- var opt = document.createElement('option');
2042
- opt.value = '';
2043
- opt.textContent = '— deploy tables first —';
2044
- tmpSelect.appendChild(opt);
2045
- return;
2046
- }
2047
-
2048
- for (var i = 0; i < _DeployedTables.length; i++)
2049
- {
2050
- var opt = document.createElement('option');
2051
- opt.value = _DeployedTables[i];
2052
- opt.textContent = _DeployedTables[i];
2053
- tmpSelect.appendChild(opt);
2054
- }
2055
-
2056
- // Restore previous selection if it exists
2057
- if (tmpCurrentValue)
2058
- {
2059
- tmpSelect.value = tmpCurrentValue;
2060
- }
2061
- }
2062
-
2063
- function loadTableData()
2064
- {
2065
- var tmpTable = document.getElementById('viewTable').value;
2066
- var tmpLimit = parseInt(document.getElementById('viewLimit').value, 10) || 100;
2067
-
2068
- if (!tmpTable)
2069
- {
2070
- setStatus('viewStatus', 'Select a table first.', 'error');
2071
- return;
2072
- }
2073
-
2074
- setStatus('viewStatus', 'Loading ' + tmpTable + '...', 'info');
2075
- document.getElementById('viewDataContainer').innerHTML = '';
2076
-
2077
- // Use the standard Meadow CRUD list endpoint: /1.0/{Entity}s/0/{Cap}
2078
- api('GET', '/1.0/' + tmpTable + 's/0/' + tmpLimit)
2079
- .then(function(data)
2080
- {
2081
- if (!Array.isArray(data))
2082
- {
2083
- setStatus('viewStatus', 'Unexpected response (not an array). The table may not be deployed yet.', 'error');
2084
- return;
2085
- }
2086
-
2087
- setStatus('viewStatus', data.length + ' row(s) returned' + (data.length >= tmpLimit ? ' (limit reached — increase Max Rows to see more)' : '') + '.', 'ok');
2088
- renderDataTable(data);
2089
- })
2090
- .catch(function(err)
2091
- {
2092
- setStatus('viewStatus', 'Request failed: ' + err.message, 'error');
2093
- });
2094
- }
2095
-
2096
- function renderDataTable(pRows)
2097
- {
2098
- var container = document.getElementById('viewDataContainer');
2099
-
2100
- if (!pRows || pRows.length === 0)
2101
- {
2102
- container.innerHTML = '<p style="color:#666; font-size:0.9em; padding:8px">No rows.</p>';
2103
- return;
2104
- }
2105
-
2106
- // Collect all column names from the first row
2107
- var tmpColumns = Object.keys(pRows[0]);
2108
-
2109
- var html = '<table class="data-table">';
2110
- html += '<thead><tr>';
2111
- for (var c = 0; c < tmpColumns.length; c++)
2112
- {
2113
- html += '<th>' + escapeHtml(tmpColumns[c]) + '</th>';
2114
- }
2115
- html += '</tr></thead>';
2116
-
2117
- html += '<tbody>';
2118
- for (var r = 0; r < pRows.length; r++)
2119
- {
2120
- html += '<tr>';
2121
- for (var c = 0; c < tmpColumns.length; c++)
2122
- {
2123
- var val = pRows[r][tmpColumns[c]];
2124
- var display = (val === null || val === undefined) ? '' : String(val);
2125
- html += '<td title="' + escapeHtml(display) + '">' + escapeHtml(display) + '</td>';
2126
- }
2127
- html += '</tr>';
2128
- }
2129
- html += '</tbody></table>';
2130
-
2131
- container.innerHTML = html;
2132
- }
2133
-
2134
- function escapeHtml(pStr)
2135
- {
2136
- var div = document.createElement('div');
2137
- div.appendChild(document.createTextNode(pStr));
2138
- return div.innerHTML;
2139
- }
2140
-
2141
- // ================================================================
2142
- // Export Config
2143
- // ================================================================
2144
-
2145
- function buildConfigObject()
2146
- {
2147
- var provider = document.getElementById('connProvider').value;
2148
- var config = {};
2149
-
2150
- // ---- Local Database ----
2151
- config.LocalDatabase = { Provider: provider, Config: {} };
2152
- var dbConfig = config.LocalDatabase.Config;
2153
-
2154
- if (provider === 'SQLite')
2155
- {
2156
- dbConfig.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || 'data/cloned.sqlite';
2157
- }
2158
- else if (provider === 'MySQL')
2159
- {
2160
- dbConfig.host = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
2161
- dbConfig.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
2162
- dbConfig.user = document.getElementById('mysqlUser').value.trim() || 'root';
2163
- dbConfig.password = document.getElementById('mysqlPassword').value;
2164
- dbConfig.database = document.getElementById('mysqlDatabase').value.trim();
2165
- dbConfig.connectionLimit = parseInt(document.getElementById('mysqlConnectionLimit').value, 10) || 20;
2166
- }
2167
- else if (provider === 'MSSQL')
2168
- {
2169
- dbConfig.server = document.getElementById('mssqlServer').value.trim() || '127.0.0.1';
2170
- dbConfig.port = parseInt(document.getElementById('mssqlPort').value, 10) || 1433;
2171
- dbConfig.user = document.getElementById('mssqlUser').value.trim() || 'sa';
2172
- dbConfig.password = document.getElementById('mssqlPassword').value;
2173
- dbConfig.database = document.getElementById('mssqlDatabase').value.trim();
2174
- dbConfig.connectionLimit = parseInt(document.getElementById('mssqlConnectionLimit').value, 10) || 20;
2175
- }
2176
- else if (provider === 'PostgreSQL')
2177
- {
2178
- dbConfig.host = document.getElementById('postgresqlHost').value.trim() || '127.0.0.1';
2179
- dbConfig.port = parseInt(document.getElementById('postgresqlPort').value, 10) || 5432;
2180
- dbConfig.user = document.getElementById('postgresqlUser').value.trim() || 'postgres';
2181
- dbConfig.password = document.getElementById('postgresqlPassword').value;
2182
- dbConfig.database = document.getElementById('postgresqlDatabase').value.trim();
2183
- dbConfig.max = parseInt(document.getElementById('postgresqlConnectionLimit').value, 10) || 10;
2184
- }
2185
-
2186
- // ---- Remote Session ----
2187
- config.RemoteSession = {};
2188
- var serverURL = document.getElementById('serverURL').value.trim();
2189
- if (serverURL) config.RemoteSession.ServerURL = serverURL + '/1.0/';
2190
-
2191
- var authMethod = document.getElementById('authMethod').value.trim();
2192
- if (authMethod) config.RemoteSession.AuthenticationMethod = authMethod;
2193
-
2194
- var authURI = document.getElementById('authURI').value.trim();
2195
- if (authURI) config.RemoteSession.AuthenticationURITemplate = authURI;
2196
-
2197
- var checkURI = document.getElementById('checkURI').value.trim();
2198
- if (checkURI) config.RemoteSession.CheckSessionURITemplate = checkURI;
2199
-
2200
- var cookieName = document.getElementById('cookieName').value.trim();
2201
- if (cookieName) config.RemoteSession.CookieName = cookieName;
2202
-
2203
- var cookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
2204
- if (cookieValueAddr) config.RemoteSession.CookieValueAddress = cookieValueAddr;
2205
-
2206
- var cookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
2207
- if (cookieValueTemplate) config.RemoteSession.CookieValueTemplate = cookieValueTemplate;
2208
-
2209
- var loginMarker = document.getElementById('loginMarker').value.trim();
2210
- if (loginMarker) config.RemoteSession.CheckSessionLoginMarker = loginMarker;
2211
-
2212
- // ---- Credentials ----
2213
- var userName = document.getElementById('userName').value.trim();
2214
- var password = document.getElementById('password').value;
2215
- if (userName || password)
2216
- {
2217
- config.Credentials = {};
2218
- if (userName) config.Credentials.UserName = userName;
2219
- if (password) config.Credentials.Password = password;
2220
- }
2221
-
2222
- // ---- Schema ----
2223
- var schemaURL = document.getElementById('schemaURL').value.trim();
2224
- if (schemaURL) config.SchemaURL = schemaURL;
2225
-
2226
- // ---- Tables ----
2227
- var selectedTables = getSelectedTables();
2228
- if (selectedTables.length > 0) config.Tables = selectedTables;
2229
-
2230
- // ---- Sync Options ----
2231
- config.Sync = {};
2232
- config.Sync.Mode = document.querySelector('input[name="syncMode"]:checked').value;
2233
- config.Sync.PageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
2234
- config.Sync.SyncDeletedRecords = document.getElementById('syncDeletedRecords').checked;
2235
- var tmpPrecision = parseInt(document.getElementById('dateTimePrecisionMS').value, 10);
2236
- if (!isNaN(tmpPrecision) && tmpPrecision !== 1000) config.Sync.DateTimePrecisionMS = tmpPrecision;
2237
- var tmpMaxRecords = parseInt(document.getElementById('exportMaxRecords').value, 10);
2238
- if (tmpMaxRecords > 0) config.Sync.MaxRecords = tmpMaxRecords;
2239
-
2240
- return config;
2241
- }
2242
-
2243
- function buildMeadowIntegrationConfig()
2244
- {
2245
- var provider = document.getElementById('connProvider').value;
2246
- var config = {};
2247
-
2248
- // ---- Source ----
2249
- var serverURL = document.getElementById('serverURL').value.trim();
2250
- config.Source = { ServerURL: serverURL ? serverURL + '/1.0/' : 'https://localhost:8080/1.0/' };
2251
- // When SessionManager handles auth, Source credentials are not needed
2252
- config.Source.UserID = false;
2253
- config.Source.Password = false;
2254
-
2255
- // ---- Destination ----
2256
- // meadow-integration clone supports MySQL and MSSQL
2257
- config.Destination = {};
2258
- if (provider === 'MySQL')
2259
- {
2260
- config.Destination.Provider = 'MySQL';
2261
- config.Destination.MySQL = {};
2262
- config.Destination.MySQL.server = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
2263
- config.Destination.MySQL.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
2264
- config.Destination.MySQL.user = document.getElementById('mysqlUser').value.trim() || 'root';
2265
- config.Destination.MySQL.password = document.getElementById('mysqlPassword').value || '';
2266
- config.Destination.MySQL.database = document.getElementById('mysqlDatabase').value.trim() || 'meadow';
2267
- config.Destination.MySQL.connectionLimit = parseInt(document.getElementById('mysqlConnectionLimit').value, 10) || 20;
2268
- }
2269
- else if (provider === 'MSSQL')
2270
- {
2271
- config.Destination.Provider = 'MSSQL';
2272
- config.Destination.MSSQL = {};
2273
- config.Destination.MSSQL.server = document.getElementById('mssqlServer').value.trim() || '127.0.0.1';
2274
- config.Destination.MSSQL.port = parseInt(document.getElementById('mssqlPort').value, 10) || 1433;
2275
- config.Destination.MSSQL.user = document.getElementById('mssqlUser').value.trim() || 'sa';
2276
- config.Destination.MSSQL.password = document.getElementById('mssqlPassword').value || '';
2277
- config.Destination.MSSQL.database = document.getElementById('mssqlDatabase').value.trim() || 'meadow';
2278
- config.Destination.MSSQL.ConnectionPoolLimit = parseInt(document.getElementById('mssqlConnectionLimit').value, 10) || 20;
2279
- }
2280
- else
2281
- {
2282
- // Default to MySQL placeholder for unsupported providers
2283
- config.Destination.Provider = 'MySQL';
2284
- config.Destination.MySQL = { server: '127.0.0.1', port: 3306, user: 'root', password: '', database: 'meadow', connectionLimit: 20 };
2285
- }
2286
-
2287
- // ---- Schema ----
2288
- var schemaURL = document.getElementById('schemaURL').value.trim();
2289
- if (schemaURL)
2290
- {
2291
- config.SchemaURL = schemaURL;
2292
- }
2293
- else
2294
- {
2295
- config.SchemaPath = './schema/Model-Extended.json';
2296
- }
2297
-
2298
- // ---- Sync ----
2299
- config.Sync = {};
2300
- config.Sync.DefaultSyncMode = document.querySelector('input[name="syncMode"]:checked').value;
2301
- config.Sync.PageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
2302
- var tmpMdwintPrecision = parseInt(document.getElementById('dateTimePrecisionMS').value, 10);
2303
- if (!isNaN(tmpMdwintPrecision)) config.Sync.DateTimePrecisionMS = tmpMdwintPrecision;
2304
- var selectedTables = getSelectedTables();
2305
- config.Sync.SyncEntityList = selectedTables.length > 0 ? selectedTables : [];
2306
- config.Sync.SyncEntityOptions = {};
2307
-
2308
- // ---- SessionManager ----
2309
- config.SessionManager = { Sessions: {} };
2310
-
2311
- var sessionConfig = {};
2312
- sessionConfig.Type = 'Cookie';
2313
-
2314
- // Authentication method
2315
- var authMethod = document.getElementById('authMethod').value.trim() || 'get';
2316
- sessionConfig.AuthenticationMethod = authMethod;
2317
-
2318
- // Build the authentication URI template
2319
- var authURI = document.getElementById('authURI').value.trim();
2320
- if (authURI)
2321
- {
2322
- // If the URI is a relative path, prepend the server URL
2323
- if (authURI.charAt(0) === '/')
2324
- {
2325
- sessionConfig.AuthenticationURITemplate = (serverURL || '') + authURI;
2326
- }
2327
- else
2328
- {
2329
- sessionConfig.AuthenticationURITemplate = authURI;
2330
- }
2331
- }
2332
- else if (serverURL)
2333
- {
2334
- // Default: Meadow-style GET authentication
2335
- if (authMethod === 'post')
2336
- {
2337
- sessionConfig.AuthenticationURITemplate = serverURL + '/1.0/Authenticate';
2338
- sessionConfig.AuthenticationRequestBody = {
2339
- UserName: '{~D:Record.UserName~}',
2340
- Password: '{~D:Record.Password~}'
2341
- };
2342
- }
2343
- else
2344
- {
2345
- sessionConfig.AuthenticationURITemplate = serverURL + '/1.0/Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}';
2346
- }
2347
- }
2348
-
2349
- // Check session URI
2350
- var checkURI = document.getElementById('checkURI').value.trim();
2351
- if (checkURI)
2352
- {
2353
- sessionConfig.CheckSessionURITemplate = checkURI.charAt(0) === '/' ? (serverURL || '') + checkURI : checkURI;
2354
- }
2355
- else if (serverURL)
2356
- {
2357
- sessionConfig.CheckSessionURITemplate = serverURL + '/1.0/CheckSession';
2358
- }
2359
-
2360
- // Login marker
2361
- var loginMarker = document.getElementById('loginMarker').value.trim();
2362
- sessionConfig.CheckSessionLoginMarkerType = 'boolean';
2363
- sessionConfig.CheckSessionLoginMarker = loginMarker || 'LoggedIn';
2364
-
2365
- // Domain match — extract from server URL for auto-injection
2366
- if (serverURL)
2367
- {
2368
- try
2369
- {
2370
- var urlObj = new URL(serverURL);
2371
- sessionConfig.DomainMatch = urlObj.host;
2372
- }
2373
- catch (e)
2374
- {
2375
- sessionConfig.DomainMatch = serverURL;
2376
- }
2377
- }
2378
-
2379
- // Cookie injection
2380
- var cookieName = document.getElementById('cookieName').value.trim();
2381
- sessionConfig.CookieName = cookieName || 'SessionID';
2382
-
2383
- var cookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
2384
- if (cookieValueAddr) sessionConfig.CookieValueAddress = cookieValueAddr;
2385
-
2386
- var cookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
2387
- if (cookieValueTemplate) sessionConfig.CookieValueTemplate = cookieValueTemplate;
2388
-
2389
- // Credentials
2390
- var userName = document.getElementById('userName').value.trim();
2391
- var password = document.getElementById('password').value;
2392
- sessionConfig.Credentials = {};
2393
- if (userName) sessionConfig.Credentials.UserName = userName;
2394
- if (password) sessionConfig.Credentials.Password = password;
2395
-
2396
- config.SessionManager.Sessions.SourceAPI = sessionConfig;
2397
-
2398
- return config;
2399
- }
2400
-
2401
- function generateConfig()
2402
- {
2403
- var config = buildConfigObject();
2404
- var json = JSON.stringify(config, null, '\t');
2405
-
2406
- var textarea = document.getElementById('configOutput');
2407
- textarea.value = json;
2408
- textarea.style.display = '';
2409
-
2410
- // Build CLI flags from export options
2411
- var logFlag = document.getElementById('exportLogFile').checked ? ' --log' : '';
2412
- var maxFlag = '';
2413
- var tmpExportMax = parseInt(document.getElementById('exportMaxRecords').value, 10);
2414
- if (tmpExportMax > 0) maxFlag = ' --max ' + tmpExportMax;
2415
-
2416
- // Build CLI command (with config file)
2417
- var cliDiv = document.getElementById('cliCommand');
2418
- cliDiv.style.display = '';
2419
- cliDiv.querySelector('div').textContent = 'npx retold-data-service-clone --config clone-config.json --run' + logFlag + maxFlag;
2420
-
2421
- // Build one-liner (no config file needed) using --config-json
2422
- var oneShotDiv = document.getElementById('cliOneShot');
2423
- oneShotDiv.style.display = '';
2424
- var compactJSON = JSON.stringify(config);
2425
- // Escape single quotes for shell wrapping
2426
- var escapedJSON = compactJSON.replace(/'/g, "'\\''");
2427
- var oneShot = "npx retold-data-service-clone --config-json '" + escapedJSON + "' --run" + logFlag + maxFlag;
2428
- oneShotDiv.querySelector('div').textContent = oneShot;
2429
-
2430
- // ---- meadow-integration (mdwint clone) config ----
2431
- var mdwintConfig = buildMeadowIntegrationConfig();
2432
- var mdwintJSON = JSON.stringify(mdwintConfig, null, '\t');
2433
-
2434
- var mdwintDiv = document.getElementById('mdwintExport');
2435
- mdwintDiv.style.display = '';
2436
-
2437
- var mdwintTextarea = document.getElementById('mdwintConfigOutput');
2438
- mdwintTextarea.value = mdwintJSON;
2439
-
2440
- // Build the mdwint CLI command
2441
- var mdwintCLI = 'mdwint clone --schema_path ./schema/Model-Extended.json';
2442
- var mdwintCLIDiv = document.getElementById('mdwintCLICommand');
2443
- mdwintCLIDiv.querySelector('div').textContent = mdwintCLI;
2444
-
2445
- // Provider compatibility note
2446
- var provider = document.getElementById('connProvider').value;
2447
- if (provider !== 'MySQL' && provider !== 'MSSQL')
2448
- {
2449
- setStatus('mdwintConfigStatus', 'Note: mdwint clone only supports MySQL and MSSQL destinations. The config defaults to MySQL; update the Destination section for your target database.', 'warn');
2450
- }
2451
- else
2452
- {
2453
- setStatus('mdwintConfigStatus', '', '');
2454
- }
2455
-
2456
- setStatus('configExportStatus', 'Config generated. Save as clone-config.json or copy the one-liner below.', 'ok');
2457
- }
2458
-
2459
- function copyConfig()
2460
- {
2461
- var textarea = document.getElementById('configOutput');
2462
- if (!textarea.value)
2463
- {
2464
- setStatus('configExportStatus', 'Generate a config first.', 'warn');
2465
- return;
2466
- }
2467
- navigator.clipboard.writeText(textarea.value).then(function()
2468
- {
2469
- setStatus('configExportStatus', 'Config copied to clipboard.', 'ok');
2470
- });
2471
- }
2472
-
2473
- function copyCLI()
2474
- {
2475
- var cmd = document.getElementById('cliCommand').querySelector('div').textContent;
2476
- navigator.clipboard.writeText(cmd).then(function()
2477
- {
2478
- setStatus('configExportStatus', 'CLI command copied to clipboard.', 'ok');
2479
- });
2480
- }
2481
-
2482
- function copyOneShot()
2483
- {
2484
- var cmd = document.getElementById('cliOneShot').querySelector('div').textContent;
2485
- navigator.clipboard.writeText(cmd).then(function()
2486
- {
2487
- setStatus('configExportStatus', 'One-liner copied to clipboard.', 'ok');
2488
- });
2489
- }
2490
-
2491
- function downloadConfig()
2492
- {
2493
- var textarea = document.getElementById('configOutput');
2494
- if (!textarea.value)
2495
- {
2496
- generateConfig();
2497
- }
2498
- var blob = new Blob([textarea.value], { type: 'application/json' });
2499
- var a = document.createElement('a');
2500
- a.href = URL.createObjectURL(blob);
2501
- a.download = 'clone-config.json';
2502
- a.click();
2503
- URL.revokeObjectURL(a.href);
2504
- setStatus('configExportStatus', 'Config downloaded as clone-config.json.', 'ok');
2505
- }
2506
-
2507
- function copyMdwintConfig()
2508
- {
2509
- var textarea = document.getElementById('mdwintConfigOutput');
2510
- if (!textarea.value)
2511
- {
2512
- setStatus('mdwintConfigStatus', 'Generate a config first.', 'warn');
2513
- return;
2514
- }
2515
- navigator.clipboard.writeText(textarea.value).then(function()
2516
- {
2517
- setStatus('mdwintConfigStatus', '.meadow.config.json copied to clipboard.', 'ok');
2518
- });
2519
- }
2520
-
2521
- function copyMdwintCLI()
2522
- {
2523
- var cmd = document.getElementById('mdwintCLICommand').querySelector('div').textContent;
2524
- navigator.clipboard.writeText(cmd).then(function()
2525
- {
2526
- setStatus('mdwintConfigStatus', 'mdwint CLI command copied to clipboard.', 'ok');
2527
- });
2528
- }
2529
-
2530
- function downloadMdwintConfig()
2531
- {
2532
- var textarea = document.getElementById('mdwintConfigOutput');
2533
- if (!textarea.value)
2534
- {
2535
- generateConfig();
2536
- }
2537
- var blob = new Blob([textarea.value], { type: 'application/json' });
2538
- var a = document.createElement('a');
2539
- a.href = URL.createObjectURL(blob);
2540
- a.download = '.meadow.config.json';
2541
- a.click();
2542
- URL.revokeObjectURL(a.href);
2543
- setStatus('mdwintConfigStatus', 'Config downloaded as .meadow.config.json.', 'ok');
2544
- }
2545
-
2546
- // ================================================================
2547
- // Live Status Indicator
2548
- // ================================================================
2549
-
2550
- var _LiveStatusTimer = null;
2551
-
2552
- function startLiveStatusPolling()
2553
- {
2554
- if (_LiveStatusTimer) clearInterval(_LiveStatusTimer);
2555
- pollLiveStatus();
2556
- _LiveStatusTimer = setInterval(pollLiveStatus, 1500);
2557
- }
2558
-
2559
- function pollLiveStatus()
2560
- {
2561
- api('GET', '/clone/sync/live-status')
2562
- .then(function(data)
2563
- {
2564
- renderLiveStatus(data);
2565
- })
2566
- .catch(function()
2567
- {
2568
- // If the server is unreachable, show disconnected
2569
- renderLiveStatus({ Phase: 'disconnected', Message: 'Cannot reach server', TotalSynced: 0, TotalRecords: 0 });
2570
- });
2571
- }
2572
-
2573
- function renderLiveStatus(pData)
2574
- {
2575
- var tmpBar = document.getElementById('liveStatusBar');
2576
- var tmpMsg = document.getElementById('liveStatusMessage');
2577
- var tmpMeta = document.getElementById('liveStatusMeta');
2578
- var tmpProgressFill = document.getElementById('liveStatusProgressFill');
2579
-
2580
- // Update phase class
2581
- tmpBar.className = 'live-status-bar phase-' + (pData.Phase || 'idle');
2582
-
2583
- // Update message
2584
- tmpMsg.textContent = pData.Message || 'Idle';
2585
-
2586
- // Update meta info
2587
- var tmpMetaParts = [];
2588
- if (pData.Phase === 'syncing' || pData.Phase === 'stopping')
2589
- {
2590
- if (pData.Elapsed)
2591
- {
2592
- tmpMetaParts.push('<span class="live-status-meta-item">\u23F1 ' + pData.Elapsed + '</span>');
2593
- }
2594
- if (pData.ETA)
2595
- {
2596
- tmpMetaParts.push('<span class="live-status-meta-item">~' + pData.ETA + ' remaining</span>');
2597
- }
2598
- if (pData.TotalTables > 0)
2599
- {
2600
- tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + pData.Completed + '</strong> / ' + pData.TotalTables + ' tables</span>');
2601
- }
2602
- if (pData.TotalSynced > 0)
2603
- {
2604
- var tmpSynced = pData.TotalSynced.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
2605
- if (pData.PreCountGrandTotal > 0)
2606
- {
2607
- var tmpGrandTotal = pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
2608
- tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> / ' + tmpGrandTotal + ' records</span>');
2609
- }
2610
- else
2611
- {
2612
- tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> records</span>');
2613
- }
2614
- }
2615
- else if (pData.PreCountGrandTotal > 0)
2616
- {
2617
- var tmpGrandTotal = pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
2618
- tmpMetaParts.push('<span class="live-status-meta-item">' + tmpGrandTotal + ' records to sync</span>');
2619
- }
2620
- if (pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables)
2621
- {
2622
- tmpMetaParts.push('<span class="live-status-meta-item">counting: ' + pData.PreCountProgress.Counted + ' / ' + pData.PreCountProgress.TotalTables + '</span>');
2623
- }
2624
- if (pData.Errors > 0)
2625
- {
2626
- tmpMetaParts.push('<span class="live-status-meta-item" style="color:#dc3545"><strong>' + pData.Errors + '</strong> error' + (pData.Errors === 1 ? '' : 's') + '</span>');
2627
- }
2628
- }
2629
- else if (pData.Phase === 'complete')
2630
- {
2631
- if (pData.TotalSynced > 0)
2632
- {
2633
- var tmpSynced = pData.TotalSynced.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
2634
- tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> records synced</span>');
2635
- }
2636
- }
2637
- tmpMeta.innerHTML = tmpMetaParts.join('');
2638
-
2639
- // Update progress bar
2640
- var tmpPct = 0;
2641
- if (pData.Phase === 'syncing' && pData.PreCountGrandTotal > 0 && pData.TotalSynced > 0)
2642
- {
2643
- // Record-level progress using pre-counted grand total
2644
- tmpPct = Math.min((pData.TotalSynced / pData.PreCountGrandTotal) * 100, 99.9);
2645
- }
2646
- else if (pData.Phase === 'syncing' && pData.TotalTables > 0)
2647
- {
2648
- // Fallback: table-level progress + current entity progress
2649
- var tmpTablePct = (pData.Completed / pData.TotalTables) * 100;
2650
- if (pData.ActiveProgress && pData.ActiveProgress.Total > 0)
2651
- {
2652
- var tmpEntityPct = (pData.ActiveProgress.Synced / pData.ActiveProgress.Total) * (100 / pData.TotalTables);
2653
- tmpPct = tmpTablePct + tmpEntityPct;
2654
- }
2655
- else
2656
- {
2657
- tmpPct = tmpTablePct;
2658
- }
2659
- }
2660
- else if (pData.Phase === 'complete')
2661
- {
2662
- tmpPct = 100;
2663
- }
2664
- tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
2665
- }
2666
-
2667
- // ================================================================
2668
- // Initialize
2669
- // ================================================================
2670
-
2671
- // Persist deployed tables across page reload
2672
- function saveDeployedTables()
2673
- {
2674
- localStorage.setItem('dataCloner_deployedTables', JSON.stringify(_DeployedTables));
2675
- }
2676
-
2677
- function restoreDeployedTables()
2678
- {
2679
- try
2680
- {
2681
- var tmpRaw = localStorage.getItem('dataCloner_deployedTables');
2682
- if (tmpRaw)
2683
- {
2684
- _DeployedTables = JSON.parse(tmpRaw);
2685
- populateViewTableDropdown();
2686
- }
2687
- }
2688
- catch (e) { /* ignore */ }
2689
- }
2690
-
2691
- initPersistence();
2692
- onProviderChange();
2693
- restoreDeployedTables();
2694
- startLiveStatusPolling();
2695
-
2696
- // Accordion — start collapsed, wire previews, generate initial preview text
2697
- initAccordionPreviews();
2698
- updateAllPreviews();
2699
- collapseAllSections();
2700
-
2701
- // Auto-process pipeline phases (skips if server is busy)
2702
- initAutoProcess();
2703
- </script>
2704
-
2705
- </body>
2706
- </html>