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.
- package/.claude/launch.json +2 -2
- package/.quackage.json +19 -0
- package/package.json +13 -6
- package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
- package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
- package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
- package/source/services/data-cloner/web/data-cloner.js +7952 -0
- package/source/services/data-cloner/web/data-cloner.js.map +1 -0
- package/source/services/data-cloner/web/data-cloner.min.js +2 -0
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
- package/source/services/data-cloner/web/index.html +17 -0
- package/test/DataCloner-Integration_tests.js +1205 -0
- package/test/DataCloner-Puppeteer_tests.js +502 -0
- package/test/integration-report.json +311 -0
- package/test/run-integration-tests.js +501 -0
- 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">▼</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">▼</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">▼</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">▼</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">▼</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">▼</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">▼</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 = '✓';
|
|
786
|
-
tmpEl.classList.add('visible', 'accordion-phase-ok');
|
|
787
|
-
}
|
|
788
|
-
else if (pState === 'error')
|
|
789
|
-
{
|
|
790
|
-
tmpEl.innerHTML = '✗';
|
|
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>
|