From bd83ef58e2d905796cd4a776bd3b69613421f012 Mon Sep 17 00:00:00 2001
From: dinodev24 <149423946+dinodev24@users.noreply.github.com>
Date: Tue, 25 Nov 2025 09:15:41 -0800
Subject: [PATCH 1/3] Run history and comparison
The run history tab shows the simulation summary of all previous runs and also has the feature to compare two runs against each other
---
cace/cace_web.py | 289 +++++++++++----
cace/parameter/parameter_manager.py | 10 +-
cace/web/html_templates.py | 42 ++-
cace/web/index.html | 527 ++++++++++++++++++++--------
cace/web/static/css/style.css | 26 +-
5 files changed, 655 insertions(+), 239 deletions(-)
diff --git a/cace/cace_web.py b/cace/cace_web.py
index 690896b..180d615 100644
--- a/cace/cace_web.py
+++ b/cace/cace_web.py
@@ -10,16 +10,15 @@
import sys
parameter_manager = ParameterManager(
- max_runs=None, run_path=None, max_jobs=None
+ max_runs=None, run_path=None, max_jobs=os.cpu_count()
)
parameter_manager.find_datasheet(os.getcwd(), False)
+datasheet = parameter_manager.datasheet
paramkey_paramdisplay = {
- i: j.get('display', i)
- for i, j in parameter_manager.datasheet['parameters'].items()
+ i: j.get('display', i) for i, j in datasheet['parameters'].items()
}
paramdisplay_paramkey = {
- j.get('display', i): i
- for i, j in parameter_manager.datasheet['parameters'].items()
+ j.get('display', i): i for i, j in datasheet['parameters'].items()
}
task_queue = queue.Queue()
@@ -28,39 +27,144 @@
app = Flask(__name__, template_folder='web', static_folder='web/static')
+# Returns the home page with all parameters and previous runs
@app.route('/')
def homepage():
+ # Restores console output
sys.stdout = sys.__stdout__
+
+ # Initialize pnames and parameter_manager
pnames = parameter_manager.get_all_pnames()
parameter_manager.results = {}
parameter_manager.result_types = {}
data = [{'name': pname} for pname in pnames]
- return render_template(template_name_or_list='index.html', data=data)
+ # Collects all previous runs
+ runs = next(os.walk(datasheet['paths']['runs']))[1]
+ results = []
+
+ # Parse the markdown summary and store it in the results list
+ for run in runs:
+ result = []
+
+ with open('runs/' + run + '/summary.md', 'r') as f:
+ content = f.read()
+
+ summary_lines = content.split('\n')[7:-2]
+
+ for row in summary_lines:
+ row = row.split('|')
+
+ if len(row) == 1:
+ continue
+
+ result.append(
+ {
+ 'parameter_str': row[1].strip(),
+ 'tool_str': row[2].strip(),
+ 'result_str': row[3].strip(),
+ 'min_limit_str': row[4].strip(),
+ 'min_value_str': row[5].strip(),
+ 'max_limit_str': row[6].strip(),
+ 'max_value_str': row[7].strip(),
+ 'typ_limit_str': row[8].strip(),
+ 'typ_value_str': row[9].strip(),
+ 'status_str': row[10].strip(),
+ }
+ )
+
+ results.append(RESULTS_SUMMARY_TEMPLATE.render(data=result))
+
+ return render_template(
+ template_name_or_list='index.html',
+ data=data,
+ runs=runs,
+ results=results,
+ )
+
+
+# Responsible for sending the SSEs back to the client
+def generate_sse():
+
+ while True:
+ tq_item = task_queue.get()
+
+ # NOTE: This is not for ending the SSE stream. Instead, it is called when a simulation ends
+ # Handles the matplotlib plots
+ if tq_item['task'] == 'end':
+ for i in parameter_manager.running_threads:
+ if i.param == tq_item['param']:
+ tq_item['status'] = i.result_type.name
+ if len(i.plots_dict) > 0:
+ figures[i.pname] = i.plots_dict
+ # This is the one that ends the SSE stream
+ elif tq_item['task'] == 'end_stream':
+ if debug:
+ print('ending sse stream')
+ return
+
+ # Convert the param key to the real param name
+ if 'param' in tq_item:
+ tq_item['param'] = list(datasheet['parameters'].keys())[
+ list(datasheet['parameters'].values()).index(tq_item['param'])
+ ]
+
+ if debug:
+ print(tq_item)
+
+ yield f'data: {json.dumps(tq_item)}\n\n'
+# Called by the client to start the SSE stream
+@app.route('/stream')
+def stream():
+ if debug:
+ print('starting sse stream')
+
+ # We return the generator which stays alive until the end_stream task is queued
+ return Response(generate_sse(), content_type='text/event-stream')
+
+
+# Starts the requested simulations
@app.route('/runsim', methods=['POST'])
def runsim():
rd = json.loads(request.get_data())
if debug:
print(rd)
+
+ # Set default values for configuration options
if rd['max_runs'] == '':
rd['max_runs'] = None
if rd['run_path'] == '':
rd['run_path'] = None
if rd['jobs'] == '':
- rd['jobs'] = None
+ rd['jobs'] = os.cpu_count()
if rd['netlist_source'] == '':
rd['netlist_source'] = 'best'
if rd['parallel_parameters'] == '':
rd['parallel_parameters'] = 4
params = rd['selected_params']
+
+ # Input the configuration to the parameter manager
parameter_manager.max_runs = rd['max_runs']
parameter_manager.run_path = rd['run_path']
parameter_manager.max_jobs = rd['jobs']
+ parameter_manager.set_runtime_options('force', rd['force'])
+ parameter_manager.set_runtime_options('noplot', rd['noplot'])
+ parameter_manager.set_runtime_options('nosim', rd['nosim'])
+ parameter_manager.set_runtime_options('sequential', rd['sequential'])
+ parameter_manager.set_runtime_options(
+ 'netlist_source', rd['netlist_source']
+ )
+ parameter_manager.set_runtime_options(
+ 'parallel_parameters', rd['parallel_parameters']
+ )
+ # Prepare the directory that stores the run results
parameter_manager.prepare_run_dir()
+
+ # Queue, not run, the selected parameters and set up the callbacks
for pname in params:
parameter_manager.queue_parameter(
pname=pname,
@@ -84,86 +188,59 @@ def runsim():
),
)
- parameter_manager.set_runtime_options('force', rd['force'])
- parameter_manager.set_runtime_options('noplot', rd['noplot'])
- parameter_manager.set_runtime_options('nosim', rd['nosim'])
- parameter_manager.set_runtime_options('sequential', rd['sequential'])
- parameter_manager.set_runtime_options(
- 'netlist_source', rd['netlist_source']
- )
- parameter_manager.set_runtime_options(
- 'parallel_parameters', rd['parallel_parameters']
- )
+ # Finally run the parameters
parameter_manager.run_parameters_async()
+
+ # Send the progress page's content to the client
task_queue.put(
{'task': 'progress', 'html': PROGRESS_TEMPLATE.render(params=params)}
)
- return '', 200
+ return json.dumps({'success': True})
-def generate_sse():
- datasheet = parameter_manager.datasheet['parameters']
-
- while True:
- tq_item = task_queue.get()
- if tq_item['task'] == 'end':
- for i in parameter_manager.running_threads:
- if i.param == tq_item['param']:
- tq_item['status'] = i.result_type.name
- if len(i.plots_dict) > 0:
- figures[i.pname] = i.plots_dict
- elif tq_item['task'] == 'end_stream':
- if debug:
- print('ending sse stream')
- return
- if 'param' in tq_item:
- tq_item['param'] = list(datasheet.keys())[
- list(datasheet.values()).index(tq_item['param'])
- ]
- if debug:
- print(tq_item)
- yield f'data: {json.dumps(tq_item)}\n\n'
+# Cancels a specific simulation
+@app.route('/cancel_sim', methods=['POST'])
+def cancel_sim():
+ data = request.get_json()
+ if debug:
+ print(data)
+ parameter_manager.cancel_parameter(pname=data['param'])
+ return json.dumps({'success': True})
-@app.route('/stream')
-def stream():
- if debug:
- print('starting sse stream')
- return Response(generate_sse(), content_type='text/event-stream')
+# Cancels all simulations
+@app.route('/cancel_sims', methods=['POST'])
+def cancel_sims():
+ parameter_manager.cancel_parameters()
+ return json.dumps({'success': True})
-@app.route('/receive_data', methods=['POST'])
-def receive_data():
- data = request.get_json()
- if debug:
- print(data)
- if data['task'] == 'end_stream':
- task_queue.put({'task': 'end_stream'})
- elif data['task'] == 'cancel_sims':
- parameter_manager.cancel_parameters()
- elif data['task'] == 'cancel_sim':
- parameter_manager.cancel_parameter(pname=data['param'])
- elif data['task'] == 'fetchresults':
- simresults()
- return '', 200
+# Ends the SSE stream
+@app.route('/end_stream', methods=['POST'])
+def end_stream():
+ task_queue.put({'task': 'end_stream'})
+ return json.dumps({'success': True})
-def simresults():
+# Sends the run results to the client
+@app.route('/fetch_results', methods=['POST'])
+def fetch_results():
parameter_manager.join_parameters()
result = []
divs = []
- summary_lines = parameter_manager.summarize_datasheet().split('\n')[7:-2]
+ # Get the MarkDown summary
+ summary_lines = parameter_manager.summarize_datasheet(save=True).split(
+ '\n'
+ )[7:-2]
lengths = {
- param: len(
- list(
- parameter_manager.datasheet['parameters'][param]['spec'].keys()
- )
- )
+ param: len(list(datasheet['parameters'][param]['spec'].keys()))
for param in parameter_manager.get_all_pnames()
}
+
+ # Parse each line of summary and collect it
for param in parameter_manager.get_result_types().keys():
total = 0
for i in parameter_manager.get_all_pnames():
@@ -172,22 +249,24 @@ def simresults():
row = summary_lines[total + j].split('|')
result.append(
{
- 'parameter_str': row[1],
- 'tool_str': row[2],
- 'result_str': row[3],
- 'min_limit_str': row[4],
- 'min_value_str': row[5],
- 'max_limit_str': row[6],
- 'max_value_str': row[7],
- 'typ_limit_str': row[8],
- 'typ_value_str': row[9],
- 'status_str': row[10],
+ 'parameter_str': row[1].strip(),
+ 'tool_str': row[2].strip(),
+ 'result_str': row[3].strip(),
+ 'min_limit_str': row[4].strip(),
+ 'min_value_str': row[5].strip(),
+ 'max_limit_str': row[6].strip(),
+ 'max_value_str': row[7].strip(),
+ 'typ_limit_str': row[8].strip(),
+ 'typ_value_str': row[9].strip(),
+ 'status_str': row[10].strip(),
}
)
total += lengths[i]
divs.append('
')
+
+ # Render the matplotlib plots using mpld3
for pname in figures.keys():
divs.append(
f'\nFigures for {paramkey_paramdisplay[pname]}
'
@@ -203,8 +282,11 @@ def simresults():
)
divs.append(' \n
')
+
if debug:
print(divs)
+
+ # Send the results to the client
task_queue.put(
{
'task': 'results',
@@ -212,9 +294,59 @@ def simresults():
'plots': RESULTS_PLOTS_TEMPLATE.render(divs=divs),
}
)
- return 200, ''
+ return json.dumps({'success': True})
+
+
+# Sends the latest list of runs and their summaries to the client
+@app.route('/refresh_history', methods=['POST'])
+def refresh_history():
+ # List all previous runs
+ runs = next(os.walk(datasheet['paths']['runs']))[1]
+
+ results = []
+ for run in runs:
+ result = []
+
+ with open('runs/' + run + '/summary.md', 'r') as f:
+ content = f.read()
+
+ summary_lines = content.split('\n')[7:-2]
+
+ # Parse each line of the summary
+ for row in summary_lines:
+ row = row.split('|')
+
+ if len(row) == 1:
+ continue
+
+ result.append(
+ {
+ 'parameter_str': row[1].strip(),
+ 'tool_str': row[2].strip(),
+ 'result_str': row[3].strip(),
+ 'min_limit_str': row[4].strip(),
+ 'min_value_str': row[5].strip(),
+ 'max_limit_str': row[6].strip(),
+ 'max_value_str': row[7].strip(),
+ 'typ_limit_str': row[8].strip(),
+ 'typ_value_str': row[9].strip(),
+ 'status_str': row[10].strip(),
+ }
+ )
+
+ results.append(RESULTS_SUMMARY_TEMPLATE.render(data=result))
+
+ # Send all the summaries to the client
+ task_queue.put(
+ {
+ 'task': 'history',
+ 'html': HISTORY_TEMPLATE.render(runs=runs, results=results),
+ }
+ )
+ return json.dumps({'success': True})
+# Called to initialize the server
def web():
try:
host = 'localhost'
@@ -222,12 +354,15 @@ def web():
print(
'Open the CACE web interface at: http://' + host + ':' + str(port)
)
+
+ # Stop console output to prevent unnecessary debug info from getting printed
sys.stdout = open(os.devnull, 'w')
debug = '--debug' in sys.argv
for prog in ['werkzeug', '__cace__']:
logger = logging.getLogger(prog)
logger.setLevel(logging.DEBUG if debug else logging.WARNING)
+ # Run the server
app.run(debug=debug, host=host, port=port, use_reloader=False)
finally:
task_queue.put({'task': 'close'})
diff --git a/cace/parameter/parameter_manager.py b/cace/parameter/parameter_manager.py
index f687e02..22466e9 100755
--- a/cace/parameter/parameter_manager.py
+++ b/cace/parameter/parameter_manager.py
@@ -250,14 +250,20 @@ def get_datasheet(self):
"""Return the datasheet"""
return self.datasheet
- def summarize_datasheet(self):
- return markdown_summary(
+ def summarize_datasheet(self, save=False):
+ md_sum = markdown_summary(
self.datasheet,
self.runtime_options,
self.results,
self.result_types,
)
+ if save:
+ with open(self.run_dir + '/summary.md', 'w') as f:
+ print(md_sum, file=f)
+
+ return md_sum
+
def generate_documentation(self):
if 'documentation' in self.datasheet['paths']:
doc_path = os.path.join(
diff --git a/cace/web/html_templates.py b/cace/web/html_templates.py
index 0b5ac16..f17a657 100644
--- a/cace/web/html_templates.py
+++ b/cace/web/html_templates.py
@@ -15,24 +15,24 @@
| {{ param }} |
-
+
|
-
+
|
{% endfor %}
| Overall Progress |
-
+
|
-
+
"""
@@ -46,12 +46,12 @@
Parameter |
Tool |
Result |
- Minimum Limit |
- Minimum Value |
- Typical Limit |
- Typical Value |
- Maximum Limit |
- Maximum Value |
+ Min Limit |
+ Min Value |
+ Typ Limit |
+ Typ Value |
+ Max Limit |
+ Max Value |
Status |
@@ -82,3 +82,25 @@
{% endfor %}
"""
)
+
+HISTORY_TEMPLATE = jinja2.Template(
+ """
+
+
+
+
+
+ {% for i in range(runs|length) %}
+
+ {{ runs[i] }}
+
+
+
+ {{ results[i] | safe }}
+
+ {% endfor %}
+
+
+
+"""
+)
diff --git a/cace/web/index.html b/cace/web/index.html
index d80c0a4..6f235bb 100644
--- a/cace/web/index.html
+++ b/cace/web/index.html
@@ -2,21 +2,23 @@
-
-
+
+
+ CACE Web Interface
-
-
-
+
+
+
+
-
+
-
+
Edit Run Settings
-
+
-
+
@@ -102,144 +106,375 @@
+
+
+
+
+
+
+ {% for i in range(runs|length) %}
+
+ {{ runs[i] }}
+
+
+
+ {{ results[i] | safe }}
+
+ {% endfor %}
+
+
+
+
+
diff --git a/cace/web/static/css/style.css b/cace/web/static/css/style.css
index 8c972c2..8d0d99a 100644
--- a/cace/web/static/css/style.css
+++ b/cace/web/static/css/style.css
@@ -3,7 +3,8 @@ table {
font-size: 18px;
text-align: left;
}
-table th, table td {
+table th,
+table td {
padding: 10px;
border: 1px solid #ddd;
}
@@ -22,11 +23,28 @@ details {
float: left;
padding: 0px 10px;
}
-.topnav{
+.topnav {
padding: 10px;
background-color: lightgray;
+ display: flex;
+ gap: 15px;
}
.topnav button {
- width: 100px;
- height: 40px
+ height: 40px;
+}
+.right-align {
+ margin-left: auto;
+}
+.comp_btn_holder {
+ display: flex;
+}
+.small_table table {
+ font-size: 12px;
+}
+.comp_holder {
+ width: 850px;
+}
+.comp_parent {
+ display: flex;
+ gap: 20px;
}
\ No newline at end of file
From 1f885b3b6a128c654803ab0d1d88e46ea1b4aa66 Mon Sep 17 00:00:00 2001
From: dinodev24 <149423946+dinodev24@users.noreply.github.com>
Date: Thu, 27 Nov 2025 22:07:33 -0800
Subject: [PATCH 2/3] Major UI Update: - Major change to UI design - Run config
now in settings page - Fix bug of no summary.md causing an error - Config can
be saved and is loaded each time
---
cace/cace_web.py | 134 +++++---
cace/web/html_templates.py | 18 +-
cace/web/index.html | 323 ++++++++++++------
cace/web/static/css/bootstrap-icons.min.css | 5 +
.../web/static/css/fonts/bootstrap-icons.woff | Bin 0 -> 180288 bytes
.../static/css/fonts/bootstrap-icons.woff2 | Bin 0 -> 134044 bytes
cace/web/static/css/style.css | 102 +++++-
7 files changed, 431 insertions(+), 151 deletions(-)
create mode 100644 cace/web/static/css/bootstrap-icons.min.css
create mode 100644 cace/web/static/css/fonts/bootstrap-icons.woff
create mode 100644 cace/web/static/css/fonts/bootstrap-icons.woff2
diff --git a/cace/cace_web.py b/cace/cace_web.py
index 180d615..db14786 100644
--- a/cace/cace_web.py
+++ b/cace/cace_web.py
@@ -39,18 +39,48 @@ def homepage():
parameter_manager.result_types = {}
data = [{'name': pname} for pname in pnames]
- # Collects all previous runs
+ config = None
+
+ if os.path.exists('.cace_config.json'):
+ with open('.cace_config.json') as f:
+ config = json.load(f)
+ else:
+ with open('.cace_config.json', 'a') as f:
+ f.write('{')
+ f.write(' "max_runs" : null,')
+ f.write(' "run_path" : null,')
+ f.write(f' "jobs" : {os.cpu_count()},')
+ f.write(' "force" : false,')
+ f.write(' "noplot" : false,')
+ f.write(' "nosim" : false,')
+ f.write(' "sequential" : false,')
+ f.write(' "netlist_source" : "best",')
+ f.write(' "parallel_parameters" : 4,')
+ f.write(' "typ_thresh" : 10')
+ f.write('}')
+
+ with open('.cace_config.json') as f:
+ config = json.load(f)
+
+ load_config(config)
+
runs = next(os.walk(datasheet['paths']['runs']))[1]
+
results = []
# Parse the markdown summary and store it in the results list
for run in runs:
result = []
- with open('runs/' + run + '/summary.md', 'r') as f:
- content = f.read()
+ summary_lines = read_summary_lines(run)
- summary_lines = content.split('\n')[7:-2]
+ if summary_lines == None:
+ results.append(
+ DANGER_ALERT_TEMPLATE.render(
+ text='ERROR: summary.md not found for this run!'
+ )
+ )
+ continue
for row in summary_lines:
row = row.split('|')
@@ -80,6 +110,23 @@ def homepage():
data=data,
runs=runs,
results=results,
+ config=json.dumps(config),
+ )
+
+
+def load_config(rd):
+ parameter_manager.max_runs = rd['max_runs']
+ parameter_manager.run_path = rd['run_path']
+ parameter_manager.max_jobs = int(rd['jobs'])
+ parameter_manager.set_runtime_options('force', rd['force'])
+ parameter_manager.set_runtime_options('noplot', rd['noplot'])
+ parameter_manager.set_runtime_options('nosim', rd['nosim'])
+ parameter_manager.set_runtime_options('sequential', rd['sequential'])
+ parameter_manager.set_runtime_options(
+ 'netlist_source', rd['netlist_source']
+ )
+ parameter_manager.set_runtime_options(
+ 'parallel_parameters', int(rd['parallel_parameters'])
)
@@ -129,38 +176,9 @@ def stream():
@app.route('/runsim', methods=['POST'])
def runsim():
rd = json.loads(request.get_data())
- if debug:
- print(rd)
-
- # Set default values for configuration options
- if rd['max_runs'] == '':
- rd['max_runs'] = None
- if rd['run_path'] == '':
- rd['run_path'] = None
- if rd['jobs'] == '':
- rd['jobs'] = os.cpu_count()
- if rd['netlist_source'] == '':
- rd['netlist_source'] = 'best'
- if rd['parallel_parameters'] == '':
- rd['parallel_parameters'] = 4
params = rd['selected_params']
- # Input the configuration to the parameter manager
- parameter_manager.max_runs = rd['max_runs']
- parameter_manager.run_path = rd['run_path']
- parameter_manager.max_jobs = rd['jobs']
- parameter_manager.set_runtime_options('force', rd['force'])
- parameter_manager.set_runtime_options('noplot', rd['noplot'])
- parameter_manager.set_runtime_options('nosim', rd['nosim'])
- parameter_manager.set_runtime_options('sequential', rd['sequential'])
- parameter_manager.set_runtime_options(
- 'netlist_source', rd['netlist_source']
- )
- parameter_manager.set_runtime_options(
- 'parallel_parameters', rd['parallel_parameters']
- )
-
# Prepare the directory that stores the run results
parameter_manager.prepare_run_dir()
@@ -211,21 +229,21 @@ def cancel_sim():
# Cancels all simulations
-@app.route('/cancel_sims', methods=['POST'])
+@app.route('/cancel_sims')
def cancel_sims():
parameter_manager.cancel_parameters()
return json.dumps({'success': True})
# Ends the SSE stream
-@app.route('/end_stream', methods=['POST'])
+@app.route('/end_stream')
def end_stream():
task_queue.put({'task': 'end_stream'})
return json.dumps({'success': True})
# Sends the run results to the client
-@app.route('/fetch_results', methods=['POST'])
+@app.route('/fetch_results')
def fetch_results():
parameter_manager.join_parameters()
result = []
@@ -298,7 +316,7 @@ def fetch_results():
# Sends the latest list of runs and their summaries to the client
-@app.route('/refresh_history', methods=['POST'])
+@app.route('/refresh_history')
def refresh_history():
# List all previous runs
runs = next(os.walk(datasheet['paths']['runs']))[1]
@@ -307,10 +325,15 @@ def refresh_history():
for run in runs:
result = []
- with open('runs/' + run + '/summary.md', 'r') as f:
- content = f.read()
+ summary_lines = read_summary_lines(run)
- summary_lines = content.split('\n')[7:-2]
+ if summary_lines == None:
+ results.append(
+ DANGER_ALERT_TEMPLATE.render(
+ text='ERROR: summary.md not found for this run!'
+ )
+ )
+ continue
# Parse each line of the summary
for row in summary_lines:
@@ -346,7 +369,34 @@ def refresh_history():
return json.dumps({'success': True})
-# Called to initialize the server
+def read_summary_lines(run):
+ try:
+ with open('runs/' + run + '/summary.md', 'r') as f:
+ content = f.read()
+
+ return content.split('\n')[7:-2]
+ except FileNotFoundError:
+ return None
+
+
+@app.route('/save_config', methods=['POST'])
+def save_config():
+ data = request.get_json()
+ with open('.cace_config.json', 'w') as f:
+ f.write(json.dumps(data))
+
+ load_config(data)
+ return json.dumps({'success': True})
+
+
+@app.route('/fetch_config')
+def fetch_config():
+ with open('.cace_config.json') as f:
+ config = json.load(f)
+
+ return json.dumps(config)
+
+
def web():
try:
host = 'localhost'
@@ -363,6 +413,6 @@ def web():
logger.setLevel(logging.DEBUG if debug else logging.WARNING)
# Run the server
- app.run(debug=debug, host=host, port=port, use_reloader=False)
+ app.run(debug=debug, host=host, port=port, use_reloader=debug)
finally:
task_queue.put({'task': 'close'})
diff --git a/cace/web/html_templates.py b/cace/web/html_templates.py
index f17a657..61b961e 100644
--- a/cace/web/html_templates.py
+++ b/cace/web/html_templates.py
@@ -15,10 +15,12 @@
| {{ param }} |
-
+
|
-
+
|
{% endfor %}
@@ -85,7 +87,7 @@
HISTORY_TEMPLATE = jinja2.Template(
"""
-
+
@@ -94,7 +96,7 @@
{{ runs[i] }}
-
+
{{ results[i] | safe }}
@@ -104,3 +106,11 @@
"""
)
+
+DANGER_ALERT_TEMPLATE = jinja2.Template(
+ """
+
+ {{ text }}
+
+"""
+)
diff --git a/cace/web/index.html b/cace/web/index.html
index 6f235bb..ff89cfa 100644
--- a/cace/web/index.html
+++ b/cace/web/index.html
@@ -4,47 +4,77 @@
+
CACE Web Interface
-
-
-
-
-
-
-
+
-
-
-
-
- Edit Run Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for i in range(runs|length) %}
+
+ {{ runs[i] }}
+
+
+
+ {{ results[i] | safe }}
+
+ {% endfor %}
+
+
+
+
+
+
+
Run Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% for i in range(runs|length) %}
-
- {{ runs[i] }}
-
-
-
- {{ results[i] | safe }}
-
- {% endfor %}
-
+
+
+
+
+
-
+