retold-data-service 2.0.13 → 2.0.14

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 (40) hide show
  1. package/example_applications/data-cloner/data/cloned.sqlite +0 -0
  2. package/example_applications/data-cloner/data/cloned.sqlite-shm +0 -0
  3. package/example_applications/data-cloner/data/cloned.sqlite-wal +0 -0
  4. package/example_applications/data-cloner/data-cloner-web.html +935 -0
  5. package/example_applications/data-cloner/data-cloner.js +1047 -0
  6. package/example_applications/data-cloner/package.json +19 -0
  7. package/package.json +13 -9
  8. package/source/Retold-Data-Service.js +225 -73
  9. package/source/services/Retold-Data-Service-ConnectionManager.js +277 -0
  10. package/source/services/Retold-Data-Service-MeadowEndpoints.js +217 -0
  11. package/source/services/Retold-Data-Service-ModelManager.js +335 -0
  12. package/source/services/meadow-integration/MeadowIntegration-Command-CSVCheck.js +85 -0
  13. package/source/services/meadow-integration/MeadowIntegration-Command-CSVTransform.js +180 -0
  14. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionIntersect.js +153 -0
  15. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionPush.js +190 -0
  16. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToArray.js +113 -0
  17. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToCSV.js +211 -0
  18. package/source/services/meadow-integration/MeadowIntegration-Command-EntityFromTabularFolder.js +244 -0
  19. package/source/services/meadow-integration/MeadowIntegration-Command-JSONArrayTransform.js +213 -0
  20. package/source/services/meadow-integration/MeadowIntegration-Command-TSVCheck.js +80 -0
  21. package/source/services/meadow-integration/MeadowIntegration-Command-TSVTransform.js +166 -0
  22. package/source/services/meadow-integration/Retold-Data-Service-MeadowIntegration.js +113 -0
  23. package/source/services/migration-manager/MigrationManager-Command-Connections.js +220 -0
  24. package/source/services/migration-manager/MigrationManager-Command-DiffMigrate.js +169 -0
  25. package/source/services/migration-manager/MigrationManager-Command-Schemas.js +532 -0
  26. package/source/services/migration-manager/MigrationManager-Command-WebUI.js +123 -0
  27. package/source/services/migration-manager/Retold-Data-Service-MigrationManager.js +357 -0
  28. package/source/services/stricture/Retold-Data-Service-Stricture.js +303 -0
  29. package/source/services/stricture/Stricture-Command-Compile.js +39 -0
  30. package/source/services/stricture/Stricture-Command-Generate-AuthorizationChart.js +14 -0
  31. package/source/services/stricture/Stricture-Command-Generate-DictionaryCSV.js +14 -0
  32. package/source/services/stricture/Stricture-Command-Generate-LaTeX.js +14 -0
  33. package/source/services/stricture/Stricture-Command-Generate-Markdown.js +14 -0
  34. package/source/services/stricture/Stricture-Command-Generate-Meadow.js +14 -0
  35. package/source/services/stricture/Stricture-Command-Generate-ModelGraph.js +14 -0
  36. package/source/services/stricture/Stricture-Command-Generate-MySQL.js +14 -0
  37. package/source/services/stricture/Stricture-Command-Generate-MySQLMigrate.js +14 -0
  38. package/source/services/stricture/Stricture-Command-Generate-Pict.js +14 -0
  39. package/source/services/stricture/Stricture-Command-Generate-TestObjectContainers.js +14 -0
  40. package/test/RetoldDataService_tests.js +161 -1
@@ -0,0 +1,935 @@
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
+ label { display: block; font-weight: 600; margin-bottom: 4px; font-size: 0.9em; }
16
+ input[type="text"], input[type="password"], input[type="number"] {
17
+ width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;
18
+ font-size: 0.95em; margin-bottom: 10px;
19
+ }
20
+ input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus {
21
+ outline: none; border-color: #4a90d9;
22
+ }
23
+
24
+ button {
25
+ padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
26
+ font-size: 0.9em; font-weight: 600; margin-right: 8px; margin-bottom: 8px;
27
+ }
28
+ button.primary { background: #4a90d9; color: #fff; }
29
+ button.primary:hover { background: #357abd; }
30
+ button.secondary { background: #6c757d; color: #fff; }
31
+ button.secondary:hover { background: #5a6268; }
32
+ button.danger { background: #dc3545; color: #fff; }
33
+ button.danger:hover { background: #c82333; }
34
+ button.success { background: #28a745; color: #fff; }
35
+ button.success:hover { background: #218838; }
36
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
37
+
38
+ .status { padding: 8px 12px; border-radius: 4px; margin-top: 10px; font-size: 0.9em; }
39
+ .status.ok { background: #d4edda; color: #155724; }
40
+ .status.error { background: #f8d7da; color: #721c24; }
41
+ .status.info { background: #d1ecf1; color: #0c5460; }
42
+ .status.warn { background: #fff3cd; color: #856404; }
43
+
44
+ .table-list { max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 8px; margin: 10px 0; }
45
+ .table-item { padding: 4px 8px; display: flex; align-items: center; }
46
+ .table-item:hover { background: #f0f0f0; }
47
+ .table-item input[type="checkbox"] { margin-right: 8px; width: auto; }
48
+ .table-item label { display: inline; font-weight: normal; margin-bottom: 0; cursor: pointer; }
49
+
50
+ .progress-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
51
+ .progress-table th, .progress-table td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #eee; font-size: 0.9em; }
52
+ .progress-table th { background: #f8f9fa; font-weight: 600; }
53
+
54
+ .progress-bar-container { width: 120px; height: 16px; background: #e9ecef; border-radius: 8px; overflow: hidden; display: inline-block; vertical-align: middle; }
55
+ .progress-bar-fill { height: 100%; background: #28a745; transition: width 0.3s; }
56
+
57
+ .inline-group { display: flex; gap: 8px; align-items: flex-end; margin-bottom: 10px; }
58
+ .inline-group > div { flex: 1; }
59
+
60
+ a { color: #4a90d9; }
61
+
62
+ select { background: #fff; }
63
+
64
+ .data-table { width: 100%; border-collapse: collapse; font-size: 0.8em; font-family: monospace; }
65
+ .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; }
66
+ .data-table td { padding: 4px 8px; border: 1px solid #eee; white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
67
+ .data-table tr:nth-child(even) { background: #fafafa; }
68
+ .data-table tr:hover { background: #f0f7ff; }
69
+ </style>
70
+ </head>
71
+ <body>
72
+
73
+ <h1>Retold Data Cloner</h1>
74
+
75
+ <!-- ======== Section 1: Connection Management ======== -->
76
+ <div class="section">
77
+ <h2>1. Database Connection</h2>
78
+ <p>Use the <a href="/meadow-migrationmanager/" target="_blank">Migration Manager UI</a> to configure and test database connections. The local SQLite database is pre-configured.</p>
79
+ </div>
80
+
81
+ <!-- ======== Section 2: Remote Session ======== -->
82
+ <div class="section">
83
+ <h2>2. Remote Session</h2>
84
+
85
+ <div class="inline-group">
86
+ <div style="flex:2">
87
+ <label for="serverURL">Remote Server URL</label>
88
+ <input type="text" id="serverURL" placeholder="http://remote-server:8086" value="">
89
+ </div>
90
+ <div style="flex:1">
91
+ <label for="authMethod">Auth Method</label>
92
+ <input type="text" id="authMethod" placeholder="get" value="get">
93
+ </div>
94
+ </div>
95
+
96
+ <details style="margin-bottom:10px">
97
+ <summary style="cursor:pointer; font-size:0.9em; color:#666">Advanced Session Options</summary>
98
+ <div style="padding:10px 0">
99
+ <label for="authURI">Authentication URI Template (leave blank for default)</label>
100
+ <input type="text" id="authURI" placeholder="/1.0/Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}">
101
+ <label for="checkURI">Check Session URI Template</label>
102
+ <input type="text" id="checkURI" placeholder="/1.0/CheckSession">
103
+ <label for="cookieName">Cookie Name</label>
104
+ <input type="text" id="cookieName" placeholder="SessionID" value="SessionID">
105
+ <label for="cookieValueAddr">Cookie Value Address</label>
106
+ <input type="text" id="cookieValueAddr" placeholder="SessionID" value="SessionID">
107
+ <label for="cookieValueTemplate">Cookie Value Template (overrides Address if set)</label>
108
+ <input type="text" id="cookieValueTemplate" placeholder="{~D:Record.SessionID~}">
109
+ <label for="loginMarker">Login Marker</label>
110
+ <input type="text" id="loginMarker" placeholder="LoggedIn" value="LoggedIn">
111
+ </div>
112
+ </details>
113
+
114
+ <button class="primary" onclick="configureSession()">Configure Session</button>
115
+ <div id="sessionConfigStatus"></div>
116
+
117
+ <hr style="margin:16px 0; border:none; border-top:1px solid #eee">
118
+
119
+ <div class="inline-group">
120
+ <div>
121
+ <label for="userName">Username</label>
122
+ <input type="text" id="userName" placeholder="username">
123
+ </div>
124
+ <div>
125
+ <label for="password">Password</label>
126
+ <input type="password" id="password" placeholder="password">
127
+ </div>
128
+ </div>
129
+
130
+ <button class="success" onclick="authenticate()">Authenticate</button>
131
+ <button class="secondary" onclick="checkSession()">Check Session</button>
132
+ <button class="danger" onclick="deauthenticate()">Deauthenticate</button>
133
+ <div id="sessionAuthStatus"></div>
134
+ </div>
135
+
136
+ <!-- ======== Section 3: Schema ======== -->
137
+ <div class="section">
138
+ <h2>3. Remote Schema</h2>
139
+
140
+ <label for="schemaURL">Schema URL (leave blank for default: /1.0/Retold/Models)</label>
141
+ <input type="text" id="schemaURL" placeholder="http://remote-server:8086/1.0/Retold/Models">
142
+
143
+ <button class="primary" onclick="fetchSchema()">Fetch Schema</button>
144
+ <div id="schemaStatus"></div>
145
+
146
+ <div id="tableSelection" style="display:none">
147
+ <h3 style="margin:12px 0 8px; font-size:1em;">Select Tables</h3>
148
+ <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px">
149
+ <input type="text" id="tableFilter" placeholder="Filter tables..." style="flex:1; margin-bottom:0" oninput="filterTableList()">
150
+ <button class="secondary" onclick="selectAllTables(true)" style="font-size:0.8em">Select All</button>
151
+ <button class="secondary" onclick="selectAllTables(false)" style="font-size:0.8em">Deselect All</button>
152
+ <span id="tableSelectionCount" style="font-size:0.85em; color:#666; white-space:nowrap"></span>
153
+ </div>
154
+ <div id="tableList" class="table-list"></div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- ======== Section 4: Deploy ======== -->
159
+ <div class="section">
160
+ <h2>4. Deploy Schema</h2>
161
+ <p style="font-size:0.9em; color:#666; margin-bottom:10px">Creates the selected tables in the local SQLite database and sets up CRUD endpoints (e.g. GET /1.0/Documents).</p>
162
+ <button class="primary" onclick="deploySchema()">Deploy Selected Tables</button>
163
+ <button class="danger" onclick="resetDatabase()">Reset Database</button>
164
+ <div id="deployStatus"></div>
165
+ </div>
166
+
167
+ <!-- ======== Section 5: Sync ======== -->
168
+ <!-- NOTE: Section 6 (View Data) is below the Sync section -->
169
+ <div class="section">
170
+ <h2>5. Synchronize Data</h2>
171
+
172
+ <div class="inline-group">
173
+ <div style="flex:0 0 150px">
174
+ <label for="pageSize">Page Size</label>
175
+ <input type="number" id="pageSize" value="100" min="1" max="10000">
176
+ </div>
177
+ <div style="flex:1; display:flex; align-items:flex-end; gap:8px">
178
+ <button class="success" onclick="startSync()">Start Sync</button>
179
+ <button class="danger" onclick="stopSync()">Stop Sync</button>
180
+ </div>
181
+ </div>
182
+
183
+ <div id="syncStatus"></div>
184
+ <div id="syncProgress"></div>
185
+ </div>
186
+
187
+ <!-- ======== Section 6: View Data ======== -->
188
+ <div class="section">
189
+ <h2>6. View Data</h2>
190
+ <div class="inline-group">
191
+ <div style="flex:1">
192
+ <label for="viewTable">Table</label>
193
+ <select id="viewTable" style="width:100%; padding:8px 12px; border:1px solid #ccc; border-radius:4px; font-size:0.95em; margin-bottom:10px;">
194
+ <option value="">— deploy tables first —</option>
195
+ </select>
196
+ </div>
197
+ <div style="flex:0 0 120px">
198
+ <label for="viewLimit">Max Rows</label>
199
+ <input type="number" id="viewLimit" value="100" min="1" max="10000">
200
+ </div>
201
+ <div style="flex:0 0 auto; display:flex; align-items:flex-end">
202
+ <button class="primary" onclick="loadTableData()">Load</button>
203
+ </div>
204
+ </div>
205
+ <div id="viewStatus"></div>
206
+ <div id="viewDataContainer" style="overflow-x:auto; margin-top:10px"></div>
207
+ </div>
208
+
209
+ <script>
210
+ // ================================================================
211
+ // LocalStorage Persistence
212
+ // ================================================================
213
+
214
+ var _PersistFields = [
215
+ 'serverURL', 'authMethod', 'authURI', 'checkURI',
216
+ 'cookieName', 'cookieValueAddr', 'cookieValueTemplate', 'loginMarker',
217
+ 'userName', 'password', 'schemaURL', 'pageSize'
218
+ ];
219
+
220
+ function saveField(pFieldId)
221
+ {
222
+ var el = document.getElementById(pFieldId);
223
+ if (el)
224
+ {
225
+ localStorage.setItem('dataCloner_' + pFieldId, el.value);
226
+ }
227
+ }
228
+
229
+ function restoreFields()
230
+ {
231
+ for (var i = 0; i < _PersistFields.length; i++)
232
+ {
233
+ var tmpId = _PersistFields[i];
234
+ var tmpSaved = localStorage.getItem('dataCloner_' + tmpId);
235
+ if (tmpSaved !== null)
236
+ {
237
+ var el = document.getElementById(tmpId);
238
+ if (el) el.value = tmpSaved;
239
+ }
240
+ }
241
+ }
242
+
243
+ // Attach change listeners to all persisted fields
244
+ function initPersistence()
245
+ {
246
+ restoreFields();
247
+ for (var i = 0; i < _PersistFields.length; i++)
248
+ {
249
+ (function(pId)
250
+ {
251
+ var el = document.getElementById(pId);
252
+ if (el)
253
+ {
254
+ el.addEventListener('input', function() { saveField(pId); });
255
+ }
256
+ })(_PersistFields[i]);
257
+ }
258
+ }
259
+
260
+ // ================================================================
261
+ // API Helper
262
+ // ================================================================
263
+
264
+ function api(method, path, body)
265
+ {
266
+ var opts = { method: method, headers: {} };
267
+ if (body)
268
+ {
269
+ opts.headers['Content-Type'] = 'application/json';
270
+ opts.body = JSON.stringify(body);
271
+ }
272
+ return fetch(path, opts).then(function(r) { return r.json(); });
273
+ }
274
+
275
+ function setStatus(elementId, message, type)
276
+ {
277
+ var el = document.getElementById(elementId);
278
+ el.className = 'status ' + (type || 'info');
279
+ el.textContent = message;
280
+ el.style.display = 'block';
281
+ }
282
+
283
+ // ================================================================
284
+ // Session Management
285
+ // ================================================================
286
+
287
+ function configureSession()
288
+ {
289
+ var serverURL = document.getElementById('serverURL').value.trim();
290
+ if (!serverURL)
291
+ {
292
+ setStatus('sessionConfigStatus', 'Server URL is required.', 'error');
293
+ return;
294
+ }
295
+
296
+ var body = { ServerURL: serverURL };
297
+
298
+ var authMethod = document.getElementById('authMethod').value.trim();
299
+ if (authMethod) body.AuthenticationMethod = authMethod;
300
+
301
+ var authURI = document.getElementById('authURI').value.trim();
302
+ if (authURI) body.AuthenticationURITemplate = authURI;
303
+
304
+ var checkURI = document.getElementById('checkURI').value.trim();
305
+ if (checkURI) body.CheckSessionURITemplate = checkURI;
306
+
307
+ var cookieName = document.getElementById('cookieName').value.trim();
308
+ if (cookieName) body.CookieName = cookieName;
309
+
310
+ var cookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
311
+ if (cookieValueAddr) body.CookieValueAddress = cookieValueAddr;
312
+
313
+ var cookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
314
+ if (cookieValueTemplate) body.CookieValueTemplate = cookieValueTemplate;
315
+
316
+ var loginMarker = document.getElementById('loginMarker').value.trim();
317
+ if (loginMarker) body.CheckSessionLoginMarker = loginMarker;
318
+
319
+ setStatus('sessionConfigStatus', 'Configuring session...', 'info');
320
+
321
+ api('POST', '/clone/session/configure', body)
322
+ .then(function(data)
323
+ {
324
+ if (data.Success)
325
+ {
326
+ setStatus('sessionConfigStatus', 'Session configured for ' + data.ServerURL + ' (domain: ' + data.DomainMatch + ')', 'ok');
327
+ }
328
+ else
329
+ {
330
+ setStatus('sessionConfigStatus', 'Configuration failed: ' + (data.Error || 'Unknown error'), 'error');
331
+ }
332
+ })
333
+ .catch(function(err)
334
+ {
335
+ setStatus('sessionConfigStatus', 'Request failed: ' + err.message, 'error');
336
+ });
337
+ }
338
+
339
+ function authenticate()
340
+ {
341
+ var userName = document.getElementById('userName').value.trim();
342
+ var password = document.getElementById('password').value.trim();
343
+
344
+ if (!userName || !password)
345
+ {
346
+ setStatus('sessionAuthStatus', 'Username and password are required.', 'error');
347
+ return;
348
+ }
349
+
350
+ setStatus('sessionAuthStatus', 'Authenticating...', 'info');
351
+
352
+ api('POST', '/clone/session/authenticate', { UserName: userName, Password: password })
353
+ .then(function(data)
354
+ {
355
+ if (data.Success && data.Authenticated)
356
+ {
357
+ setStatus('sessionAuthStatus', 'Authenticated successfully.', 'ok');
358
+ }
359
+ else
360
+ {
361
+ setStatus('sessionAuthStatus', 'Authentication failed: ' + (data.Error || 'Not authenticated'), 'error');
362
+ }
363
+ })
364
+ .catch(function(err)
365
+ {
366
+ setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
367
+ });
368
+ }
369
+
370
+ function checkSession()
371
+ {
372
+ setStatus('sessionAuthStatus', 'Checking session...', 'info');
373
+
374
+ api('GET', '/clone/session/check')
375
+ .then(function(data)
376
+ {
377
+ if (data.Authenticated)
378
+ {
379
+ setStatus('sessionAuthStatus', 'Session is active. Server: ' + (data.ServerURL || 'N/A'), 'ok');
380
+ }
381
+ else if (data.Configured)
382
+ {
383
+ setStatus('sessionAuthStatus', 'Session configured but not authenticated.', 'warn');
384
+ }
385
+ else
386
+ {
387
+ setStatus('sessionAuthStatus', 'No session configured.', 'warn');
388
+ }
389
+ })
390
+ .catch(function(err)
391
+ {
392
+ setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
393
+ });
394
+ }
395
+
396
+ function deauthenticate()
397
+ {
398
+ api('POST', '/clone/session/deauthenticate')
399
+ .then(function(data)
400
+ {
401
+ setStatus('sessionAuthStatus', 'Session deauthenticated.', 'info');
402
+ })
403
+ .catch(function(err)
404
+ {
405
+ setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
406
+ });
407
+ }
408
+
409
+ // ================================================================
410
+ // Schema Management
411
+ // ================================================================
412
+
413
+ var _FetchedTables = [];
414
+
415
+ function fetchSchema()
416
+ {
417
+ var schemaURL = document.getElementById('schemaURL').value.trim();
418
+ var body = {};
419
+ if (schemaURL) body.SchemaURL = schemaURL;
420
+
421
+ setStatus('schemaStatus', 'Fetching schema...', 'info');
422
+
423
+ api('POST', '/clone/schema/fetch', body)
424
+ .then(function(data)
425
+ {
426
+ if (data.Success)
427
+ {
428
+ _FetchedTables = data.Tables || [];
429
+ setStatus('schemaStatus', 'Fetched ' + data.TableCount + ' tables from ' + data.SchemaURL, 'ok');
430
+ renderTableList();
431
+ }
432
+ else
433
+ {
434
+ setStatus('schemaStatus', 'Fetch failed: ' + (data.Error || 'Unknown error'), 'error');
435
+ }
436
+ })
437
+ .catch(function(err)
438
+ {
439
+ setStatus('schemaStatus', 'Request failed: ' + err.message, 'error');
440
+ });
441
+ }
442
+
443
+ function loadSavedSelections()
444
+ {
445
+ try
446
+ {
447
+ var tmpRaw = localStorage.getItem('dataCloner_selectedTables');
448
+ if (tmpRaw) return JSON.parse(tmpRaw);
449
+ }
450
+ catch (e) { /* ignore */ }
451
+ return null;
452
+ }
453
+
454
+ function saveSelections()
455
+ {
456
+ var tmpSelected = getSelectedTables();
457
+ localStorage.setItem('dataCloner_selectedTables', JSON.stringify(tmpSelected));
458
+ updateSelectionCount();
459
+ }
460
+
461
+ function updateSelectionCount()
462
+ {
463
+ var tmpCount = getSelectedTables().length;
464
+ var tmpEl = document.getElementById('tableSelectionCount');
465
+ if (tmpEl) tmpEl.textContent = tmpCount + ' / ' + _FetchedTables.length + ' selected';
466
+ }
467
+
468
+ function renderTableList()
469
+ {
470
+ var container = document.getElementById('tableList');
471
+ container.innerHTML = '';
472
+
473
+ // Load previously saved selections; if none, default to none checked
474
+ var tmpSaved = loadSavedSelections();
475
+ var tmpSavedSet = null;
476
+ if (tmpSaved)
477
+ {
478
+ tmpSavedSet = {};
479
+ for (var s = 0; s < tmpSaved.length; s++) tmpSavedSet[tmpSaved[s]] = true;
480
+ }
481
+
482
+ for (var i = 0; i < _FetchedTables.length; i++)
483
+ {
484
+ var name = _FetchedTables[i];
485
+ var div = document.createElement('div');
486
+ div.className = 'table-item';
487
+ div.setAttribute('data-table', name.toLowerCase());
488
+
489
+ var checkbox = document.createElement('input');
490
+ checkbox.type = 'checkbox';
491
+ checkbox.id = 'tbl_' + name;
492
+ checkbox.value = name;
493
+ // If we have saved selections, restore them; otherwise default unchecked
494
+ checkbox.checked = tmpSavedSet ? (tmpSavedSet[name] === true) : false;
495
+ checkbox.addEventListener('change', saveSelections);
496
+
497
+ var label = document.createElement('label');
498
+ label.htmlFor = 'tbl_' + name;
499
+ label.textContent = name;
500
+
501
+ div.appendChild(checkbox);
502
+ div.appendChild(label);
503
+ container.appendChild(div);
504
+ }
505
+
506
+ document.getElementById('tableSelection').style.display = _FetchedTables.length > 0 ? 'block' : 'none';
507
+ document.getElementById('tableFilter').value = '';
508
+ updateSelectionCount();
509
+ }
510
+
511
+ function filterTableList()
512
+ {
513
+ var tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
514
+ var tmpItems = document.getElementById('tableList').children;
515
+ for (var i = 0; i < tmpItems.length; i++)
516
+ {
517
+ var tmpName = tmpItems[i].getAttribute('data-table') || '';
518
+ tmpItems[i].style.display = (!tmpFilter || tmpName.indexOf(tmpFilter) >= 0) ? '' : 'none';
519
+ }
520
+ }
521
+
522
+ function selectAllTables(pChecked)
523
+ {
524
+ // Only affect visible (non-filtered) items
525
+ var tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
526
+ for (var i = 0; i < _FetchedTables.length; i++)
527
+ {
528
+ var name = _FetchedTables[i];
529
+ if (tmpFilter && name.toLowerCase().indexOf(tmpFilter) < 0) continue;
530
+ var cb = document.getElementById('tbl_' + name);
531
+ if (cb) cb.checked = pChecked;
532
+ }
533
+ saveSelections();
534
+ }
535
+
536
+ function getSelectedTables()
537
+ {
538
+ var selected = [];
539
+ for (var i = 0; i < _FetchedTables.length; i++)
540
+ {
541
+ var cb = document.getElementById('tbl_' + _FetchedTables[i]);
542
+ if (cb && cb.checked) selected.push(_FetchedTables[i]);
543
+ }
544
+ return selected;
545
+ }
546
+
547
+ // ================================================================
548
+ // Deploy
549
+ // ================================================================
550
+
551
+ function deploySchema()
552
+ {
553
+ var selectedTables = getSelectedTables();
554
+
555
+ if (selectedTables.length === 0)
556
+ {
557
+ setStatus('deployStatus', 'No tables selected. Fetch a schema and select tables first.', 'error');
558
+ return;
559
+ }
560
+
561
+ setStatus('deployStatus', 'Deploying ' + selectedTables.length + ' tables...', 'info');
562
+
563
+ api('POST', '/clone/schema/deploy', { Tables: selectedTables })
564
+ .then(function(data)
565
+ {
566
+ if (data.Success)
567
+ {
568
+ setStatus('deployStatus', data.Message, 'ok');
569
+ _DeployedTables = data.TablesDeployed || selectedTables;
570
+ saveDeployedTables();
571
+ populateViewTableDropdown();
572
+ }
573
+ else
574
+ {
575
+ setStatus('deployStatus', 'Deploy failed: ' + (data.Error || 'Unknown error'), 'error');
576
+ }
577
+ })
578
+ .catch(function(err)
579
+ {
580
+ setStatus('deployStatus', 'Request failed: ' + err.message, 'error');
581
+ });
582
+ }
583
+
584
+ function resetDatabase()
585
+ {
586
+ if (!confirm('This will delete ALL data in the local SQLite database. Continue?'))
587
+ {
588
+ return;
589
+ }
590
+
591
+ setStatus('deployStatus', 'Resetting database...', 'info');
592
+
593
+ api('POST', '/clone/reset')
594
+ .then(function(data)
595
+ {
596
+ if (data.Success)
597
+ {
598
+ setStatus('deployStatus', data.Message, 'ok');
599
+ // Clear the sync progress display
600
+ document.getElementById('syncProgress').innerHTML = '';
601
+ }
602
+ else
603
+ {
604
+ setStatus('deployStatus', 'Reset failed: ' + (data.Error || 'Unknown error'), 'error');
605
+ }
606
+ })
607
+ .catch(function(err)
608
+ {
609
+ setStatus('deployStatus', 'Request failed: ' + err.message, 'error');
610
+ });
611
+ }
612
+
613
+ // ================================================================
614
+ // Sync
615
+ // ================================================================
616
+
617
+ var _SyncPollTimer = null;
618
+
619
+ function startSync()
620
+ {
621
+ var selectedTables = getSelectedTables();
622
+ var pageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
623
+
624
+ if (selectedTables.length === 0)
625
+ {
626
+ setStatus('syncStatus', 'No tables selected for sync.', 'error');
627
+ return;
628
+ }
629
+
630
+ setStatus('syncStatus', 'Starting sync...', 'info');
631
+
632
+ api('POST', '/clone/sync/start', { Tables: selectedTables, PageSize: pageSize })
633
+ .then(function(data)
634
+ {
635
+ if (data.Success)
636
+ {
637
+ setStatus('syncStatus', 'Sync started for ' + data.Tables.length + ' tables.', 'ok');
638
+ startPolling();
639
+ }
640
+ else
641
+ {
642
+ setStatus('syncStatus', 'Sync start failed: ' + (data.Error || 'Unknown error'), 'error');
643
+ }
644
+ })
645
+ .catch(function(err)
646
+ {
647
+ setStatus('syncStatus', 'Request failed: ' + err.message, 'error');
648
+ });
649
+ }
650
+
651
+ function stopSync()
652
+ {
653
+ api('POST', '/clone/sync/stop')
654
+ .then(function(data)
655
+ {
656
+ setStatus('syncStatus', 'Sync stop requested.', 'warn');
657
+ })
658
+ .catch(function(err)
659
+ {
660
+ setStatus('syncStatus', 'Request failed: ' + err.message, 'error');
661
+ });
662
+ }
663
+
664
+ function startPolling()
665
+ {
666
+ if (_SyncPollTimer) clearInterval(_SyncPollTimer);
667
+ _SyncPollTimer = setInterval(pollSyncStatus, 2000);
668
+ pollSyncStatus();
669
+ }
670
+
671
+ function stopPolling()
672
+ {
673
+ if (_SyncPollTimer)
674
+ {
675
+ clearInterval(_SyncPollTimer);
676
+ _SyncPollTimer = null;
677
+ }
678
+ }
679
+
680
+ function pollSyncStatus()
681
+ {
682
+ api('GET', '/clone/sync/status')
683
+ .then(function(data)
684
+ {
685
+ renderSyncProgress(data);
686
+
687
+ if (!data.Running && !data.Stopping)
688
+ {
689
+ stopPolling();
690
+ if (Object.keys(data.Tables || {}).length > 0)
691
+ {
692
+ // Check if any tables had errors or partial sync
693
+ var tables = data.Tables || {};
694
+ var hasErrors = false;
695
+ var hasPartial = false;
696
+ var names = Object.keys(tables);
697
+ for (var i = 0; i < names.length; i++)
698
+ {
699
+ if (tables[names[i]].Status === 'Error') hasErrors = true;
700
+ if (tables[names[i]].Status === 'Partial') hasPartial = true;
701
+ }
702
+
703
+ if (hasErrors)
704
+ {
705
+ setStatus('syncStatus', 'Sync finished with errors. Check the table below for details.', 'error');
706
+ }
707
+ else if (hasPartial)
708
+ {
709
+ setStatus('syncStatus', 'Sync finished. Some records were skipped (GUID conflicts or permission issues).', 'warn');
710
+ }
711
+ else
712
+ {
713
+ setStatus('syncStatus', 'Sync complete.', 'ok');
714
+ }
715
+ }
716
+ }
717
+ })
718
+ .catch(function(err)
719
+ {
720
+ // Silently ignore poll errors
721
+ });
722
+ }
723
+
724
+ function renderSyncProgress(data)
725
+ {
726
+ var container = document.getElementById('syncProgress');
727
+ var tables = data.Tables || {};
728
+ var tableNames = Object.keys(tables);
729
+
730
+ if (tableNames.length === 0)
731
+ {
732
+ container.innerHTML = '';
733
+ return;
734
+ }
735
+
736
+ var html = '<table class="progress-table">';
737
+ html += '<tr><th>Table</th><th>Status</th><th>Progress</th><th>Synced</th><th>Details</th></tr>';
738
+
739
+ for (var i = 0; i < tableNames.length; i++)
740
+ {
741
+ var name = tableNames[i];
742
+ var t = tables[name];
743
+
744
+ // Calculate percentage: if total is 0, show 100% (nothing to sync)
745
+ var pct = 0;
746
+ if (t.Total === 0 && (t.Status === 'Complete' || t.Status === 'Error'))
747
+ {
748
+ pct = 100;
749
+ }
750
+ else if (t.Total > 0)
751
+ {
752
+ pct = Math.round((t.Synced / t.Total) * 100);
753
+ }
754
+
755
+ // Color the progress bar based on status
756
+ var barColor = '#28a745'; // green
757
+ if (t.Status === 'Error') barColor = '#dc3545'; // red
758
+ else if (t.Status === 'Partial') barColor = '#ffc107'; // yellow
759
+ else if (t.Status === 'Syncing') barColor = '#4a90d9'; // blue
760
+
761
+ // Status badge
762
+ var statusBadge = t.Status;
763
+ if (t.Status === 'Complete' && t.Total === 0) statusBadge = 'Complete (empty)';
764
+ if (t.Status === 'Partial') statusBadge = 'Partial \u26A0';
765
+ if (t.Status === 'Error') statusBadge = 'Error \u2716';
766
+
767
+ // Details column
768
+ var details = '';
769
+ if (t.ErrorMessage) details = t.ErrorMessage;
770
+ else if (t.Skipped > 0) details = t.Skipped + ' record(s) skipped';
771
+ else if ((t.Errors || 0) > 0) details = t.Errors + ' error(s)';
772
+ else if (t.Status === 'Complete' && t.Total === 0) details = 'No records on server';
773
+ else if (t.Status === 'Complete') details = '\u2714 OK';
774
+
775
+ html += '<tr>';
776
+ html += '<td><strong>' + name + '</strong></td>';
777
+ html += '<td>' + statusBadge + '</td>';
778
+ html += '<td>';
779
+ html += '<div class="progress-bar-container"><div class="progress-bar-fill" style="width:' + pct + '%; background:' + barColor + '"></div></div>';
780
+ html += ' ' + pct + '%';
781
+ html += '</td>';
782
+ html += '<td>' + t.Synced + ' / ' + t.Total + '</td>';
783
+ html += '<td>' + details + '</td>';
784
+ html += '</tr>';
785
+ }
786
+
787
+ html += '</table>';
788
+ container.innerHTML = html;
789
+ }
790
+
791
+ // ================================================================
792
+ // View Data
793
+ // ================================================================
794
+
795
+ var _DeployedTables = [];
796
+
797
+ function populateViewTableDropdown()
798
+ {
799
+ var tmpSelect = document.getElementById('viewTable');
800
+ var tmpCurrentValue = tmpSelect.value;
801
+
802
+ tmpSelect.innerHTML = '';
803
+
804
+ if (_DeployedTables.length === 0)
805
+ {
806
+ var opt = document.createElement('option');
807
+ opt.value = '';
808
+ opt.textContent = '— deploy tables first —';
809
+ tmpSelect.appendChild(opt);
810
+ return;
811
+ }
812
+
813
+ for (var i = 0; i < _DeployedTables.length; i++)
814
+ {
815
+ var opt = document.createElement('option');
816
+ opt.value = _DeployedTables[i];
817
+ opt.textContent = _DeployedTables[i];
818
+ tmpSelect.appendChild(opt);
819
+ }
820
+
821
+ // Restore previous selection if it exists
822
+ if (tmpCurrentValue)
823
+ {
824
+ tmpSelect.value = tmpCurrentValue;
825
+ }
826
+ }
827
+
828
+ function loadTableData()
829
+ {
830
+ var tmpTable = document.getElementById('viewTable').value;
831
+ var tmpLimit = parseInt(document.getElementById('viewLimit').value, 10) || 100;
832
+
833
+ if (!tmpTable)
834
+ {
835
+ setStatus('viewStatus', 'Select a table first.', 'error');
836
+ return;
837
+ }
838
+
839
+ setStatus('viewStatus', 'Loading ' + tmpTable + '...', 'info');
840
+ document.getElementById('viewDataContainer').innerHTML = '';
841
+
842
+ // Use the standard Meadow CRUD list endpoint: /1.0/{Entity}s/0/{Cap}
843
+ api('GET', '/1.0/' + tmpTable + 's/0/' + tmpLimit)
844
+ .then(function(data)
845
+ {
846
+ if (!Array.isArray(data))
847
+ {
848
+ setStatus('viewStatus', 'Unexpected response (not an array). The table may not be deployed yet.', 'error');
849
+ return;
850
+ }
851
+
852
+ setStatus('viewStatus', data.length + ' row(s) returned' + (data.length >= tmpLimit ? ' (limit reached — increase Max Rows to see more)' : '') + '.', 'ok');
853
+ renderDataTable(data);
854
+ })
855
+ .catch(function(err)
856
+ {
857
+ setStatus('viewStatus', 'Request failed: ' + err.message, 'error');
858
+ });
859
+ }
860
+
861
+ function renderDataTable(pRows)
862
+ {
863
+ var container = document.getElementById('viewDataContainer');
864
+
865
+ if (!pRows || pRows.length === 0)
866
+ {
867
+ container.innerHTML = '<p style="color:#666; font-size:0.9em; padding:8px">No rows.</p>';
868
+ return;
869
+ }
870
+
871
+ // Collect all column names from the first row
872
+ var tmpColumns = Object.keys(pRows[0]);
873
+
874
+ var html = '<table class="data-table">';
875
+ html += '<thead><tr>';
876
+ for (var c = 0; c < tmpColumns.length; c++)
877
+ {
878
+ html += '<th>' + escapeHtml(tmpColumns[c]) + '</th>';
879
+ }
880
+ html += '</tr></thead>';
881
+
882
+ html += '<tbody>';
883
+ for (var r = 0; r < pRows.length; r++)
884
+ {
885
+ html += '<tr>';
886
+ for (var c = 0; c < tmpColumns.length; c++)
887
+ {
888
+ var val = pRows[r][tmpColumns[c]];
889
+ var display = (val === null || val === undefined) ? '' : String(val);
890
+ html += '<td title="' + escapeHtml(display) + '">' + escapeHtml(display) + '</td>';
891
+ }
892
+ html += '</tr>';
893
+ }
894
+ html += '</tbody></table>';
895
+
896
+ container.innerHTML = html;
897
+ }
898
+
899
+ function escapeHtml(pStr)
900
+ {
901
+ var div = document.createElement('div');
902
+ div.appendChild(document.createTextNode(pStr));
903
+ return div.innerHTML;
904
+ }
905
+
906
+ // ================================================================
907
+ // Initialize
908
+ // ================================================================
909
+
910
+ // Persist deployed tables across page reload
911
+ function saveDeployedTables()
912
+ {
913
+ localStorage.setItem('dataCloner_deployedTables', JSON.stringify(_DeployedTables));
914
+ }
915
+
916
+ function restoreDeployedTables()
917
+ {
918
+ try
919
+ {
920
+ var tmpRaw = localStorage.getItem('dataCloner_deployedTables');
921
+ if (tmpRaw)
922
+ {
923
+ _DeployedTables = JSON.parse(tmpRaw);
924
+ populateViewTableDropdown();
925
+ }
926
+ }
927
+ catch (e) { /* ignore */ }
928
+ }
929
+
930
+ initPersistence();
931
+ restoreDeployedTables();
932
+ </script>
933
+
934
+ </body>
935
+ </html>