ivoryos 1.2.8__tar.gz → 1.3.0a0__tar.gz

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.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (112) hide show
  1. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/PKG-INFO +1 -1
  2. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/app.py +38 -1
  3. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/api/api.py +2 -1
  4. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/templates/controllers.html +9 -0
  5. ivoryos-1.3.0a0/ivoryos/routes/data/data.py +170 -0
  6. ivoryos-1.3.0a0/ivoryos/routes/data/templates/components/step_card.html +42 -0
  7. ivoryos-1.3.0a0/ivoryos/routes/data/templates/workflow_view.html +351 -0
  8. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/socket_handler.js +1 -1
  9. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/db_models.py +44 -8
  10. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/form.py +4 -2
  11. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/script_runner.py +103 -48
  12. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/utils.py +6 -3
  13. ivoryos-1.3.0a0/ivoryos/version.py +1 -0
  14. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos.egg-info/PKG-INFO +1 -1
  15. ivoryos-1.2.8/ivoryos/routes/data/data.py +0 -131
  16. ivoryos-1.2.8/ivoryos/routes/data/templates/components/step_card.html +0 -13
  17. ivoryos-1.2.8/ivoryos/routes/data/templates/workflow_view.html +0 -130
  18. ivoryos-1.2.8/ivoryos/version.py +0 -1
  19. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/LICENSE +0 -0
  20. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/README.md +0 -0
  21. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/__init__.py +0 -0
  22. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/config.py +0 -0
  23. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/optimizer/ax_optimizer.py +0 -0
  24. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/optimizer/base_optimizer.py +0 -0
  25. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  26. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/optimizer/registry.py +0 -0
  27. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/__init__.py +0 -0
  28. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/auth/__init__.py +0 -0
  29. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/auth/auth.py +0 -0
  30. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/auth/templates/login.html +0 -0
  31. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/auth/templates/signup.html +0 -0
  32. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/__init__.py +0 -0
  33. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/control.py +0 -0
  34. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/control_file.py +0 -0
  35. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/control_new_device.py +0 -0
  36. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  37. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/control/utils.py +0 -0
  38. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/data/__init__.py +0 -0
  39. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  40. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/__init__.py +0 -0
  41. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/design.py +0 -0
  42. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/design_file.py +0 -0
  43. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/design_step.py +0 -0
  44. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  45. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  46. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  47. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  48. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  49. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  50. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
  51. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  52. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
  53. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  54. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  55. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  56. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  57. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  58. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  59. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/modals.html +0 -0
  60. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  61. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  62. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  63. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  64. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/__init__.py +0 -0
  65. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/execute.py +0 -0
  66. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/execute_file.py +0 -0
  67. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  68. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  69. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  70. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  71. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  72. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  73. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  74. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  75. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  76. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/library/__init__.py +0 -0
  77. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/library/library.py +0 -0
  78. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/library/templates/library.html +0 -0
  79. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/main/__init__.py +0 -0
  80. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/main/main.py +0 -0
  81. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/main/templates/help.html +0 -0
  82. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/routes/main/templates/home.html +0 -0
  83. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/server.py +1 -1
  84. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/socket_handlers.py +0 -0
  85. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/favicon.ico +0 -0
  86. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  87. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  88. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/action_handlers.js +0 -0
  89. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/db_delete.js +0 -0
  90. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/overlay.js +0 -0
  91. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/script_metadata.js +0 -0
  92. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/sortable_card.js +0 -0
  93. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/sortable_design.js +0 -0
  94. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/js/ui_state.js +0 -0
  95. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/logo.webp +0 -0
  96. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/static/style.css +0 -0
  97. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/templates/base.html +0 -0
  98. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/__init__.py +0 -0
  99. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/bo_campaign.py +0 -0
  100. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/client_proxy.py +0 -0
  101. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/decorators.py +0 -0
  102. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/global_config.py +0 -0
  103. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/llm_agent.py +0 -0
  104. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/py_to_json.py +0 -0
  105. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/serilize.py +0 -0
  106. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos/utils/task_runner.py +0 -0
  107. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos.egg-info/SOURCES.txt +0 -0
  108. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos.egg-info/dependency_links.txt +0 -0
  109. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos.egg-info/requires.txt +0 -0
  110. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/ivoryos.egg-info/top_level.txt +0 -0
  111. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/pyproject.toml +0 -0
  112. {ivoryos-1.2.8 → ivoryos-1.3.0a0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.2.8
3
+ Version: 1.3.0a0
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -17,6 +17,42 @@ from ivoryos.routes.api.api import api
17
17
  from ivoryos.socket_handlers import socketio
18
18
  from ivoryos.routes.main.main import main
19
19
  from ivoryos.version import __version__ as ivoryos_version
20
+ from sqlalchemy import inspect, text
21
+ from flask import current_app
22
+
23
+
24
+ def reset_old_schema(engine, db_dir):
25
+ inspector = inspect(engine)
26
+ tables = inspector.get_table_names()
27
+
28
+
29
+ # Check if old tables exist (no workflow_phases table)
30
+ has_workflow_phase = 'workflow_phases' in tables
31
+ old_workflow_run = 'old_workflow_run' in tables
32
+ old_workflow_step = 'workflow_steps' in tables
33
+
34
+ if not has_workflow_phase:
35
+ print("⚠️ Old workflow database detected! All previous workflows have been reset to support the new schema.")
36
+ # Backup old DB
37
+ db_path = os.path.join(db_dir, "ivoryos.db")
38
+ if os.path.exists(db_path):
39
+ # os.makedirs(backup_dir, exist_ok=True)
40
+ from datetime import datetime
41
+ import shutil
42
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
43
+ backup_path = os.path.join(db_dir, f"ivoryos_backup_{ts}.db")
44
+ shutil.copy(db_path, backup_path)
45
+ print(f"Backup created at {backup_path}")
46
+ with engine.begin() as conn:
47
+ # Drop old tables
48
+ if old_workflow_step:
49
+ conn.execute(text("DROP TABLE IF EXISTS workflow_steps"))
50
+ if old_workflow_run:
51
+ conn.execute(text("DROP TABLE IF EXISTS workflow_runs"))
52
+
53
+ # Recreate new schema
54
+ db.create_all() # creates workflow_runs, workflow_phases, workflow_steps
55
+
20
56
 
21
57
  def create_app(config_class=None):
22
58
  """
@@ -45,7 +81,8 @@ def create_app(config_class=None):
45
81
 
46
82
  # Create database tables
47
83
  with app.app_context():
48
- db.create_all()
84
+ # db.create_all()
85
+ reset_old_schema(db.engine, app.config['OUTPUT_FOLDER'])
49
86
 
50
87
  # Additional setup
51
88
  utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import os
2
3
  from flask import Blueprint, jsonify, request, current_app
3
4
 
@@ -46,7 +47,7 @@ def backend_control(instrument: str=None):
46
47
  current_app=current_app._get_current_object())
47
48
  return jsonify(output), 200
48
49
 
49
- snapshot = global_config.deck_snapshot.copy()
50
+ snapshot = copy.deepcopy(global_config.deck_snapshot)
50
51
  # Iterate through each instrument in the snapshot
51
52
  for instrument_key, instrument_data in snapshot.items():
52
53
  # Iterate through each function associated with the current instrument
@@ -107,6 +107,15 @@
107
107
  {{ field(class="btn btn-dark") }}
108
108
  {% elif field.type == "BooleanField" %}
109
109
  {{ field(class="form-check-input") }}
110
+ {% elif field.type == "FlexibleEnumField" %}
111
+ <input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
112
+ list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
113
+ class="form-control">
114
+ <datalist id="{{ field.id }}_options">
115
+ {% for key in field.choices %}
116
+ <option value="{{ key }}">{{ key }}</option>
117
+ {% endfor %}
118
+ </datalist>
110
119
  {% else %}
111
120
  {{ field(class="form-control") }}
112
121
  {% endif %}
@@ -0,0 +1,170 @@
1
+ import os
2
+
3
+ from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
4
+ from flask_login import login_required
5
+
6
+ from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep, WorkflowPhase
7
+
8
+ data = Blueprint('data', __name__, template_folder='templates')
9
+
10
+
11
+
12
+ @data.route('/executions/records')
13
+ @login_required
14
+ def list_workflows():
15
+ """
16
+ .. :quickref: Workflow Execution Database; list all workflow execution records
17
+
18
+ list all workflow execution records
19
+
20
+ .. http:get:: /executions/records
21
+
22
+ """
23
+ query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
24
+ search_term = request.args.get("keyword", None)
25
+ if search_term:
26
+ query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
27
+ page = request.args.get('page', default=1, type=int)
28
+ per_page = 10
29
+
30
+ workflows = query.paginate(page=page, per_page=per_page, error_out=False)
31
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
32
+ workflows = query.all()
33
+ workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
34
+ return jsonify({
35
+ "workflow_data": workflow_data,
36
+ })
37
+ else:
38
+ return render_template('workflow_database.html', workflows=workflows)
39
+
40
+ @data.get("/executions/records/<int:workflow_id>")
41
+ def workflow_logs(workflow_id:int):
42
+ """
43
+ .. :quickref: Workflow Data Database; get workflow data, steps, and logs
44
+
45
+ get workflow data logs by workflow id
46
+
47
+ .. http:get:: /executions/<int:workflow_id>
48
+
49
+ :param workflow_id: workflow id
50
+ :type workflow_id: int
51
+ """
52
+ workflow = db.session.get(WorkflowRun, workflow_id)
53
+ if not workflow:
54
+ return jsonify({"error": "Workflow not found"}), 404
55
+
56
+ # Query all phases for this run, ordered by start_time
57
+ phases = WorkflowPhase.query.filter_by(run_id=workflow_id).order_by(WorkflowPhase.start_time).all()
58
+
59
+ # Prepare grouped data for template (full objects)
60
+ grouped = {
61
+ "prep": [],
62
+ "script": {},
63
+ "cleanup": [],
64
+ }
65
+
66
+ # Prepare grouped data for JSON (dicts)
67
+ grouped_json = {
68
+ "prep": [],
69
+ "script": {},
70
+ "cleanup": [],
71
+ }
72
+
73
+ for phase in phases:
74
+ phase_dict = phase.as_dict()
75
+
76
+ # Steps sorted by step_index
77
+ steps = sorted(phase.steps, key=lambda s: s.step_index)
78
+ phase_steps_dicts = [s.as_dict() for s in steps]
79
+
80
+ if phase.name == "prep":
81
+ grouped["prep"].append(phase)
82
+ grouped_json["prep"].append({
83
+ **phase_dict,
84
+ "steps": phase_steps_dicts
85
+ })
86
+
87
+ elif phase.name == "main":
88
+ grouped["script"].setdefault(phase.repeat_index, []).append(phase)
89
+ grouped_json["script"].setdefault(phase.repeat_index, []).append({
90
+ **phase_dict,
91
+ "steps": phase_steps_dicts
92
+ })
93
+
94
+ elif phase.name == "cleanup":
95
+ grouped["cleanup"].append(phase)
96
+ grouped_json["cleanup"].append({
97
+ **phase_dict,
98
+ "steps": phase_steps_dicts
99
+ })
100
+
101
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
102
+ return jsonify({
103
+ "workflow_info": workflow.as_dict(),
104
+ "phases": grouped_json,
105
+ })
106
+ else:
107
+ return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
108
+
109
+
110
+ @data.get("/executions/data/<int:workflow_id>")
111
+ def workflow_phase_data(workflow_id: int):
112
+ workflow = db.session.get(WorkflowRun, workflow_id)
113
+ if not workflow:
114
+ return jsonify({})
115
+
116
+ phase_data = {}
117
+ # Only plot 'main' phases
118
+ main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main').order_by(
119
+ WorkflowPhase.repeat_index).all()
120
+
121
+ for phase in main_phases:
122
+ outputs = phase.outputs or {}
123
+ phase_index = phase.repeat_index
124
+ # Convert each key to list of dicts for x (phase_index) and y (value)
125
+ phase_data[phase_index] = {}
126
+ for k, v in outputs.items():
127
+ if isinstance(v, (int, float)):
128
+ phase_data[phase_index][k] = [{"x": phase_index, "y": v}]
129
+ elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
130
+ phase_data[phase_index][k] = v.map(lambda val, idx=0: {"x": phase_index, "y": val})
131
+
132
+ return jsonify(phase_data)
133
+
134
+
135
+ @data.delete("/executions/records/<int:workflow_id>")
136
+ @login_required
137
+ def delete_workflow_record(workflow_id: int):
138
+ """
139
+ .. :quickref: Workflow Data Database; delete a workflow execution record
140
+
141
+ delete a workflow execution record by workflow id
142
+
143
+ .. http:delete:: /executions/records/<int:workflow_id>
144
+
145
+ :param workflow_id: workflow id
146
+ :type workflow_id: int
147
+ :status 200: return success message
148
+ """
149
+ run = WorkflowRun.query.get(workflow_id)
150
+ db.session.delete(run)
151
+ db.session.commit()
152
+ return jsonify(success=True)
153
+
154
+
155
+ @data.route('/files/execution-data/<string:filename>')
156
+ @login_required
157
+ def download_results(filename:str):
158
+ """
159
+ .. :quickref: Workflow data; download a workflow data file (.CSV)
160
+
161
+ .. http:get:: /files/execution-data/<string:filename>
162
+
163
+ :param filename: workflow data filename
164
+ :type filename: str
165
+
166
+ # :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions`
167
+ """
168
+
169
+ filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
170
+ return send_file(os.path.abspath(filepath), as_attachment=True)
@@ -0,0 +1,42 @@
1
+ <div class="card mb-2 {{ 'border-danger text-danger bg-light' if phase.run_error else 'border-secondary' }}">
2
+ <div class="card-body p-2">
3
+ <small class="text-muted">
4
+ <i class="fas fa-play-circle me-1"></i> Start: {{ phase.start_time.strftime('%H:%M:%S') if phase.start_time else 'N/A' }}
5
+ <i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ phase.end_time.strftime('%H:%M:%S') if phase.end_time else 'N/A' }}
6
+ </small>
7
+ {% if phase.parameters %}
8
+ <div class="mt-2">
9
+ <strong>Parameters: </strong>
10
+ {% for key, value in phase.parameters.items() %}
11
+ <span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
12
+ {% endfor %}
13
+ </div>
14
+ {% endif %}
15
+ {% if phase.steps %}
16
+ <div class="mt-2">
17
+ <strong>Steps:</strong>
18
+ <ul class="mb-0">
19
+ {% for step in phase.steps %}
20
+ <li class="{{ 'text-danger' if step.run_error else '' }}">
21
+ {{ step.method_name }}
22
+ <small class="text-muted">
23
+ ({{ step.start_time.strftime('%H:%M:%S') if step.start_time else 'N/A' }} –
24
+ {{ step.end_time.strftime('%H:%M:%S') if step.end_time else 'N/A' }})
25
+ </small>
26
+ </li>
27
+ {% endfor %}
28
+ </ul>
29
+ </div>
30
+ {% endif %}
31
+ {% if phase.outputs %}
32
+ <div class="mt-1">
33
+ <strong>Outputs:</strong>
34
+ <ul class="mb-0">
35
+ {% for key, value in phase.outputs.items() %}
36
+ <li>{{ key }}: {{ value }}</li>
37
+ {% endfor %}
38
+ </ul>
39
+ </div>
40
+ {% endif %}
41
+ </div>
42
+ </div>
@@ -0,0 +1,351 @@
1
+ {% extends 'base.html' %}
2
+
3
+ {% block title %}IvoryOS | Experiment Results{% endblock %}
4
+
5
+ {% block body %}
6
+ <style>
7
+ .vis-time-axis .vis-text.vis-minor,
8
+ .vis-time-axis .vis-text.vis-major {
9
+ color: #666;
10
+ }
11
+ .vis-item.stop {
12
+ background-color: #dc3545;
13
+ color: white;
14
+ border: none;
15
+ font-weight: bold;
16
+ }
17
+ .vis-item.prep {
18
+ background-color: #17a2b8;
19
+ border-color: #138496;
20
+ }
21
+ .vis-item.script {
22
+ background-color: #28a745;
23
+ border-color: #1e7e34;
24
+ }
25
+ .vis-item.cleanup {
26
+ background-color: #ffc107;
27
+ border-color: #d39e00;
28
+ color: #212529;
29
+ }
30
+ #visualization {
31
+ border: 1px solid #dee2e6;
32
+ border-radius: 0.375rem;
33
+ background-color: #fff;
34
+ min-height: 200px;
35
+ }
36
+
37
+ .section-header {
38
+ border-bottom: 2px solid #dee2e6;
39
+ padding-bottom: 0.5rem;
40
+ margin-bottom: 1rem;
41
+ color: #495057;
42
+ }
43
+ .loading-spinner {
44
+ display: none;
45
+ }
46
+ </style>
47
+
48
+
49
+ <div class="timeline-section" style="margin-bottom: 2rem;">
50
+ <h3 class="section-header">
51
+ <i class="fas fa-clock me-2"></i>Execution Timeline
52
+ </h3>
53
+ <div class="alert alert-info" role="alert">
54
+ <i class="fas fa-info-circle me-2"></i>
55
+ <strong>Tip:</strong> Click on timeline items to navigate to detailed views. Use Ctrl+scroll to zoom.
56
+ </div>
57
+ <div id="visualization"></div>
58
+ </div>
59
+
60
+ <!-- Phase Output Plot Section -->
61
+
62
+ <div class="data-section" style="margin-bottom: 2rem;">
63
+ <h3 class="section-header">
64
+ <div class="col-md-6">
65
+ <label for="output-select" class="form-label">Select Data Type:</label>
66
+ <select id="output-select" class="form-select">
67
+ <option value="">Loading data types...</option>
68
+ </select>
69
+ </div>
70
+ </h3>
71
+
72
+ <div class="plot-controls">
73
+ <div>
74
+ <div class="col-md-6 text-md-end">
75
+ <div class="loading-spinner">
76
+ <div class="spinner-border spinner-border-sm text-primary me-2" role="status">
77
+ <span class="visually-hidden">Loading...</span>
78
+ </div>
79
+ Loading plot data...
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div id="phase-plot" style="height:450px;" class="border rounded bg-white shadow-sm"></div>
85
+ </div>
86
+
87
+ <!-- Workflow Details Section -->
88
+
89
+ <div>
90
+ <h2 class="section-header">
91
+ <i class="fas fa-project-diagram me-2"></i>Workflow: {{ workflow.name }}
92
+ </h2>
93
+
94
+ <!-- Prep Phase -->
95
+ {% if grouped.prep %}
96
+ <div class="mb-4">
97
+ <h4 class="text-info mb-3">
98
+ <i class="fas fa-tools me-2"></i>Prep
99
+ </h4>
100
+ <div class="row">
101
+ {% for phase in grouped.prep %}
102
+ <div class="col-lg-6 mb-3">
103
+ {% include "components/step_card.html" %}
104
+ </div>
105
+ {% endfor %}
106
+ </div>
107
+ </div>
108
+ {% endif %}
109
+
110
+ <!-- Script Iterations -->
111
+ {% for repeat_index, phase_list in grouped.script.items()|sort %}
112
+ <div class="mb-4" id="card-iter{{ repeat_index }}">
113
+ <h4 class="text-success mb-3">
114
+ <i class="fas fa-redo me-2"></i>Iteration {{ repeat_index }}
115
+ </h4>
116
+ <div class="row">
117
+ {% for phase in phase_list %}
118
+ <div class="col-lg-6 mb-3">
119
+ {% include "components/step_card.html" %}
120
+ </div>
121
+ {% endfor %}
122
+ </div>
123
+ </div>
124
+ {% endfor %}
125
+
126
+ <!-- Cleanup Phase -->
127
+ {% if grouped.cleanup %}
128
+ <div>
129
+ <h4 class="text-warning mb-3">
130
+ <i class="fas fa-broom me-2"></i>Cleanup
131
+ </h4>
132
+ <div class="row">
133
+ {% for phase in grouped.cleanup %}
134
+ <div class="col-lg-6 mb-3">
135
+ {% include "components/step_card.html" %}
136
+ </div>
137
+ {% endfor %}
138
+ </div>
139
+ </div>
140
+ {% endif %}
141
+ </div>
142
+
143
+
144
+
145
+ <!-- External Dependencies -->
146
+ <script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
147
+ <link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
148
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
149
+
150
+ <script type="text/javascript">
151
+ document.addEventListener('DOMContentLoaded', function() {
152
+ // ---------------- Timeline Setup ----------------
153
+ const container = document.getElementById('visualization');
154
+ const groups = [
155
+ { id: 'all', content: 'Workflow Execution' }
156
+ ];
157
+
158
+ const items = [
159
+ {% if grouped.prep %}
160
+ {
161
+ id: 'prep',
162
+ content: 'Prep',
163
+ start: '{{ grouped.prep[0].start_time }}',
164
+ end: '{{ grouped.prep[-1].end_time }}',
165
+ className: 'prep',
166
+ group: 'all',
167
+ },
168
+ {% endif %}
169
+
170
+ {% for repeat_index, step_list in grouped.script.items()|sort %}
171
+ {
172
+ id: 'iter{{ repeat_index }}',
173
+ content: 'Iteration {{ repeat_index }}',
174
+ start: '{{ step_list[0].start_time }}',
175
+ end: '{{ step_list[-1].end_time }}',
176
+ className: 'script',
177
+ group: 'all',
178
+ },
179
+ {% for step in step_list %}
180
+ {% if step.method_name == "stop" %}
181
+ {
182
+ id: 'stop-{{ step.id }}',
183
+ content: '🛑 Stop',
184
+ start: '{{ step.start_time }}',
185
+ type: 'point',
186
+ className: 'stop',
187
+ group: 'all',
188
+ title: 'Stop event at {{ step.start_time }}'
189
+ },
190
+ {% endif %}
191
+ {% endfor %}
192
+ {% endfor %}
193
+
194
+ {% if grouped.cleanup %}
195
+ {
196
+ id: 'cleanup',
197
+ content: 'Cleanup ',
198
+ start: '{{ grouped.cleanup[0].start_time }}',
199
+ end: '{{ grouped.cleanup[-1].end_time }}',
200
+ className: 'cleanup',
201
+ group: 'all',
202
+ },
203
+ {% endif %}
204
+ ];
205
+
206
+ var timeline = new vis.Timeline(container, items, groups, {
207
+ clickToUse: true,
208
+ stack: false, // keep items from overlapping vertically
209
+ horizontalScroll: true,
210
+ zoomKey: 'ctrlKey'
211
+ });
212
+
213
+ timeline.on('select', function (props) {
214
+ const id = props.items[0];
215
+ if (id && id.startsWith('iter')) {
216
+ const card = document.getElementById('card-' + id);
217
+ if (card) {
218
+ const yOffset = -80;
219
+ const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
220
+ window.scrollTo({ top: y, behavior: 'smooth' });
221
+ }
222
+ }
223
+ });
224
+
225
+ // ---------------- Phase Data Plot ----------------
226
+ const loadingSpinner = document.querySelector('.loading-spinner');
227
+ const select = document.getElementById('output-select');
228
+
229
+ loadingSpinner.style.display = 'block';
230
+
231
+
232
+ fetch("{{ url_for('data.workflow_phase_data', workflow_id=workflow.id) }}")
233
+ .then(res => {
234
+ if (!res.ok) {
235
+ throw new Error(`HTTP error! status: ${res.status}`);
236
+ }
237
+ return res.json();
238
+ })
239
+ .then(data => {
240
+ loadingSpinner.style.display = 'none';
241
+
242
+ const repeatKeys = Object.keys(data).sort((a, b) => a - b);
243
+ const dataSection = document.querySelector('.data-section'); // Get the entire section
244
+
245
+ if (!repeatKeys.length) {
246
+ // Hide the entire data section if no data
247
+ dataSection.style.display = 'none';
248
+ return;
249
+ }
250
+
251
+ const allKeys = new Set();
252
+ repeatKeys.forEach(k => {
253
+ Object.keys(data[k]).forEach(key => allKeys.add(key));
254
+ });
255
+
256
+ // If no keys found, also hide the section
257
+ if (allKeys.size === 0) {
258
+ dataSection.style.display = 'none';
259
+ return;
260
+ }
261
+
262
+ // Show the data section since we have data
263
+ dataSection.style.display = 'block';
264
+
265
+ // Clear and populate select options
266
+ select.innerHTML = '';
267
+ allKeys.forEach(k => {
268
+ const option = new Option(k, k);
269
+ select.appendChild(option);
270
+ });
271
+
272
+ function plotData(selectedKey) {
273
+ const x = [];
274
+ const y = [];
275
+
276
+ repeatKeys.forEach(repeat_index => {
277
+ const arr = data[repeat_index][selectedKey];
278
+ if (arr && arr.length) {
279
+ arr.forEach(d => {
280
+ if (typeof d === 'object' && d.x !== undefined && d.y !== undefined) {
281
+ x.push(d.x);
282
+ y.push(d.y);
283
+ } else if (typeof d === 'number') {
284
+ x.push(parseInt(repeat_index));
285
+ y.push(d);
286
+ }
287
+ });
288
+ }
289
+ });
290
+
291
+ const trace = {
292
+ x: x,
293
+ y: y,
294
+ mode: 'markers',
295
+ name: selectedKey,
296
+ };
297
+
298
+ const layout = {
299
+ xaxis: {
300
+ title: 'Iteration Index',
301
+ gridcolor: '#e9ecef'
302
+ },
303
+ yaxis: {
304
+ title: selectedKey,
305
+ gridcolor: '#e9ecef'
306
+ },
307
+ plot_bgcolor: '#ffffff',
308
+ paper_bgcolor: '#ffffff',
309
+ margin: { t: 60, r: 40, b: 60, l: 80 }
310
+ };
311
+
312
+ const config = {
313
+ responsive: true,
314
+ displayModeBar: true,
315
+ modeBarButtonsToRemove: ['lasso2d', 'select2d']
316
+ };
317
+
318
+ Plotly.newPlot('phase-plot', [trace], layout, config);
319
+ }
320
+
321
+ select.addEventListener('change', e => {
322
+ if (e.target.value) {
323
+ plotData(e.target.value);
324
+ }
325
+ });
326
+
327
+ // Plot first available data type
328
+ if (allKeys.size > 0) {
329
+ plotData([...allKeys][0]);
330
+ }
331
+ })
332
+ .catch(error => {
333
+ loadingSpinner.style.display = 'none';
334
+ console.error('Error loading phase data:', error);
335
+
336
+ const dataSection = document.querySelector('.data-section');
337
+ // Hide the section on error as well
338
+ dataSection.style.display = 'none';
339
+
340
+ // Optionally, you could show an error message instead:
341
+ // dataSection.innerHTML = `
342
+ // <div class="alert alert-danger m-3" role="alert">
343
+ // <i class="fas fa-exclamation-triangle me-2"></i>
344
+ // <strong>Error:</strong> Unable to load phase data. ${error.message}
345
+ // </div>
346
+ // `;
347
+ });
348
+ });
349
+ </script>
350
+
351
+ {% endblock %}
@@ -1,5 +1,5 @@
1
1
  document.addEventListener("DOMContentLoaded", function() {
2
- var socket = io.connect('http://' + document.domain + ':' + location.port);
2
+ var socket = io();
3
3
  socket.on('connect', function() {
4
4
  console.log('Connected');
5
5
  });