ivoryos 1.2.5__py3-none-any.whl → 1.4.4__py3-none-any.whl
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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +16 -246
- ivoryos/app.py +154 -0
- ivoryos/optimizer/ax_optimizer.py +55 -28
- ivoryos/optimizer/base_optimizer.py +20 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +3 -1
- ivoryos/routes/auth/auth.py +35 -8
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +58 -28
- ivoryos/routes/control/control_file.py +12 -15
- ivoryos/routes/control/control_new_device.py +21 -11
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +110 -44
- ivoryos/routes/data/templates/components/step_card.html +78 -13
- ivoryos/routes/data/templates/workflow_view.html +343 -113
- ivoryos/routes/design/design.py +59 -10
- ivoryos/routes/design/design_file.py +3 -3
- ivoryos/routes/design/design_step.py +43 -17
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +3 -0
- ivoryos/routes/execute/execute.py +82 -22
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/routes/library/library.py +9 -11
- ivoryos/routes/main/main.py +30 -2
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +259 -88
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +29 -11
- ivoryos/templates/base.html +61 -2
- ivoryos/utils/bo_campaign.py +18 -17
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +286 -60
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +52 -19
- ivoryos/utils/global_config.py +21 -0
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +80 -10
- ivoryos/utils/script_runner.py +573 -189
- ivoryos/utils/task_runner.py +69 -22
- ivoryos/utils/utils.py +48 -5
- ivoryos/version.py +1 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- ivoryos-1.4.4.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +133 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +80 -0
- tests/integration/test_route_control.py +94 -0
- tests/integration/test_route_database.py +61 -0
- tests/integration/test_route_design.py +36 -0
- tests/integration/test_route_main.py +35 -0
- tests/integration/test_sockets.py +26 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/api/api.py +0 -56
- ivoryos-1.2.5.dist-info/RECORD +0 -100
- ivoryos-1.2.5.dist-info/top_level.txt +0 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -68,24 +68,35 @@
|
|
|
68
68
|
</tbody>
|
|
69
69
|
</table>
|
|
70
70
|
</div>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
|
|
72
|
+
<div class="card-footer">
|
|
73
|
+
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
|
74
|
+
<!-- Left side: Action buttons -->
|
|
75
|
+
<div class="d-flex gap-2 flex-wrap">
|
|
76
|
+
<button type="button" class="btn btn-success" onclick="addRow()">
|
|
77
|
+
<i class="bi bi-plus-circle"></i> Add Row
|
|
78
|
+
</button>
|
|
79
|
+
<button type="button" class="btn btn-warning" onclick="clearAllRows()">
|
|
80
|
+
<i class="bi bi-trash"></i> Clear All
|
|
81
|
+
</button>
|
|
82
|
+
<button type="button" class="btn btn-info" onclick="resetToFile()" id="resetToFileBtn" style="display: none;">
|
|
83
|
+
<i class="bi bi-arrow-clockwise"></i> Reset to File
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Right side: Batch size and Run button -->
|
|
88
|
+
<div class="d-flex gap-2 align-items-center flex-wrap">
|
|
89
|
+
<div class="input-group" style="width: auto;">
|
|
90
|
+
<label class="input-group-text" for="batch_size">Batch size</label>
|
|
91
|
+
<input class="form-control" type="number" id="batch_size" name="batch_size" min="1" max="1000" value="1" style="width: 80px;">
|
|
92
|
+
</div>
|
|
93
|
+
<button type="submit" name="online-config" class="btn btn-primary btn-lg">
|
|
94
|
+
<i class="bi bi-play-circle"></i> Run
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
83
98
|
</div>
|
|
84
|
-
|
|
85
|
-
<i class="bi bi-play-circle"></i> Run
|
|
86
|
-
</button>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
99
|
+
|
|
89
100
|
</form>
|
|
90
101
|
</div>
|
|
91
102
|
<!-- Config Preview (if loaded from file) -->
|
|
@@ -95,4 +106,278 @@
|
|
|
95
106
|
</div>
|
|
96
107
|
{% endif %}
|
|
97
108
|
</div>
|
|
98
|
-
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<script>
|
|
111
|
+
var rowCount = 0;
|
|
112
|
+
var configColumns = [
|
|
113
|
+
{% for column in config_list %}
|
|
114
|
+
'{{ column }}'{{ ',' if not loop.last else '' }}
|
|
115
|
+
{% endfor %}
|
|
116
|
+
];
|
|
117
|
+
var configTypes = {
|
|
118
|
+
{% for column, type in config_type_list.items() %}
|
|
119
|
+
'{{ column }}': '{{ type }}'{{ ',' if not loop.last else '' }}
|
|
120
|
+
{% endfor %}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// State management
|
|
124
|
+
var originalFileData = null;
|
|
125
|
+
var isModifiedFromFile = false;
|
|
126
|
+
var saveTimeout = null;
|
|
127
|
+
var lastSavedData = null;
|
|
128
|
+
|
|
129
|
+
function addRow(data = null, skipSave = false) {
|
|
130
|
+
rowCount++;
|
|
131
|
+
var tableBody = document.getElementById("tableBody");
|
|
132
|
+
var newRow = tableBody.insertRow(-1);
|
|
133
|
+
|
|
134
|
+
// Row number cell
|
|
135
|
+
var rowNumCell = newRow.insertCell(-1);
|
|
136
|
+
rowNumCell.innerHTML = '<span class="badge bg-secondary">' + rowCount + '</span>';
|
|
137
|
+
|
|
138
|
+
// Data cells
|
|
139
|
+
configColumns.forEach(function(column, index) {
|
|
140
|
+
var cell = newRow.insertCell(-1);
|
|
141
|
+
var value = data && data[column] ? data[column] : '';
|
|
142
|
+
var placeholder = configTypes[column] || 'value';
|
|
143
|
+
cell.innerHTML = '<input type="text" class="form-control form-control-sm" name="' +
|
|
144
|
+
column + '[' + rowCount + ']" value="' + value + '" placeholder="' + placeholder +
|
|
145
|
+
'" oninput="onInputChange()" onchange="onInputChange()">';
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Action cell
|
|
149
|
+
var actionCell = newRow.insertCell(-1);
|
|
150
|
+
actionCell.innerHTML = '<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRow(this)" title="Remove row">' +
|
|
151
|
+
'<i class="bi bi-trash"></i></button>';
|
|
152
|
+
|
|
153
|
+
if (!skipSave) {
|
|
154
|
+
markAsModified();
|
|
155
|
+
debouncedSave();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function removeRow(button) {
|
|
160
|
+
var row = button.closest('tr');
|
|
161
|
+
row.remove();
|
|
162
|
+
updateRowNumbers();
|
|
163
|
+
markAsModified();
|
|
164
|
+
debouncedSave();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function updateRowNumbers() {
|
|
168
|
+
var tableBody = document.getElementById("tableBody");
|
|
169
|
+
var rows = tableBody.getElementsByTagName('tr');
|
|
170
|
+
for (var i = 0; i < rows.length; i++) {
|
|
171
|
+
var badge = rows[i].querySelector('.badge');
|
|
172
|
+
if (badge) {
|
|
173
|
+
badge.textContent = i + 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function clearAllRows() {
|
|
179
|
+
if (confirm('Are you sure you want to clear all rows?')) {
|
|
180
|
+
var tableBody = document.getElementById("tableBody");
|
|
181
|
+
tableBody.innerHTML = '';
|
|
182
|
+
rowCount = 0;
|
|
183
|
+
markAsModified();
|
|
184
|
+
clearSavedData();
|
|
185
|
+
// Add 5 empty rows by default
|
|
186
|
+
for (let i = 0; i < 5; i++) {
|
|
187
|
+
addRow(null, true);
|
|
188
|
+
}
|
|
189
|
+
debouncedSave();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resetToFile() {
|
|
194
|
+
if (originalFileData && confirm('Reset to original file data? This will lose all manual changes.')) {
|
|
195
|
+
loadDataFromSource(originalFileData, false);
|
|
196
|
+
isModifiedFromFile = false;
|
|
197
|
+
updateStatusIndicators();
|
|
198
|
+
debouncedSave();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function onInputChange() {
|
|
203
|
+
markAsModified();
|
|
204
|
+
debouncedSave();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function markAsModified() {
|
|
208
|
+
if (originalFileData) {
|
|
209
|
+
isModifiedFromFile = true;
|
|
210
|
+
updateStatusIndicators();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function updateStatusIndicators() {
|
|
215
|
+
var modifiedStatus = document.getElementById('modifiedStatus');
|
|
216
|
+
var resetBtn = document.getElementById('resetToFileBtn');
|
|
217
|
+
|
|
218
|
+
if (isModifiedFromFile && originalFileData) {
|
|
219
|
+
modifiedStatus.style.display = 'inline-block';
|
|
220
|
+
resetBtn.style.display = 'inline-block';
|
|
221
|
+
} else {
|
|
222
|
+
modifiedStatus.style.display = 'none';
|
|
223
|
+
resetBtn.style.display = 'none';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function showSaveStatus() {
|
|
228
|
+
var saveStatus = document.getElementById('saveStatus');
|
|
229
|
+
saveStatus.style.display = 'inline-block';
|
|
230
|
+
setTimeout(function() {
|
|
231
|
+
saveStatus.style.display = 'none';
|
|
232
|
+
}, 2000);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function debouncedSave() {
|
|
236
|
+
clearTimeout(saveTimeout);
|
|
237
|
+
saveTimeout = setTimeout(function() {
|
|
238
|
+
saveFormData();
|
|
239
|
+
showSaveStatus();
|
|
240
|
+
}, 1000); // Save 1 second after user stops typing
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function saveFormData() {
|
|
244
|
+
var formData = getCurrentFormData();
|
|
245
|
+
try {
|
|
246
|
+
sessionStorage.setItem('configFormData', JSON.stringify(formData));
|
|
247
|
+
sessionStorage.setItem('configModified', isModifiedFromFile.toString());
|
|
248
|
+
lastSavedData = formData;
|
|
249
|
+
console.log('Tab2: Saved form data', formData.length, 'rows');
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.warn('Could not save form data to sessionStorage:', e);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getCurrentFormData() {
|
|
256
|
+
var tableBody = document.getElementById("tableBody");
|
|
257
|
+
var rows = tableBody.getElementsByTagName('tr');
|
|
258
|
+
var data = [];
|
|
259
|
+
|
|
260
|
+
for (var i = 0; i < rows.length; i++) {
|
|
261
|
+
var inputs = rows[i].getElementsByTagName('input');
|
|
262
|
+
var rowData = {};
|
|
263
|
+
var hasData = false;
|
|
264
|
+
|
|
265
|
+
for (var j = 0; j < inputs.length; j++) {
|
|
266
|
+
var input = inputs[j];
|
|
267
|
+
var name = input.name;
|
|
268
|
+
if (name) {
|
|
269
|
+
var columnName = name.substring(0, name.indexOf('['));
|
|
270
|
+
rowData[columnName] = input.value;
|
|
271
|
+
if (input.value.trim() !== '') {
|
|
272
|
+
hasData = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (hasData) {
|
|
278
|
+
data.push(rowData);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return data;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function loadSavedData() {
|
|
286
|
+
try {
|
|
287
|
+
var savedData = sessionStorage.getItem('configFormData');
|
|
288
|
+
var savedModified = sessionStorage.getItem('configModified');
|
|
289
|
+
|
|
290
|
+
if (savedData) {
|
|
291
|
+
var parsedData = JSON.parse(savedData);
|
|
292
|
+
isModifiedFromFile = savedModified === 'true';
|
|
293
|
+
console.log('Tab2: Loaded saved data', parsedData.length, 'rows');
|
|
294
|
+
return parsedData;
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn('Could not load saved form data:', e);
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function clearSavedData() {
|
|
303
|
+
try {
|
|
304
|
+
sessionStorage.removeItem('configFormData');
|
|
305
|
+
sessionStorage.removeItem('configModified');
|
|
306
|
+
console.log('Tab2: Cleared saved data');
|
|
307
|
+
} catch (e) {
|
|
308
|
+
console.warn('Could not clear saved data:', e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function loadDataFromSource(data, isFromFile = false) {
|
|
313
|
+
// Clear existing rows
|
|
314
|
+
var tableBody = document.getElementById("tableBody");
|
|
315
|
+
tableBody.innerHTML = '';
|
|
316
|
+
rowCount = 0;
|
|
317
|
+
|
|
318
|
+
// Add rows with data
|
|
319
|
+
data.forEach(function(rowData) {
|
|
320
|
+
addRow(rowData, true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Add a few empty rows for additional input
|
|
324
|
+
for (let i = 0; i < 3; i++) {
|
|
325
|
+
addRow(null, true);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (isFromFile) {
|
|
329
|
+
originalFileData = JSON.parse(JSON.stringify(data)); // Deep copy
|
|
330
|
+
isModifiedFromFile = false;
|
|
331
|
+
clearSavedData(); // Clear saved data when loading from file
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
updateStatusIndicators();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function loadConfigData() {
|
|
338
|
+
// Check for saved form data first
|
|
339
|
+
var savedData = loadSavedData();
|
|
340
|
+
|
|
341
|
+
{% if config_preview %}
|
|
342
|
+
var fileData = {{ config_preview | tojson | safe }};
|
|
343
|
+
originalFileData = JSON.parse(JSON.stringify(fileData)); // Deep copy
|
|
344
|
+
|
|
345
|
+
if (savedData && savedData.length > 0) {
|
|
346
|
+
// Load saved data if available
|
|
347
|
+
loadDataFromSource(savedData, false);
|
|
348
|
+
console.log('Tab2: Loaded saved form data');
|
|
349
|
+
} else {
|
|
350
|
+
// Load from file
|
|
351
|
+
loadDataFromSource(fileData, true);
|
|
352
|
+
console.log('Tab2: Loaded file data');
|
|
353
|
+
}
|
|
354
|
+
{% else %}
|
|
355
|
+
if (savedData && savedData.length > 0) {
|
|
356
|
+
// Load saved data
|
|
357
|
+
loadDataFromSource(savedData, false);
|
|
358
|
+
console.log('Tab2: Loaded saved form data');
|
|
359
|
+
} else {
|
|
360
|
+
// Add default empty rows
|
|
361
|
+
for (let i = 0; i < 5; i++) {
|
|
362
|
+
addRow(null, true);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
{% endif %}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Handle page unload
|
|
369
|
+
window.addEventListener('beforeunload', function() {
|
|
370
|
+
saveFormData();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Initialize table when page loads
|
|
374
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
375
|
+
loadConfigData();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// ============================================
|
|
379
|
+
// EXPOSE FUNCTIONS GLOBALLY FOR COORDINATION
|
|
380
|
+
// ============================================
|
|
381
|
+
window.saveFormData = saveFormData;
|
|
382
|
+
window.loadConfigData = loadConfigData;
|
|
383
|
+
</script>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{# Repeat tab component #}
|
|
2
2
|
<div class="tab-pane fade {{ 'show active' if not config_list else '' }}" id="tab1" role="tabpanel" aria-labelledby="tab1-tab">
|
|
3
|
-
<p
|
|
3
|
+
<p></p>
|
|
4
4
|
<form role="form" method='POST' name="run" action="{{url_for('execute.experiment_run')}}">
|
|
5
|
+
<div class="input-group mb-3">
|
|
6
|
+
<label class="input-group-text" for="batch_size">Batch size </label>
|
|
7
|
+
<input class="form-control" type="number" id="batch_size" name="batch_size" min="1" max="1000" value="1">
|
|
8
|
+
</div>
|
|
5
9
|
<div class="input-group mb-3">
|
|
6
10
|
<label class="input-group-text" for="repeat">Repeat for </label>
|
|
7
11
|
<input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="1">
|
|
@@ -11,4 +15,4 @@
|
|
|
11
15
|
<button class="form-control" type="submit" class="btn btn-dark">Run</button>
|
|
12
16
|
</div>
|
|
13
17
|
</form>
|
|
14
|
-
</div>
|
|
18
|
+
</div>
|
|
@@ -26,269 +26,5 @@
|
|
|
26
26
|
{% include 'components/error_modal.html' %}
|
|
27
27
|
|
|
28
28
|
<script src="{{ url_for('static', filename='js/socket_handler.js') }}"></script>
|
|
29
|
-
<script>
|
|
30
|
-
var rowCount = 0;
|
|
31
|
-
var configColumns = [
|
|
32
|
-
{% for column in config_list %}
|
|
33
|
-
'{{ column }}'{{ ',' if not loop.last else '' }}
|
|
34
|
-
{% endfor %}
|
|
35
|
-
];
|
|
36
|
-
var configTypes = {
|
|
37
|
-
{% for column, type in config_type_list.items() %}
|
|
38
|
-
'{{ column }}': '{{ type }}'{{ ',' if not loop.last else '' }}
|
|
39
|
-
{% endfor %}
|
|
40
|
-
};
|
|
41
29
|
|
|
42
|
-
// State management
|
|
43
|
-
var originalFileData = null;
|
|
44
|
-
var isModifiedFromFile = false;
|
|
45
|
-
var saveTimeout = null;
|
|
46
|
-
var lastSavedData = null;
|
|
47
|
-
|
|
48
|
-
function addRow(data = null, skipSave = false) {
|
|
49
|
-
rowCount++;
|
|
50
|
-
var tableBody = document.getElementById("tableBody");
|
|
51
|
-
var newRow = tableBody.insertRow(-1);
|
|
52
|
-
|
|
53
|
-
// Row number cell
|
|
54
|
-
var rowNumCell = newRow.insertCell(-1);
|
|
55
|
-
rowNumCell.innerHTML = '<span class="badge bg-secondary">' + rowCount + '</span>';
|
|
56
|
-
|
|
57
|
-
// Data cells
|
|
58
|
-
configColumns.forEach(function(column, index) {
|
|
59
|
-
var cell = newRow.insertCell(-1);
|
|
60
|
-
var value = data && data[column] ? data[column] : '';
|
|
61
|
-
var placeholder = configTypes[column] || 'value';
|
|
62
|
-
cell.innerHTML = '<input type="text" class="form-control form-control-sm" name="' +
|
|
63
|
-
column + '[' + rowCount + ']" value="' + value + '" placeholder="' + placeholder +
|
|
64
|
-
'" oninput="onInputChange()" onchange="onInputChange()">';
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Action cell
|
|
68
|
-
var actionCell = newRow.insertCell(-1);
|
|
69
|
-
actionCell.innerHTML = '<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRow(this)" title="Remove row">' +
|
|
70
|
-
'<i class="bi bi-trash"></i></button>';
|
|
71
|
-
|
|
72
|
-
if (!skipSave) {
|
|
73
|
-
markAsModified();
|
|
74
|
-
debouncedSave();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function removeRow(button) {
|
|
79
|
-
var row = button.closest('tr');
|
|
80
|
-
row.remove();
|
|
81
|
-
updateRowNumbers();
|
|
82
|
-
markAsModified();
|
|
83
|
-
debouncedSave();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function updateRowNumbers() {
|
|
87
|
-
var tableBody = document.getElementById("tableBody");
|
|
88
|
-
var rows = tableBody.getElementsByTagName('tr');
|
|
89
|
-
for (var i = 0; i < rows.length; i++) {
|
|
90
|
-
var badge = rows[i].querySelector('.badge');
|
|
91
|
-
if (badge) {
|
|
92
|
-
badge.textContent = i + 1;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function clearAllRows() {
|
|
98
|
-
if (confirm('Are you sure you want to clear all rows?')) {
|
|
99
|
-
var tableBody = document.getElementById("tableBody");
|
|
100
|
-
tableBody.innerHTML = '';
|
|
101
|
-
rowCount = 0;
|
|
102
|
-
markAsModified();
|
|
103
|
-
clearSavedData();
|
|
104
|
-
// Add 5 empty rows by default
|
|
105
|
-
for (let i = 0; i < 5; i++) {
|
|
106
|
-
addRow(null, true);
|
|
107
|
-
}
|
|
108
|
-
debouncedSave();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function resetToFile() {
|
|
113
|
-
if (originalFileData && confirm('Reset to original file data? This will lose all manual changes.')) {
|
|
114
|
-
loadDataFromSource(originalFileData, false);
|
|
115
|
-
isModifiedFromFile = false;
|
|
116
|
-
updateStatusIndicators();
|
|
117
|
-
debouncedSave();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function onInputChange() {
|
|
122
|
-
markAsModified();
|
|
123
|
-
debouncedSave();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function markAsModified() {
|
|
127
|
-
if (originalFileData) {
|
|
128
|
-
isModifiedFromFile = true;
|
|
129
|
-
updateStatusIndicators();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function updateStatusIndicators() {
|
|
134
|
-
var modifiedStatus = document.getElementById('modifiedStatus');
|
|
135
|
-
var resetBtn = document.getElementById('resetToFileBtn');
|
|
136
|
-
|
|
137
|
-
if (isModifiedFromFile && originalFileData) {
|
|
138
|
-
modifiedStatus.style.display = 'inline-block';
|
|
139
|
-
resetBtn.style.display = 'inline-block';
|
|
140
|
-
} else {
|
|
141
|
-
modifiedStatus.style.display = 'none';
|
|
142
|
-
resetBtn.style.display = 'none';
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function showSaveStatus() {
|
|
147
|
-
var saveStatus = document.getElementById('saveStatus');
|
|
148
|
-
saveStatus.style.display = 'inline-block';
|
|
149
|
-
setTimeout(function() {
|
|
150
|
-
saveStatus.style.display = 'none';
|
|
151
|
-
}, 2000);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function debouncedSave() {
|
|
155
|
-
clearTimeout(saveTimeout);
|
|
156
|
-
saveTimeout = setTimeout(function() {
|
|
157
|
-
saveFormData();
|
|
158
|
-
showSaveStatus();
|
|
159
|
-
}, 1000); // Save 1 second after user stops typing
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function saveFormData() {
|
|
163
|
-
var formData = getCurrentFormData();
|
|
164
|
-
try {
|
|
165
|
-
sessionStorage.setItem('configFormData', JSON.stringify(formData));
|
|
166
|
-
sessionStorage.setItem('configModified', isModifiedFromFile.toString());
|
|
167
|
-
lastSavedData = formData;
|
|
168
|
-
} catch (e) {
|
|
169
|
-
console.warn('Could not save form data to sessionStorage:', e);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function getCurrentFormData() {
|
|
174
|
-
var tableBody = document.getElementById("tableBody");
|
|
175
|
-
var rows = tableBody.getElementsByTagName('tr');
|
|
176
|
-
var data = [];
|
|
177
|
-
|
|
178
|
-
for (var i = 0; i < rows.length; i++) {
|
|
179
|
-
var inputs = rows[i].getElementsByTagName('input');
|
|
180
|
-
var rowData = {};
|
|
181
|
-
var hasData = false;
|
|
182
|
-
|
|
183
|
-
for (var j = 0; j < inputs.length; j++) {
|
|
184
|
-
var input = inputs[j];
|
|
185
|
-
var name = input.name;
|
|
186
|
-
if (name) {
|
|
187
|
-
var columnName = name.substring(0, name.indexOf('['));
|
|
188
|
-
rowData[columnName] = input.value;
|
|
189
|
-
if (input.value.trim() !== '') {
|
|
190
|
-
hasData = true;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (hasData) {
|
|
196
|
-
data.push(rowData);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return data;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function loadSavedData() {
|
|
204
|
-
try {
|
|
205
|
-
var savedData = sessionStorage.getItem('configFormData');
|
|
206
|
-
var savedModified = sessionStorage.getItem('configModified');
|
|
207
|
-
|
|
208
|
-
if (savedData) {
|
|
209
|
-
var parsedData = JSON.parse(savedData);
|
|
210
|
-
isModifiedFromFile = savedModified === 'true';
|
|
211
|
-
return parsedData;
|
|
212
|
-
}
|
|
213
|
-
} catch (e) {
|
|
214
|
-
console.warn('Could not load saved form data:', e);
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function clearSavedData() {
|
|
220
|
-
try {
|
|
221
|
-
sessionStorage.removeItem('configFormData');
|
|
222
|
-
sessionStorage.removeItem('configModified');
|
|
223
|
-
} catch (e) {
|
|
224
|
-
console.warn('Could not clear saved data:', e);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function loadDataFromSource(data, isFromFile = false) {
|
|
229
|
-
// Clear existing rows
|
|
230
|
-
var tableBody = document.getElementById("tableBody");
|
|
231
|
-
tableBody.innerHTML = '';
|
|
232
|
-
rowCount = 0;
|
|
233
|
-
|
|
234
|
-
// Add rows with data
|
|
235
|
-
data.forEach(function(rowData) {
|
|
236
|
-
addRow(rowData, true);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Add a few empty rows for additional input
|
|
240
|
-
for (let i = 0; i < 3; i++) {
|
|
241
|
-
addRow(null, true);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (isFromFile) {
|
|
245
|
-
originalFileData = JSON.parse(JSON.stringify(data)); // Deep copy
|
|
246
|
-
isModifiedFromFile = false;
|
|
247
|
-
clearSavedData(); // Clear saved data when loading from file
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
updateStatusIndicators();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function loadConfigData() {
|
|
254
|
-
// Check for saved form data first
|
|
255
|
-
var savedData = loadSavedData();
|
|
256
|
-
|
|
257
|
-
{% if config_preview %}
|
|
258
|
-
var fileData = {{ config_preview | tojson | safe }};
|
|
259
|
-
originalFileData = JSON.parse(JSON.stringify(fileData)); // Deep copy
|
|
260
|
-
|
|
261
|
-
if (savedData && savedData.length > 0) {
|
|
262
|
-
// Load saved data if available
|
|
263
|
-
loadDataFromSource(savedData, false);
|
|
264
|
-
console.log('Loaded saved form data');
|
|
265
|
-
} else {
|
|
266
|
-
// Load from file
|
|
267
|
-
loadDataFromSource(fileData, true);
|
|
268
|
-
console.log('Loaded file data');
|
|
269
|
-
}
|
|
270
|
-
{% else %}
|
|
271
|
-
if (savedData && savedData.length > 0) {
|
|
272
|
-
// Load saved data
|
|
273
|
-
loadDataFromSource(savedData, false);
|
|
274
|
-
console.log('Loaded saved form data');
|
|
275
|
-
} else {
|
|
276
|
-
// Add default empty rows
|
|
277
|
-
for (let i = 0; i < 5; i++) {
|
|
278
|
-
addRow(null, true);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
{% endif %}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Handle page unload
|
|
285
|
-
window.addEventListener('beforeunload', function() {
|
|
286
|
-
saveFormData();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Initialize table when page loads
|
|
290
|
-
document.addEventListener("DOMContentLoaded", function() {
|
|
291
|
-
loadConfigData();
|
|
292
|
-
});
|
|
293
|
-
</script>
|
|
294
30
|
{% endblock %}
|
|
@@ -13,25 +13,25 @@ library = Blueprint('library', __name__, template_folder='templates')
|
|
|
13
13
|
def workflow_script(script_name:str):
|
|
14
14
|
# todo: split this into two routes, one for GET and POST, another for DELETE
|
|
15
15
|
"""
|
|
16
|
-
.. :quickref: Workflow Script Database; get, post, delete workflow script
|
|
16
|
+
.. :quickref: Workflow Script Database; get, post, delete a workflow script
|
|
17
17
|
|
|
18
|
-
.. http:get:: /<string:script_name>
|
|
18
|
+
.. http:get:: /library/<string: script_name>
|
|
19
19
|
|
|
20
20
|
:param script_name: script name
|
|
21
21
|
:type script_name: str
|
|
22
|
-
:status 302: redirect to :http:get:`/ivoryos/
|
|
22
|
+
:status 302: redirect to :http:get:`/ivoryos/draft`
|
|
23
23
|
|
|
24
|
-
.. http:post:: /<string:script_name>
|
|
24
|
+
.. http:post:: /library/<string: script_name>
|
|
25
25
|
|
|
26
26
|
:param script_name: script name
|
|
27
27
|
:type script_name: str
|
|
28
28
|
:status 200: json response with success status
|
|
29
29
|
|
|
30
|
-
.. http:delete:: /<string:script_name>
|
|
30
|
+
.. http:delete:: /library/<string: script_name>
|
|
31
31
|
|
|
32
32
|
:param script_name: script name
|
|
33
33
|
:type script_name: str
|
|
34
|
-
:status 302: redirect to :http:get:`/ivoryos/
|
|
34
|
+
:status 302: redirect to :http:get:`/ivoryos/draft`
|
|
35
35
|
|
|
36
36
|
"""
|
|
37
37
|
row = Script.query.get(script_name)
|
|
@@ -89,10 +89,8 @@ def load_from_database():
|
|
|
89
89
|
|
|
90
90
|
backend control through http requests
|
|
91
91
|
|
|
92
|
-
.. http:get:: /
|
|
92
|
+
.. http:get:: /library
|
|
93
93
|
|
|
94
|
-
:param deck_name: filter for deck name
|
|
95
|
-
:type deck_name: str
|
|
96
94
|
|
|
97
95
|
"""
|
|
98
96
|
session.pop('edit_action', None) # reset cache
|
|
@@ -132,10 +130,10 @@ def save_as():
|
|
|
132
130
|
|
|
133
131
|
save the current workflow script as
|
|
134
132
|
|
|
135
|
-
.. http:post:: /library
|
|
133
|
+
.. http:post:: /library
|
|
136
134
|
|
|
137
135
|
: form run_name: new workflow name
|
|
138
|
-
:status 302: redirect to :http:get:`/ivoryos/
|
|
136
|
+
:status 302: redirect to :http:get:`/ivoryos/draft`
|
|
139
137
|
|
|
140
138
|
"""
|
|
141
139
|
if request.method == "POST":
|