Repository: 3dem/emhub (language: python)

Issue:
Add a DataContent method to return content functions
As EMHub is implemented, many of the content functions registered with an app instance are nested functions of a content-registration function, and therefore not directly accessible by name after registration. References to them are stored internally by the app's DataContent, but it does not make them accessible.  This prevents EMHub instances from (cleanly) implementing customizations by wrapping the standard content functions.

This commit factors the content-function retrieval mechanism out of DataContent.get() into a separate, public get_content_func() method, by which the stored content functions can be retrieved.

This commit also updates the DataContent.content() annotation / method such that it no longer wraps content functions in do-nothing wrapper functions.


Discussion / hints:

Fix patch:
diff --git a/emhub/__init__.py b/emhub/__init__.py
index 931c7507..e0e367a3 100644
--- a/emhub/__init__.py
+++ b/emhub/__init__.py
@@ -31,7 +31,7 @@
 from glob import glob
 
 
-__version__ = '1.0.1'
+__version__ = '1.0.3'
 
 
 def create_app(test_config=None):
@@ -165,6 +165,15 @@ def main():
 
         dm = app.dm  # shortcut
 
+        # Special case to load a processing project
+        # PROCESSING_PROJECT = 1 should be in config.py
+        # this will login admin by default and also redirect to
+        # the flowchart page template
+        if processing := app.config.get('PROCESSING_PROJECT', None):
+            params['login_user'] = 1  # admin
+            kwargs['content_id'] = 'processing_flowchart'
+            kwargs['params'] = {'path': processing}
+
         if 'login_user' in params and app.is_devel:
             user_id = params['login_user']
             user = dm.get_user_by(id=user_id)
@@ -209,7 +218,6 @@ def login():
     @app.route('/do_login', methods=['POST'])
     def do_login():
         """ This view will be called as POST from the user login page. """
-
         username = flask.request.form['username']
         password = flask.request.form['password']
         next_content = flask.request.form.get('next_content', 'index')
@@ -414,11 +422,11 @@ def url_for_content(contentId, **kwargs):
     app.jinja_env.filters['shortname'] = shortname
     app.jinja_env.filters['pairname'] = pairname
     app.jinja_env.filters['pretty_size'] = Pretty.size
+    app.jinja_env.filters['isoformat'] = datetime_to_isoformat
 
     from emhub.data.data_manager import DataManager
     app.user = flask_login.current_user
 
-
     from .data.content import dc
     app.dc = dc
 
diff --git a/emhub/blueprints/api.py b/emhub/blueprints/api.py
index 3281810a..2e880d01 100644
--- a/emhub/blueprints/api.py
+++ b/emhub/blueprints/api.py
@@ -571,12 +571,18 @@ def _session_users(**attrs):
 
 
 def _loadFileLines(fn):
-    lines = ''
     if os.path.exists(fn):
-        for line in open(fn):
-            lines += line
+        with open(fn) as f:
+            lines = f.readlines()
+            o = len(lines) - 2000
+            if o > 0:
+                result = ''.join(lines[:1000])
+                result += f'\n=============== OMITTED {o} LINES ============\n\n'
+                result += ''.join(lines[-1000:])
+            else:
+                result = ''.join(lines)
 
-    return lines
+    return result
 
 
 @api_bp.route('/get_session_run', methods=['POST'])
@@ -616,13 +622,21 @@ def _get_run(**attrs):
 @api_bp.route("/get_classes2d", methods=['POST'])
 def get_classes2d():
     """ Load 2d classification data. """
-    proc = app.dm.get_processing_project(entry_id=request.form['entry_id'])
-    run = proc.get_run(request.form['run_id'])
-    classes = run.get_classes2d(iteration=request.form.get('iteration', None))
-
+    kwargs = request.form.to_dict()
+    run = app.dm.get_processing_project(**kwargs)['run']
+    classes = run.get_classes2d(iteration=kwargs.get('iteration', None))
     return send_json_data(classes)
 
 
+@api_bp.route("/get_file_info", methods=['POST'])
+def get_file_info():
+    """ Load 2d classification data. """
+    kwargs = request.form.to_dict()
+    project = app.dm.get_processing_project(**kwargs)['project']
+    file_info = project.get_file_info(kwargs['path'])
+    return send_json_data(file_info)
+
+
 def get_worker_token(worker):
     return jwt.encode(
         {'worker': worker},
@@ -821,7 +835,7 @@ def delete_transaction():
 @api_bp.route('/get_forms', methods=['GET', 'POST'])
 @flask_login.login_required
 def get_forms():
-    return send_json_data([f.json() for f in app.dm.get_forms()])
+    return filter_request(app.dm.get_forms)
 
 
 @api_bp.route('/create_form', methods=['POST'])
@@ -856,7 +870,7 @@ def _get_config(**attrs):
 @api_bp.route('/get_projects', methods=['GET', 'POST'])
 @flask_login.login_required
 def get_projects():
-    return send_json_data([p.json() for p in app.dm.get_projects()])
+    return filter_request(app.dm.get_projects)
 
 
 @api_bp.route('/create_project', methods=['POST'])
@@ -907,6 +921,8 @@ def handle(**attrs):
         entry = app.dm.get_entry_by(id=attrs['id'])
         old_files = set(app.dm.get_entry_files(entry))
         save_entry_files(entry, attrs['extra']['data'])
+        # There is a validation about the entry type
+        attrs['type'] = entry.type
         entry = app.dm.update_entry(**attrs)
         new_files = set(app.dm.get_entry_files(entry))
         clean_files(old_files - new_files)
@@ -928,12 +944,12 @@ def handle(**attrs):
     return _handle_item(handle, 'entry')
 
 
-# ------------------------------ ENTRIES ---------------------------------
+# ------------------------------ PUCKS ---------------------------------
 
 @api_bp.route('/get_pucks', methods=['GET', 'POST'])
 @flask_login.login_required
 def get_pucks():
-    return send_json_data([p.json() for p in app.dm.get_pucks()])
+    return filter_request(app.dm.get_pucks)
 
 
 @api_bp.route('/create_puck', methods=['POST'])
@@ -953,7 +969,6 @@ def update_puck():
 def delete_puck():
     return handle_puck(app.dm.delete_puck)
 
-
 # -------------------- UTILS functions ----------------------------------------
 
 def filter_request(func):
diff --git a/emhub/blueprints/images.py b/emhub/blueprints/images.py
index 6183e8e1..2ddefe64 100644
--- a/emhub/blueprints/images.py
+++ b/emhub/blueprints/images.py
@@ -90,16 +90,9 @@ def get_mic_data():
 
 @images_bp.route("/get_micrograph_gridsquare", methods=['POST'])
 def get_micrograph_gridsquare():
-    form = request.form  # shortcut
-    sessionId = int(form['session_id'])
-    session = app.dm.load_session(sessionId)
-    kwargs = {}
-    if 'gsId' in form:
-        kwargs['gsId'] = form['gsId']
-    if 'fhId' in form:
-        kwargs['fhId'] = form['fhId']
-    data = session.data.get_micrograph_gridsquare(**kwargs)
-    session.data.close()
+    kwargs = request.form.to_dict()
+    project = app.dm.get_processing_project(**kwargs)['project']
+    data = project.get_micrograph_gridsquare(**kwargs)
     return send_json_data(data)
 
 
diff --git a/emhub/client/scripts/20240627_update_session.py b/emhub/client/scripts/20240627_update_session.py
new file mode 100755
index 00000000..c2f74c5f
--- /dev/null
+++ b/emhub/client/scripts/20240627_update_session.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+# **************************************************************************
+# *
+# * Authors:     J.M. De la Rosa Trevin
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'delarosatrevin@gmail.com'
+# *
+# **************************************************************************
+
+""" 
+This script will create new bookings for specific bookings
+"""
+import argparse
+import os
+import sys
+import secrets
+from pprint import pprint
+from datetime import timezone, timedelta, datetime
+
+from emtools.utils import Pretty, Color
+from emhub.client import open_client, config, DataClient
+from emhub.utils import datetime_from_isoformat
+
+update = '--update' in sys.argv
+
+
+def update_session(args):
+    dcSrc = DataClient()
+    dcSrc.login(config.EMHUB_USER, config.EMHUB_PASSWORD)
+
+    sessions = dcSrc.request('get_sessions', jsonData=None).json()
+
+    print("Session id: ", args.session_id)
+    for s in sessions:
+        if s['id'] == int(args.session_id):
+            print(f"Updating session {args.session_id}")
+            acq = dict(s['acquisition'])
+            acq['magnification'] = 81000
+            acq['pixel_size'] = 1.057
+            s['acquisition'] = acq
+            r = dcSrc.update_session(s)
+            pprint(r)
+
+    dcSrc.logout()
+
+
+def main():
+    p = argparse.ArgumentParser(prog='update-session')
+
+    p.add_argument('session_id')
+   
+    update_session(p.parse_args())
+
+
+if __name__ == '__main__':
+    main()
+
+
+
+
+
diff --git a/emhub/client/scripts/20240726_update_entry.py b/emhub/client/scripts/20240726_update_entry.py
new file mode 100755
index 00000000..caead5b2
--- /dev/null
+++ b/emhub/client/scripts/20240726_update_entry.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# **************************************************************************
+# *
+# * Authors:     J.M. De la Rosa Trevin
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'delarosatrevin@gmail.com'
+# *
+# **************************************************************************
+
+""" 
+This script update a given entry
+"""
+import argparse
+import os
+import sys
+import secrets
+from pprint import pprint
+from datetime import timezone, timedelta, datetime
+
+from emtools.utils import Pretty, Color
+from emhub.client import open_client, config, DataClient
+from emhub.utils import datetime_from_isoformat
+
+update = '--update' in sys.argv
+
+
+def update_entry(args):
+    entry_id = args.entry_id
+
+    with open_client() as dc:
+        entries = dc.request('get_entries', jsonData=None).json()
+
+        print("Entry id: ", entry_id)
+        for e in entries:
+            if e['id'] == int(entry_id):
+                extra = e['extra']
+                extra['data']['days'] = '1'
+                print(f"Updating entry {entry_id}")
+                r = dc.request('update_entry',
+                               {'attrs': {'id': entry_id,
+                                          'type': e['type'],
+                                          'extra': extra}})
+                pprint(r.json())
+
+def main():
+    p = argparse.ArgumentParser(prog='update-session')
+
+    p.add_argument('entry_id')
+   
+    update_entry(p.parse_args())
+
+
+if __name__ == '__main__':
+    main()
+
+
+
+
+
diff --git a/emhub/client/scripts/20241001_create_project.py b/emhub/client/scripts/20241001_create_project.py
new file mode 100755
index 00000000..068669d8
--- /dev/null
+++ b/emhub/client/scripts/20241001_create_project.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# **************************************************************************
+# *
+# * Authors:     J.M. De la Rosa Trevin
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 2 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, write to the Free Software
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# * 02111-1307  USA
+# *
+# *  All comments concerning this program package may be sent to the
+# *  e-mail address 'delarosatrevin@gmail.com'
+# *
+# **************************************************************************
+
+""" 
+This script update a given entry
+"""
+import argparse
+import os
+import sys
+import secrets
+from pprint import pprint
+from datetime import timezone, timedelta, datetime
+
+from emtools.utils import Pretty, Color
+from emhub.client import open_client, config, DataClient
+from emhub.utils import datetime_from_isoformat
+
+
+def create_project():
+    with open_client() as dc:
+        attrs = {
+            'status': "special:news",
+            'title': "News Project",
+            'user_id': 59,
+            'validate': False
+        }
+        r = dc.request('create_project', {'attrs': attrs})
+        pprint(r.json())
+
+def create_news():
+    with open_client() as dc:
+        r = dc.request('get_projects', jsonData={'condition': 'status="special:news"'})
+        project = r.json()[0]
+        pid = project['id']
+
+        print(r.json())
+
+        print("Project News: ", pid)
+        newsConfig = dc.get_config('news')
+        attrs = {
+            'project_id': pid,
+            'type': 'news',
+            'extra': {}
+        }
+        # Delete all entries from that project
+        print("------ Deleting OLD entries: ")
+        r = dc.request('get_entries', {'condition': 'project_id=%d' % pid})
+        for e in r.json():
+            print("   Deleting ", e['id'], "...")
+            dc.request('delete_entry', {'attrs': {'id': e['id']}})
+
+        print("-------- Creating News: ")
+        for n in newsConfig['news']:
+            attrs['date'] = n['date']
+            attrs['title'] = n['title']
+            attrs['description'] = n['text']
+            attrs['extra']['data'] = {
+                'active': n['status'] == 'active',
+                'type': n['type']
+            }
+            print("   ", attrs)
+            r = dc.request('create_entry', {'attrs': attrs})
+            print("   ", r.json())
+
+
+def main():
+    #create_project()
+    create_news()
+
+
+if __name__ == '__main__':
+    main()
+
+
+
+
+
diff --git a/emhub/client/scripts/disguise.py b/emhub/client/scripts/disguise.py
new file mode 100644
index 00000000..058f28d8
--- /dev/null
+++ b/emhub/client/scripts/disguise.py
@@ -0,0 +1,29 @@
+from emhub.client import DataClient, open_client
+from faker import Faker
+
+with open_client() as dc:
+
+    # Update user's name and email with fake values
+    users = dc.request('get_users', jsonData=None).json()
+    f = Faker()
+    for u in users:
+        name = ' '.join(f.name().split(' ')[-2:])
+        email = name.lower().replace(' ', '.') + '@emhub.org'
+        attrs = {'id': u['id'], 'name': name, 'email': email}
+        dc.request('update_user', jsonData={'attrs': attrs})
+
+    # Modify sessions to hide real name
+    sessions = dc.request('get_sessions', jsonData=None).json()
+    for s in sessions:
+        dc.update_session({'id': s['id'], 'name': f"S{s['id']:05d}"})
+
+    # Hide project's title and make all 'confidential'
+    projects = dc.request('get_projects', jsonData=None).json()
+    for p in projects:
+        # read 'extra' property where 'is_confidential' is stored
+        extra = dict(p['extra'])
+        extra['is_confidential'] = True
+        attrs = {'id': p['id'], 'extra': extra, 'title': 'Project Title'}
+        dc.request('update_project', jsonData={'attrs': attrs})
+
+
diff --git a/emhub/client/scripts/instrument_data_sync.py b/emhub/client/scripts/instrument_data_sync.py
new file mode 100755
index 00000000..e9f3cf7a
--- /dev/null
+++ b/emhub/client/scripts/instrument_data_sync.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+# **************************************************************************
+# *
+# * Authors:     J.M. de la Rosa Trevin (delarosatrevin@gmail.com)
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 3 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# **************************************************************************
+
+"""
+Synchronize
+Monitor a folder with Negative-Stain images and copy them to each group's folder.
+Raw image path and groups folder is read from EMhub configuration.
+"""
+
+import os
+import sys
+import argparse
+from pprint import pprint
+
+from emtools.utils import Color, Process, Pretty
+from emhub.client import open_client
+
+
+def main():
+    p = argparse.ArgumentParser(prog='instrument_data_sync.py')
+    p.add_argument('data_root',
+                   help="Root directory where data is mounted. "
+                        "It is expected that each group has a folder "
+                        "there. ")
+    p.add_argument('instrument_folder',
+                   help="Instrument folder name that will be used "
+                        "to copy the data to. ")
+    p.add_argument('--copy', '-c', action="store_true",
+                   help="By default it will do a dry execution. When "
+                        "this option is used, the real copy is done. ")
+    p.add_argument('--log', metavar='LOG_FILE',
+                   help="Provide a file to log operation. ")
+
+    args = p.parse_args()
+    data_root = args.data_root
+    instrument_folder = args.instrument_folder
+    dry = not args.copy
+    dryStr = '(DRY)' if dry else ''
+
+    if args.log:
+        with open(args.log, 'a') as f:
+            f.write(f"{Pretty.now()}: Sync {instrument_folder} data {dryStr}.\n")
+
+    logger = Process.Logger(only_log=dry)
+
+    def _color(path):
+        color = Color.bold if os.path.exists(path) else Color.red
+        return color(path)
+
+    def _rsync(src, dst):
+        """ Synchronize content from 'src' to 'dst'. """
+        if not os.path.exists(dst):
+            logger.mkdir(dst)
+        cmd = f"rsync -av '{src}/' '{dst}/'"
+        logger.system(cmd)
+
+    with open_client() as dc:
+        sconfig = dc.get_config('sessions')
+        raw = sconfig['raw']
+        jude_folder = raw['jude_group_folder']
+        for group in sconfig['groups'].values():
+            print('\n', Color.warn(group))
+            data_path = os.path.join(data_root, group)
+            print("  Data root: ", _color(data_path))
+            gscem_path = os.path.join(raw['root'], group)
+            print("     GSCEM: ", _color(gscem_path))
+            jude_path = jude_folder.format(group=group)
+            print("      Jude: ", _color(jude_path))
+
+            if not os.path.exists(data_path):
+                continue
+
+            if os.path.exists(gscem_path):
+                _rsync(data_path, os.path.join(gscem_path, instrument_folder))
+
+            if os.path.exists(jude_path):
+                _rsync(data_path, os.path.join(jude_path, instrument_folder))
+
+
+if __name__ == '__main__':
+    main()
+
+
+
+
+
diff --git a/emhub/client/scripts/negstain_sync.py b/emhub/client/scripts/negstain_sync.py
deleted file mode 100755
index b51cbdb5..00000000
--- a/emhub/client/scripts/negstain_sync.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python
-# **************************************************************************
-# *
-# * Authors:     J.M. de la Rosa Trevin (delarosatrevin@gmail.com)
-# *
-# * This program is free software; you can redistribute it and/or modify
-# * it under the terms of the GNU General Public License as published by
-# * the Free Software Foundation; either version 3 of the License, or
-# * (at your option) any later version.
-# *
-# * This program is distributed in the hope that it will be useful,
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# * GNU General Public License for more details.
-# *
-# **************************************************************************
-
-""" 
-Monitor a folder with Negative-Stain images and copy them to each group's folder.
-Raw image path and groups folder is read from EMhub configuration.
-"""
-
-import os, sys
-from pprint import pprint
-
-from emtools.utils import Color, Process
-from emhub.client import open_client
-from emhub.client.worker import TaskHandler, DefaultTaskHandler, Worker
-
-
-logger = Process.Logger()
-
-def _color(path):
-    color = Color.bold if os.path.exists(path) else Color.red
-    return color(path)
-
-
-def _rsync(src, dst):
-    """ Synchronize content from 'src' to 'dst'. """
-    if not os.path.exists(dst):
-        logger.mkdir(dst)
-    cmd = f"rsync -av '{src}/' '{dst}/'"
-    logger.system(cmd)
-    #print(cmd)
-
-
-def main():
-    with open_client() as dc:
-        microscope = 'TalosL120C'
-        sconfig = dc.get_config('sessions')
-        raw = sconfig['raw']
-        jude_folder = raw['jude_group_folder']
-        for group in sconfig['groups'].values():
-            print('\n', Color.warn(group))
-            negstain_path = os.path.join(raw['root_negstain'], group)
-            print("  NegStain: ", _color(negstain_path))
-            gscem_path = os.path.join(raw['root'], group)
-            print("     GSCEM: ", _color(gscem_path))
-            jude_path = jude_folder.format(group=group)
-            print("      Jude: ", _color(jude_path))
-
-            if not os.path.exists(negstain_path):
-                continue
-
-            if os.path.exists(gscem_path):
-                _rsync(negstain_path, os.path.join(gscem_path, microscope))
-
-            if os.path.exists(jude_path):
-                _rsync(negstain_path, os.path.join(jude_path, microscope))
-
-
-
-if __name__ == '__main__':
-    main()
-    #worker = NegStainWorker(debug=True)
-    #worker.run()
-
-
-
-
-
diff --git a/emhub/data/__main__.py b/emhub/data/__main__.py
index f73abbc8..d4c5d2a4 100644
--- a/emhub/data/__main__.py
+++ b/emhub/data/__main__.py
@@ -1,3 +1,19 @@
+#!/usr/bin/env python
+# **************************************************************************
+# *
+# * Authors:     J.M. de la Rosa Trevin (delarosatrevin@gmail.com)
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 3 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# **************************************************************************
 
 import os
 import sys
@@ -31,7 +47,7 @@ def dump(keys, json_file):
 
 
 def main():
-    p = argparse.ArgumentParser(prog='emt-data')
+    p = argparse.ArgumentParser(prog='emh-data')
     g = p.add_mutually_exclusive_group()
 
     g.add_argument('--create_instance', nargs='*',
diff --git a/emhub/data/content/dc_base.py b/emhub/data/content/dc_base.py
index 11ec1460..ec5458fa 100644
--- a/emhub/data/content/dc_base.py
+++ b/emhub/data/content/dc_base.py
@@ -61,12 +61,14 @@ def __init__(self):
     def _dateStr(self, datetime):
         return
 
+    def get_content_func(self, name):
+        return self._contentDict.get(name, getattr(self, name, None))
+
     def get(self, **kwargs):
         content_id = kwargs['content_id']
         get_func_name = content_id.replace('-', '_')
         dataDict = {}
-        get_func = self._contentDict.get(get_func_name,
-                                         getattr(self, get_func_name, None))
+        get_func = self.get_content_func(get_func_name)
         if get_func is None:
             raise Exception(f"Missing content function for '{content_id}'")
 
@@ -74,13 +76,10 @@ def get(self, **kwargs):
         return dataDict
 
     def content(self, func):
+        self._contentDict[func.__name__] = func
 
-        def wrapper(**kwargs):
-            return func(**kwargs)
-
-        self._contentDict[func.__name__] = wrapper
-
-        return wrapper
+        # No need for a do-nothing wrapper
+        return func
 
     def get_lab_members(self, user):
         unit = user.staff_unit
@@ -552,6 +551,8 @@ def get_resources(self, **kwargs):
 
         all = kwargs.get('all', False)
         get_image = kwargs.get('image', False)
+        # Return only one resource with that id
+        resource_id = kwargs.get('resource_id', None)
 
         def _image(r):
             if not get_image or r.id is None:
@@ -565,7 +566,7 @@ def _image(r):
                 return flask.url_for('images.static', filename=r.image)
 
         def _filter(r):
-            return all or r.is_active
+            return (all or r.is_active) and ((not resource_id) or r.id == int(resource_id))
 
         resource_list = [
             {'id': r.id,
@@ -589,7 +590,7 @@ def _filter(r):
 
     def get_user_projects(self, user, **kwargs):
         dm = self.app.dm
-        status = kwargs.get('status', None)
+        status = kwargs.get('status', 'active')
         extra = 'extra' in kwargs
         pid = int(kwargs.get('pid', 0))
         scope = kwargs.get('scope', 'lab')
@@ -776,7 +777,8 @@ def _ts(fn):
                 'defocus_bins': dbins.toList(),
                 'resolution_bins': rbins.toList(),
                 'gridsquares': gridsquares,
-                'gs_info': epuData is not None,
+                'gs_info': True, # epuData is not None,
+                'ctfs_run_id': sdata.get_ctfs_runid()
             })
 
         elif result == 'classes2d':
@@ -785,6 +787,34 @@ def _ts(fn):
 
         return data
 
+    def get_news(self, **kwargs):
+        """ Return news after creating HTML markup. """
+        from markupsafe import Markup
+        status = kwargs.get('status', 'all')
+        dm = self.app.dm  # shortcut
+        news = ([], [])  # active/inactive lists
+
+        project = dm.get_project_by(status='special:news')
+        if project is not None:
+            for e in project.entries:
+                data = e.extra['data']
+                active = data.get('active', False)
+                i = 0 if active else 1
+                news[i].append({
+                    'id': e.id,
+                    'title': e.title,
+                    'text': e.description,
+                    'html': Markup(e.description),
+                    'active': active,
+                    'type': data['type']
+                })
+
+        return {
+            'news': news,
+            'display': kwargs.get('display', 'table'),
+            'project_id': project.id if project else 0
+        }
+
 
 def register_content(dc):
 
@@ -942,21 +972,15 @@ def add_booking(b):
             for k, bookingValues in rbookings.items():
                 bookingValues.sort(key=lambda b: b.start)
 
-        from markupsafe import Markup
-        value = Markup('<strong>The HTML String</strong>')
-
-        newsConfig = dm.get_config('news')
-        allNews = newsConfig['news'] if newsConfig else []
-        news = []
-        for n in allNews:
-            if n['status'] == 'active':
-                n['html'] = Markup(n['text'])
-                news.append(n)
-
+        resource_create_session = dm.get_config('sessions').get('create_session', {})
         dataDict.update({'bookings': bookings,
                          'resource_bookings': resource_bookings,
-                         'news': news,
+                         'resource_create_session': resource_create_session,
                          'local_resources': local_scopes
                          })
+        dataDict.update(dc.get_news(**kwargs))
         return dataDict
 
+    @dc.content
+    def news(**kwargs):
+        return dc.get_news(**kwargs)
diff --git a/emhub/data/content/dc_bookings.py b/emhub/data/content/dc_bookings.py
index 741bc1e6..7dee372f 100644
--- a/emhub/data/content/dc_bookings.py
+++ b/emhub/data/content/dc_bookings.py
@@ -129,8 +129,11 @@ def booking_form(**kwargs):
             entry = dm.get_entry_by(id=kwargs['entry_id'])
             scopes = {r.id: r for r in dm.get_resources() if r.is_microscope}
             booking = dc.booking_from_entry(entry, scopes)
-        else:  # New Application
-            booking = dm.create_basic_booking(dates)
+        else:  # New Booking
+            args = dict(kwargs)
+            args.pop('content_id')
+            args.update(dates)
+            booking = dm.create_basic_booking(args)
             allowed_resources = [r for r in dm.get_resources()]
 
         display = dm.get_config('bookings')['display']
diff --git a/emhub/data/content/dc_raw.py b/emhub/data/content/dc_raw.py
index 3002566f..f1e03cc4 100644
--- a/emhub/data/content/dc_raw.py
+++ b/emhub/data/content/dc_raw.py
@@ -189,4 +189,11 @@ def task_history(**kwargs):
     def raw_test_page(**kwargs):
         return {}
 
+    @dc.content
+    def raw_projects_list(**kwargs):
+        if 'status' not in kwargs:
+            kwargs['status'] = None  # projects with any status
+        data = dc.get_user_projects(dc.app.user, **kwargs)
+        return data
+
 
diff --git a/emhub/data/content/dc_sessions.py b/emhub/data/content/dc_sessions.py
index 7ba94710..1f55f4d6 100644
--- a/emhub/data/content/dc_sessions.py
+++ b/emhub/data/content/dc_sessions.py
@@ -199,7 +199,7 @@ def get_processing_run_func(funcName, kwargs):
         pp = ppDict['project']
         run = ppDict['run']
         func = getattr(run, funcName)
-        result = func()
+        result = func(**kwargs)
         data = {
             'processing_project': pp,
             'get_project_args': ppDict['args'],
diff --git a/emhub/data/data_manager.py b/emhub/data/data_manager.py
index 214c550b..a62dcf81 100644
--- a/emhub/data/data_manager.py
+++ b/emhub/data/data_manager.py
@@ -105,6 +105,7 @@ def create_admin(self, password='admin'):
                                  name='admin',
                                  roles=['admin'],
                                  pi_id=None)
+
         if self._user is None:
             self._user = admin
 
@@ -125,12 +126,11 @@ def create_basic_users(self):
 
     def create_user(self, **attrs):
         """ Create a new user in the DB. """
-        if not self._user.is_manager:
+        if self._user is not None and not self._user.is_manager:
             raise Exception("Only 'managers' or 'admins' can register new users.")
 
         def __check_uniq(attrName):
             attr = attrs.get(attrName, None)
-            print(f"Checking attr '{attrName}, value: '{attr}'")
             if not attr or not attr.strip():
                 raise Exception(f"Input '{attrName}' should have a value")
             if self.get_user_by(**{attrName: attr}) is not None:
@@ -145,6 +145,7 @@ def __check_uniq(attrName):
         del attrs['password']
 
         user = self.__create_item(self.User, **attrs)
+        return user
 
     def update_user(self, **attrs):
         """ Update an existing user. """
@@ -697,11 +698,12 @@ def create_session(self, **attrs):
             self.update_session_counter(session_info['code'],
                                         session_info['counter'] + 1)
 
-        for action, worker in tasks:
+        for worker, args in tasks:
+            args['session_id'] = session.id
             # Update some values for the task
             task = {
                 'name': 'session',
-                'args': {'session_id': session.id, 'action': action}
+                'args': args
             }
             self.get_worker_stream(worker).create_task(task)
 
@@ -772,7 +774,7 @@ def get_processing_project(self, **kwargs):
         result = {'project': pp, 'args': args}
 
         if 'run_id' in kwargs:
-            run_id = int(kwargs['run_id'])
+            run_id = kwargs['run_id']
             result['run'] = pp.get_run(run_id)
             args['run_id'] = run_id
             
@@ -863,12 +865,14 @@ def __check_project(self, **attrs):
             if not attrs['user_id']:
                 raise Exception("Provide a valid User ID for the Project.")
 
-        if 'status' in attrs:
-            if not attrs['status'].strip() in self.Project.STATUS:
+        if status := attrs.get('status', None):
+            s = status.strip()
+            if not (s.startswith('special:') or s in self.Project.STATUS):
                 raise Exception("Provide a valid status: active/inactive")
 
     def create_project(self, **attrs):
-        self.__check_project(**attrs)
+        if validate := attrs.pop('validate', True):
+            self.__check_project(**attrs)
 
         now = self.now()
         attrs.update({
@@ -1217,17 +1221,12 @@ def check_resource_access(self, resource, permissionKey):
             return True
 
         perms = self.get_config('permissions')
-        return (self._user.can_book_resource(resource) and
+        return (self._user.can_book_resource(resource) or True and
                 any(t in resource.tags and 'user' in u
                     for t, u in perms.get(permissionKey, {}).items()))
 
     # ------------------- BOOKING helper functions -----------------------------
     def create_basic_booking(self, attrs, **kwargs):
-        # if 'creator_id' not in attrs:
-        #     attrs['creator_id'] = self._user.id
-        #
-        # if 'owner_id' not in attrs:
-        #     attrs['owner_id'] = self._user.id
         if 'type' not in attrs:
             attrs['type'] = 'booking'
 
@@ -1241,6 +1240,7 @@ def _set_user(key):
 
         _set_user('creator')
         _set_user('owner')
+        _set_user('operator')
 
         return b
 
@@ -1310,23 +1310,15 @@ def __validate_booking(self, booking, **kwargs):
         app = None
 
         if not booking.is_slot:
-            margin = dt.timedelta(seconds=1)
-            def _in_range(x):
-                return s < x < e
-            def _soft_overlap(b):
-                """ Allow events to start/end at the same time without reporting
-                it as overlap. """
-                return b.id != booking.id and _in_range(b.start) or _in_range(b.end)
-
             # Check there is not overlapping with other non-slot events
             overlap_noslots = [b for b in overlap
-                               if not b.is_slot and _soft_overlap(b)]
+                               if not b.is_slot and booking.overlap(b)]
             if overlap_noslots:
                 raise Exception("Booking is overlapping with other events: %s"
                                 % overlap_noslots)
 
             overlap_slots = [b for b in overlap
-                             if b.is_slot and _soft_overlap(b)]
+                             if b.is_slot and booking.overlap_slot(b)]
 
             # Always try to find the Application to set in the booking unless
             # the owner is a manager
diff --git a/emhub/data/data_models.py b/emhub/data/data_models.py
index c4cfc857..295dd774 100644
--- a/emhub/data/data_models.py
+++ b/emhub/data/data_models.py
@@ -870,6 +870,23 @@ def application_in_slot(self, application):
 
             return application.code in self.slot_auth.get('applications', [])
 
+        def overlap(self, b):
+            """ Check overlap with another booking.
+            Allow events to start/end at the same time without reporting
+            it as overlap.
+            Bookings with same ID are not reported as overlap.
+            """
+            s, e = self.start, self.end
+            return self.id != b.id and (s < b.start < e or s < b.end < e)
+
+        def overlap_slot(self, b):
+            """ Check overlap with another booking that is a slot.
+            Start/end at the same time are reported as overlap.
+            """
+            s, e = self.start, self.end
+            return self.id != b.id and (s <= b.start <= e or s <= b.end <= e)
+
+
     class Session(Base):
         """Model for sessions."""
         __tablename__ = 'sessions'
diff --git a/emhub/data/imports/__init__.py b/emhub/data/imports/__init__.py
index 2769ee27..fd0248b1 100644
--- a/emhub/data/imports/__init__.py
+++ b/emhub/data/imports/__init__.py
@@ -26,10 +26,11 @@
 # *
 # **************************************************************************
 
-import datetime as dt
-import json
 import os
+from .test import create_instance
 
+here = os.path.abspath(os.path.dirname(__file__))
+MINIMAL_JSON = os.path.join(here, 'minimal_instance_data.json')
 
 
 
diff --git a/emhub/data/processing/base.py b/emhub/data/processing/base.py
index ff80114a..112c5c09 100644
--- a/emhub/data/processing/base.py
+++ b/emhub/data/processing/base.py
@@ -39,6 +39,7 @@ def __init__(self, project, path):
         self.project = project
         self._path = path
         self._epuData = None
+        self._ios = None  # Input/outputs cache
 
     def join(self, *paths):
         return os.path.join(self._path, *paths)
@@ -65,9 +66,19 @@ def __init__(self, path, mode='r'):
         self._path = path
         self._epuData = None
 
+    @property
+    def path(self):
+        return self._path
+
     def join(self, *paths):
         return os.path.join(self._path, *paths)
 
+    def relpath(self, path):
+        return os.path.relpath(path, self._path)
+
+    def exists(self, path):
+        return os.path.exists(self.join(path))
+
     def getEpuData(self):
         if not self._epuData:
             epuFolder = self.join('EPU')
diff --git a/emhub/data/processing/processing_relion/__init__.py b/emhub/data/processing/processing_relion/__init__.py
index 5ddaf4cf..13bc8ea5 100644
--- a/emhub/data/processing/processing_relion/__init__.py
+++ b/emhub/data/processing/processing_relion/__init__.py
@@ -14,592 +14,7 @@
 # *
 # **************************************************************************
 
-import os
-import datetime as dt
-from glob import glob
-from collections import defaultdict
-import json
-import flask
-import numpy as np
-from flask import current_app as app
+from .project import RelionSessionData
+from .runs import RelionRun
 
-import mrcfile
-from emtools.utils import Path, Timer, Pretty
-from emtools.metadata import StarFile, EPU, SqliteFile
-from emtools.image import Thumbnail
 
-from ..base import SessionRun, SessionData, hours
-
-location = os.path.dirname(__file__)
-
-
-class RelionRun(SessionRun):
-    """ Helper class to manipulate Relion run data. """
-    def __init__(self, project, path):
-        SessionRun.__init__(self, project, path)
-        d, self.id = os.path.split(Path.rmslash(path))
-        with StarFile(self.join('job.star')) as sf:
-            self.job = sf.getTable('job')
-            self.values = {row.rlnJobOptionVariable: row.rlnJobOptionValue
-                           for row in sf.iterTable('joboptions_values', guessType=False)}
-        parts = self.job[0].rlnJobTypeLabel.split('.')
-        self.package = parts[0]
-        self.className = parts[1]
-        self.classSuffix = '' if len(parts) < 3 else '.'.join(parts[2:])
-
-        with StarFile(self.join('job_pipeline.star')) as sf:
-            t = sf.getTable('pipeline_processes')
-            row = t[0]
-            self.name = row.rlnPipeLineProcessName
-            self.alias = row.rlnPipeLineProcessAlias
-
-    def getInfo(self):
-        return {'id': self.id, 'className': self.className, 'label': self.id,
-                'name': self.name, 'alias': self.alias}
-
-    def getFormDefinition(self):
-        default = {'valueClass': 'String',
-                   'paramClass': 'StringParam',
-                   'important': False,
-                   'expert': False
-                   }
-
-        formDef = {
-            'package': 'relion',
-            'name': self.className,
-            'logo': '',
-            'sections': []
-        }
-
-        allParams = set()
-
-        def _set_defaults(paramDef):
-            for k, v in default.items():
-                if k not in paramDef:
-                    paramDef[k] = v
-            return paramDef
-
-        config = f"{self.package}.{self.className}.json"
-        configFn = os.path.join(location, 'forms', config)
-        print("Config file", configFn)
-
-        if os.path.exists(configFn):
-            with open(configFn) as f:
-                formConf = json.load(f)
-                for sectionDef in formConf['sections']:
-                    for paramDef in sectionDef['params']:
-                        if 'name' in paramDef:
-                            _set_defaults(paramDef)
-                            allParams.add(paramDef['name'])
-                    formDef['sections'].append(sectionDef)
-
-        extraParams = []
-        for k, v in self.values.items():
-            if k not in allParams:
-                extraParams.append(_set_defaults({'label': k, 'name': k}))
-
-        if extraParams:
-            formDef['sections'].append({'label': 'extra params',
-                                        'params': extraParams})
-
-        return formDef
-
-    def getValues(self):
-        return self.values
-
-    def getStdOut(self):
-        """ Return run's stdout file. """
-        return self.join('run.out')
-
-    def getStdError(self):
-        """ Return run's stdout file. """
-        return self.join('run.err')
-
-    def getInputsOutputs(self):
-        with StarFile(self.join('job_pipeline.star')) as sf:
-            inputsTable = sf.getTable('pipeline_input_edges')
-            outputsTable = sf.getTable('pipeline_output_edges')
-            return {
-                'inputs': [row.rlnPipeLineEdgeFromNode for row in inputsTable],
-                'outputs': [row.rlnPipeLineEdgeToNode for row in outputsTable]
-            }
-
-    def _load_ctfvalues(self, index=False):
-        data_values = {
-            'rlnDefocusU': {
-                'label': 'Defocus U',
-                'scale': 0.0001,
-                'unit': 'µm',
-                'color': '#852999',
-                'maxX': 4,
-                'data': []
-            },
-            'rlnDefocusV': {
-                'label': 'Defocus V',
-                'scale': 0.0001,
-                'unit': 'µm',
-                'color': '#852999',
-                'maxX': 4,
-                'data': []
-            },
-            'rlnCtfFigureOfMerit': {
-                'data': []
-            },
-            'rlnCtfMaxResolution': {
-                'color': '#EF9A53',
-                'label': 'Resolution',
-                'unit': 'Å',
-                'data': []
-            }
-        }
-        indexes = []
-        with StarFile(self.join('micrographs_ctf.star')) as sf:
-            for i, row in enumerate(sf.iterTable('micrographs')):
-                indexes.append(i + 1)
-                rowDict = row._asdict()
-                for k, v in data_values.items():
-                    scale = v.get('scale', 1)
-                    v['data'].append(rowDict[k] * scale)
-        if index:
-            data_values['index'] = {
-                'label': 'Index',
-                'data': indexes
-            }
-        return data_values
-
-    def getSummary(self):
-        """ Function to return the template and data used for this run summary.
-        """
-        summary = {'template': '', 'data': {}}
-        data_values = None
-
-        if self.className == 'ctffind':
-            summary['template'] = 'processing_ctf_summary.html'
-            data_values = self._load_ctfvalues()
-
-        elif self.className == 'autopick':
-            summary['template'] = 'processing_picking_summary.html'
-            data_values = {
-                'numberOfParticles': {
-                    'label': 'Particles',
-                    'color': '#852999',
-                    'data': []
-                },
-                'averageFOM': {
-                    'data': []
-                }
-            }
-            # TODO: Write this info in a star file to avoid recalculating all the time
-            pts = data_values['numberOfParticles']['data']
-            fom = data_values['averageFOM']['data']
-            with StarFile(self.join('autopick.star')) as sfCoords:
-                for row1 in sfCoords.iterTable('coordinate_files'):
-                    n = 0
-                    fomSum = 0
-                    with StarFile(self.project.join(row1.rlnMicrographCoordinates)) as sf:
-                        for row in sf.iterTable(''):
-                            n += 1
-                            fomSum += row.rlnAutopickFigureOfMerit
-                    pts.append(n)
-                    fom.append(fomSum / n)
-
-        elif self.className == 'class2d':
-            summary['template'] = 'processing_2d_summary.html'
-            data_values = {'iterations': [1, 2, 3]}
-
-        elif self.className == 'class3d':
-            summary['template'] = 'processing_3d_volumes.html'
-            ios = self.getInputsOutputs()
-            data_values = {
-                'volumes': [os.path.basename(o) for o in ios['outputs'] if o.endswith('.mrc')]
-            }
-
-        if data_values:
-            summary['data'] = {'data_values': data_values}
-
-        return summary
-
-    def getOverview(self):
-        """
-
-        """
-        overview = {'template': '', 'data': {}}
-        data_values = None
-
-        if self.className == 'ctffind':
-            overview['template'] = 'processing_ctf_overview.html'
-            data_values = self._load_ctfvalues(index=True)
-
-        if data_values:
-            overview['data'] = {'data_values': data_values}
-
-        return overview
-
-    def _load_micrograph_data(self, micId, micsStar):
-        with StarFile(micsStar) as sf:
-            otable = sf.getTable('optics')
-            row = sf.getTableRow('micrographs', micId - 1)
-            micThumb = Thumbnail.Micrograph()
-            psdThumb = Thumbnail.Psd()
-            micFn = self.project.join(row.rlnMicrographName)
-            micThumbBase64 = micThumb.from_mrc(micFn)
-            psdFn = self.project.join(row.rlnCtfImage).replace(":mrc", "")
-            pixelSize = otable[0].rlnMicrographPixelSize
-            ctfProfile = psdFn.replace('.ctf', '_avrot.txt')
-
-            if os.path.exists(ctfProfile):
-                with open(ctfProfile) as f:
-                    ctfPlot = [line.split() for line in f
-                               if not line.startswith('#')]
-            else:
-                ctfPlot = []
-
-            print(f"PSD: {row.rlnMicrographName}")
-
-            return {
-                'micThumbData': micThumbBase64,
-                'psdData': psdThumb.from_mrc(psdFn),
-                'ctfDefocusU': round(row.rlnDefocusU / 10000., 2),
-                'ctfDefocusV': round(row.rlnDefocusV / 10000., 2),
-                'ctfDefocusAngle': round(row.rlnDefocusAngle, 2),
-                'ctfAstigmatism': round(row.rlnCtfAstigmatism / 10000, 2),
-                'ctfResolution': round(row.rlnCtfMaxResolution, 2),
-                'coordinates': [],  # Check for picking
-                'micThumbPixelSize': pixelSize * micThumb.scale,
-                'pixelSize': pixelSize,
-                'gridSquare': '',
-                'foilHole': '',
-                'ctfPlot': ctfPlot
-            }
-
-    def get_micrograph_data(self, micId):
-        data = {}
-        micsStar = None
-
-        if self.className == 'ctffind':
-            data = self._load_micrograph_data(micId, self.join('micrographs_ctf.star'))
-        elif self.className == 'autopick':
-            for i in self.getInputsOutputs()['inputs']:
-                if i.endswith('micrographs_ctf.star'):
-                    data = self._load_micrograph_data(micId, self.project.join(i))
-                    data['coordinates'] = self.project.get_micrograph_coordinates(
-                        self.join('autopick.star'), micId)
-                    break
-
-        return data
-
-    def get_classes2d(self, iteration=None):
-        """ Get classes information from a class 2d run. """
-        items = []
-
-        if iteration:
-            avgMrcs = self.join('*_it%03d_classes.mrcs' % iteration)
-        else:
-            avgMrcs = self.join('*_it*_classes.mrcs')
-
-        if files := glob(avgMrcs):
-            files.sort()
-            avgMrcs = files[-1]
-            dataStar = avgMrcs.replace('_classes.mrcs', '_data.star')
-            modelStar = dataStar.replace('_data.', '_model.')
-
-            mrc_stack = None
-            avgThumb = Thumbnail(max_size=(100, 100),
-                                 output_format='base64')
-
-            with StarFile(dataStar) as sf:
-                n = sf.getTableSize('particles')
-
-            with StarFile(modelStar) as sf:
-                modelTable = sf.getTable('model_classes', guessType=False)
-
-                for row in modelTable:
-                    i, fn = row.rlnReferenceImage.split('@')
-                    if not mrc_stack:
-                        mrc_stack = mrcfile.open(avgMrcs, permissive=True)
-                    items.append({
-                        'id': '%03d' % int(i),
-                        'size': round(float(row.rlnClassDistribution) * n),
-                        'average': avgThumb.from_array(mrc_stack.data[int(i) - 1, :, :])
-                    })
-            items.sort(key=lambda c: c['size'], reverse=True)
-            mrc_stack.close()
-
-        return items
-
-    def get_volume_data(self, volName, **kwargs):
-        volPath = self.join(volName)
-
-        if not os.path.exists(volPath):
-            raise Exception("Volume path %s does not exists." % volPath)
-
-        data = {}
-        volume_data = kwargs.get('volume_data', 'info')
-
-        mrc = mrcfile.open(volPath, permissive=True)
-        zdim, ydim, xdim = mrc.data.shape
-        data['path'] = volPath
-        data['dimensions'] = [xdim, ydim, zdim]
-
-        if volume_data == "info":
-            return data
-        elif volume_data == "slices":
-            axis = kwargs.get('axis', 'z')
-
-            thumbSize = 128
-            volThumb = Thumbnail(max_size=(thumbSize, thumbSize),
-                                 output_format='base64',
-                                 min_max=(mrc.data.min(), mrc.data.max()))
-            slices = {}
-            idx = np.round(np.linspace(0, xdim - 1, min(xdim, thumbSize))).astype(int)
-
-            print('idx', idx)
-
-            if axis == 'x':
-                for i in idx:
-                    slices = {int(i): volThumb.from_array(mrc.data[:, :, i])}
-            elif axis == 'y':
-                for i in idx:
-                    slices = {i: volThumb.from_array(mrc.data[:, i, :])}
-            else:
-                for i in idx:
-                    slices = {int(i): volThumb.from_array(mrc.data[i, :, :])}
-
-            data.update({
-                'slices': slices,
-                'axis': axis
-            })
-        elif volume_data == 'array':
-            iMax = mrc.data.max()  # min(imean + 10 * isd, imageArray.max())
-            iMin = mrc.data.min()  # max(imean - 10 * isd, imageArray.min())
-            im255 = ((mrc.data - iMin) / (iMax - iMin) * 255).astype(np.uint8)
-            data['array'] = base64.b64encode(im255).decode("utf-8")
-        else:
-            raise Exception('Unknown volume_data value: %s' % volume_data)
-
-        return data
-
-
-class RelionSessionData(SessionData):
-    """
-    Adapter class for reading Session data from Relion OTF
-    """
-    def get_stats(self):
-
-        def _stats_from_star(jobType, starFn, tableName, attribute):
-            fn = self.get_last_star(jobType, starFn)
-            if not fn or not os.path.exists(fn):
-                return {'count': 0}
-            with StarFile(fn) as sf:
-                if attribute == 'count':
-                    return {'count': sf.getTableSize(tableName)}
-                t = sf.getTable(tableName)
-                firstRow, lastRow = t[0], t[-1]
-                try:
-                    first = self.mtime(getattr(firstRow, attribute))
-                    last = self.mtime(getattr(lastRow, attribute))
-                    h = hours(first, last)
-                except:
-                    first = last = h = 0
-
-                return {
-                    'hours': h,
-                    'count': t.size(),
-                    'first': first,
-                    'last': last,
-                }
-
-        moviesStar = self.get_last_star('Import', 'movies.star')
-        if not moviesStar:
-            return {'movies': {'count': 0}, 'ctfs': {'count': 0}}
-
-        countEpu = 0
-
-        if epuData := self.getEpuData():
-            moviesTable = epuData.moviesTable
-            first = moviesTable[0].timeStamp
-            last = moviesTable[-1].timeStamp
-            countEpu = moviesTable.size()
-            msEpu = {
-                'count': countEpu, 'first': first, 'last': last,
-                'hours': hours(first, last)
-            }
-
-        msImport = _stats_from_star('Import', 'movies.star',
-                                    'movies', 'rlnMicrographMovieName')
-        movieStats = msEpu if countEpu > msImport['count'] else msImport
-
-        return {
-            'movies': movieStats,
-            'ctfs': _stats_from_star('CtfFind', 'micrographs_ctf.star',
-                                     'micrographs', 'rlnMicrographName'),
-            'classes2d': len(self.get_classes2d_runs()),
-            'coordinates': {'count': 0}  # FIXME if there are picking jobs or from extraction
-        }
-
-        t.toc()
-
-    def get_micrographs(self):
-        """ Return an iterator over the micrographs' CTF information. """
-        micFn = self._last_micFn()
-
-        if not micFn:
-            return []
-
-        with StarFile(micFn) as sf:
-            for row in sf.iterTable('micrographs'):
-                micData = {
-                    'micrograph': row.rlnMicrographName,
-                    'ctfImage': row.rlnCtfImage,
-                    'ctfDefocus': row.rlnDefocusU,
-                    'ctfResolution': min(row.rlnCtfMaxResolution, 10),
-                    'ctfDefocusAngle': row.rlnDefocusAngle,
-                    'ctfAstigmatism': row.rlnCtfAstigmatism
-                }
-                yield micData
-
-    def get_micrograph_data(self, micId):
-        micFn = self._last_micFn()
-        data = {}
-        if micFn:
-            with StarFile(micFn) as sf:
-                otable = sf.getTable('optics')
-                row = sf.getTableRow('micrographs', micId - 1)
-                micThumb = Thumbnail.Micrograph()
-                psdThumb = Thumbnail.Psd()
-                micFn = self.join(row.rlnMicrographName)
-                micThumbBase64 = micThumb.from_mrc(micFn)
-                psdFn = self.join(row.rlnCtfImage).replace(':mrc', '')
-                pixelSize = otable[0].rlnMicrographPixelSize
-
-                loc = EPU.get_movie_location(micFn)
-
-                if pickStar := self.get_last_star('*Pick', '*pick.star'):
-                    coords = self.get_micrograph_coordinates(pickStar, micId)
-                else:
-                    coords = []
-
-                data = {
-                    'micThumbData': micThumbBase64,
-                    'psdData': psdThumb.from_mrc(psdFn),
-                    # 'shiftPlotData': None,
-                    'ctfDefocusU': round(row.rlnDefocusU/10000., 2),
-                    'ctfDefocusV': round(row.rlnDefocusV/10000., 2),
-                    'ctfDefocusAngle': round(row.rlnDefocusAngle, 2),
-                    'ctfAstigmatism': round(row.rlnCtfAstigmatism/10000, 2),
-                    'ctfResolution': round(row.rlnCtfMaxResolution, 2),
-                    'coordinates': coords,
-                    'micThumbPixelSize': pixelSize * micThumb.scale,
-                    'pixelSize': pixelSize,
-                    'gridSquare': loc['gs'],
-                    'foilHole': loc['fh']
-                }
-        return data
-
-    def get_workflow(self):
-        protList = []
-        protDict = {}
-        status_map = {
-            'Succeeded': 'finished',
-            'Running': 'running',
-            'Aborted': 'aborted',
-            'Failed': 'failed'
-        }
-        pipelineStar = self.join('default_pipeline.star')
-        outputs = {}
-
-        with StarFile(pipelineStar) as sf:
-            for row in sf.iterTable('pipeline_processes'):
-                name = row.rlnPipeLineProcessName
-                alias = row.rlnPipeLineProcessAlias
-                status = status_map.get(row.rlnPipeLineProcessStatusLabel, 'unknown')
-                prot = {
-                    'id': name,
-                    'label': alias if alias != 'None' else name,
-                    'links': [],
-                    'status': status,
-                    'type': row.rlnPipeLineProcessTypeLabel
-                }
-                protList.append(prot)
-                protDict[name] = prot
-
-            for row in sf.iterTable('pipeline_output_edges'):
-                outputs[row.rlnPipeLineEdgeToNode] = row.rlnPipeLineEdgeProcess
-
-            for row in sf.iterTable('pipeline_input_edges'):
-                node = row.rlnPipeLineEdgeFromNode
-                if output := outputs.get(node, None):
-                    procName = row.rlnPipeLineEdgeProcess
-                    childProt = protDict[procName]
-                    parentProt = protDict[output]
-                    if childProt not in parentProt['links']:
-                        parentProt['links'].append(procName)
-
-        return protList
-
-    def get_run(self, runId):
-        return RelionRun(self, self.join(runId))
-
-    def get_classes2d_runs(self):
-        return [r.replace(self._path, '')[1:] for r in self._jobs('Class2D')]
-
-    def get_classes2d_from_run(self, runId=None):
-        """ Iterate over 2D classes. """
-        runs2d = self.get_classes2d_runs()
-        return {
-            'runs': [{'id': i, 'label': r} for i, r in enumerate(runs2d)],
-            'items': [] if runId is None else self.get_classes2d(self.join(runs2d[runId])),
-            'selection': []
-        }
-
-    def get_coords_from_star(self, starFn):
-        """ Return x,y coordinates from a given star file,
-        from the root of the project. """
-        coords = []
-        starPath = self.join(starFn)
-
-        with StarFile(self.join(starPath)) as sf:
-            for row in sf.iterTable(''):
-                coords.append((round(row.rlnCoordinateX),
-                               round(row.rlnCoordinateY)))
-        return coords
-
-    def get_micrograph_coordinates(self, pickStar, micId):
-        with StarFile(self.join(pickStar)) as sf:
-            for i, row in enumerate(sf.iterTable('coordinate_files')):
-                if i == micId:
-                    return self.get_coords_from_star(row.rlnMicrographCoordinates)
-        return []
-
-    # ----------------------- UTILS ---------------------------
-    def _jobs(self, jobType):
-        jobs = glob(self.join(jobType, 'job*'))
-
-        def _jobNumber(j):
-            return int(j.split('/job')[1])
-
-        jobs.sort(key=_jobNumber)
-
-        return jobs
-
-    def get_last_star(self, jobType, starFn):
-        jobDirs = self._jobs(jobType)
-        if not jobDirs:
-            return None
-        fn = self.join(jobDirs[-1], starFn)
-        if '*' in starFn:  # it is a glob pattern, let's find the last file
-            files = glob(fn)
-            files.sort()
-            fn = files[-1] if files else None
-        return fn
-
-    def _last_micFn(self):
-        """ Return micrographs star file from the last CTF job or None
-        if there is no run yet or output file.
-        """
-        jobs = self._jobs('CtfFind')
-        if jobs:
-            micFn = self.join(jobs[-1], 'micrographs_ctf.star')
-            if os.path.exists(micFn):
-                return micFn
-        return None
diff --git a/emhub/data/processing/processing_relion/__main__.py b/emhub/data/processing/processing_relion/__main__.py
new file mode 100644
index 00000000..5c4eac96
--- /dev/null
+++ b/emhub/data/processing/processing_relion/__main__.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+# **************************************************************************
+# *
+# * Authors:     J.M. de la Rosa Trevin (delarosatrevin@gmail.com)
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 3 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# **************************************************************************
+
+import os
+import sys
+import argparse
+import json
+
+from emtools.utils import Process, Color
+from emhub.data import DataManager
+from emhub.data.imports import MINIMAL_JSON, create_instance
+
+cwd = os.getcwd()
+
+
+def main():
+    p = argparse.ArgumentParser(prog='emh-relion')
+    p.add_argument('--port', '-p', type=int, default=5000,
+                   help="Port where to run Flask debug server. "
+                        "By default it will be 5000")
+    args = p.parse_args()
+
+    instance_path = os.path.join(cwd, '.emhub_instance')
+
+    if not os.path.exists(instance_path):
+        print(Color.bold("Creating EMhub instance at: "))
+        print(Color.warn("    " + instance_path))
+
+        dm = create_instance(instance_path, MINIMAL_JSON, False)
+
+        with open(os.path.join(instance_path, 'config.py'), 'w') as config:
+            config.write(f'PROCESSING_PROJECT = "{cwd}"\n')
+
+        project = dm.get_projects()[0]  # get first project
+        dm.create_entry(project_id=project.id,
+                        type='data_processing',
+                        extra={"data": {"project_path": cwd}})
+
+    else:
+        print(Color.bold("Loading EMhub instance from: "))
+        print(Color.green("    " + instance_path))
+
+    Process.system(f"source {instance_path}/bashrc && "
+                   f"flask run --debug --port {args.port}")
+
+
+if __name__ == '__main__':
+    main()
diff --git a/emhub/data/processing/processing_relion/project.py b/emhub/data/processing/processing_relion/project.py
new file mode 100644
index 00000000..812c24d7
--- /dev/null
+++ b/emhub/data/processing/processing_relion/project.py
@@ -0,0 +1,486 @@
+# **************************************************************************
+# *
+# * Authors:     J.M. de la Rosa Trevin (delarosatrevin@gmail.com)
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 3 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# **************************************************************************
+
+import os
+from glob import glob
+import numpy as np
+import base64
+
+import mrcfile
+from emtools.utils import Path, Timer, Pretty
+from emtools.metadata import StarFile, EPU, SqliteFile
+from emtools.image import Thumbnail
+
+from ..base import SessionRun, SessionData, hours
+from .runs import RelionRun
+
+location = os.path.dirname(__file__)
+
+
+class RelionSessionData(SessionData):
+    """
+    Adapter class for reading Session data from Relion OTF
+    """
+    def get_stats(self):
+
+        def _stats_from_star(jobType, starFn, tableName, attribute):
+            fn = self.get_last_star(jobType, starFn)
+            if not fn or not os.path.exists(fn):
+                return {'count': 0}
+            with StarFile(fn) as sf:
+                if attribute == 'count':
+                    return {'count': sf.getTableSize(tableName)}
+                t = sf.getTable(tableName)
+                firstRow, lastRow = t[0], t[-1]
+                try:
+                    first = self.mtime(getattr(firstRow, attribute))
+                    last = self.mtime(getattr(lastRow, attribute))
+                    h = hours(first, last)
+                except:
+                    first = last = h = 0
+
+                return {
+                    'hours': h,
+                    'count': t.size(),
+                    'first': first,
+                    'last': last,
+                }
+
+        moviesStar = self.get_last_star('Import', 'movies.star')
+        if not moviesStar:
+            return {'movies': {'count': 0}, 'ctfs': {'count': 0}}
+
+        countEpu = 0
+
+        if epuData := self.getEpuData():
+            moviesTable = epuData.moviesTable
+            first = moviesTable[0].timeStamp
+            last = moviesTable[-1].timeStamp
+            countEpu = moviesTable.size()
+            msEpu = {
+                'count': countEpu, 'first': first, 'last': last,
+                'hours': hours(first, last)
+            }
+
+        msImport = _stats_from_star('Import', 'movies.star',
+                                    'movies', 'rlnMicrographMovieName')
+        movieStats = msEpu if countEpu > msImport['count'] else msImport
+
+        return {
+            'movies': movieStats,
+            'ctfs': _stats_from_star('CtfFind', 'micrographs_ctf.star',
+                                     'micrographs', 'rlnMicrographName'),
+            'classes2d': len(self.get_classes2d_runs()),
+            'coordinates': {'count': 0}  # FIXME if there are picking jobs or from extraction
+        }
+
+        t.toc()
+
+    def get_micrographs(self):
+        """ Return an iterator over the micrographs' CTF information. """
+        micFn = self._last_micFn()
+
+        if not micFn:
+            return []
+
+        with StarFile(micFn) as sf:
+            for row in sf.iterTable('micrographs'):
+                micData = {
+                    'micrograph': row.rlnMicrographName,
+                    'ctfImage': row.rlnCtfImage,
+                    'ctfDefocus': row.rlnDefocusU,
+                    'ctfResolution': min(row.rlnCtfMaxResolution, 10),
+                    'ctfDefocusAngle': row.rlnDefocusAngle,
+                    'ctfAstigmatism': row.rlnCtfAstigmatism
+                }
+                yield micData
+
+    def get_micrograph_data(self, micId):
+        micFn = self._last_micFn()
+        data = {}
+        if micFn:
+            with StarFile(micFn) as sf:
+                otable = sf.getTable('optics')
+                row = sf.getTableRow('micrographs', micId - 1)
+                micThumb = Thumbnail.Micrograph()
+                psdThumb = Thumbnail.Psd()
+                micFn = self.join(row.rlnMicrographName)
+                micThumbBase64 = micThumb.from_mrc(micFn)
+                psdFn = self.join(row.rlnCtfImage).replace(':mrc', '')
+                pixelSize = otable[0].rlnMicrographPixelSize
+
+                loc = EPU.get_movie_location(micFn)
+
+                if pickStar := self.get_last_star('*Pick', '*pick.star'):
+                    coords = self.get_micrograph_coordinates(pickStar, micId)
+                else:
+                    coords = []
+
+                data = {
+                    'micThumbData': micThumbBase64,
+                    'psdData': psdThumb.from_mrc(psdFn),
+                    # 'shiftPlotData': None,
+                    'ctfDefocusU': round(row.rlnDefocusU/10000., 2),
+                    'ctfDefocusV': round(row.rlnDefocusV/10000., 2),
+                    'ctfDefocusAngle': round(row.rlnDefocusAngle, 2),
+                    'ctfAstigmatism': round(row.rlnCtfAstigmatism/10000, 2),
+                    'ctfResolution': round(row.rlnCtfMaxResolution, 2),
+                    'coordinates': coords,
+                    'micThumbPixelSize': pixelSize * micThumb.scale,
+                    'pixelSize': pixelSize,
+                    'gridSquare': loc['gs'],
+                    'foilHole': loc['fh']
+                }
+        return data
+
+    def get_workflow(self):
+        protList = []
+        protDict = {}
+        status_map = {
+            'Succeeded': 'finished',
+            'Running': 'running',
+            'Aborted': 'aborted',
+            'Failed': 'failed'
+        }
+        pipelineStar = self.join('default_pipeline.star')
+        outputs = {}
+
+        with StarFile(pipelineStar) as sf:
+            for row in sf.iterTable('pipeline_processes'):
+                name = row.rlnPipeLineProcessName
+                alias = row.rlnPipeLineProcessAlias
+                status = status_map.get(row.rlnPipeLineProcessStatusLabel, 'unknown')
+                prot = {
+                    'id': name,
+                    'label': alias if alias != 'None' else name,
+                    'links': [],
+                    'status': status,
+                    'type': row.rlnPipeLineProcessTypeLabel
+                }
+                protList.append(prot)
+                protDict[name] = prot
+
+            for row in sf.iterTable('pipeline_output_edges'):
+                outputs[row.rlnPipeLineEdgeToNode] = row.rlnPipeLineEdgeProcess
+
+            for row in sf.iterTable('pipeline_input_edges'):
+                node = row.rlnPipeLineEdgeFromNode
+                if output := outputs.get(node, None):
+                    procName = row.rlnPipeLineEdgeProcess
+                    childProt = protDict[procName]
+                    parentProt = protDict[output]
+                    if childProt not in parentProt['links']:
+                        parentProt['links'].append(procName)
+
+        return protList
+
+    def get_run(self, runId):
+        print(">>> runId: ", runId)
+        return RelionRun(self, self.join(runId))
+
+    def get_classes2d_runs(self):
+        return [r.replace(self._path, '')[1:] for r in self._jobs('Class2D')]
+
+    def get_classes2d_from_run(self, runId=None):
+        """ Iterate over 2D classes. """
+        runs2d = self.get_classes2d_runs()
+        return {
+            'runs': [{'id': i, 'label': r} for i, r in enumerate(runs2d)],
+            'items': [] if runId is None else self.get_run(runId).get_classes2d(),
+            'selection': []
+        }
+
+    @classmethod
+    def get_classes2d_data(cls, pattern):
+        """ Get classes information from a relion *classes.mrcs files pattern. """
+        items = []
+        if files := glob(pattern):
+            files.sort()
+            avgMrcs = files[-1]
+            dataStar = avgMrcs.replace('_classes.mrcs', '_data.star')
+            modelStar = dataStar.replace('_data.', '_model.')
+            mrc_stack = None
+            avgThumb = Thumbnail(max_size=(128, 128), output_format='base64')
+
+            with StarFile(dataStar) as sf:
+                n = sf.getTableSize('particles')
+
+            with StarFile(modelStar) as sf:
+                modelTable = sf.getTable('model_classes', guessType=False)
+
+                for row in modelTable:
+                    i, fn = row.rlnReferenceImage.split('@')
+                    if not mrc_stack:
+                        mrc_stack = mrcfile.open(avgMrcs, permissive=True)
+                    items.append({
+                        'id': '%03d' % int(i),
+                        'size': round(float(row.rlnClassDistribution) * n),
+                        'average': avgThumb.from_array(mrc_stack.data[int(i) - 1, :, :])
+                    })
+            items.sort(key=lambda c: c['size'], reverse=True)
+            mrc_stack.close()
+
+        return items
+
+    def coords_from_row(self, row):
+        """ Iterate coordinates from a row containing the STAR file
+         path with the coordinates. """
+        coordStar = self.join(row.rlnMicrographCoordinates)
+        with StarFile(coordStar) as sf:
+            for c in sf.iterTable(''):
+                yield c
+
+    def load_micrograph_data(self, micId, micsStar):
+        with StarFile(micsStar) as sf:
+            otable = sf.getTable('optics')
+            row = sf.getTableRow('micrographs', micId - 1)
+            micThumb = Thumbnail.Micrograph()
+            micFn = self.join(row.rlnMicrographName)
+            micThumbBase64 = micThumb.from_mrc(micFn)
+            pixelSize = otable[0].rlnMicrographPixelSize
+
+            micData = {
+                'micThumbData': micThumbBase64,
+                'coordinates': [],  # Check for picking
+                'micThumbPixelSize': pixelSize * micThumb.scale,
+                'pixelSize': pixelSize,
+                'gridSquare': '',
+                'foilHole': ''
+            }
+
+            if hasattr(row, 'rlnCtfImage'):
+                psdThumb = Thumbnail.Psd()
+                psdFn = self.join(row.rlnCtfImage).replace(":mrc", "")
+                ctfProfile = psdFn.replace('.ctf', '_avrot.txt')
+
+                if os.path.exists(ctfProfile) and ctfProfile.endswith('_avrot.txt'):
+                    with open(ctfProfile) as f:
+                        ctfPlot = [line.split() for line in f
+                                   if not line.startswith('#')]
+                else:
+                    ctfPlot = []
+
+                micData.update({
+                    'psdData': psdThumb.from_mrc(psdFn),
+                    'ctfDefocusU': round(row.rlnDefocusU / 10000., 2),
+                    'ctfDefocusV': round(row.rlnDefocusV / 10000., 2),
+                    'ctfDefocusAngle': round(row.rlnDefocusAngle, 2),
+                    'ctfAstigmatism': round(row.rlnCtfAstigmatism / 10000, 2),
+                    'ctfResolution': round(row.rlnCtfMaxResolution, 2),
+                    'ctfPlot': ctfPlot
+                })
+
+            return micData
+
+    def get_micrograph_coordinates(self, pickStar, micId):
+        with StarFile(self.join(pickStar)) as sf:
+            for i, row in enumerate(sf.iterTable('coordinate_files')):
+                if i == micId - 1:
+                    return [(round(c.rlnCoordinateX), round(c.rlnCoordinateY))
+                            for c in self.coords_from_row(row)]
+        return []
+
+    def load_coordinates_values(self, coordStar, index=False):
+        data_values = {
+            'numberOfParticles': {
+                'label': 'Particles',
+                'color': '#852999',
+                'data': []
+            },
+            'averageFOM': {
+                'data': []
+            },
+            'default_y': 'numberOfParticles',
+            'default_color': 'averageFOM',
+            'has_particles': True
+        }
+        # TODO: Write this info in a star file to avoid recalculating all the time
+        pts = data_values['numberOfParticles']['data']
+        fom = data_values['averageFOM']['data']
+        indexes = []
+
+        with StarFile(coordStar) as sf:
+            for i, row in enumerate(sf.iterTable('coordinate_files')):
+                indexes.append(i + 1)
+                n = 0
+                fomSum = 0
+                for c in self.coords_from_row(row):
+                    n += 1
+                    fomSum += c.rlnAutopickFigureOfMerit
+                pts.append(n)
+                fom.append(fomSum / n)
+
+        if index:
+            data_values.update({
+                'index': {'label': 'Index', 'data': indexes},
+                'default_x': 'index'
+            })
+
+        return data_values
+
+    def load_ctf_values(self, ctfStar, index=False):
+        possible_labels = {
+            'rlnDefocusU': {
+                'label': 'Defocus U',
+                'scale': 0.0001,
+                'unit': 'µm',
+                'color': '#852999',
+                'maxX': 4,
+                'data': []
+            },
+            'rlnDefocusV': {
+                'label': 'Defocus V',
+                'scale': 0.0001,
+                'unit': 'µm',
+                'color': '#852999',
+                'maxX': 4,
+                'data': []
+            },
+            'rlnCtfFigureOfMerit': {
+                'data': []
+            },
+            'rlnCtfMaxResolution': {
+                'color': '#EF9A53',
+                'label': 'Resolution',
+                'unit': 'Å',
+                'data': []
+            },
+            'rlnCtfIceRingDensity': {
+              'label': 'Ice Ring Density',
+              'data': []
+            }}
+        data_values = {
+            'default_y': 'rlnCtfMaxResolution',
+            'default_x': '',
+            'has_ctf': True
+        }
+        indexes = []
+        with StarFile(ctfStar) as sf:
+            ti = sf.getTableInfo('micrographs')
+            for k, v in possible_labels.items():
+                if ti.hasColumn(k):
+                    data_values[k] = v
+
+            for i, row in enumerate(sf.iterTable('micrographs')):
+                indexes.append(i + 1)
+                rowDict = row._asdict()
+                for k, v in data_values.items():
+                    if isinstance(v, dict):
+                        scale = v.get('scale', 1)
+                        v['data'].append(rowDict[k] * scale)
+        if index:
+            data_values.update({
+                'index': {'label': 'Index', 'data': indexes},
+                'default_x': 'index'
+            })
+
+        return data_values
+
+    def get_volume_data(self, volName, **kwargs):
+        volPath = self.join(volName)
+
+        if not os.path.exists(volPath):
+            raise Exception("Volume path %s does not exists." % volPath)
+
+        data = {}
+        volume_data = kwargs.get('volume_data', 'info')
+
+        mrc = mrcfile.open(volPath, permissive=True)
+        zdim, ydim, xdim = mrc.data.shape
+        data['path'] = volPath
+        data['dimensions'] = [xdim, ydim, zdim]
+
+        if volume_data == "info":
+            return data
+
+        if "slices" in volume_data:
+            axis = kwargs.get('axis', 'z')
+
+            slice_dim = kwargs.get('slice_dim', 128)
+            volThumb = Thumbnail(max_size=(slice_dim, slice_dim),
+                                 output_format='base64',
+                                 min_max=(mrc.data.min(), mrc.data.max()))
+            if slice_number := kwargs.get('slice_number', None):
+                # Do not take slices from star/end since they are usually empty
+                n4 = np.round(xdim / 4)
+                idx = np.round(np.linspace(n4, xdim - n4, slice_number)).astype(int)
+                pass
+            else:
+                slice_number = min(xdim, slice_dim)
+                idx = np.round(np.linspace(0, xdim - 1, slice_number)).astype(int)
+
+            slices = {}
+
+            if 'x' in axis:
+                slices['x'] = {int(i): volThumb.from_array(mrc.data[:, :, i])
+                               for i in idx}
+            if 'y' in axis:
+                slices['y'] = {int(i): volThumb.from_array(mrc.data[:, i, :])
+                               for i in idx}
+
+            if 'z' in axis:
+                slices['z'] = {int(i): volThumb.from_array(mrc.data[i, :, :])
+                               for i in idx}
+
+            data.update({
+                'slices': slices,
+                'axis': axis
+            })
+
+        if 'array' in volume_data:
+            iMax = mrc.data.max()  # min(imean + 10 * isd, imageArray.max())
+            iMin = mrc.data.min()  # max(imean - 10 * isd, imageArray.min())
+            im255 = ((mrc.data - iMin) / (iMax - iMin) * 255).astype(np.uint8)
+            data['array'] = base64.b64encode(im255).decode("utf-8")
+
+        #
+        # else:
+        #     raise Exception('Unknown volume_data value: %s' % volume_data)
+
+        return data
+
+    # ----------------------- UTILS ---------------------------
+    def _jobs(self, jobType):
+        jobs = glob(self.join(jobType, 'job*'))
+
+        def _jobNumber(j):
+            return int(j.split('/job')[1])
+
+        jobs.sort(key=_jobNumber)
+
+        return jobs
+
+    def get_last_star(self, jobType, starFn):
+        jobDirs = self._jobs(jobType)
+        if not jobDirs:
+            return None
+        fn = self.join(jobDirs[-1], starFn)
+        if '*' in starFn:  # it is a glob pattern, let's find the last file
+            files = glob(fn)
+            files.sort()
+            fn = files[-1] if files else None
+        return fn
+
+    def _last_micFn(self):
+        """ Return micrographs star file from the last CTF job or None
+        if there is no run yet or output file.
+        """
+        jobs = self._jobs('CtfFind')
+        if jobs:
+            micFn = self.join(jobs[-1], 'micrographs_ctf.star')
+            if os.path.exists(micFn):
+                return micFn
+        return None
diff --git a/emhub/data/processing/processing_relion/runs.py b/emhub/data/processing/processing_relion/runs.py
new file mode 100644
index 00000000..6b7d5689
--- /dev/null
+++ b/emhub/data/processing/processing_relion/runs.py
@@ -0,0 +1,370 @@
+# **************************************************************************
+# *
+# * Authors:     J.M. de la Rosa Trevin (delarosatrevin@gmail.com)
+# *
+# * This program is free software; you can redistribute it and/or modify
+# * it under the terms of the GNU General Public License as published by
+# * the Free Software Foundation; either version 3 of the License, or
+# * (at your option) any later version.
+# *
+# * This program is distributed in the hope that it will be useful,
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# * GNU General Public License for more details.
+# *
+# **************************************************************************
+
+import os
+import datetime as dt
+from glob import glob
+from collections import defaultdict
+import json
+import flask
+import numpy as np
+from flask import current_app as app
+
+import mrcfile
+from emtools.utils import Path, Timer, Pretty
+from emtools.metadata import StarFile, EPU, SqliteFile
+from emtools.image import Thumbnail
+
+from ..base import SessionRun, SessionData, hours
+
+location = os.path.dirname(__file__)
+
+
+class RelionRun(SessionRun):
+    """ Helper class to manipulate Relion run data. """
+    def __init__(self, project, path):
+        SessionRun.__init__(self, project, path)
+        d, self.id = os.path.split(Path.rmslash(path))
+        with StarFile(self.join('job.star')) as sf:
+            self.job = sf.getTable('job')
+            self.values = {row.rlnJobOptionVariable: row.rlnJobOptionValue
+                           for row in sf.iterTable('joboptions_values', guessType=False)}
+        parts = self.job[0].rlnJobTypeLabel.split('.')
+        self.package = parts[0]
+        self.className = parts[1]
+        self.classSuffix = '' if len(parts) < 3 else '.'.join(parts[2:])
+
+        with StarFile(self.join('job_pipeline.star')) as sf:
+            t = sf.getTable('pipeline_processes')
+            row = t[0]
+            self.name = row.rlnPipeLineProcessName
+            self.alias = row.rlnPipeLineProcessAlias
+
+    def getInfo(self):
+        return {'id': self.id, 'className': self.className, 'label': self.id,
+                'name': self.name, 'alias': self.alias}
+
+    def getFormDefinition(self):
+        default = {'valueClass': 'String',
+                   'paramClass': 'StringParam',
+                   'important': False,
+                   'expert': False
+                   }
+
+        formDef = {
+            'package': 'relion',
+            'name': self.className,
+            'logo': '',
+            'sections': []
+        }
+
+        allParams = set()
+
+        def _set_defaults(paramDef):
+            for k, v in default.items():
+                if k not in paramDef:
+                    paramDef[k] = v
+            return paramDef
+
+        config = f"{self.package}.{self.className}.json"
+        configFn = os.path.join(location, 'forms', config)
+        print("Config file", configFn)
+
+        if os.path.exists(configFn):
+            with open(configFn) as f:
+                formConf = json.load(f)
+                for sectionDef in formConf['sections']:
+                    for paramDef in sectionDef['params']:
+                        if 'name' in paramDef:
+                            _set_defaults(paramDef)
+                            allParams.add(paramDef['name'])
+                    formDef['sections'].append(sectionDef)
+
+        extraParams = []
+        for k, v in self.values.items():
+            if k not in allParams:
+                extraParams.append(_set_defaults({'label': k, 'name': k}))
+
+        if extraParams:
+            formDef['sections'].append({'label': 'extra params',
+                                        'params': extraParams})
+
+        return formDef
+
+    def getValues(self):
+        return self.values
+
+    def getStdOut(self):
+        """ Return run's stdout file. """
+        return self.join('run.out')
+
+    def getStdError(self):
+        """ Return run's stdout file. """
+        return self.join('run.err')
+
+    def getInputsOutputs(self):
+        if self._ios is None:
+            ios = {'inputs': [], 'outputs': []}
+
+            jobJson = self.join('job.json')
+            jobPipeline = self.join('job_pipeline.star')
+
+            if os.path.exists(jobJson):
+                with open(jobJson) as f:
+                    job = json.load(f)
+                    ios.update(job)
+            elif os.path.exists(jobPipeline):
+                with StarFile(jobPipeline) as sf:
+                    tables = sf.getTableNames()
+                    if 'pipeline_input_edges' in tables:
+                        inputsTable = sf.getTable('pipeline_input_edges')
+                        ios['inputs'] = [row.rlnPipeLineEdgeFromNode for row in inputsTable]
+                    if 'pipeline_output_edges' in tables:
+                        outputsTable = sf.getTable('pipeline_output_edges')
+                        ios['outputs'] = [row.rlnPipeLineEdgeToNode for row in outputsTable]
+            self._ios = ios
+
+        return self._ios
+
+    def _getOutputMics(self):
+        """ Find if there are micrographs (with/without CTF) in outputs. """
+        mics = micsCtf = None
+        for o in self.getInputsOutputs()['outputs']:
+            if o.endswith('ctf.star'):
+                micsCtf = o
+            elif o.endswith('micrographs.star'):
+                mics = o
+        return mics, micsCtf
+
+    def getSummary(self, **kwargs):
+        """ Function to return the template and data used for this run summary.
+        """
+        summary = {'template': '', 'data': {}}
+        data_values = None
+        mics, micsCtf = self._getOutputMics()
+
+        #if self.className == 'motioncorr':
+        if mics is not None:
+            summary['template'] = 'processing_ctf_summary.html'
+            data_values = {}
+            columns = ['rlnAccumMotionTotal', 'rlnAccumMotionEarly', 'rlnAccumMotionLate']
+            for col in columns:
+                data_values[col] = {
+                    'label': col,
+                    'color': '#852999',
+                    'data': []
+                }
+
+            #mics = self.join('corrected_micrographs.star')
+            with StarFile(mics) as sf:
+                for row in sf.iterTable('micrographs'):
+                    print(row, type(row))
+
+                    for col in columns:
+                        d = row._asdict()
+                        data_values[col]['data'].append(d[col])
+
+        #elif self.className == 'ctffind':
+        elif micsCtf is not None:
+            summary['template'] = 'processing_ctf_summary.html'
+            #ctfStar = self.join('micrographs_ctf.star')
+            #data_values = self.project.load_ctf_values(ctfStar)
+            data_values = self.project.load_ctf_values(micsCtf)
+
+        elif self.className in ['autopick', 'manualpick']:
+            summary['template'] = 'processing_picking_summary.html'
+            coordStar = self.join(f'{self.className}.star')
+            data_values = self.project.load_coordinates_values(coordStar)
+
+        elif self.className == 'class2d':
+            summary['template'] = 'processing_2d_summary.html'
+            data_values = {'iterations': [1, 2, 3]}
+
+        elif self.className == 'class3d':
+            volumes = []
+            # Load completed iterations base on _it*_model.star
+            iterations = glob(self.join('*_it*_model.star'))
+            iterations.sort(reverse=True)
+
+            data = {'volumes': volumes, 'iterations': iterations}
+            summary = {
+                'template': 'processing_3d_summary.html',
+                'data': data
+            }
+
+            iterFile = kwargs.get('iter_file', iterations[0] if iterations else None)
+            if iterFile:
+                data['iter_file'] = iterFile
+                ios = self.getInputsOutputs()
+                inputParticles = [i for i in ios['inputs'] if i.endswith('.star')][0]
+                with StarFile(inputParticles) as sf:
+                    nParticles = sf.getTableSize('particles')
+
+                with StarFile(iterFile) as sf:
+                    for row in sf.iterTable('model_classes'):
+                        volPath = row.rlnReferenceImage
+                        if self.project.exists(volPath):
+                            # TODO: Load other volumes when more than one class
+                            # or when the job is still running
+                            data = self.project.get_volume_data(volPath,
+                                                                volume_data='slices',
+                                                                axis='zyx',
+                                                                slice_dim=64,
+                                                                slice_number=32)
+                            volumes.append({
+                                'file_path': volPath,
+                                'slices': data['slices'],
+                                'slice_dim': 64,
+                                'data': {'%': '%0.2f' % (row.rlnClassDistribution * 100),
+                                         'Particles': round(nParticles * row.rlnClassDistribution)
+                                         }
+                            })
+
+        elif self.className == 'initialmodel':
+            volumes = []
+            summary = {
+                'template': 'processing_volume_summary.html',
+                'data': {'volumes': volumes}
+            }
+            volPath = self.join('initial_model.mrc')
+            if os.path.exists(volPath):
+                #TODO: Load other volumes when more than one class
+                # or when the job is still running
+                data = self.project.get_volume_data(volPath,
+                                                    volume_data='slices',
+                                                    axis='zyx',
+                                                    slice_dim=128,
+                                                    slice_number=32)
+                volumes.append({
+                    'file_path': self.project.relpath(volPath),
+                    'slices': data['slices'],
+                    'slice_dim': 128
+                })
+
+        if data_values:
+            summary['data'] = {'data_values': data_values}
+
+        return summary
+
+    def getOverview(self, **kwargs):
+        if 'file_path' in kwargs:
+            return self.getFileInfo(**kwargs)
+
+        overview = {'template': '', 'data': {}}
+        data_values = None
+        mics, micsCtf = self._getOutputMics()
+
+        #if self.className == 'ctffind':
+        if micsCtf is not None:
+            overview['template'] = 'processing_ctf_overview.html'
+            data_values = self.project.load_ctf_values(micsCtf, index=True)
+        elif self.className in ['autopick', 'manualpick']:
+            overview['template'] = 'processing_ctf_overview.html'
+            coordStar = self.join(f'{self.className}.star')
+            data_values = self.project.load_coordinates_values(coordStar, index=True)
+
+        if data_values:
+            overview['data'] = {'data_values': data_values}
+
+        return overview
+
+    def getFileInfo(self, **kwargs):
+        path = kwargs['file_path']
+        if path.endswith('.star'):
+            result = self._load_star_file(self.project.join(path), **kwargs)
+        elif path.endswith('.mrc'):
+            result = self._load_volume_file(self.project.join(path), **kwargs)
+        else:
+            raise Exception("File type not supported")
+
+        result['data']['file_path'] = path
+        return result
+
+    def _load_star_file(self, starFile, **kwargs):
+        result = {
+            'template': 'processing_star_overview.html',
+            'data': {}
+        }
+        data = result['data']  # shortcut
+
+        with StarFile(starFile) as sf:
+            if tn := kwargs.get('table_name', None):
+                # only load rows for this table, not the all tables info
+                ti = sf.getTableInfo(tn)
+                data.update({
+                    'default_table': tn,
+                    'columns': ti.getColumnNames(),
+                    'rows': [r._asdict() for r in sf.iterTable(tn, limit=10)]
+                })
+                result['template'] = 'processing_star_card.html'
+            else:
+                # load all tables info and default table rows
+                tables = {}
+                tableNames = sf.getTableNames()
+                defaultTable = kwargs.get('default_table', tableNames[0])
+                for tn in tableNames:
+                    ti = sf.getTableInfo(tn)
+                    tables[tn] = {
+                        'columns': ti.getColumnNames(),
+                        'rows': sf.getTableSize(tn)
+                    }
+                    if tn == defaultTable:
+                        data['default_table'] = defaultTable
+                        data['rows'] = [r._asdict() for r in sf.iterTable(tn, limit=10)]
+
+                    data['tables'] = tables
+
+        return result
+
+    def _load_volume_file(self, volumeFile, **kwargs):
+        result = {
+            'template': 'processing_volume_card.html',
+            'data': self.project.get_volume_data(volumeFile,
+                                                 volume_data='slices array',
+                                                 axis='zyx',
+                                                 slice_number=32)
+        }
+        return result
+
+    def get_micrograph_data(self, micId):
+        data = {}
+        mics, micsCtf = self._getOutputMics()
+
+        def _load_micrograph_data(micStar):
+            return self.project.load_micrograph_data(micId, micStar)
+
+        if micsCtf is not None:
+            data = _load_micrograph_data(micsCtf)
+        elif mics is not None:
+            data = _load_micrograph_data(mics)
+        elif self.className in ['autopick', 'manualpick']:
+            coordStar = self.join(f'{self.className}.star')
+            for i in self.getInputsOutputs()['inputs']:
+                iBase = os.path.basename(i)
+                if iBase.startswith('micrographs'):
+                    data = _load_micrograph_data(self.project.join(i))
+                    data['coordinates'] = self.project.get_micrograph_coordinates(
+                        coordStar, micId)
+                    break
+        return data
+
+    def get_classes2d(self, iteration=None):
+        """ Get classes information from a class 2d run. """
+        it = "%03d" % iteration if iteration else "*"
+        pattern = self.join(f"*_it{it}_classes.mrcs")
+        return self.project.get_classes2d_data(pattern)
+
+
diff --git a/emhub/data/processing/processing_scipion.py b/emhub/data/processing/processing_scipion.py
index 07a460e6..44dc973f 100644
--- a/emhub/data/processing/processing_scipion.py
+++ b/emhub/data/processing/processing_scipion.py
@@ -26,17 +26,21 @@
 from emtools.image import Thumbnail
 
 from .base import SessionRun, SessionData, hours
+from ..processing.processing_relion import RelionSessionData
 
 
 class ScipionRun(SessionRun):
     """ Helper class to manipulate Scipion run data. """
     def __init__(self, project, runDict):
         self.dict = runDict
+        if '' not in runDict:
+            raise Exception("Expecting empty string keyword in runDict.")
+
         runRow = runDict['']  # first protocol row, empty name
         self.id = runRow['id']
         self.className = runRow['classname']
         wdRow = runDict[f'{self.id}.workingDir']
-        self.label = runDict['']['label']
+        self.label = runRow['label']
 
         SessionRun.__init__(self, project, project.join(wdRow['value']))
 
@@ -77,28 +81,36 @@ def getStdError(self):
         """ Return run's stdout file. """
         return self.join('logs', 'run.stderr')
 
-    def getSummary(self):
-        summary = {'template': '', 'data': {}}
+    def getSummary(self, **kwargs):
+        data = {}
+        summary = {'template': '', 'data': data}
         data_values = {}
 
         ctfsSqlite = self.join('ctfs.sqlite')
         if os.path.exists(ctfsSqlite):
             summary['template'] = 'processing_ctf_summary.html'
             data_values = self._load_ctfvalues(ctfsSqlite)
+        elif self.className == 'ProtRelionClassify2D':
+            summary['template'] = 'processing_2d_summary.html'
+            data_values = {'iterations': [1, 2, 3]}
 
         if data_values:
-            summary['data'] = {'data_values': data_values}
+            data['data_values'] = data_values
 
         return summary
 
-    def getOverview(self):
+    def getOverview(self, **kwargs):
         overview = {'template': '', 'data': {}}
         data_values = {}
 
         ctfsSqlite = self.join('ctfs.sqlite')
+        coordsSqlite = self.join('coordinates.sqlite')
+
         if os.path.exists(ctfsSqlite):
             overview['template'] = 'processing_ctf_overview.html'
             data_values = self._load_ctfvalues(ctfsSqlite, index=True)
+        elif os.path.exists(coordsSqlite):
+            overview['template'] = 'processing_ctf_overview.html'
 
         if data_values:
             overview['data'] = {'data_values': data_values}
@@ -131,7 +143,11 @@ def _load_ctfvalues(self, ctfsSqlite, index=False):
                 'label': 'Resolution',
                 'unit': 'Å',
                 'data': []
-            }
+            },
+            'default_y': 'ctfResolution',
+            'default_x': '',
+            'default_color': 'ctfDefocusV',
+            'has_ctf': True
         }
         indexes = []
         with SqliteFile(ctfsSqlite) as sf:
@@ -146,9 +162,16 @@ def _load_ctfvalues(self, ctfsSqlite, index=False):
                 'label': 'Id',
                 'data': indexes
             }
+            data_values['default_x'] = 'id'
 
         return data_values
 
+    def get_classes2d(self, iteration=None):
+        """ Get classes information from a class 2d run. """
+        it = "%03d" % iteration if iteration else "*"
+        pattern = self.join('extra', f"*_it{it}_classes.mrcs")
+        return RelionSessionData.get_classes2d_data(pattern)
+
 
 class ScipionSessionData(SessionData):
     """
@@ -226,6 +249,17 @@ def get_stats(self):
             'classes2d': len(self.outputs['classes2d'])
         }
 
+    def runid_from_sqlite(self, sqliteFn):
+        parts = sqliteFn.split('/')
+        runFolder = parts[-2]
+        return int(runFolder.split('_')[0])
+
+    def get_ctfs_runid(self):
+        """ Return the run_id for the ctfs used for the general session overview. """
+        if ctfs := self.outputs.get('ctfs', None):
+            return self.runid_from_sqlite(ctfs)
+        return None
+
     def get_micrographs(self):
         """ Return an iterator over the micrographs' CTF information. """
         if 'ctfs' not in self.outputs:
@@ -342,8 +376,9 @@ def _label(fn):
                             classes2d['selection'] = [int(row.rlnReferenceImage.split('@')[0])
                                                       for row in table if row.rlnEstimatedResolution < 30]
                             break
-            runFolder = os.path.join(os.path.dirname(classesSqlite), 'extra')
-            classes2d['items'] = RelionSessionData.get_classes2d_from_run(runFolder)
+            runFolder = os.path.join(os.path.dirname(classesSqlite), 'extra',
+                                     '*_classes.mrcs')
+            classes2d['items'] = RelionSessionData.get_classes2d_data(runFolder)
 
         return classes2d
 
@@ -402,7 +437,8 @@ def get_run(self, runId):
         with SqliteFile(self.join('project.sqlite')) as sf:
             for row in sf.iterTable('Objects'):
                 name = row['name']
-                if row['id'] == rid or name.startswith(prefix):
+                # Get all rows related to that run as a dict
+                if row['id'] == rid or row['parent_id'] == rid:
                     runDict[name] = row
 
         return ScipionRun(self, runDict)
@@ -412,7 +448,6 @@ def getFormDefinition(className):
         """ Return the json definition of a form defined by className. """
         from pyworkflow.protocol import ElementGroup
         import pwem
-
         ProtClass = pwem.Domain.findClass(className)
         prot = ProtClass()
         logoPath = prot.getPluginLogoPath()
diff --git a/emhub/static/libs/css/emhub.css b/emhub/static/libs/css/emhub.css
index 654cfbfe..ae2452d3 100644
--- a/emhub/static/libs/css/emhub.css
+++ b/emhub/static/libs/css/emhub.css
@@ -96,7 +96,7 @@ a:active {
 /* Cute radiogroups or checkbox with toggle */
 .switch-field {
 	display: flex;
-	margin-bottom: 36px;
+	/*margin-bottom: 36px */
 	overflow: hidden;
 }
 
@@ -137,4 +137,42 @@ a:active {
 
 .switch-field label:last-of-type {
 	border-radius: 0 4px 4px 0;
-}
\ No newline at end of file
+}
+
+.formgroup {
+  border: 2px groove threedface;
+  border-top: none;
+  padding: 0.5em;
+  margin: 1em 2px;
+}
+
+.formgroup>h1 {
+    font-style: italic;
+    font-size: 14px;
+    font-family: 'Circular Std Book';
+    margin: -1em -0.5em 0;
+}
+
+.formgroup>h1>label {
+  float: left;
+}
+
+.formgroup>h1:before {
+  border-top: 2px groove threedface;
+  content: ' ';
+  float: left;
+  margin: 0.5em 2px 0 -1px;
+  width: 0.75em;
+}
+
+.formgroup>h1:after {
+  border-top: 2px groove threedface;
+  content: ' ';
+  display: block;
+  height: 1.5em;
+  left: 2px;
+  margin: 0 1px 0 0;
+  overflow: hidden;
+  position: relative;
+  top: 0.5em;
+}
diff --git a/emhub/static/libs/js/emhub_charts.js b/emhub/static/libs/js/emhub_charts.js
index 1fd440a2..6e4275c9 100644
--- a/emhub/static/libs/js/emhub_charts.js
+++ b/emhub/static/libs/js/emhub_charts.js
@@ -402,7 +402,7 @@ function create_hc_hourly(containerId, data, title, subtitle){
 
 
 /* Draw the micrograph images with coordinates(optional) */
-function drawMicrograph(containerId, micrograph, drawCoordinates) {
+function drawMicrograph(containerId, micrograph, drawValue) {
     var canvas = document.getElementById(containerId);
     var ctx = canvas.getContext("2d");
 
@@ -410,27 +410,43 @@ function drawMicrograph(containerId, micrograph, drawCoordinates) {
 
     image.onload = function() {
         if (canvas.height != image.height) {
-            canvas.height = image.height;
+            let image_ratio = image.width / parseFloat(image.height);
+
             canvas.width = image.width;
-            canvas_ratio = canvas.width / parseFloat(canvas.height);
-        }
+            canvas.height = canvas.width / image_ratio;
+            //canvas.width = image.width;
+            //canvas_ratio = canvas.width / parseFloat(canvas.height);
 
+        }
+        let canvas_image_ratio = canvas.width / parseFloat(image.width);
         ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
 
-        if (!drawCoordinates || micrograph.coordinates.length == 0)
+
+        if (drawValue === 'none' || micrograph.coordinates.length == 0)
             return;
 
         ctx.fillStyle = '#00ff00';
+        ctx.strokeStyle = '#00ff00';
 
-        var scale = parseFloat(micrograph.pixelSize) / micrograph.thumbnailPixelSize;
+        // Adjust the scale of the coordinates taking into account two factors:
+        // 1) scale between the original micrograph and the thumbnail size
+        // 2) scale between the thumbnail and the canvas size
+        var scale =  canvas_image_ratio * parseFloat(micrograph.pixelSize) / (micrograph.thumbnailPixelSize);
         var coords = micrograph.coordinates;
 
         for (var i = 0; i < coords.length; ++i){
             var x = Math.round(coords[i][0] * scale);
             var y = Math.round(coords[i][1] * scale);
             ctx.beginPath();
-            ctx.arc(x, y, 2, 0, 2 * Math.PI);
-            ctx.fill();
+            if (drawValue === 'circle') {
+                ctx.arc(x, y, 10, 0, 2 * Math.PI);
+                ctx.stroke();
+            }
+            else {
+                ctx.arc(x, y, 2, 0, 2 * Math.PI);
+                ctx.fill();
+            }
+
         }
     };
 
@@ -444,7 +460,7 @@ function drawClasses2d(containerId, classes, header, showSel){
 
     for (var cls2d of classes) {
         let borderColor = showSel && cls2d.sel ? 'limegreen' : 'white';
-        imgStr = '<img src="data:image/png;base64,' + cls2d.average + '" style="border: solid 3px ' + borderColor + ';">';
+        imgStr = '<img width="100px" src="data:image/png;base64,' + cls2d.average + '" style="border: solid 3px ' + borderColor + ';">';
         infoStr = '<p class="text-muted mb-0"><small>size: ' + cls2d.size + ', id: ' + cls2d.id + '</small></p>';
         html += '<div style="padding: 3px; min-width: 90px;">' + imgStr + infoStr + '</div>';
 
@@ -455,16 +471,24 @@ function drawClasses2d(containerId, classes, header, showSel){
 function drawVolData(containerId, volSlices){
     var container = document.getElementById(containerId);
     var imgStr, infoStr = null;
-    var html = '<div class="col-12"></div>';
-
-    for(let slice in volSlices) {
-        imgStr = '<img src="data:image/png;base64,' + volSlices[slice] + '" style="border: solid 3px;">';
-        infoStr = '<p class="text-muted mb-0"><small>' + slice + '</small></p>';
-        html += '<div style="padding: 3px; min-width: 90px;">' + imgStr + infoStr + '</div>';
+    var html = '<div class="col-12 row">';
+
+    // for(let slice in volSlices) {
+    //     imgStr = '<img src="data:image/png;base64,' + volSlices[slice] + '" style="border: solid 3px;">';
+    //     infoStr = '<p class="text-muted mb-0"><small>' + slice + '</small></p>';
+    //     html += '<div style="padding: 3px; min-width: 90px;">' + imgStr + infoStr + '</div>';
+    // }
+    for (const [sliceIndex, sliceImg] of Object.entries(volSlices)) {
+      //console.log(`${key}: ${value}`);
+        imgStr = '<img src="data:image/png;base64,' + sliceImg + '" style="border: solid 3px; width: 128px">';
+        infoStr = '<p class="text-muted mb-0"><small>' + sliceIndex + '</small></p>';
+        html += '<div style="padding: 1px; min-width: 128px;">' + imgStr + infoStr + '</div>';
     }
-    container.innerHTML = html;
+    container.innerHTML = html + '</div>';
 }
 
+
+
 class Overlay {
     constructor(containerId) {
         this.container = document.getElementById(containerId);
@@ -482,9 +506,128 @@ class Overlay {
 }
 
 
-class MicrographCard {
-    constructor(containerId, params, gsCard) {
+class Card {
+    constructor(containerId) {
         this.containerId = containerId;
+        this.container = document.getElementById(containerId);
+    }
+
+    id(suffix) {
+        return this.containerId + '_' + suffix;
+    }
+
+    jid(suffix) {
+        return '#' + this.id(suffix)
+    }
+} // class Card
+
+
+class ImageSliderCard extends Card {
+    constructor(containerId, slices, config) {
+        super(containerId);
+        let self = this;
+        this.indexes = [];
+        this.slices = slices;
+        this.slider_prefix = getObjectValue(config, 'slider_prefix', '');
+
+        const width = getObjectValue(config, 'slice_dim', 128);
+        var styleStr = '" style="width: ' + width + 'px"';
+
+        var html = '<div class="row"><div class="col-12"><img id="' + this.id('image') + styleStr + '></div>';
+
+        for (const [sliceIndex, sliceImg] of Object.entries(slices)) {
+            this.indexes.push(sliceIndex);
+            //console.log(`${sliceIndex}: ${sliceImg.length}`);
+        }
+
+        const N = this.indexes.length;
+        html += '<div class="col-12"><input id="' + this.id('slider_input') + styleStr + '" type="range" min="0" value="0" max="' + (N - 1) + '" step="1"></div>';
+        html += '<div class="col-12"><div id="' + this.id('slider_text') + '"></div></div>';
+
+        this.container.innerHTML = html;
+
+        $(this.jid('slider_input')).on("input", function() {
+            self.setSlice($(this).val());
+        });
+
+        let initialSlice = getObjectValue(config, 'initial_slice', Math.floor(N / 2));
+        this.setSlice(initialSlice);
+
+    }
+
+    setSlice(sliceIndex) {
+        $(this.jid('slider_input')).val(sliceIndex);
+        const sliceNumber = this.indexes[sliceIndex];
+        const sliceImg = this.slices[sliceNumber];
+        var img = document.getElementById(this.id('image'));
+        img.src = "data:image/png;base64, " + sliceImg;
+        $(this.jid('slider_text')).text(this.slider_prefix + sliceNumber);
+    }
+}
+
+class VolumeSliderCard extends Card {
+    constructor(containerId, slices, config) {
+        super(containerId);
+        let self = this;
+        this.slices = slices;
+        const slice_dim = getObjectValue(config, 'slice_dim', 128);
+        const width = Math.floor(slice_dim * 1.2);
+
+        const minWidth = 3 * width;
+        var html = '<div class="row ml-1 mr-1" style="min-width: ' + minWidth + 'px">';
+        const styleStr = '" style="width: ' + width + 'px">';
+        html += '<div id="' + this.id('slider-z') + styleStr + '</div>';
+        html += '<div id="' + this.id('slider-y') + styleStr + '</div>';
+        html += '<div id="' + this.id('slider-x') + styleStr + '</div></div>';
+
+        this.container.innerHTML = html;
+
+        this.isz = new ImageSliderCard(this.id('slider-z'), slices.z,
+            {slider_prefix: 'Z slice: ', slice_dim: slice_dim});
+        this.isy = new ImageSliderCard(this.id('slider-y'), slices.y,
+            {slider_prefix: 'Y slice: ', slice_dim: slice_dim});
+        this.isx = new ImageSliderCard(this.id('slider-x'), slices.x,
+            {slider_prefix: 'X slice: ', slice_dim: slice_dim});
+
+    }
+}
+
+
+function createVolume(array, dimensions) {
+
+    //volComp.autoView();
+
+    console.log(surface.getParameters());
+}
+
+
+function renderVolume3D(containerId, arrayJson, dimensions) {
+    //var data = JSON.parse(dataJson);
+    var array = new Uint8Array(atob(arrayJson).split("").map(function (c) {
+            return c.charCodeAt(0);
+        }));
+        const nx = dimensions[0], ny = dimensions[1], nz = dimensions[2];
+        const cx = nx / 2, cy = ny / 2, cz = nz / 2;
+        var volume = new NGL.Volume("volume", '', array, nx, ny, nz);
+        var stage = new NGL.Stage(containerId);
+        var volComp = stage.addComponentFromObject(volume);
+        var surface = volComp.addRepresentation("surface",
+            {isolevelType: 'value', isolevel: 180});
+        volComp.setPosition([-cx, -cy, -cz]);
+
+        const sliderId = '#' + containerId + "_slider";
+
+        $(sliderId).val(surface.getParameters()['isolevel']);
+        $(sliderId).on("input", function() {
+            surface.setParameters({isolevel: parseInt($(this).val())});
+        });
+} // function drawVolume3D
+
+
+
+class MicrographCard extends Card {
+    constructor(containerId, params, gsCard) {
+        super(containerId);
         this.overlay = new Overlay(this.id('overlay'));
         this.gsCard = gsCard;
         // Parameters used to load the micrograph data
@@ -506,22 +649,26 @@ class MicrographCard {
         });
 
         // Bind to on/off coordinates display
-        $(self.jid('show_particles')).change(function() {
-            self.drawMicrograph();
-        });
-    }
+        // $(self.jid('show_particles')).change(function() {
+        //     self.drawMicrograph();
+        // });
+
+        if (self.contains('particles')) {
+            $('input[name="' + self.id('pts_switch') + '"]').change(function () {
+                self.drawMicrograph();
+            })
+        }
 
-    id(suffix) {
-        return this.containerId + '_' + suffix;
     }
 
-    jid(suffix) {
-        return '#' + this.id(suffix)
+    /* Check if the card has an element with that id */
+    contains(elementId) {
+        return $(this.jid(elementId)).length > 0;
     }
 
     drawMicrograph() {
-       drawMicrograph(this.id('mic_canvas'), this.micrograph,
-           $(this.jid('show_particles')).prop('checked'));
+        let value= this.contains('particles') ? $('input[name="' + this.id('pts_switch') + '"]:checked').val() : 'none';
+        drawMicrograph(this.id('mic_canvas'), this.micrograph, value);
     }
 
     loadMicData(micId, doneCallback) {
@@ -557,7 +704,12 @@ class MicrographCard {
                 self.gsCard.loadData(data.gridSquare);
             }
 
-            $(self.jid('img_psd')).attr('src', 'data:image/png;base64,' + data.psdData);
+            if (data.psdData) {
+                $(self.jid('img_psd')).attr('src', 'data:image/png;base64,' + data.psdData);
+            }
+            else
+                $(self.jid('img_psd')).hide();
+
             function setLabel(containerId, value){
                 var elem = document.getElementById(containerId);
                 var parts = elem.innerHTML.split(":");
@@ -571,7 +723,7 @@ class MicrographCard {
                 $(self.jid('mic_defocus_v')).text(data['ctfDefocusV']);
                 $(self.jid('mic_defocus_angle')).text(data['ctfDefocusAngle']);
                 $(self.jid('mic_astigmatism')).text(data['ctfAstigmatism']);
-                if (nonEmpty(data['ctfPlot']))
+                if (self.contains('ctf_plot') && nonEmpty(data['ctfPlot']))
                     create_pl_ctfplot(self.id('ctf_plot'), data['ctfPlot']);
             }
             else {
@@ -580,7 +732,9 @@ class MicrographCard {
             }
 
             $(self.jid('mic_resolution')).text(data['ctfResolution']);
-            $(self.jid('particles')).text(micrograph.coordinates.length);
+
+            if (self.contains('particles'))
+                $(self.jid('particles')).val(micrograph.coordinates.length);
 
             self.overlay.hide();
 
@@ -597,21 +751,13 @@ class MicrographCard {
 }
 
 
-class GridSquareCard {
+class GridSquareCard extends Card {
     constructor(containerId) {
-        this.containerId = containerId;
+        super(containerId);
         this.overlay = new Overlay(this.id('overlay'));
         this.last = null;
     }
 
-    id(suffix) {
-        return this.containerId + '_' + suffix;
-    }
-
-    jid(suffix) {
-        return '#' + this.id(suffix)
-    }
-
     loadData(gridSquare) {
         // Do not load if it is the same GridSquare
         if (gridSquare == this.last)
@@ -649,6 +795,115 @@ class GridSquareCard {
 
 }  // class GridSquareCard
 
+class PlotCard extends Card {
+    constructor(containerId, dataDict) {
+        super(containerId);
+        let self = this;
+        this.dataDict = dataDict;
+        this.set_default('labelX-select', 'default_x')
+        this.set_default('labelY-select', 'default_y')
+        this.set_default('labelColor-select', 'default_color')
+
+         $(this.jid('update-plots-btn')).on('click', function (e) {
+            self.makePlots();
+         });
+    }
+
+    set_default(selectId, valueKey){
+        let value = this.dataDict[valueKey]
+        if (nonEmpty(value)) {
+            $(this.jid(selectId)).selectpicker('val', value);
+        }
+    }
+    makePlots() {
+        //$('#update-plots-btn').toggleClass('btn-dark btn-outline-dark');
+        let self = this;
+        let label_x = $(this.jid('labelX-select')).selectpicker('val');
+        let data_x = this.dataDict[label_x].data;
+
+        let label_y = $(this.jid('labelY-select')).selectpicker('val');
+        let data_y = this.dataDict[label_y].data;
+
+        let label_color = $(this.jid('labelColor-select')).selectpicker('val');
+        let data_color = this.dataDict[label_color].data;
+
+        let colorscale = [[0.0, '#440154'], [0.1111111111111111,
+                                            '#482878'], [0.2222222222222222,
+                                            '#3e4989'], [0.3333333333333333,
+                                            '#31688e'], [0.4444444444444444,
+                                            '#26828e'], [0.5555555555555556,
+                                            '#1f9e89'], [0.6666666666666666,
+                                            '#35b779'], [0.7777777777777778,
+                                            '#6ece58'], [0.8888888888888888,
+                                            '#b5de2b'], [1.0, '#fde725']];
+        var data = [
+            {'fillcolor': 'white',
+                  'marker': {'color': data_color,
+                             'colorscale': 'Portland',
+                             'opacity': 0.75,
+                                'size': 10,
+                             'showscale': true},
+                  'mode': 'markers',
+                  'type': 'scattergl',
+                  'x': data_x,
+                  'y': data_y },
+             {'marker': {'color': 'cornflowerblue', 'opacity': 0.5},
+              'name': label_x,
+              'type': 'violin',
+              'x': data_x,
+              'yaxis': 'y2'},
+             {'marker': {'color': 'cornflowerblue', 'opacity': 0.5},
+              'name': label_y,
+              'type': 'violin',
+              'xaxis': 'x2',
+              'y': data_y}
+        ];
+
+        var layout = {'bargap': 0,
+           'hovermode': 'closest',
+           'margin': {'t': 50, 'l': 0, 'r': 0},
+           'autosize': true,
+           'plot_bgcolor': 'rgba(0,0,0,0)',
+           'showlegend': false,
+           'template': '...',
+           'xaxis': {'domain': [0, 0.85], 'gridcolor': '#CBCBCB', 'title': {'text': label_x}, 'zeroline': true},
+           'xaxis2': {'domain': [0.85, 1], 'showgrid': true, 'zeroline': false},
+           'yaxis': {'domain': [0, 0.85],
+                     'gridcolor': '#CBCBCB',
+                     'title': {'text': label_y},
+                     'zeroline': true},
+           'yaxis2': {'domain': [0.85, 1], 'showgrid': true, 'zeroline': false}};
+
+        Plotly.newPlot(self.id('plotDiv'), data, layout);
+        let graphDiv = document.getElementById(self.id('plotDiv'));
+
+        graphDiv.on('plotly_selected', function(eventData) {
+            if (!eventData) {
+                $('#selection-label').text("No points selected.");
+                $('#export-btn').hide();
+                return;
+            }
+            let points = eventData.points;
+            $(self.jid('selection-label')).html("Selected <label style='color: firebrick; font-size: medium;'>" + points.length + "</label> points")
+            $(self.jid('export-btn')).show();
+        });
+
+        graphDiv.on('plotly_doubleclick', function(eventData) {
+            alert('click');
+        });
+
+        graphDiv.on('plotly_click', function(eventData) {
+            let index = eventData.points[0].pointNumber;
+            if (self.onPointClick) {
+                self.onPointClick(index);
+            }
+            //
+        });
+
+    }
+
+}  // class GridSquareCard
+
 /* --------------------------- Session Live functions ------------------------*/
 function session_getData2D(run_id){
     overlay_2d.show("Loading Class2D, run " + run_id);
@@ -1099,28 +1354,46 @@ function create_hc_sessions_histogram(containerId, data) {
 });
 } // function hc_create_histogram
 
-function create_pl_ctfplot(containerId, ctfvalues) {
-    // var trace = {
-    //   x: ctfvalues[0],
-    //   y: ctfvalues[1],
-    //   mode: 'lines',
-    //   connectgaps: true
-    // };
-    //
-    // var trace2 = {
-    //   x: ctfvalues[0],
-    //   y: ctfvalues[2],
-    //   mode: 'lines',
-    //   connectgaps: true
-    // };
-    //
-    //  var trace3 = {
-    //   x: ctfvalues[0],
-    //   y: ctfvalues[3],
-    //   mode: 'lines',
-    //   connectgaps: true
-    // };
 
+function create_hc_motionplot() {
+    var seriesData = [[100, 29.9], [150,71.5], [300,106.4]];
+    var plotLines = [];
+    var step = 100;
+    var N = 500;
+
+    for (var i = 0; i <= N; i+=step) {
+    		plotLines.push({
+        	value: i, // Value of where the line will appear
+          width: 1 // Width of the line
+        });
+    }
+
+    Highcharts.chart(containerId, {
+        chart: {
+        //height: 700,
+            style: "dash",
+    		height: "100%",
+        //margin: 50
+        },
+        xAxis: {
+        		min: 0,
+            max: N,
+            tickInterval: step,
+            plotLines: plotLines
+        },
+        yAxis: {
+        		min: 0,
+            max: N,
+            tickInterval: step,
+        },
+
+        series: [{
+            data: seriesData
+        }]
+    });
+} // function create_hc_motionplot
+
+function create_pl_ctfplot(containerId, ctfvalues) {
      var data = [];
      for (i = 1; i <= 4; i++) {
          data.push({
@@ -1134,7 +1407,7 @@ function create_pl_ctfplot(containerId, ctfvalues) {
     // var data = [trace, trace2, trace3];
 
     var layout = {
-      showlegend: true,
+        showlegend: true,
         autoscale: true,
         margin: {'t': 0, 'l': 10, 'r': 50},
     };
diff --git a/emhub/static/libs/js/emhub_forms.js b/emhub/static/libs/js/emhub_forms.js
index d9aa814d..a9634c61 100644
--- a/emhub/static/libs/js/emhub_forms.js
+++ b/emhub/static/libs/js/emhub_forms.js
@@ -258,19 +258,21 @@ function updateSession(session) {
     send_ajax_json(Api.urls.session.update, session, sessionAjaxDone)
 }
 
-function showCreateSession(bookingId) {
-        var content = get_ajax_content("create_session_form", {booking_id: bookingId});
+function showCreateSession(bookingId, create_session_func) {
+        var content = get_ajax_content(create_session_func, {booking_id: bookingId});
         show_modal_from_ajax("session-modal", content);
 } // function showCreateSession
 
 /* Show the Resource Form, either for a new booking or an existing one */
-function createSession(bookingId, totalSessions) {
+function createSession(bookingId, totalSessions, create_session_func) {
+    console.log("createSession, func: " + create_session_func);
+
     if (totalSessions === 0)
-        showCreateSession(bookingId);
+        showCreateSession(bookingId, create_session_func);
     else
         confirm("Create Session", "There are already created sessions, " +
                 "Do you want to create another one?", 'No', 'Yes', function () {
-                showCreateSession(bookingId);
+                showCreateSession(bookingId, create_session_func);
         });
 
 }  // function showResource
@@ -305,10 +307,31 @@ function createSession(bookingId, totalSessions) {
  /* Create row with new elements under this parent */
  function form_addRows(parent, params, values){
 
+     function get_param_value(param){
+         return (param.name in values) ? values[param.name] : '';
+     }
+     function create_label(col, labelText){
+        var label = document.createElement('label');
+        label.className = col + ' col-form-label text-sm-right text-wrap';
+        label.textContent = labelText;
+        return label;
+     }
+    function create_input(col, key, value) {
+        var div = document.createElement('div');
+        div.className = col;
+        var input = document.createElement('input');
+        input.className = 'form-control form-control-sm';
+        input.value = value;
+        input.dataset.key = key;
+        div.appendChild(input)
+        return div;
+    }
+
+
     for (var i = 0;  i < params.length; i++) {
         var param = params[i];
         var base_id = param.name;
-        var param_value = (base_id in values) ? values[base_id] : '';
+        var param_value = get_param_value(param);
 
         var row = document.createElement('div');
         row.className = 'row form-group';
@@ -318,6 +341,21 @@ function createSession(bookingId, totalSessions) {
             // Empty param is a separator
             row.className += ' mt-1 mb-1';
         }
+        else if (param.paramClass === "Group") {  // Groups are special and don't have labels
+                var div = document.createElement('div');
+                div.className = "formgroup col-12"
+                div.innerHTML = "<h1><label class='col-1 text-sm-right mr-3'>" + param.label + "</label></h1></br>"
+                row.appendChild(div);
+                form_addRows(div, param.params, values);
+
+                // <fieldset>
+                //     <legend>Legend</legend> Fieldset
+                // </fieldset>
+                //
+                // <div className="fieldset">
+                //     <h1><span>Legend</span></h1> Fieldset
+                // </div>
+        }
         else if (param.label) {
 
             if (param.expert == 1) {
@@ -327,20 +365,29 @@ function createSession(bookingId, totalSessions) {
             }
 
             row.id = base_id + '-row';
-            var label = document.createElement('label');
-            label.className = 'col-6 col-form-label text-sm-right text-wrap';
-            label.textContent = param.label;
+            var label = create_label('col-4', param.label);
             label.dataset.toggle = "tooltip";
             label.dataset.placement = "top";
             label.title = param.help;
             row.appendChild(label);
 
             if (param.paramClass === "LabelParam"){
-                label.classList.replace('col-6', 'col-12');
+                label.classList.replace('col-4', 'col-12');
+            }
+            else if (param.paramClass === "Line") {
+                var div = document.createElement('div');
+                div.className = 'row col-8 form-group';
+                //div.style.backgroundColor = 'red';
+                for (var j = 0;  j < param.params.length; j++) {
+                    let p = param.params[j];
+                    div.appendChild(create_label('ml-0 pl-0 col-2', p.label))
+                    div.appendChild(create_input('col-2', p.name, get_param_value(p)));
+                }
+                row.appendChild(div);
             }
             else if (param.paramClass === "EnumParam") {
                 var div = document.createElement('div');
-                div.className = 'col-6 form-group';
+                div.className = 'col-8 form-group';
 
                 if (param.display == 1) { // Combo
                     var select = document.createElement('select');
@@ -352,7 +399,7 @@ function createSession(bookingId, totalSessions) {
                     for (var j = 0; j < param.choices.length; j++) {
                         var opt = document.createElement('option');
                         opt.textContent = param.choices[j];
-                        opt.selected = param_value === j;
+                        opt.selected = param_value == j; // Let JS compare string integer values with indexes
                         opt.value = j;
                         select.appendChild(opt);
                     }
@@ -364,20 +411,13 @@ function createSession(bookingId, totalSessions) {
 
                 if (param.paramClass === "BooleanParam") {
                     var div = document.createElement('div');
-                    div.className = 'row col-6 ml-1';
+                    div.className = 'row col-8 ml-1';
                     form_addRadio(div, base_id + '-yes', base_id, true, 'Yes', param_value === '1');
                     form_addRadio(div, base_id + '-no', base_id, false, 'No', param_value === '0');
                     row.appendChild(div);
                 }
                 else {
-                    var div = document.createElement('div');
-                    div.className = 'col-6';
-                    var input = document.createElement('input');
-                    input.className = 'form-control form-control-sm';
-                    input.value = param_value;
-                    input.dataset.key = param.name;
-                    div.appendChild(input)
-                    row.appendChild(div);
+                    row.appendChild(create_input('col-8', param.name, param_value ));
                 }
             }
         }
@@ -433,6 +473,7 @@ function createSession(bookingId, totalSessions) {
          section_label = replaceAll(section_label, '.', '_');
          section_label = replaceAll(section_label, '/', '_');
 
+         // FIXME: this is Scipion-specific
          if (section.label === "General" || section.label === "Parallelization")
              continue;
 
diff --git a/emhub/templates/booking_calendar.html b/emhub/templates/booking_calendar.html
index 8fe51d4a..9c11d5b6 100644
--- a/emhub/templates/booking_calendar.html
+++ b/emhub/templates/booking_calendar.html
@@ -27,7 +27,7 @@ <h5> Display </h5>
                         </div>
                         <div class="col col-auto nopadding">
                             <select id="selectpicker-resource-display" class="selectpicker show-tick" multiple data-width="auto" data-selected-text-format="count > 2" title="Select Resource to display..."  data-actions-box="true"  onchange="filterBookingsByResources()">
-                            {% for r in resources %}#}
+                            {% for r in resources %}
                               <option value="{{r['id']}}" class="badge" style="background: {{ r['color'] }}; color: #fff;">{{r['name']}}</option>
                             {% endfor %}
                             </select>
diff --git a/emhub/templates/booking_form.html b/emhub/templates/booking_form.html
index c087beaa..c06c47c4 100644
--- a/emhub/templates/booking_form.html
+++ b/emhub/templates/booking_form.html
@@ -37,7 +37,7 @@ <h3 class="modal-title"> Booking ({{ title_suffix }}) {{ " READ-ONLY" if read_on
                             <select id="selectpicker-resource" class="selectpicker show-tick" data-width="auto" data-key="resource_id" title="Select Resource to book..." {{ select_readonly }}>
                             {% for r in resources %}
                                 {% if r['user_can_book'] %}
-                                    {% set selected = 'selected' if r['id'] == booking.resource.id else '' %}
+                                    {% set selected = 'selected' if r['id'] == booking.resource_id|int else '' %}
                                     <option {{ selected }} value="{{r['id']}}" data-content="<span class='badge' style='background: {{ r['color'] }}; color: #fff;'>{{r['name']}}</span>">{{r['name']}}</option>
                                 {% endif %}
                             {% endfor %}
@@ -55,7 +55,7 @@ <h3 class="modal-title"> Booking ({{ title_suffix }}) {{ " READ-ONLY" if read_on
                                     <option data-divider="true"></option>
                                     {% for u in lab %}
                                         {% set style = 'background: #71748d; color: #fff;' if loop.index0 == 0 else '' %}
-                                        {% set selected = 'selected' if (u['id'] == booking.owner.id or u['id'] == booking.owner_id) else '' %}
+                                        {% set selected = 'selected' if (u['id'] == booking.owner.id or u['id'] == booking.owner_id|int) else '' %}
                                         <option value="{{u['id']}}" {{ selected }} style="{{style}}">{{u['name']}}</option>
                                     {% endfor %}
                                 {% endfor %}
@@ -158,12 +158,12 @@ <h4 class="col-12 text-sm-left">Admin</h4>
                                         <select id="booking-operator-select" class="selectpicker show-tick" data-live-search="true" data-key="operator_id" >
                                             <option value="0">Not set</option>
                                             {% for u in possible_operators %}
-                                                {% set selected = 'selected' if u['id'] == booking.operator_id else '' %}
+                                                {% set selected = 'selected' if u['id'] == booking.operator_id|int else '' %}
                                                 <option {{ selected }} value="{{u['id']}}">{{u['name']}}</option>
                                             {% endfor %}
                                         </select>
                                     {% else %}
-                                        <input type="text" required="" placeholder="Staff" id="booking-owner-text" class="form-control" data-key="operator_id" >
+                                        <input type="text" required="" placeholder="Staff" id="booking-staff-text" class="form-control" data-key="operator_id" >
                                     {% endif %}
                                 </div>
                             </div>
diff --git a/emhub/templates/dashboard_news.html b/emhub/templates/dashboard_news.html
index 8052bcc6..ef22e26f 100644
--- a/emhub/templates/dashboard_news.html
+++ b/emhub/templates/dashboard_news.html
@@ -1,19 +1,4 @@
+{% import "entry_macros.html" as macros %}
 
-{% if news %}
-    <div class="row">
-        <h3 class="col-12">News</h3>
-        {% for n in news %}
-{#        {% for type in ['info', 'warning', 'danger'] * 1 %}#}
-        <div class="col-auto m-0 p-0 mb-3" style="min-width: 700px; max-width: 700px;">
-            <div class="card-body pl-1 ml-1 mb-0 mt-0 pb-0 pt-0 h-100 news-card">
-                <div class="alert alert-{{ n['type'] }} m-0" role="alert">
-                    <h4 class="alert-heading">{{ n['title'] }}</h4>
-                    <hr>
-                    <p>{{ n['html'] }}</p>
-                </div>
-            </div>
-        </div>
-        {% endfor %}
-    </div>
-{% endif %}
+{{ macros.news_card_div('News', news[0], 'cards', current_user.is_manager) }}
 
diff --git a/emhub/templates/dashboard_right.html b/emhub/templates/dashboard_right.html
index 816d8299..63bc07ba 100644
--- a/emhub/templates/dashboard_right.html
+++ b/emhub/templates/dashboard_right.html
@@ -45,12 +45,15 @@
                     {% if not compact %}
                     <tr style="{{ row_style }}">
                         <td colspan="3">
-                            {% if active %}
-                                    <a href="javascript:createSession({{ b.id }}, {{ sessions|length }})" class="btn btn-primary mr-0">
-                                                    <i class="fas fa-plus-circle"></i> New Session </a>
-                            {% else %}
-                                {% if current_user.is_manager %}
-                                    <a href="javascript:createSession({{ b.id }}, {{ sessions|length }})"><i class="fas fa-plus-circle"></i></a>
+                            {% set create_session_func = resource_create_session.get(b.resource.name, None) %}
+                            {% if create_session_func %}
+                                {% if active %}
+                                        <a href="javascript:createSession({{ b.id }}, {{ sessions|length }}, '{{ create_session_func }}')" class="btn btn-primary mr-0">
+                                                        <i class="fas fa-plus-circle"></i> New Session </a>
+                                {% else %}
+                                    {% if current_user.is_manager %}
+                                        <a href="javascript:createSession({{ b.id }}, {{ sessions|length }}, '{{ create_session_func }}')"><i class="fas fa-plus-circle"></i></a>
+                                    {% endif %}
                                 {% endif %}
                             {% endif %}
                             {% if b.session %}
diff --git a/emhub/templates/entry_macros.html b/emhub/templates/entry_macros.html
index 9e0fa85a..8d5e095e 100644
--- a/emhub/templates/entry_macros.html
+++ b/emhub/templates/entry_macros.html
@@ -280,4 +280,90 @@ <h4 class="pr-5 text-white-50" id="{{ overlay_id }}_text"></h4>
         </div>
     </div>
 
+{%- endmacro %}
+
+{% macro single_news_card(n) %}
+    <div class="col-auto m-0 p-0 mb-3" style="min-width: 700px; max-width: 700px;">
+        <div class="card-body pl-1 ml-1 mb-0 mt-0 pb-0 pt-0 h-100 news-card">
+            <div class="alert alert-{{ n['type'] }} m-0" role="alert">
+                <h4 class="alert-heading">{{ n['title'] }}</h4>
+                <hr>
+                <p>{{ n['html'] }}</p>
+            </div>
+        </div>
+    </div>
+{% endmacro %}
+
+{% macro news_cards(label, news, edit) -%}
+    {% if news %}
+        <div class="row">
+            <div class="col-12 row mb-3">
+                <h3 class="mr-3 mt-1">{{ label }}</h3>
+                <a href="{{ url_for_content('news') }}" class="btn btn-outline-light btn-sm"><i class="far fa-edit"></i></a>
+            </div>
+            {% for n in news %}
+                {{ single_news_card(n) }}
+            {% endfor %}
+        </div>
+    {% endif %}
+{%- endmacro %}
+
+{% macro news_table(label, news, is_manager) -%}
+    {% if news %}
+        <div class="row">
+            <h3 class="col-12">{{ label }}</h3>
+
+            <div class="table-responsive" id="news-table-content-{{ label }}">
+
+                <table id="news-table-{{ label }}" class="table table-news">
+                    <thead>
+                        <tr>
+                            {% if is_manager %}
+                                <th>actions</th>
+                            {% endif %}
+                            <th>id</th>
+                            <th>type</th>
+                            <th>title</th>
+                            <th>text</th>
+                            <th>preview</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for n in news %}
+                            <tr>
+                                {% if is_manager %}
+                                <td>
+                                   <a href="javascript:showEntryForm({{ n['id'] }}, {{ project_id }})" class="btn btn-outline-light">
+                                             <i class="far fa-edit"></i></a>
+                                </td>
+                                {% endif %}
+
+                                <td>{{ n['id'] }}</td>
+                                <td>{{ n['type'] }}</td>
+                                <td>{{ n['title'] }}</td>
+                                <td>{{ n['text'] }}</td>
+                                <td>{{  single_news_card(n) }}</td>
+                            </tr>
+                        {% endfor %}
+
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    {% endif %}
+{%- endmacro %}
+
+
+{% macro news_card_div(label, news, display, is_manager, edit) -%}
+    {% if news %}
+        <div class="card">
+            <div class="card-body">
+                {% if display == 'cards' %}
+                    {{ news_cards(label, news, edit) }}
+                {% else %}
+                    {{ news_table(label, news, is_manager) }}
+                {% endif %}
+            </div>
+        </div>
+    {% endif %}
 {%- endmacro %}
\ No newline at end of file
diff --git a/emhub/templates/main_javascript.html b/emhub/templates/main_javascript.html
index a7342b3a..5ce8a8eb 100644
--- a/emhub/templates/main_javascript.html
+++ b/emhub/templates/main_javascript.html
@@ -52,6 +52,11 @@
 {#<script src="https://cdnjs.cloudflare.com/ajax/libs/highcharts/10.3.2/modules/export-data.js"></script>#}
 {#<script src="https://cdnjs.cloudflare.com/ajax/libs/highcharts/10.3.2/modules/accessibility.js"></script>#}
 
+<!-- NGL viewer -->
+<!-- <script src="https://unpkg.com/ngl"></script> -->
+<script src="https://unpkg.com/ngl@2.0.0-dev.39/dist/ngl.js"></script>
+
+
 <!-- emhub -->
 <script src="{{ url_for('static', filename='libs/js/emhub.js') }}"></script>
 <script src="{{ url_for('static', filename='libs/js/emhub_applications.js') }}"></script>
diff --git a/emhub/templates/main_left_sidebar.html b/emhub/templates/main_left_sidebar.html
index e16788e1..fa47b9ab 100644
--- a/emhub/templates/main_left_sidebar.html
+++ b/emhub/templates/main_left_sidebar.html
@@ -39,6 +39,8 @@
                                     <i class="fas fa-rocket"></i>Applications</a>
                             </li>
 
+                            {% if current_user.is_manager %}
+
                             <li class="nav-item">
                                 <a class="nav-link" href="{{ url_for_content('invoice_periods_list') }}">
                                     <i class="far fa-money-bill-alt"></i>Invoices</a>
@@ -52,14 +54,24 @@
                                     <li class="nav-item">
                                         <a class="nav-link" href="{{ url_for_content('report_microscopes_usage') }}">Microscopes Usage</a>
                                     </li>
-                                    {% if current_user.is_manager %}
+
                                     <li class="nav-item">
                                         <a class="nav-link" href="{{ url_for_content('report_pis_usage') }}">All PIs Usage</a>
                                     </li>
-                                    {% endif %}
+
+
+
                                 </ul>
                             </div>
 
+                            <!-- News -->
+                            <li class="nav-item">
+                                <a class="nav-link" href="{{ url_for_content('news', display='table') }}">
+                                    <i class="fas fa-newspaper"></i>News</a>
+                            </li>
+
+                            {% endif %}
+
                             <!-- Processing -->
                             <li class="nav-item">
                                 <a class="nav-link" href="{{ url_for_content('processing_projects_list') }}">
diff --git a/emhub/templates/micrograph_card.html b/emhub/templates/micrograph_card.html
index e6809271..348859fd 100644
--- a/emhub/templates/micrograph_card.html
+++ b/emhub/templates/micrograph_card.html
@@ -52,19 +52,34 @@ <h5 class="input-group-text" id="{{ card_id }}_inputGroup-sizing-sm" style="font
                 {% endif %}
 
                 {{ mic_row('Resolution (Å)', '', card_id + '_mic_resolution') }}
-                <div class="row col-12 p-0 m-0">
-                    <label class="text-muted col-6 text-right mt-1">Particles</label>
-                    <label class="col-4 text-right mt-1" id="{{ card_id }}_particles">{{ value }}</label>
-                    <label class="custom-control custom-checkbox col-1">
-                        <input type="checkbox" checked class="custom-control-input" id="{{ card_id }}_show_particles">
-                            <span class="custom-control-label"></span>
-                    </label>
+
+                <div class="col-12 ml-0 pl-0 mr-0 pr-0 mt-3">
+                 <div class="input-group input-group-sm mb-3">
+                      <div class="input-group-prepend">
+                        <h5 class="input-group-text" id="{{ card_id }}_particles-sm" style="font-size: 13px">Particles</h5>
+                      </div>
+                      <input type="text" readonly id="{{ card_id }}_particles" class="form-control" aria-label="Small" aria-describedby="{{ card_id }}_particles-sm">
+                    <div class="switch-field ml-2 mr-2">
+                         {% set radio_name = card_id + "_pts_switch" %}
+                        <input type="radio" id="{{ radio_name }}-one" name="{{ radio_name }}" value="none"/>
+                        <label for="{{ radio_name }}-one"><i class="far fa-times-circle"></i></label>
+                        <input type="radio" id="{{ radio_name }}-two" name="{{ radio_name }}" value="dot" checked />
+                        <label for="{{ radio_name }}-two"><i class="far fa-dot-circle"></i></label>
+                         <input type="radio" id="{{ radio_name }}-three" name="{{ radio_name }}" value="circle" />
+                        <label for="{{ radio_name }}-three"><i class="far fa-circle"></i></label>
+                    </div>
+
                 </div>
-{#                {{ mic_row('Compact', '', card_id + '_testing') }}#}
+             </div>
+
             </div>
-            <div class="row col-12 mt-2 ml-3 mr-3" style="min-height: 400px">
-             <div id="{{ card_id }}_ctf_plot"></div>
-         </div>
+
+           {% if show_ctf_plot %}
+                <div class="row col-12 mt-2 ml-3 mr-3" style="min-height: 400px">
+                    <div id="{{ card_id }}_ctf_plot"></div>
+               </div>
+            {% endif %}
+
         </div>
     </div>
 </div>
\ No newline at end of file
diff --git a/emhub/templates/micrograph_card_big.html b/emhub/templates/micrograph_card_big.html
new file mode 100644
index 00000000..1f6f5214
--- /dev/null
+++ b/emhub/templates/micrograph_card_big.html
@@ -0,0 +1,104 @@
+{% import "entry_macros.html" as macros %}
+
+{% macro mic_row(label, value, id) -%}
+    <div class="row col-12 p-0 m-0">
+        <label class="col-6 text-right ml-0 pl-0" style="font-size: 13px">{{ label }}</label>
+        <label class="col-4 text-right ml-0 pl-0" id="{{ id }}" style="font-size: 13px">{{ value }}</label>
+    </div>
+{%- endmacro  %}
+
+
+{# card_id should be defined before including this macro #}
+{% set card_compact = card_compact or False %}
+
+<div class="col-12">
+     <div class="card card-figure image_card" id="{{ card_id }}">
+        <input type="hidden" value="{{ card_compact }}" id="{{ card_id }}_compact">
+
+         <!-- Display overlay when loading micrograph -->
+         {{ macros.overlay(card_id + "_overlay") }}
+
+     <div class="row">
+
+         <!-- Micrograph thumbnail panel -->
+        <div class="col-6" style="min-width: 350px;">
+            <figure class="figure">
+                <canvas id="{{ card_id }}_mic_canvas"></canvas>
+            </figure>
+        </div>
+
+         <!-- PSD panel with CTF values -->
+         <div class="row col-6 mt-2 ml-0 pl-2 mr-0 pr-0"  style="min-width: 250px;">
+            <div class="col-12 ml-0 pl-0 mr-0 pr-0">
+                <div class="input-group input-group-sm mb-3">
+                  <div class="input-group-prepend">
+                    <h5 class="input-group-text" id="{{ card_id }}_inputGroup-sizing-sm" style="font-size: 13px">Micrograph</h5>
+                  </div>
+                  <input type="text" id="{{ card_id }}_mic_id" class="form-control" aria-label="Small" aria-describedby="{{ card_id }}_inputGroup-sizing-sm">
+                   <div class="row ml-2 mr-2">
+                        <a id="{{ card_id }}_mic_left"><i class="fas fa-arrow-circle-left mr-1"></i></a>
+                        <a id="{{ card_id }}_mic_right"><i class="fas fa-arrow-circle-right"></i></a>
+                   </div>
+                </div>
+            </div>
+
+            <div class="col-12 ml-0 pl-0 mr-0 pr-0">
+                <label>{{ has_ctf }}</label>
+            </div>
+            {% if has_ctf %}
+            <div class="col-4 ml-0 pl-0 mr-0 pr-0">
+                <figure class="figure">
+                    <img class="img-fluid" id="{{ card_id }}_img_psd" src="">
+                </figure>
+            </div>
+
+            <div class="col-8 mr-0 pr-0">
+                    <div class="col-12 mt-1">
+                         {{ mic_row('Defocus U (µm)', '', card_id + '_mic_defocus_u') }}
+                         {{ mic_row('Defocus V (µm)', '', card_id + '_mic_defocus_v') }}
+                         {{ mic_row('Defocus Angle (°)', '', card_id + '_mic_defocus_angle') }}
+                         {{ mic_row('Astigmatism  (µm)', '', card_id + '_mic_astigmatism') }}
+                         {{ mic_row('Resolution (Å)', '', card_id + '_mic_resolution') }}
+
+                     </div>
+            </div>
+
+
+             {% if show_ctf_plot %}
+                 <div class="row col-12 mt-2 ml-3 mr-3" style="min-height: 300px">
+                    <div id="{{ card_id }}_ctf_plot"></div>
+                </div>
+             {% endif %}
+
+            {% endif %}
+
+            {% if has_particles %}
+             <div class="col-12 ml-0 pl-0 mr-0 pr-0 mt-3">
+                 <div class="input-group input-group-sm mb-3">
+                      <div class="input-group-prepend">
+                        <h5 class="input-group-text" id="{{ card_id }}_particles-sm" style="font-size: 13px">Particles</h5>
+                      </div>
+                      <input type="text" readonly id="{{ card_id }}_particles" class="form-control" aria-label="Small" aria-describedby="{{ card_id }}_particles-sm">
+                    <div class="switch-field ml-2 mr-2">
+                         {% set radio_name = card_id + "_pts_switch" %}
+                        <input type="radio" id="{{ radio_name }}-one" name="{{ radio_name }}" value="none"/>
+                        <label for="{{ radio_name }}-one"><i class="far fa-times-circle"></i></label>
+                        <input type="radio" id="{{ radio_name }}-two" name="{{ radio_name }}" value="dot" checked />
+                        <label for="{{ radio_name }}-two"><i class="far fa-dot-circle"></i></label>
+                         <input type="radio" id="{{ radio_name }}-three" name="{{ radio_name }}" value="circle" />
+                        <label for="{{ radio_name }}-three"><i class="far fa-circle"></i></label>
+                    </div>
+
+                </div>
+             </div>
+            {% endif %}
+
+
+
+        </div>
+
+
+    </div>
+
+     </div>
+</div>
\ No newline at end of file
diff --git a/emhub/templates/micrograph_card_side.html b/emhub/templates/micrograph_card_side.html
index 6cef4817..eb01380c 100644
--- a/emhub/templates/micrograph_card_side.html
+++ b/emhub/templates/micrograph_card_side.html
@@ -21,35 +21,40 @@
      <div class="row">
 
          <!-- Micrograph thumbnail panel -->
-        <div class="col-6" style="min-width: 350px;">
-            <figure class="figure">
-                <canvas id="{{ card_id }}_mic_canvas"></canvas>
-            </figure>
+        <div class="col-9 row" style="background-color: blue">
+
+                 <div clas="col-12 ml-2">
+                    <figure class="figure" style="min-width: 350px">
+                        <canvas id="{{ card_id }}_mic_canvas"></canvas>
+                    </figure>
+                </div>
+
+                <div class="col-12">
+                    <br/>
+                        <div class="input-group input-group-sm mb-3">
+                          <div class="input-group-prepend">
+                            <h5 class="input-group-text" id="{{ card_id }}_inputGroup-sizing-sm" style="font-size: 13px">Micrograph</h5>
+                          </div>
+                          <input type="text" id="{{ card_id }}_mic_id" class="form-control" aria-label="Small" aria-describedby="{{ card_id }}_inputGroup-sizing-sm">
+                           <div class="row ml-2 mr-2">
+                                <a id="{{ card_id }}_mic_left"><i class="fas fa-arrow-circle-left mr-1"></i></a>
+                                <a id="{{ card_id }}_mic_right"><i class="fas fa-arrow-circle-right"></i></a>
+                           </div>
+                        </div>
+                </div>
         </div>
 
          <!-- PSD panel with CTF values -->
-         <div class="row col-6 mt-2 ml-0 pl-2 mr-0 pr-0"  style="min-width: 350px;">
+         <div class="row col-3">
 
-            <div class="col-4 ml-0 pl-0 mr-0 pr-0">
+            <div class="col-9">
                 <figure class="figure">
                     <img class="img-fluid" id="{{ card_id }}_img_psd" src="">
                 </figure>
             </div>
 
-            <div class="col-8 mr-0 pr-0">
-                <br/>
-                    <div class="input-group input-group-sm mb-3">
-                      <div class="input-group-prepend">
-                        <h5 class="input-group-text" id="{{ card_id }}_inputGroup-sizing-sm" style="font-size: 13px">Micrograph</h5>
-                      </div>
-                      <input type="text" id="{{ card_id }}_mic_id" class="form-control" aria-label="Small" aria-describedby="{{ card_id }}_inputGroup-sizing-sm">
-                       <div class="row ml-2 mr-2">
-                            <a id="{{ card_id }}_mic_left"><i class="fas fa-arrow-circle-left mr-1"></i></a>
-                            <a id="{{ card_id }}_mic_right"><i class="fas fa-arrow-circle-right"></i></a>
-                       </div>
-                    </div>
-            </div>
-             <div class="col-12 mt-1">
+
+             <div class="col-12 mt-3">
 
                  {{ mic_row('Defocus U (µm)', '', card_id + '_mic_defocus_u') }}
                  {{ mic_row('Defocus V (µm)', '', card_id + '_mic_defocus_v') }}
@@ -57,15 +62,26 @@ <h5 class="input-group-text" id="{{ card_id }}_inputGroup-sizing-sm" style="font
                  {{ mic_row('Astigmatism  (µm)', '', card_id + '_mic_astigmatism') }}
                  {{ mic_row('Resolution (Å)', '', card_id + '_mic_resolution') }}
 
-                 <div class="row col-12 p-0 m-0">
-                    <label class="text-muted col-6 text-right mt-1" style="font-size: 13px">Particles</label>
-                    <label class="col-4 text-right mt-1" id="{{ card_id }}_particles">{{ value }}</label>
-                    <label class="custom-control custom-checkbox col-1 m-0 p-0">
-                        <input type="checkbox" checked class="custom-control-input" id="{{ card_id }}_show_particles">
-                            <span class="custom-control-label"></span>
-                    </label>
+             <div class="col-12 ml-0 pl-0 mr-0 pr-0 mt-3">
+                 <div class="input-group input-group-sm mb-3">
+                      <div class="input-group-prepend">
+                        <h5 class="input-group-text" id="{{ card_id }}_particles-sm" style="font-size: 13px">Particles</h5>
+                      </div>
+                      <input type="text" readonly id="{{ card_id }}_particles" class="form-control" aria-label="Small" aria-describedby="{{ card_id }}_particles-sm">
+                    <div class="switch-field ml-2 mr-2">
+                         {% set radio_name = card_id + "_pts_switch" %}
+                        <input type="radio" id="{{ radio_name }}-one" name="{{ radio_name }}" value="none"/>
+                        <label for="{{ radio_name }}-one"><i class="far fa-times-circle"></i></label>
+                        <input type="radio" id="{{ radio_name }}-two" name="{{ radio_name }}" value="dot" checked />
+                        <label for="{{ radio_name }}-two"><i class="far fa-dot-circle"></i></label>
+                         <input type="radio" id="{{ radio_name }}-three" name="{{ radio_name }}" value="circle" />
+                        <label for="{{ radio_name }}-three"><i class="far fa-circle"></i></label>
+                    </div>
+
                 </div>
              </div>
+
+             </div>
         </div>
 
          <div class="row col-12 mt-2 ml-3 mr-3" style="min-height: 400px">
diff --git a/emhub/templates/news.html b/emhub/templates/news.html
new file mode 100644
index 00000000..9634a88b
--- /dev/null
+++ b/emhub/templates/news.html
@@ -0,0 +1,34 @@
+
+{% import "entry_macros.html" as macros %}
+
+<div class="col-12 pl-0 mb-3">
+    <a href="javascript:showEntryForm(null, {{ project_id }}, 'news')" class="btn btn-primary"><i class="fas fa-plus-circle"></i> Create News</a>
+</div>
+
+
+{{ macros.news_card_div('News', news[0], display, current_user.is_manager) }}
+{{ macros.news_card_div('Inactive', news[1], display, current_user.is_manager) }}
+
+<!-- ============================================================== -->
+<!-- Entry Modal -->
+<!-- ============================================================== -->
+<div class="modal fade" id="entry-modal" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static" data-keyboard="false">
+</div>
+
+
+<script>
+    (function(window, document, $, undefined) {
+    "use strict";
+
+    $('.table-news').DataTable({
+        sDom: 'lrtip',
+        order: [[ 1, "desc" ]], // sorted by 'last_updated' in reverse order
+        //ordering: true,
+        paging: false,
+        info: false
+    });
+
+    })(window, document, window.jQuery);
+
+
+</script>
\ No newline at end of file
diff --git a/emhub/templates/plot_card.html b/emhub/templates/plot_card.html
new file mode 100644
index 00000000..dcfd2eb9
--- /dev/null
+++ b/emhub/templates/plot_card.html
@@ -0,0 +1,35 @@
+{% macro row_plot_label(sel_id, title, selectedValue) -%}
+    <div class="row" style="min-width: 325px;">
+        <h4 class="col-form-label text-right mr-2">{{ title }}</h4>
+        <select id="{{ card_id }}_{{ sel_id }}" class="selectpicker mr-3" style="width: 100%">
+            {% for k, v in data_values.items() %}
+                {% if v.data %}
+                    {% set selected = 'selected' if k == selectedValue else '' %}
+                    <option value="{{ k }}" {{ selected }}>{{ k }} </option>
+                {% endif %}
+            {% endfor %}
+        </select>
+    </div>
+{%- endmacro %}
+
+<div class="col-12 row align-content-start mb-3 ml-3">
+    {{ row_plot_label('labelX-select', 'X Label', 'index') }}
+    {{ row_plot_label('labelY-select', 'Y Label', 'rlnCtfMaxResolution') }}
+    {{ row_plot_label('labelColor-select', 'Color Label') }}
+    <div class="row ml-3">
+        <a  class="btn btn-dark float-right" id="{{ card_id }}_update-plots-btn"> Update Plots</a>
+    </div>
+</div>
+
+<div id='{{ card_id }}_plotDiv' class="col-12" style="height: 700px;">
+<!-- Plotly chart will be drawn inside this DIV -->
+</div>
+
+<div class="col-12 row m-3">
+    <div class="col-auto">
+        <h4 id="{{ card_id }}_selection-label">No points selected.</h4>
+    </div>
+    <div class="col-auto">
+        <a href="javascript:exportSelection()" class="btn btn-dark ml-3 float-left" id="export-btn" style="display: none">Export Selection</a>
+    </div>
+</div>
\ No newline at end of file
diff --git a/emhub/templates/processing_2d_summary.html b/emhub/templates/processing_2d_summary.html
index 4b23acd1..7527c7d5 100644
--- a/emhub/templates/processing_2d_summary.html
+++ b/emhub/templates/processing_2d_summary.html
@@ -4,30 +4,28 @@
 <div class="col-12 m-0 p-0" id="classes2d_plot">
     {{ macros.overlay('overlay_classes2d') }}
     <div class="row col-12 m-0 p-0">
-            <div id="distr-iter-plot" class="col-6"></div>
-            <div id="distr-overall-plot" class="col-6"></div>
-        <div class="row col-12 m-0 p-0 mt-2" id="classes2d_container"></div>
+        <div class="row col-8 m-0 p-0 mt-2" id="classes2d_container"></div>
+            <div id="distr-iter-plot" class="col-4"></div>
+{#            <div id="distr-overall-plot" class="col-6"></div>#}
+
     </div>
 </div>
 
 <script>
     var iterations = {{ data_values['iterations']|tojson }};
     var classes2d = null;
-    var attrs = {
-        entry_id: {{ entry_id|tojson }},
-        runId: {{ runId|tojson }}
-    };
+    var args = getArgs();
 
     var overlay_2d = null;
 
     function load_classes2d(){
-        overlay_2d.show("Loading Class2D, run " + attrs.runId);
+        overlay_2d.show("Loading Class2D, run " + args.runId);
         // Update template values
         var ajaxContent = $.ajax({
             url: Api.urls.get_classes2d,
             type: "POST",
             //contentType: 'application/json; charset=utf-8',
-            data: attrs,
+            data: args,
             dataType: "json"
         });
 
@@ -159,7 +157,7 @@
         overlay_2d = new Overlay('overlay_classes2d');
 
         load_classes2d();
-        plot_distribution_overall();
+        //plot_distribution_overall();
 });
 })(window, document, window.jQuery);
 </script>
\ No newline at end of file
diff --git a/emhub/templates/processing_3d_summary.html b/emhub/templates/processing_3d_summary.html
new file mode 100644
index 00000000..991103cf
--- /dev/null
+++ b/emhub/templates/processing_3d_summary.html
@@ -0,0 +1,72 @@
+{% set card_id = 'vol_summary_card' %}
+
+<div class="row col-12">
+    <div class="col-12 row mb-2">
+            <h4 class="mt-1 mr-2">Iteration </h4>
+            <select id="iteration-select" class="selectpicker" onchange="change_plots()">
+                {% for iter in iterations %}
+                    {% set selected = 'selected' if iter == iter_file else '' %}
+                    <option value="{{ iter }}" {{ selected }}>{{ iter|basename }} </option>
+                {% endfor %}
+            </select>
+        </div>
+
+    <table id="table-volumes" class="table table-responsive table-m-0 p-0" style="width: 100%">
+        <thead class="bg-light">
+            <tr class="m-0 p-0">
+                <th>Path</th>
+                <th>Slices</th>
+                {% for k in volumes[0]['data'].keys() %}
+                    <td>{{ k }}</td>
+                {% endfor %}
+            </tr>
+        </thead>
+
+        <tbody>
+            {% for vol in volumes %}
+            <tr>
+                <td><a href="javascript:loadRunFileInfo('{{ vol['file_path'] }}')">{{ vol['file_path'] }}</a></td>
+                <td>
+                    <div id="{{ card_id }}_vol{{ loop.index0 }}_slider"></div>
+                </td>
+                 {% for k, v in vol['data'].items() %}
+                    <td>{{ v }}</td>
+                 {% endfor %}
+            </tr>
+            {% endfor %}
+        </tbody>
+        </table>
+    </div>
+</div>
+
+<script>
+    var volumes = {{ volumes|tojson }};
+
+    (function(window, document, $, undefined) {
+    "use strict";
+    $(function() {
+        $('.selectpicker').selectpicker();
+
+        $('#table-volumes').DataTable({
+            sDom: 'lrtip',
+            paging: false,
+            info: false,
+            ordering: false
+        });
+
+        for (var i = 0; i < volumes.length; ++i) {
+            new VolumeSliderCard("{{ card_id }}_vol" + i + "_slider", volumes[i].slices,
+                {slice_dim: getObjectValue(volumes[i], 'slice_dim', 128)});
+        }
+
+        $('#iteration-select').on('change', function (){
+            let iter_file = $(this).val();
+            setLoading('summary_row');
+            load_html_from_ajax('summary_row',
+                  get_ajax_content('processing_run_summary', getArgs({iter_file: iter_file})));
+        })
+
+
+});
+})(window, document, window.jQuery);
+</script>
\ No newline at end of file
diff --git a/emhub/templates/processing_content.html b/emhub/templates/processing_content.html
index b76258a0..f9fd75ab 100644
--- a/emhub/templates/processing_content.html
+++ b/emhub/templates/processing_content.html
@@ -168,6 +168,11 @@ <h3 class="modal-title" id="overview-modal-title"> Testing modal  </h3>
         });
     })(window, document, window.jQuery);
 
+    function setLoading(containerId) {
+        var loadingHtml = '<div class="row align-items-start ml-3 mt-3"><div class="row"><h3 class="mt-2 mr-3 text-black-50">Loading...</h3><div class="spinner-border ml-auto" role="status" aria-hidden="true"></div></div></div>';
+          // Set loading message and load summary page
+          $('#' + containerId).html(loadingHtml);
+    }
 
     function clickOnCanvas(params){
         var  nodeId = params.nodes[0];
@@ -187,11 +192,13 @@ <h3 class="modal-title" id="overview-modal-title"> Testing modal  </h3>
               stderrEditor.setValue('');
               jsonEditor.setValue('');
 
+              setLoading('run_form_container');
+              setLoading('summary_row');
+
               loadRun(['json']);
               loadRun(['stdout']);
               loadRun(['stderr']);
 
-              // Load summary page
               load_html_from_ajax('summary_row',
                   get_ajax_content('processing_run_summary', getArgs()));
           }
@@ -213,7 +220,7 @@ <h3 class="modal-title" id="overview-modal-title"> Testing modal  </h3>
             //alert(JSON.stringify(data));
             if ('json' in run_data) {
                 runDict = run_data['json']
-                jsonEditor.setValue(JSON.stringify(run_data, null, 4), 1);
+                //jsonEditor.setValue(JSON.stringify(run_data, null, 4), 1);
                 if (selected_node.type in forms) {
                     form_data = forms[selected_node.type]
                     //jsonEditor.setValue(JSON.stringify(form_data, null, 4), 1);
@@ -247,7 +254,7 @@ <h3 class="modal-title" id="overview-modal-title"> Testing modal  </h3>
                 {#$('#numberOfMpi').val(values.numberOfMpi);#}
 
                 form_create(form_data, values, 'run_form_container');
-                //jsonEditor.setValue(JSON.stringify(form_data, null, 4));
+                jsonEditor.setValue(JSON.stringify(values, null, 4));
             }
 
             if ('stdout' in run_data)
@@ -264,11 +271,26 @@ <h3 class="modal-title" id="overview-modal-title"> Testing modal  </h3>
 
     function loadRunOverview(title){
         $('#overview-modal-title').text(title);
+        setLoading('overview-content');
         $('#overview-modal').modal('show');
          load_html_from_ajax('overview-content',
                   get_ajax_content('processing_run_overview', getArgs()));
     }
 
+    function loadRunFileInfo(path) {
+        $('#overview-modal-title').text(path);
+        setLoading('overview-content');
+        $('#overview-modal').modal('show');
+         load_html_from_ajax('overview-content',
+                  get_ajax_content('processing_run_overview', getArgs({file_path: path})));
+    }
+
+    function loadStarRows(path, tableName) {
+         load_html_from_ajax('star_card_content',
+                  get_ajax_content('processing_run_overview',
+                      getArgs({file_path: path, table_name: tableName})));
+    }
+
     function clickOnExpert() {
         display_expert = $('#switch-expert').prop('checked') ? 'flex' : 'none';
         $('.scn-expert-param').css('display', display_expert);
diff --git a/emhub/templates/processing_ctf_overview.html b/emhub/templates/processing_ctf_overview.html
index dcede3ec..2d379dd1 100644
--- a/emhub/templates/processing_ctf_overview.html
+++ b/emhub/templates/processing_ctf_overview.html
@@ -1,44 +1,19 @@
-{% macro row_plot_label(sel_id, title, selectedValue) -%}
-    <div class="row" style="min-width: 325px;">
-        <h4 class="col-form-label text-right mr-2">{{ title }}</h4>
-        <select id="{{ sel_id }}" class="selectpicker mr-3" style="width: 100%" onchange="highlightButton()">
-            {% for s in data_values %}
-                {% set selected = 'selected' if s == selectedValue else '' %}
-                <option value="{{ s }}" {{ selected }}>{{ s }} </option>
-            {% endfor %}
-        </select>
-    </div>
-{%- endmacro %}
 
-<div class="row col-12 p-0 m-0">
-        <div class="col-12 row align-content-start mb-3 ml-3">
-            {{ row_plot_label('plot-labelX-select', 'X Label', 'index') }}
-            {{ row_plot_label('plot-labelY-select', 'Y Label', 'rlnCtfMaxResolution') }}
-            {{ row_plot_label('plot-labelColor-select', 'Color Label') }}
-            <div class="row ml-3">
-                <a href="javascript:makePlot()" class="btn btn-dark float-right" id="update-plots-btn"> Update Plots</a>
-            </div>
-        </div>
 
+<div class="row col-12 p-0 m-0">
 
     <div class="col-6">
-        <div id='myDiv' class="col-12" style="height: 700px;">
-        <!-- Plotly chart will be drawn inside this DIV -->
-        </div>
-        <div class="col-12 row m-3">
-            <div class="col-auto">
-                <h4 id="selection-label">No points selected.</h4>
-            </div>
-            <div class="col-auto">
-                <a href="javascript:exportSelection()" class="btn btn-dark ml-3 float-left" id="export-btn" style="display: none">Export Selection</a>
-            </div>
-        </div>
+        {% set card_id = 'ctf_plot' %}
+        {% include 'plot_card.html' %}
     </div>
 
     <div id="micDiv" class="col-6">
         <!-- Div for Micrograph -->
         {% set card_id = 'mic_card_overview' %}
-        {% include 'micrograph_card_side.html' %}
+        {% set has_ctf = data_values.get('has_ctf', False) %}
+        {% set has_particles = data_values.get('has_particles', False) %}
+        {% set show_ctf_plot = True %}
+        {% include 'micrograph_card_big.html' %}
     </div>
 
 
@@ -47,102 +22,13 @@ <h4 id="selection-label">No points selected.</h4>
 <script>
     var data_values = {{ data_values|tojson }};
     var mic_card_overview = null;
+    var plot_card = null;
 
-    function highlightButton(){
-        //$('#update-plots-btn').toggleClass('btn-outline-dark btn-dark');
-    }
-
-    function makePlot(){
-        //$('#update-plots-btn').toggleClass('btn-dark btn-outline-dark');
-
-        let label_x = $('#plot-labelX-select').selectpicker('val');
-        let data_x = data_values[label_x].data;
-
-        let label_y = $('#plot-labelY-select').selectpicker('val');
-        let data_y = data_values[label_y].data;
-
-        let label_color = $('#plot-labelColor-select').selectpicker('val');
-        let data_color = data_values[label_color].data;
-
-        let colorscale = [[0.0, '#440154'], [0.1111111111111111,
-                                            '#482878'], [0.2222222222222222,
-                                            '#3e4989'], [0.3333333333333333,
-                                            '#31688e'], [0.4444444444444444,
-                                            '#26828e'], [0.5555555555555556,
-                                            '#1f9e89'], [0.6666666666666666,
-                                            '#35b779'], [0.7777777777777778,
-                                            '#6ece58'], [0.8888888888888888,
-                                            '#b5de2b'], [1.0, '#fde725']];
-        var data = [
-            {'fillcolor': 'white',
-                  'marker': {'color': data_color,
-                             'colorscale': 'Portland',
-                             'opacity': 0.75,
-                                'size': 10,
-                             'showscale': true},
-                  'mode': 'markers',
-                  'type': 'scattergl',
-                  'x': data_x,
-                  'y': data_y },
-             {'marker': {'color': 'cornflowerblue', 'opacity': 0.5},
-              'name': label_x,
-              'type': 'violin',
-              'x': data_x,
-              'yaxis': 'y2'},
-             {'marker': {'color': 'cornflowerblue', 'opacity': 0.5},
-              'name': label_y,
-              'type': 'violin',
-              'xaxis': 'x2',
-              'y': data_y}
-        ];
-
-        var layout = {'bargap': 0,
-           'hovermode': 'closest',
-           'margin': {'t': 50, 'l': 0, 'r': 0},
-           'autosize': true,
-           'plot_bgcolor': 'rgba(0,0,0,0)',
-           'showlegend': false,
-           'template': '...',
-           'xaxis': {'domain': [0, 0.85], 'gridcolor': '#CBCBCB', 'title': {'text': label_x}, 'zeroline': true},
-           'xaxis2': {'domain': [0.85, 1], 'showgrid': true, 'zeroline': false},
-           'yaxis': {'domain': [0, 0.85],
-                     'gridcolor': '#CBCBCB',
-                     'title': {'text': label_y},
-                     'zeroline': true},
-           'yaxis2': {'domain': [0.85, 1], 'showgrid': true, 'zeroline': false}};
-
-        Plotly.newPlot('myDiv', data, layout);
-
-        let graphDiv = document.getElementById('myDiv');
-
-        graphDiv.on('plotly_selected', function(eventData) {
-            if (!eventData) {
-                $('#selection-label').text("No points selected.");
-                $('#export-btn').hide();
-                return;
-            }
-
-
-            let points = eventData.points;
-
-            $('#selection-label').html("Selected <label style='color: firebrick; font-size: medium;'>" + points.length + "</label> points")
-            $('#export-btn').show();
-
-            points.forEach(function(pt) {
-                console.log(pt.pointNumber + " x: " + pt.x);
-            });
-        });
-
-        graphDiv.on('plotly_doubleclick', function(eventData) {
-            alert('click');
-
-        });
-
-        graphDiv.on('plotly_click', function(eventData) {
-            let micNumber = eventData.points[0].pointNumber;
-            mic_card_overview.loadMicData(micNumber + 1)
-        });
-
+    function createPlotCard(){
+        plot_card = new PlotCard('ctf_plot', data_values);
+        plot_card.onPointClick = function (index) {
+            mic_card_overview.loadMicData(index + 1) };
+        plot_card.makePlots();
     }
 
     (function(window, document, $, undefined) {
@@ -150,7 +36,7 @@ <h4 id="selection-label">No points selected.</h4>
     $(function() {
 
         $('.selectpicker').selectpicker();
-        setTimeout(makePlot, 500);
+        setTimeout(createPlotCard, 500);
         mic_card_overview = new MicrographCard('mic_card_overview', getArgs({micId: null}));
         mic_card_overview.loadMicData(1);
 
diff --git a/emhub/templates/processing_ctf_summary.html b/emhub/templates/processing_ctf_summary.html
index 694d5c9d..0120e531 100644
--- a/emhub/templates/processing_ctf_summary.html
+++ b/emhub/templates/processing_ctf_summary.html
@@ -6,8 +6,10 @@
         <div class="col-6 row">
             <h4 class="mt-1 mr-2">Plot Label </h4>
             <select id="plot-label-select" class="selectpicker" onchange="change_plots()">
-                {% for s in data_values %}
-                    <option value="{{ s }}">{{ s }} </option>
+                {% for k, v in data_values.items() %}
+                    {% if v.data %}
+                        <option value="{{ k }}">{{ k }} </option>
+                    {% endif %}
                 {% endfor %}
             </select>
         </div>
@@ -26,7 +28,9 @@ <h4 class="mt-1 mr-2">Plot Label </h4>
     <div class="col-8">
         <!-- Div for Micrograph -->
         {% set card_id = 'mic_card' %}
-        {% include 'micrograph_card_side.html' %}
+        {% set has_ctf = data_values.get('has_ctf', False) %}
+        {% set has_particles = data_values.get('has_particles', False) %}
+        {% include 'micrograph_card_big.html' %}
     </div>
 
 
diff --git a/emhub/templates/processing_run_summary.html b/emhub/templates/processing_run_summary.html
index b0f5f74b..8f258aa9 100644
--- a/emhub/templates/processing_run_summary.html
+++ b/emhub/templates/processing_run_summary.html
@@ -1,5 +1,6 @@
 
 <div class="card col-12" id="summary-div">
+
     <div class="card-body m-0 p-0">
         {% set ios = run.getInputsOutputs() %}
         <div class="row mt-2">
@@ -7,7 +8,7 @@
                 <h4>Inputs</h4>
                 <ul>
                     {% for i in ios['inputs'] %}
-                        <li>{{ i }}</li>
+                        <li><a href="javascript:loadRunFileInfo('{{ i }}')">{{ i }}</a></li>
                     {% endfor %}
                 </ul>
             </div>
@@ -15,20 +16,17 @@ <h4>Inputs</h4>
                 <h4>Outputs</h4>
                 <ul>
                     {% for o in ios['outputs'] %}
-                        <li>{{ o }}</li>
+                        <li><a href="javascript:loadRunFileInfo('{{ o }}')">{{ o }}</a></li>
                     {% endfor %}
                 </ul>
             </div>
         </div>
-
-
-
     </div>
 
 </div>
 
 {% if template %}
-    <div class="card col-12 overflow-auto p-0 m-0" id="summary_row"  style="max-height: 880px">
+    <div class="card col-12 overflow-auto p-0 m-0" id="summary_row"  style="max-height: 800px">
         <div class="card-body">
             {% include template %}
         </div>
diff --git a/emhub/templates/processing_star_card.html b/emhub/templates/processing_star_card.html
new file mode 100644
index 00000000..df4df6fa
--- /dev/null
+++ b/emhub/templates/processing_star_card.html
@@ -0,0 +1,26 @@
+<div class="card">
+    <h4 class="card-header">Table: {{ default_table }}</h4>
+    <div class="card-body p-0">
+
+        <table id="table-rows" class="table table-responsive">
+            <thead class="bg-light">
+            <tr class="border-0">
+                {% for col in columns %}
+                    <th class="border-0"><h5>{{ col }}</h5></th>
+                {% endfor %}
+            </tr>
+            </thead>
+
+            <tbody>
+                {% for row in rows %}
+                    <tr>
+                    {% for col in columns %}
+                        <td>{{ row[col] }}</td>
+                    {% endfor %}
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+
+    </div>
+</div>
diff --git a/emhub/templates/processing_star_overview.html b/emhub/templates/processing_star_overview.html
new file mode 100644
index 00000000..30ef8a1b
--- /dev/null
+++ b/emhub/templates/processing_star_overview.html
@@ -0,0 +1,65 @@
+
+
+<div class="row col-12 p-0 m-0">
+
+    <div class="col-3">
+        <div class="card p-0">
+            <h4 class="card-header">All Tables</h4>
+            <div class="card-body p-0">
+                <table id="table-all" class="table" style="width: 100%">
+                    <thead class="bg-light">
+                    <tr class="border-0">
+                        <th class="border-0"><h5>Table Name</h5></th>
+                        <td class="border-0"><h5>Columns</h5></td>
+                        <td class="border-0"><h5>Rows</h5></td>
+                    </tr>
+                    </thead>
+
+                    <tbody>
+                        {% for tn, t in tables.items() %}
+                            <tr>
+                                <td><a href="javascript:loadStarRows(file_path, '{{ tn }}')">{{ tn }}</a> </td>
+                                <td>{{ t['columns']|length }}</td>
+                                <td>{{ t['rows'] }}</td>
+                            </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+
+    {% set columns = tables[default_table]['columns'] %}
+
+    <div id="star_card_content" class="col-9">
+        {% include "processing_star_card.html" %}
+    </div>
+
+
+</div>
+
+<script>
+    var file_path = {{ file_path|tojson }};
+
+    (function(window, document, $, undefined) {
+    "use strict";
+    $(function() {
+
+        $('.selectpicker').selectpicker();
+        $('#table-all').DataTable({
+            sDom: 'lrtip',
+            paging: false,
+            info: false,
+            ordering: false
+        });
+        $('#table-rows').DataTable({
+            sDom: 'lrtip',
+            paging: false,
+            info: false,
+            ordering: false
+        });
+
+
+});
+})(window, document, window.jQuery);
+</script>
\ No newline at end of file
diff --git a/emhub/templates/processing_volume_card.html b/emhub/templates/processing_volume_card.html
new file mode 100644
index 00000000..f7162acf
--- /dev/null
+++ b/emhub/templates/processing_volume_card.html
@@ -0,0 +1,79 @@
+{% set card_id = card_id or 'vol_card' %}
+
+<div class="row">
+
+    <!-- Slider Column -->
+    <div class="col-2">
+        <div class="card ml-3">
+            <h4 class="card-header">Axis Slider</h4>
+            <div class="card-body" id="{{ card_id }}_vol_slider">
+            </div>
+        </div>
+    </div>
+
+    <!-- Slices Gallery Column -->
+    <div class="col-5">
+       <div class="card ml-3">
+            <h4 class="card-header">Slices Gallery</h4>
+            <div class="card-body">
+                <div class="row">
+                <label class="mt-1 ml-4">Slice Axis: </label>
+                <div class="switch-field ml-2 mr-2">
+
+                     {% set radio_name = card_id + "_axis_switch" %}
+                    <input type="radio" id="{{ radio_name }}-one" checked name="{{ radio_name }}" value="z"/>
+                    <label for="{{ radio_name }}-one">Z</label>
+                    <input type="radio" id="{{ radio_name }}-two" name="{{ radio_name }}" value="y" />
+                    <label for="{{ radio_name }}-two">Y</label>
+                     <input type="radio" id="{{ radio_name }}-three"  name="{{ radio_name }}" value="x" />
+                    <label for="{{ radio_name }}-three">X</label>
+                </div>
+                </div>
+
+                <!-- div used to draw the volume slices -->
+                <div class="col" id="{{ card_id }}_volume-slices"></div>
+
+            </div>
+        </div>
+    </div>
+
+    <!-- 3D viewer column -->
+    <div class="col-5">
+        <div class="card ml-3">
+            <h4 class="card-header">3D Viewer</h4>
+            <div class="card-body" id="{{ card_id }}_vol_3d">
+
+                <!-- div used to render 3d volume -->
+                <div id="{{ card_id }}_vol_viewport" style="width:100%; height: 800px"></div>
+                <div class="mt-2"><input id="{{ card_id }}_vol_viewport_slider" type="range" min="0" max="255", step="1"></div>
+            </div>
+        </div>
+    </div>
+
+
+</div>
+
+
+<script>
+    var file_path = {{ file_path|tojson }};
+    var slices = {{ slices|tojson }};
+    var isx, isy, isz = null;
+    var dimensions = {{ dimensions|tojson }};
+    var array = {{ array|tojson }};
+
+    (function(window, document, $, undefined) {
+    "use strict";
+    $(function() {
+        drawVolData("{{ card_id }}_volume-slices", slices.z);
+
+        var vs = new VolumeSliderCard("{{ card_id }}_vol_slider", slices);
+
+        $('input[name="{{ card_id }}_axis_switch"]').change(function () {
+                drawVolData("{{ card_id }}_volume-slices", slices[$(this).val()]);
+            })
+
+        renderVolume3D("{{ card_id }}_vol_viewport", array, dimensions);
+
+});
+})(window, document, window.jQuery);
+</script>
\ No newline at end of file
diff --git a/emhub/templates/processing_volume_summary.html b/emhub/templates/processing_volume_summary.html
new file mode 100644
index 00000000..9eac6dbf
--- /dev/null
+++ b/emhub/templates/processing_volume_summary.html
@@ -0,0 +1,46 @@
+{% set card_id = 'vol_summary_card' %}
+
+<div class="row col-12">
+    <table id="table-volumes" class="table table-responsive table-m-0 p-0">
+        <thead class="bg-light">
+            <tr class="m-0 p-0">
+                <th>Path</th>
+                <th>Slices</th>
+            </tr>
+        </thead>
+
+        <tbody>
+            {% for vol in volumes %}
+            <tr>
+                <td><a href="javascript:loadRunFileInfo('{{ vol['file_path'] }}')">{{ vol['file_path'] }}</a></td>
+                <td>
+                    <div id="{{ card_id }}_vol{{ loop.index0 }}_slider"></div>
+                </td>
+            </tr>
+            {% endfor %}
+        </tbody>
+        </table>
+    </div>
+</div>
+
+<script>
+    var volumes = {{ volumes|tojson }};
+
+    (function(window, document, $, undefined) {
+    "use strict";
+    $(function() {
+        for (var i = 0; i < volumes.length; ++i) {
+            new VolumeSliderCard("{{ card_id }}_vol" + i + "_slider", volumes[i].slices,
+                {slice_dim: getObjectValue(volumes[i], 'slice_dim', 128)});
+        }
+
+        $('#table-volumes').DataTable({
+            sDom: 'lrtip',
+            paging: false,
+            info: false,
+            ordering: false
+        });
+
+});
+})(window, document, window.jQuery);
+</script>
\ No newline at end of file
diff --git a/emhub/templates/raw_booking_list.html b/emhub/templates/raw_booking_list.html
index a3c9f8b1..28bdef05 100644
--- a/emhub/templates/raw_booking_list.html
+++ b/emhub/templates/raw_booking_list.html
@@ -32,6 +32,7 @@ <h5 class="mb-0">Bookings List</h5>
                                 <th>project</th>
                                 <th>dates</th>
                                 <th>extra</th>
+                                <th>slot_auth</th>
                                 <th>experiment</th>
                                 <th>sessions</th>
                                 <th> actions </th>
@@ -56,6 +57,7 @@ <h5 class="mb-0">Bookings List</h5>
                                 {% endif %}
                                 <td>{{ b.start|pretty_datetime }} - {{ b.end|pretty_datetime }}</td>
                                 <td>{{ b.extra }}</td>
+                                <td>{{ b.slot_auth }}</td>
                                 <td>{{ b.experiment }}</td>
                                 <td>
                                     {% set sessions = b.session %}
diff --git a/emhub/templates/raw_projects_list.html b/emhub/templates/raw_projects_list.html
new file mode 100644
index 00000000..aef4b52e
--- /dev/null
+++ b/emhub/templates/raw_projects_list.html
@@ -0,0 +1,96 @@
+{% import "entry_macros.html" as macros %}
+
+<div class="container-fluid dashboard-content">
+    <!-- Header -->
+    {% set title = "Projects List" %}
+    {% set navigation = [('Projects', '')] %}
+    {% include 'include_header.html' %}
+    {% set is_manager = current_user.is_manager %}
+
+    <!-- table row -->
+    {{ macros.hidden_hour_id() }}
+
+    <div class="row">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-body">
+
+                    <div class="table-responsive" id="table-content">
+
+                        <table id="projects-table" class="table">
+                            <thead>
+                                <tr>
+                                    <th>id</th>
+                                    <th>status</th>
+                                    <th>last updated</th>
+                                    <th>user/pi</th>
+                                    <th>title</th>
+                                    <th>extra</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                            {% for p in projects %}
+                                {% set status = p['status'] %}
+                                {% if is_manager or status != 'disabled' %}
+                                    {% set color = 'black' if status == 'active' else 'gray' %}
+                                    <tr style="color: {{ color }}">
+                                        <td>
+                                            <div class="col-12">{{ macros.project(p) }}</div>
+                                            {% if p.user_can_edit %}
+                                                 <a href="javascript:showProjectForm({{ p['id'] }})" class="btn btn-outline-light">
+                                                     <i class="far fa-edit"></i></a>
+                                            {% endif %}
+                                            {% if  current_user.can_delete_project(p) %}
+                                                 <a href="javascript:deleteProject({{ p['id'] }})" class="btn btn-outline-light">
+                                                     <i class="far fa-trash-alt"></i></a>
+                                            {% endif %}
+                                        </td>
+                                        <td>{{ p.status }}</td>
+                                        <td>{{ p.last_update_date|pretty_datetime }} by {{ p.last_update_user.name }}</td>
+                                        {% set pi = p.user.get_pi() %}
+                                        {% set apps = pi.get_applications() if pi else [] %}
+                                        <td>{{ p.user|pairname }} {{ app }}</td>
+                                        <td>{{ p.display_title }}</td>
+                                        <td> {{ p.extra }} </td>
+                                    </tr>
+                                {% endif %}
+                            {% endfor %}
+
+                            </tbody>
+                        </table>
+
+                    </div>
+
+
+                </div>
+            </div>
+        </div>
+    </div>
+    <!-- end table row -->
+
+</div>
+
+<!-- ============================================================== -->
+<!-- Project Modal -->
+<!-- ============================================================== -->
+<div class="modal fade" id="project-modal" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static" data-keyboard="false">
+</div>
+
+
+<script>
+    (function(window, document, $, undefined) {
+    "use strict";
+
+    $('select').selectpicker();
+
+    var costs_table = $('#projects-table').DataTable({
+        sDom: 'lrtip',
+        ordering: true,
+        paging: false,
+        info: false
+    });
+
+    })(window, document, window.jQuery);
+
+
+</script>
\ No newline at end of file
diff --git a/emhub/templates/report_microscopes_usage_content.html b/emhub/templates/report_microscopes_usage_content.html
index 3f0e399f..8684c602 100644
--- a/emhub/templates/report_microscopes_usage_content.html
+++ b/emhub/templates/report_microscopes_usage_content.html
@@ -120,6 +120,9 @@ <h3>Usage Distribution</h3>
                                             {% endif %}
                                         </strong></td>
                                         {% endif %}
+                                        {% if nres > 1 %}
+                                            <td>{{ entry['total_days'] }}</td>
+                                        {% endif %}
                                         <td>{{ '%0.2f'|format(entry['total_days'] * 100 / total_usage) }}</td>
                                         <td>
                                             {% if entry['bookings'] %}
diff --git a/emhub/templates/session_live.html b/emhub/templates/session_live.html
index 24b3f425..cba4be54 100644
--- a/emhub/templates/session_live.html
+++ b/emhub/templates/session_live.html
@@ -221,6 +221,7 @@ <h5 class="card-header">Polar Charts</h5>
 
                                      <!-- Div for Micrograph -->
                                     {% set card_id = 'mic_card' %}
+                                    {% set show_ctf_plot = False %}
                                     {% include 'micrograph_card.html' %}
 
                                     <div class="col-12 text-sm-right mb-3">
@@ -386,7 +387,7 @@ <h3 class="modal-title" id="plot-title"> Plot </h3>
                 gs_card = new GridSquareCard('gs_card');
             let params = {
                 session_id: session_id,
-                run_id: 199 // FIXME: This should be CTF job
+                run_id: {{ ctfs_run_id|tojson }} // FIXME: This should be CTF job
             };
             mic_card = new MicrographCard('mic_card', params, gs_card);
             overlay_2d = new Overlay('overlay_classes2d');
diff --git a/emhub/utils/__init__.py b/emhub/utils/__init__.py
index a5aa63ca..149c4abf 100644
--- a/emhub/utils/__init__.py
+++ b/emhub/utils/__init__.py
@@ -109,6 +109,8 @@ def shortname(user):
 
 def pairname(user):
     """ Show PI/User in a shortname. """
+    if user is None:
+        return ''
     pi = user.get_pi()
     if pi and not user.is_pi:
         return f"{shortname(pi)} / {shortname(user)}"
diff --git a/extras/stjude_cryoem/app_setup.py b/extras/stjude_cryoem/app_setup.py
new file mode 100644
index 00000000..61a511b1
--- /dev/null
+++ b/extras/stjude_cryoem/app_setup.py
@@ -0,0 +1,37 @@
+
+import os
+from glob import glob
+import json
+import datetime as dt
+
+from emtools.utils import Pretty
+
+#
+# def pretty_date(input_dt):
+#     if input_dt is None:
+#         return 'None'
+#
+#     if isinstance(input_dt, str):
+#         input_dt = datetime_from_isoformat(input_dt)
+#
+#     return input_dt.strftime("%Y/%m/%d")
+
+
+
+def setup_app(app):
+
+    def _local_time(dt):
+        return app.dm.dt_as_local(dt).strftime("%I %p")
+
+    @app.template_filter('time_range')
+    def time_range(b):
+        ss = _local_time(b.start)
+        es = _local_time(b.end)
+        return f'{ss} - {es}'
+
+    @app.template_filter('booking_color')
+    def booking_color(b):
+        c = b.resource.color
+        if b.is_slot:
+            c = c.replace('1.0', '0.5')  # 0.5 transparency for slots
+        return c
diff --git a/extras/stjude_cryoem/data_content.py b/extras/stjude_cryoem/data_content.py
index 7615d04e..4705c6ee 100644
--- a/extras/stjude_cryoem/data_content.py
+++ b/extras/stjude_cryoem/data_content.py
@@ -1,10 +1,10 @@
-
 import os
 from glob import glob
 import json
 import datetime as dt
 
 from emtools.utils import Pretty
+from emhub.utils import datetime_from_isoformat
 
 
 def register_content(dc):
@@ -43,6 +43,7 @@ def cluster_queues(**kwargs):
     def cluster_queues_content(**kwargs):
         return cluster_queues(**kwargs)
 
+    @dc.content
     def access_microscopes(**kwargs):
         """ Load one of all batches. """
         dm = dc.app.dm  # shortcut
@@ -101,8 +102,8 @@ def _key(model):
             'cryolo_models': {_key(cm): cm for cm in cryolo_models}
         }
         data.update(dc.get_user_projects(b.owner, status='active'))
-        r = workers_frames(hours=10)['folderGroups'][transfer_host]
-        data['frame_folders'] = r['entries']
+        frames = workers_frames(hours=10)['folderGroups']
+        data['frame_folders'] = frames.get(transfer_host, {'entries': []})['entries']
         return data
 
     @dc.content
@@ -145,3 +146,215 @@ def workers_frames(**kwargs):
         return {
             'folderGroups': folderGroups
         }
+
+    @dc.content
+    def create_session_negstain(**kwargs):
+        """ Specific information needed to render the create-form template
+        # for St.Jude CryoEM center.
+        """
+        return create_session_form(**kwargs)
+
+    @dc.content
+    def dashboard_instrument_card(**kwargs):
+        """ Load data for a single instrument. """
+        data = dashboard(**kwargs)
+        r = None
+        for r in data['resources']:
+            if r['id'] == int(kwargs['resource_id']):
+                break
+        data['r'] = r
+        data['alignment'] = kwargs.get('alignment', 'v')
+        return data
+
+    @dc.content
+    def dashboard_createslots_card(**kwargs):
+        dm = dc.app.dm
+        kwargs['load_requests'] = False
+        data = dashboard_instrument_card(**kwargs)
+        next_week = data['next_week']
+        rid = int(kwargs['resource_id'])
+        resource = dm.get_resource_by(id=rid)
+
+        create_slots = int(kwargs.get('create_slots', 0))
+        bookings = data['resource_bookings'][rid].get('next_week', [])
+        slots_config = dm.get_config('resources').get('slots', {})
+        ranges = []
+
+        def _time(timeStr):
+            return dt.datetime.strptime(timeStr, '%H:%M').time()
+
+        for start, end in slots_config.get(resource.name, []):
+            ranges.append((_time(start), _time(end)))
+        #range1 = _time('9:00'), _time("12:59")
+        #range2 = dt.time(13), dt.time(23, minute=59)
+
+        def _create_day_slots(d):
+            day_slots = []
+            for r in ranges:
+                args = {
+                    'resource_id': rid,
+                    'type': 'slot',
+                    'start': dm.date(d, r[0]),
+                    'end': dm.date(d, r[1]),
+                    'slot_auth': {'applications': ['any'], 'users': []}
+                }
+
+                s = dm.Booking(**args)
+                o = [b for b in bookings if b.overlap_slot(s)]
+                day_slots.append((s, o))
+                if create_slots and not o:
+                    dm.create_booking(**args)
+            return day_slots
+
+        slots = []
+        for i in range(5):
+            d = next_week + dt.timedelta(days=i)
+            slots.append(_create_day_slots(d))
+
+        data['slots'] = slots
+
+        return data
+
+    @dc.content
+    def dashboard(**kwargs):
+        """ Customized Dashboard data for the CryoEM center at St.Jude. """
+        dm = dc.app.dm  # shortcut
+        user = dc.app.user  # shortcut
+        # If 'resource_id' is passed as argument, only display
+        # that resource in the dashboard
+        resource_id = int(kwargs.get('resource_id', 0))
+        dataDict = dc.get_resources(image=True)
+        resources = dataDict['resources']
+        selected_resources = [r for r in resources if r['id'] == resource_id] or resources
+
+
+        resource_bookings = {}
+
+        # Provide upcoming bookings sorted by proximity
+        bookings = [('Today', []),
+                    ('Next 7 days', []),
+                    ('Next 30 days', [])]
+
+        def week_start(d):
+            return (d - dt.timedelta(days=d.weekday())).date()
+
+        if 'date' in kwargs:
+            now = datetime_from_isoformat(kwargs['date'])
+        else:
+            now = dm.now()
+        this_week = week_start(now)
+        d7 = dt.timedelta(days=7)
+        next_week = week_start(now + d7)
+        prev7 = now - dt.timedelta(days=8)
+        next7 = now + d7
+        next30 = now + dt.timedelta(days=30)
+
+        def is_same_week(d):
+            return this_week == week_start(d)
+
+        def is_next_week(d):
+            return this_week == week_start(d - d7)
+
+        def add_booking(b):
+            start = dm.dt_as_local(b.start)
+            end = dm.dt_as_local(b.end)
+
+            r = b.resource
+            if r.id not in resource_bookings:
+                resource_bookings[r.id] = {
+                    'today': [],
+                    'this_week': [],
+                    'next_week': []
+                }
+
+            if is_same_week(start):
+                k = 'this_week'
+            elif is_next_week(start):
+                k = 'next_week'
+            else:
+                k = None
+
+            if k:
+                resource_bookings[r.id][k].append(b)
+
+                if start.date() <= now.date() <= end.date():  # also add in today
+                    resource_bookings[r.id]["today"].append(b)
+                    bookings[0][1].append(b)
+                elif k == 'next_week':
+                    bookings[1][1].append(b)
+            else:
+                bookings[2][1].append(b)
+
+        local_tag = dm.get_config('bookings').get('local_tag', '')
+        local_scopes = {}
+
+        for b in dm.get_bookings_range(prev7, next30):
+            # if not user.is_manager and not user.same_pi(b.owner):
+            #     continue
+            r = b.resource
+            if not local_tag or local_tag in r.tags:
+                local_scopes[r.id] = r
+                add_booking(b)
+
+        scopes = {r.id: r for r in dm.get_resources()}
+
+        for rbookings in resource_bookings.values():
+            for k, bookingValues in rbookings.items():
+                bookingValues.sort(key=lambda b: b.start)
+
+        # Remove slots if there are overlapping bookings
+        for rbookings in resource_bookings.values():
+            for k, bookingValues in rbookings.items():
+                def _slot_overlap(s):
+                    return s.is_slot and any(s.overlap_slot(b)
+                                             for b in bookingValues)
+
+                slots_overlap = [s for s in bookingValues if _slot_overlap(s)]
+                for s in slots_overlap:
+                    bookingValues.remove(s)
+
+                bookingValues.sort(key=lambda b: b.start)
+
+        # Retrieve open requests for each scope from entries and bookings
+        if kwargs.get('load_requests', True):
+            for p in dm.get_projects():
+                if p.is_active:
+                    last_bookings = {}
+                    # Find last bookings for each scope
+                    for b in sorted(p.bookings, key=lambda b: b.end, reverse=True):
+                        if len(last_bookings) < len(local_scopes) and b.resource_id not in last_bookings:
+                            last_bookings[b.resource.id] = b
+
+                    reqs = {}
+                    for e in reversed(p.entries):
+                        # Requests found for each scope, no need to continue
+                        if len(reqs) == len(local_scopes):
+                            break
+                        if b := dc.booking_from_entry(e, scopes):
+                            rid = b.resource_id
+                            if (rid not in reqs and
+                                    (rid not in last_bookings or
+                                     b.start.date() > last_bookings[rid].end.date())):
+                                b.id = e.id
+                                add_booking(b)
+                                reqs[rid] = b
+
+        # Sort all entries
+        for rbookings in resource_bookings.values():
+            for k, bookingValues in rbookings.items():
+                bookingValues.sort(key=lambda b: b.start)
+
+        resource_create_session = dm.get_config('sessions').get('create_session', {})
+        slots_config = dm.get_config('resources').get('slots', {})
+
+        dataDict.update({'resource_bookings': resource_bookings,
+                         'resource_create_session': resource_create_session,
+                         'local_resources': local_scopes,
+                         'next_week': next_week,
+                         'date': now,
+                         'create_slots': slots_config,
+                         'resource_id': resource_id,
+                         'selected_resources': selected_resources
+                         })
+        dataDict.update(dc.get_news(**kwargs))
+        return dataDict
diff --git a/extras/stjude_cryoem/templates/create_session_form.html b/extras/stjude_cryoem/templates/create_session_form.html
index b6e6ab0f..e4380e68 100644
--- a/extras/stjude_cryoem/templates/create_session_form.html
+++ b/extras/stjude_cryoem/templates/create_session_form.html
@@ -195,6 +195,7 @@ <h5>{{ booking.resource.name }}</h5>
     var transfer_host = {{ transfer_host|tojson }};
     var workflow_default = {{ workflow_default|tojson }};
     var otf_host_default = {{ otf_host_default|tojson }};
+    var otf_host = null;
 
     function onCreateClick(){
         var formValues = getFormAsJson('session-form');
@@ -209,27 +210,29 @@ <h5>{{ booking.resource.name }}</h5>
             },
         }
 
-        if (formValues.session_name) {
-            var otf_task = [
-                "otf",
-                getObjectValue(formValues, 'otf_host', otf_host_default)
-            ];
+        // Raw folder is provided only for speciall debugging cases
+        // then, no transfer task will be schedule and the session name
+        // will be taken from the raw path
+        let sessionName = nonEmpty(formValues.raw_folder) ? formValues.raw_folder.split('/').pop() : formValues.session_name;
 
-            attrs.name = formValues.session_name;
+        if (nonEmpty(sessionName)) {
+            attrs.name = sessionName;
+            otf_host = getObjectValue(formValues, 'otf_host', otf_host_default);
+            var otf_task = [otf_host, {action: 'otf'}];
 
             if (!formValues.raw_folder){
                 attrs.extra = {
                     raw: {},
                     project_id: formValues.project_id,
                 }
-                attrs.tasks = [["transfer", transfer_host], otf_task]
+                attrs.tasks = [[transfer_host, {action: 'transfer'}], otf_task]
             }
             else {
                 attrs.extra = {
                     raw: {path: formValues.raw_folder},
                     project_id: formValues.project_id
                 }
-                attrs.tasks = [["monitor", transfer_host], otf_task]
+                attrs.tasks = [[otf_host, {action: "monitor"}], otf_task]
             }
 
             if (!formValues.otf_folder)
@@ -243,8 +246,7 @@ <h5>{{ booking.resource.name }}</h5>
         }
 
         // If there is a session
-        {#if (attrs.extra.raw_folder)#}
-        var regex = /^[a-zA-Z0-9-_]{2,32}$/;
+        var regex = /^[a-zA-Z0-9-_]{2,64}$/;
         var valid_name = nonEmpty(attrs.name) && regex.test(attrs.name);
 
         if (!valid_name) {
diff --git a/extras/stjude_cryoem/templates/create_session_negstain.html b/extras/stjude_cryoem/templates/create_session_negstain.html
new file mode 100644
index 00000000..413373f9
--- /dev/null
+++ b/extras/stjude_cryoem/templates/create_session_negstain.html
@@ -0,0 +1,292 @@
+{% import "entry_macros.html" as macros %}
+
+{% set is_manager = current_user.is_manager %}
+{% set is_admin = current_user.is_admin %}
+
+<!-- Modal to with a Form to create a new Session -->
+<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
+  <div class="modal-content">
+
+    <!-- Modal header -->
+    <div class="modal-header">
+        <h3 class="modal-title" id="session-modal-title"> Create Session:  </h3>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+    </div>
+    <!-- End of Modal header -->
+
+    <!-- Modal body -->
+    <div class="modal-body">
+    <input type="hidden" id="booking-id" value="{{ booking.id }}">
+
+    <!-- Create Session Form -->
+    <div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-12">
+        <form id="session-form" data-parsley-validate="" novalidate="">
+
+            <div class="tab-regular">
+                <ul class="nav nav-tabs nav-fill" id="myTab7" role="tablist">
+
+                    <li class="nav-item">
+                        <a class="nav-link active" id="session-general-tab" data-toggle="tab" href="#session-general" role="tab" aria-controls="current" aria-selected="true">General</a>
+                    </li>
+{##}
+{#                    <li class="nav-item">#}
+{#                        <a class="nav-link" id="session-acq-tab" data-toggle="tab" href="#session-acq" role="tab" aria-controls="past" aria-selected="false">Acquisition</a>#}
+{#                    </li>#}
+
+                    {% if is_admin %}
+                    <li class="nav-item">
+                        <a class="nav-link" id="session-advanced-tab" data-toggle="tab" href="#session-advanced" role="tab" aria-controls="summary" aria-selected="false">Advanced</a>
+                    </li>
+                    {% endif %}
+
+                </ul>
+
+                <div class="tab-content" id="myTabContent7">
+
+                    <!-- tab "Current" -->
+                    <div class="tab-pane fade show active" id="session-general" role="tabpanel" aria-labelledby="session-general-tab">
+
+                    <div class="row">
+                        <div class="col-7">
+                            <!-- Session's Name -->
+                            <div class="form-group row col">
+                                <h3 class="col-12 col-sm-3 col-form-label text-sm-right">Session Folder</h3>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                        <select id="session-folder-select" class="selectpicker show-tick" data-key="session_name">
+                                            <option value=""></option>
+                                            {% for f in frame_folders %}
+                                                <option value="{{ f['name'] }}" data-subtext="{{ f['modified']|pretty_elapsed }}">{{ f['name'] }}</option>
+                                            {% endfor %}
+                                        </select>
+                                </div>
+                            </div>
+
+                            <!-- Owner Row -->
+                            <div class="form-group row">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">Owner</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                    <h4 class="col-12 text-sm-left">{{ booking.owner.name }}</h4>
+                                </div>
+                            </div>
+
+                            <!-- Operator Row -->
+                            <div class="form-group row">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">Staff</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                    <label class="col-12 col-sm-3 col-form-label text-sm-left">{{ booking.operator.name }}</label>
+                                </div>
+                            </div>
+
+                            <!-- Microscope Row -->
+                            <div class="form-group row">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">Microscope</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                    <div class="row align-items-center justify-content-start">
+                                    <div class="col">
+                                        <img src="{{ url_for('images.static', filename=booking.resource.image) }}" alt="{{ booking.resource.name }}" width="48" style="margin-right: 0;">
+                                    </div>
+                                    <div class="col-10 text-sm-left">
+                                        <h5>{{ booking.resource.name }}</h5>
+                                    </div>
+                                </div>
+
+                                </div>
+                            </div>
+
+                            <!-- Project id -->
+                            <div class="form-group row">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">Project ID</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                    <select id="session-projectid-select" class="selectpicker show-tick" data-live-search="true" data-key="project_id">
+                                        <option value="0">Not set</option>
+                                        {% for p in projects %}
+                                            {% set selected = 'selected' if p.id == booking.project.id else '' %}
+                                            <option {{ selected }} value="{{ p.id }}">{{ p.title }}</option>
+                                        {% endfor %}
+                                    </select>
+                                </div>
+                            </div>
+
+                        </div>
+
+                        <div class="col-5 p-0 m-0">
+                            {{ macros.session_dialog_row('Voltage (kV)', 'acq_voltage', acquisition['voltage'], '') }}
+                            {{ macros.session_dialog_row('Magnification', 'acq_magnification', acquisition['magnification'], '') }}
+                            {{ macros.session_dialog_row('Pixel Size (A/px)', 'acq_pixel_size', acquisition['pixel_size'], '') }}
+                            {{ macros.session_dialog_row('CS', 'acq_cs', acquisition['cs'], '') }}
+                            {{ macros.session_dialog_row('Dose (per frame)', 'acq_dose', acquisition['dose'], '') }}
+                        </div>
+
+                    </div>
+
+                    </div>
+
+{#                <div class="tab-pane fade" id="session-acq" role="tabpanel" aria-labelledby="session-acq-tab">#}
+{##}
+{#                </div>#}
+
+                {% if is_admin %}
+                    <div class="tab-pane fade" id="session-advanced" role="tabpanel" aria-labelledby="session-advanced-tab">
+                        {{ macros.session_dialog_row('RAW data folder', 'raw_folder', '', 'Provide RAW data folder') }}
+                        {{ macros.session_dialog_row('OTF folder', 'otf_folder', '', 'If OTF folder is not provided it will be created') }}
+
+                            <!-- OTF host -->
+                            <div class="form-group row ">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">Transfer Host</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                    <label class="col-12 col-sm-3 col-form-label text-sm-left">{{ transfer_host }}</label>
+                                </div>
+                            </div>
+
+                            <!-- OTF host -->
+                            <div class="form-group row ">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">OTF Host</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                   <select id="otf_host-select" class="selectpicker" data-key="otf_host">
+                                        {% for h in otf_hosts %}
+                                            {% set selected = 'selected' if h == otf_host_default else '' %}
+                                            <option value="{{ h }}" {{ selected }}>{{ h }}</option>
+                                        {% endfor %}
+                                    </select>
+                                </div>
+                            </div>
+
+                            <!-- OTF workflow -->
+                            <div class="form-group row ">
+                                <label class="col-12 col-sm-3 col-form-label text-sm-right">OTF Workflow</label>
+                                <div class="col-12 col-sm-8 col-lg-6">
+                                   <select id="otf_workflow-select" class="selectpicker" data-key="otf_workflow">
+                                       {% for w in workflows %}
+                                           {% set selected = 'selected' if w == workflow_default else '' %}
+                                           <option value="{{ w }}" {{ selected }}>{{ w }}</option>
+                                       {% endfor %}
+                                    </select>
+                                </div>
+                            </div>
+                    </div>
+                {% endif %}
+
+                </div>
+
+            </div>
+        </form>
+    </div>
+    <!-- end Booking Form -->
+
+</div>
+    <!-- End of Modal body -->
+
+    <!-- Modal footer -->
+    <div class="modal-footer">
+        <button type="button" id="booking-btn-cancel"  class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
+        <button type="button" id="booking-btn-ok" class="btn btn-outline-dark" onclick="onCreateClick()">Create</button>
+    </div>
+    <!-- End of Modal footer -->
+
+  </div> <!-- End of modal-content -->
+</div> <!-- end of modal-dialog -->
+
+<!-- javascript  -->
+<script>
+    $('select').selectpicker();
+
+    var transfer_host = {{ transfer_host|tojson }};
+    var workflow_default = {{ workflow_default|tojson }};
+    var otf_host_default = {{ otf_host_default|tojson }};
+    var otf_host = null;
+
+    function onCreateClick(){
+        var formValues = getFormAsJson('session-form');
+        var attrs = {
+            booking_id: parseInt(document.getElementById('booking-id').value),
+            acquisition: {
+                voltage: formValues.acq_voltage,
+                magnification: formValues.acq_magnification,
+                pixel_size: formValues.acq_pixel_size,
+                dose: formValues.acq_dose,
+                cs: formValues.acq_cs
+            },
+        }
+
+        // Raw folder is provided only for speciall debugging cases
+        // then, no transfer task will be schedule and the session name
+        // will be taken from the raw path
+        let sessionName = nonEmpty(formValues.raw_folder) ? formValues.raw_folder.split('/').pop() : formValues.session_name;
+
+        if (nonEmpty(sessionName)) {
+            attrs.name = sessionName;
+            otf_host = getObjectValue(formValues, 'otf_host', otf_host_default);
+            var otf_task = ["otf", otf_host];
+
+            if (!formValues.raw_folder){
+                attrs.extra = {
+                    raw: {},
+                    project_id: formValues.project_id,
+                }
+                attrs.tasks = [["transfer", transfer_host], otf_task]
+            }
+            else {
+                attrs.extra = {
+                    raw: {path: formValues.raw_folder},
+                    project_id: formValues.project_id
+                }
+                attrs.tasks = [["monitor", otf_host], otf_task]
+            }
+
+            if (!formValues.otf_folder)
+                attrs.extra.otf = {
+                    workflow: getObjectValue(formValues, 'otf_workflow', workflow_default),
+                    host: getObjectValue(formValues, 'otf_host', otf_host_default),
+                    cryolo_model: getObjectValue(formValues, 'cryolo_model', '')
+                }
+            else
+                attrs.extra.otf  = {path: otf_folder}
+        }
+
+        // If there is a session
+        var regex = /^[a-zA-Z0-9-_]{2,64}$/;
+        var valid_name = nonEmpty(attrs.name) && regex.test(attrs.name);
+
+        if (!valid_name) {
+            showError("Provide a valid <strong>Session Name</strong>.<br>" +
+                "<p>It must have from 2 to 32 digits, containing letters, numbers, slash or underscore.</p>");
+            return;
+        }
+
+        // showError(JSON.stringify(attrs, null, 4));
+        // return
+
+        // Add unique prefix to avoid session name clashes
+        attrs.name = "{{ session_name_prefix }}" + attrs.name;
+
+        var ajaxContent = $.ajax({
+            url: "{{ url_for('api.create_session') }}",
+            type: "POST",
+            contentType: 'application/json; charset=utf-8',
+            data: JSON.stringify({attrs: attrs}),
+            dataType: "json"
+        });
+
+        ajaxContent.done(function(jsonResponse) {
+            //alert(JSON.stringify(jsonResponse, null, 4));
+
+            if ('error' in jsonResponse)
+                showError(jsonResponse['error']);
+            else {
+                //window.location.reload();
+                window.location = "{{ url_for_content('session_default') }}" + "&session_id=" + jsonResponse.session.id;
+            }
+        });
+
+        ajaxContent.fail(function(jqXHR, textStatus) {
+            alert( "Request failed: " + textStatus );
+        });
+    }
+
+    (function(window, document, $, undefined) {
+        // Initialization code goes here
+    })(window, document, window.jQuery);
+
+</script>
diff --git a/extras/stjude_cryoem/templates/dashboard_createslots_card.html b/extras/stjude_cryoem/templates/dashboard_createslots_card.html
new file mode 100644
index 00000000..76ff67a8
--- /dev/null
+++ b/extras/stjude_cryoem/templates/dashboard_createslots_card.html
@@ -0,0 +1,67 @@
+{% import "entry_macros.html" as macros %}
+
+<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
+<div class="modal-content">
+
+    <!-- Modal header -->
+    <div class="modal-header">
+        <h3 class="modal-title"> Next week - SLOTS ({{ next_week|pretty_date }})</h3>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+    </div> <!-- End of Modal header -->
+
+    <!-- Modal body -->
+    <div class="modal-body">
+        <div class="table-responsive-sm m-0 p-0">
+            <table class="table borderless m-0 p-0">
+            <tbody>
+                {% for daySlots in slots %}
+                {% for b, overlaps in daySlots %}
+                    {% set n = overlaps|length %}
+                    {% set active = False %}
+                    <tr class="row p-0 m-0" style="{{ row_style }}" >
+                        <td class="col-2 text-left" style="font-weight: bold">
+                            {% if loop.index0 == 0 %}
+                                {{ b|booking_span }}
+                            {% endif %}
+                        </td>
+                        <td class="col-2">
+                            <span style="color: rgb(255,107,53);">{{ b|time_range }}</span>
+                        </td>
+                        <td class="col-2">
+                            {% if n %}
+                                <span class="badge-danger p-1 badge-pill">Overlap</span>
+                            {% else %}
+                                <span class="badge-success p-1 badge-pill">Ready</span>
+                            {% endif %}
+
+                        </td>
+                        <td class="col-6 row">
+                            {% for o in overlaps %}
+                                <div class="col-12 row p-0 m-0">
+                                    <div class="col-2">Id: {{ o.id }}</div>
+                                    <div class="col-3">{{ o|time_range }}</div>
+                                    <div class="col-7">{{ o.owner|pairname}}</div>
+                                </div>
+                            {% endfor %}
+                        </td>
+                    </tr>
+                {% endfor %}
+                {% endfor %}
+
+            </tbody>
+            </table>
+        </div>
+    </div>
+
+    <div class="modal-footer">
+      <button type="button" id="slots-btn-cancel"  class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
+      <button class="btn btn-outline-dark" onclick="onSlotsCreateClick()">Create</button>
+    </div>
+
+</div>
+</div>
+
+
+
diff --git a/extras/stjude_cryoem/templates/dashboard_instrument_card.html b/extras/stjude_cryoem/templates/dashboard_instrument_card.html
new file mode 100644
index 00000000..f6de1fc9
--- /dev/null
+++ b/extras/stjude_cryoem/templates/dashboard_instrument_card.html
@@ -0,0 +1,127 @@
+{% import "entry_macros.html" as macros %}
+
+{% macro table(bookings, compact) -%}
+    {% set borderclass = 'borderless' if not compact else '' %}
+        <div class="table-responsive-sm m-0 p-0">
+            <table class="table {{ borderclass }} m-0 p-0">
+            <tbody>
+                {% for b in bookings %}
+                    {% set slot_color = b|booking_color %}
+                    {% set active = b|booking_active_today %}
+                    {% set style = 'font-weight: bold' if active else '' %}
+                    {% set row_style = 'background-color: #ededed' if active else '' %}
+                    <tr class="row p-0 m-0" style="{{ row_style }}" >
+                        <td class="col-3 text-left" style="{{ style }}">{{ b|booking_span }}</td>
+                        {% if b.type == 'downtime' %}
+                            <td class="col-4"><span style="color: rgb(181,4,0);">Downtime</span></td>
+                            <td class="col-4">{{ b.title }}</td>
+                        {% elif b.type == 'maintenance' %}
+                            <td class="col-4"><span style="color: rgb(255,107,53);">Maintenance</span></td>
+                            <td class="col-4">{{ b.title }}</td>
+                        {% elif b.type == 'slot' %}
+                            <td class="col-4">
+                                {% set style_str = "background-color: %s; border-color: %s"|format(slot_color, slot_color) %}
+                                <a href="javascript:createBookingFromSlot({{ b.resource_id }}, {{ b.operator_id }},'{{ b.start|isoformat }}', '{{ b.end|isoformat }}')"
+                                   class="btn btn-brand btn-sm mr-0 col-12" style="{{ style_str }}">
+                                    <i class="fas fa-calendar-plus mr-1"> </i> {{ b|time_range }} </a>
+                            </td>
+                            <td class="col-4">
+                                {% if b.operator %}
+                                    {{ b.operator|shortname}}
+                                {% endif %}
+                            </td>
+                        {% else %}
+
+                            <td class="col-4" style="{{ style }}">{{ b.owner|pairname}} </td>
+                            <td class="col-4" style="{{ style }}">
+                                {% if b.type == 'request' %}
+                                    {% set pid = b.project_id %}
+                                    {{ macros.project(b.project) }}
+                                {% else %}
+                                    {{ b.operator|shortname}}
+                                {% endif %}
+                            </td>
+
+                        {% endif %}
+                        <td class="col-1">
+                            {% if b.type in ['booking', 'downtime', 'maintenance'] %}
+                                <a href="javascript:showBookingFromDashboard({{ b.resource_id }}, {{ b.id }})"><i class="far fa-calendar-alt"></i></a>
+
+                            {% elif b.type == 'request' %}
+                                <a href="javascript:createBookingFromEntry({{ b.resource_id }}, {{ b.id }})"><i class="far fa-calendar" style="color: red"></i></a>
+                            {% elif b.type == 'slot' %}
+                                {% if current_user.is_manager %}
+                                    <a href="javascript:showBookingFromDashboard({{ b.resource_id }}, {{ b.id }})"><i class="far fa-edit" style="color: {{ slot_color }}"></i></a>
+                                {% endif %}
+                            {% endif %}
+                        </td>
+                    </tr>
+                    {% if not compact %}
+                    <tr style="{{ row_style }}">
+                        <td colspan="3">
+                            {% set create_session_func = resource_create_session.get(b.resource.name, None) %}
+                            {% if create_session_func %}
+                                {% if active %}
+                                        <a href="javascript:createSession({{ b.id }}, {{ sessions|length }}, '{{ create_session_func }}')" class="btn btn-primary btn-sm mr-0">
+                                                        <i class="fas fa-plus-circle mr-1"></i> New Session </a>
+                                {% else %}
+                                    {% if current_user.is_manager %}
+                                        <a href="javascript:createSession({{ b.id }}, {{ sessions|length }}, '{{ create_session_func }}')"><i class="fas fa-plus-circle"></i></a>
+                                    {% endif %}
+                                {% endif %}
+                            {% endif %}
+                            {% if b.session %}
+                                <span class="ml-5 mr-2">Sessions:</span>
+                                {{ macros.session_list(b.session) }}
+                            {% endif %}
+                            <hr/>
+                        </td>
+                    </tr>
+                    {% endif %}
+                {% endfor %}
+
+            </tbody>
+            </table>
+        </div>
+{%- endmacro %}
+
+{% set cres = 5 if alignment == 'h' else 12 %}
+
+<div class="card h-100">
+    <div class="card-header">
+        <div class="row m-0 p-0">
+            <div class="row align-items-left justify-content-start align-items-center col-6">
+                <img src="{{ r['image'] }}" alt="{{ r['name'] }}" width="48" style="margin-right: 0;">
+                <h3 class="mt-2"><span class="badge" style="background: {{ r['color'] }}; color: #fff; font-size: 20px">{{r['name']}}</span></h3>
+            </div>
+            <div class="row align-items-right justify-content-end col-6">
+                {% if current_user.is_manager and create_slots.get(r['name'], False) %}
+                    <div>
+                    <a href="javascript:createSlots({{ r['id'] }})" class="btn btn-primary btn-sm mr-0">
+                        <i class="fas fa-plus-circle mr-1"></i> Create Slots </a>
+                    </div>
+
+                {% endif %}
+            </div>
+        </div>
+
+    </div>
+    <div class="card-body pt-0">
+        {% if r['id'] in resource_bookings %}
+            <div class="row">
+                <div class="card-body this_week_container mt-3 col-{{ cres }}">
+                    <div class="row"><h3>This week</h3></div>
+                    {% set this_week = resource_bookings[r['id']].get('this_week', []) %}
+                    {{ table(this_week, not r.is_microscope) }}
+                </div>
+                <div class="card-body next_week_container mt-3 ml-5 col-{{ cres }}">
+                    <div class="row"><h3>Next week</h3></div>
+                    {% set next_week = resource_bookings[r['id']].get('next_week', []) %}
+                    {{ table(next_week, True) }}
+                </div>
+            </div>
+    {% endif %}
+    </div>
+</div>
+
+
diff --git a/extras/stjude_cryoem/templates/dashboard_right.html b/extras/stjude_cryoem/templates/dashboard_right.html
new file mode 100644
index 00000000..4f11c8fe
--- /dev/null
+++ b/extras/stjude_cryoem/templates/dashboard_right.html
@@ -0,0 +1,121 @@
+<!-- ============================================================== -->
+<!-- Dashboard right content -->
+<!-- ============================================================== -->
+<div class="col-xl-12 col-lg-12 col-md-7 col-sm-12 col-12">
+    <div class="influence-profile-content pills-regular">
+
+        {% include 'dashboard_news.html' %}
+
+         {% set nres = selected_resources|length %}
+         {% set wres = 700 if nres > 1 else 1500 %}
+         {% set alignment = 'v' if nres > 1 else 'h' %}
+
+        <div class="row">
+        <div class="row col-12">
+            <div class="col col-auto" style="margin-top: 7px;"><h3> Dashboard </h3></div>
+            <div class="col text-right" style="margin-top: 7px;"><h5> Display </h5></div>
+            <div class="col col-auto nopadding">
+                <select id="selectpicker-resource-display" class="selectpicker show-tick" data-width="auto" title="Select Instrument..."  onchange="selectResource()">
+                    {% set selected = 'selected' if resource_id == 0 else '' %}
+                    <option value="0" {{ selected }}>All</option>
+                    {% for r in resources %}
+                        {% set selected = 'selected' if r['id'] == resource_id else '' %}
+                      <option value="{{r['id']}}" class="badge" {{ selected }} style="background: {{ r['color'] }}; color: #fff;">{{r['name']}}</option>
+                    {% endfor %}
+                </select>
+            </div>
+        </div>
+
+        <div class="row col-12">
+            {% for r in selected_resources %}
+                <div class="col-auto mb-4" style="min-width: {{ wres }}px; max-width: {{ wres }}px;" id="instrument_card_{{ r['id'] }}">
+                    {% include 'dashboard_instrument_card.html' %}
+                </div>
+            {% endfor %}
+        </div>
+
+        </div>
+
+</div>
+
+</div>
+<!-- ============================================================== -->
+<!-- End  of Dashboard right content -->
+<!-- ============================================================== -->
+
+<script>
+
+    function showBookingFromDashboard(resource_id, booking_id){
+        last_resource_id = resource_id;
+        showBookingForm({booking_id: booking_id });
+    }
+
+    function createBookingFromEntry(resource_id, entry_id){
+        last_resource_id = resource_id;
+        showBookingForm({entry_id: entry_id });
+    }
+
+    function createBookingFromSlot(resource_id, operator_id, start, end){
+        last_resource_id = resource_id;
+        showBookingForm({
+            resource_id: resource_id,
+            start: start,
+            end: end,
+            operator_id: operator_id
+        });
+    }
+
+    function onBookingChanged(response){
+        load_html_from_ajax('instrument_card_' + last_resource_id,
+            get_ajax_content('dashboard_instrument_card', {
+                resource_id: last_resource_id,
+                alignment: {{alignment|tojson}},
+                date: "{{ date|isoformat }}"
+            }));
+    }
+
+    function createslots_content(create_slots) {
+        return get_ajax_content("dashboard_createslots_card", {
+                resource_id: last_resource_id,
+                date: "{{ date|isoformat }}",
+                create_slots: create_slots
+            });
+    }
+    function createSlots(resource_id){
+        // Let's reuse the booking dialog to display the slots
+        last_resource_id = resource_id;
+        show_modal_from_ajax('booking-modal', createslots_content(0));
+    }
+
+    function onSlotsCreateClick(){
+        var ajaxContent = createslots_content(1);
+        ajaxContent.done(function(html) {
+            $('#booking-modal').modal('hide');
+            onBookingChanged(html);
+         });
+    }
+
+    function selectResource() {
+        var resource_id = $('#selectpicker-resource-display').selectpicker('val');
+        window.location = "{{ url_for_content('dashboard') }}" + "&resource_id=" + resource_id;
+    }  // function selectPI
+
+
+    (function(window, document, $, undefined) {
+        "use strict";
+
+        var last_resource_id = null;
+
+        $('select').selectpicker();
+
+        on_bookings_deleted = on_bookings_updated = on_bookings_created = onBookingChanged;
+
+        makeSameHeight('current_session');
+        makeSameHeight('this_week_container');
+        makeSameHeight('next_week_container');
+
+        makeSameHeight('news-card');
+
+    })(window, document, window.jQuery);
+
+</script>
\ No newline at end of file
diff --git a/extras/stjude_cryoem/templates/main_left_sidebar.html b/extras/stjude_cryoem/templates/main_left_sidebar.html
index 68c0c852..ab2adc11 100644
--- a/extras/stjude_cryoem/templates/main_left_sidebar.html
+++ b/extras/stjude_cryoem/templates/main_left_sidebar.html
@@ -44,7 +44,7 @@
                                         <a class="nav-link" href="{{ url_for_content('report_microscopes_usage') }}">Microscopes Usage</a>
                                     </li>
 
-                                    {% if current_user.is_admin %}
+                                    {% if current_user.is_manager %}
                                         <li class="nav-item">
                                             <a class="nav-link" href="{{ url_for_content('report_projects_overview') }}">Projects Overview</a>
                                         </li>
@@ -78,19 +78,27 @@
                                     <i class="fas fa-boxes"></i>Resources</a>
                             </li>
 
-                            <!-- Workers -->
-                            <a class="nav-link" href="#" data-toggle="collapse" aria-expanded="false" data-target="#submenu-workers" aria-controls="submenu-workers">
-                                    <i class="fas fa-server"></i>Workers</a>
-                            <div id="submenu-workers" class="collapse submenu" style="">
-                                <ul class="nav flex-column">
-                                    <li class="nav-item">
-                                        <a class="nav-link" href="{{ url_for_content('workers') }}">Tasks</a>
-                                    </li>
-                                    <li class="nav-item">
-                                        <a class="nav-link" href="{{ url_for_content('workers_frames') }}">Frames</a>
-                                    </li>
-                                </ul>
-                            </div>
+                            {% if current_user.is_manager %}
+                                <!-- News -->
+                                <li class="nav-item">
+                                    <a class="nav-link" href="{{ url_for_content('news', display='table') }}">
+                                        <i class="fas fa-newspaper"></i>News</a>
+                                </li>
+
+                                <!-- Workers -->
+                                <a class="nav-link" href="#" data-toggle="collapse" aria-expanded="false" data-target="#submenu-workers" aria-controls="submenu-workers">
+                                        <i class="fas fa-server"></i>Workers</a>
+                                <div id="submenu-workers" class="collapse submenu" style="">
+                                    <ul class="nav flex-column">
+                                        <li class="nav-item">
+                                            <a class="nav-link" href="{{ url_for_content('workers') }}">Tasks</a>
+                                        </li>
+                                        <li class="nav-item">
+                                            <a class="nav-link" href="{{ url_for_content('workers_frames') }}">Frames</a>
+                                        </li>
+                                    </ul>
+                                </div>
+                            {% endif %}
 
                         </ul>
 
diff --git a/setup.py b/setup.py
index 0cc168ad..553c6237 100644
--- a/setup.py
+++ b/setup.py
@@ -180,7 +180,8 @@
     entry_points={  # Optional
        'console_scripts': [
            'emh-data = emhub.data.__main__:main',
-           'emh-client = emhub.client.__main__:main'
+           'emh-client = emhub.client.__main__:main',
+           'emh-relion = emhub.data.processing.processing_relion.__main__:main',
        ],
     },
 


Test patch:
diff --git a/emhub/data/imports/test.py b/emhub/data/imports/test.py
index 3f7db97b..38520488 100644
--- a/emhub/data/imports/test.py
+++ b/emhub/data/imports/test.py
@@ -182,12 +182,12 @@ def _populateSessions(self, dm):
 
     def _populatePucks(self, dm):
         self._action('Populating Pucks')
-        for puck in self.json_data['pucks']:
+        for puck in self.json_data.get('pucks', []):
             dm.create_puck(**puck)
 
     def _populateEntries(self, dm):
         self._action('Populating Entries')
-        for entry in self.json_data['entries']:
+        for entry in self.json_data.get('entries', []):
             self.__fix_dates(entry, 'date',
                              'creation_date',
                              'last_update_date')
@@ -276,4 +276,6 @@ def create_instance(instance_path, json_file, force):
     dm = DataManager(instance_path, cleanDb=True)
     TestData(dm, json_file)
 
+    return dm
+
 
diff --git a/emhub/data/imports/test_instance_data.json b/emhub/data/imports/test_instance_data.json
index 731d726a..eee9b691 100644
--- a/emhub/data/imports/test_instance_data.json
+++ b/emhub/data/imports/test_instance_data.json
@@ -209,6 +209,13 @@
           "iconClass": "fas fa-sticky-note fa-inverse",
           "imageClass": "img--picture",
           "label": "Note"},
+        "news": {
+            "group": 1,
+            "iconClass": "fas fa-box fa-inverse",
+            "imageClass": "img--picture",
+            "label": "News"
+        },
+
         "screening": {"group": 2,
           "iconClass": "fas fa-search fa-inverse",
           "imageClass": "img--location",
@@ -648,6 +655,32 @@
                 }
             ]
         }
+    },
+    {
+        "name": "entry_form:news",
+        "definition": {
+            "params": [
+                {
+                    "id": "active",
+                    "label": "Active",
+                    "type": "bool"
+                },
+                {
+                    "enum": {
+                        "choices": [
+                            "info",
+                            "warning",
+                            "danger"
+                        ],
+                        "display": "radio"
+                    },
+                    "id": "type",
+                    "label": "Type",
+                    "default": "info"
+                }
+            ],
+            "title": "News Form"
+        }
     }
 ],
   
@@ -942,6 +975,7 @@
   ],
 
   "projects": [
+{"id": 1, "date": "2022-09-01T09:00:00+00:00", "user_id": 1, "creation_date": "2022-09-19T02:23:27.003189+00:00", "creation_user_id": 1, "last_update_date": "2022-09-19T02:23:27.003189+00:00", "status":  "special:news", "title": "News Project"},
 {"id": 4, "date": "2022-09-01T09:00:00+00:00", "user_id": 55, "creation_date": "2022-09-19T02:23:27.003189+00:00", "creation_user_id": 59, "last_update_date": "2023-03-09T01:36:04.311014+00:00", "last_update_user_id": 16, "extra": {"user_can_edit": true, "is_confidential": true, "collaborators_ids": []}},
 {"id": 5, "date": "2023-01-01T09:00:00+00:00", "user_id": 55, "creation_date": "2022-09-21T03:25:14.483439+00:00", "creation_user_id": 59, "last_update_date": "2023-01-13T23:23:23.825343+00:00", "last_update_user_id": 59, "extra": {"user_can_edit": true}},
 {"id": 9, "date": "2023-01-03T09:00:00+00:00", "user_id": 179, "creation_date": "2023-01-03T19:02:14.358867+00:00", "creation_user_id": 59, "last_update_date": "2023-01-03T19:22:07.174230+00:00", "last_update_user_id": 59, "extra": {"user_can_edit": true}},
@@ -1428,6 +1462,59 @@
   {"id": 260, "label": "Puck_0260", "color": "Moccasin", "dewar": 2, "cane": 6, "position": 10, "extra": {}}
 ],
   "entries": [
+    {
+        "date": "2023-03-01T16:30:13.796273+00:00",
+        "type": "news",
+        "title": "EMhub paper published!",
+        "description": "",
+        "project_id": 1,
+        "creation_date": "2023-03-01T16:30:13.796273+00:00",
+        "creation_user_id": 1,
+        "last_update_date": "2023-03-01T16:30:13.796273+00:00",
+        "last_update_user_id": 1,
+        "extra": {
+            "data": {
+                "type": "info",
+                "active": true
+                }
+            }
+    },
+    {
+      "date": "2023-03-03T16:30:13.796273+00:00",
+        "type": "news",
+        "title": "Preventive Maintenance Week",
+        "description": "",
+        "project_id": 1,
+        "creation_date": "2023-03-03T16:30:13.796273+00:00",
+        "creation_user_id": 1,
+        "last_update_date": "2023-03-03T16:30:13.796273+00:00",
+        "last_update_user_id": 1,
+        "extra": {
+            "data": {
+                "type": "warning",
+                "active": true
+                }
+            },
+      "description": "Most of the CryoEM center instruments will undergo preventive maintenance during next week."
+    },
+    {
+      "date": "2023-03-06T16:30:13.796273+00:00",
+        "type": "news",
+        "title": "Krios02 is UNAVAILABLE.",
+        "description": "Falcon 4i camera order placed, replacement to be done.",
+        "project_id": 1,
+        "creation_date": "2023-03-06T16:30:13.796273+00:00",
+        "creation_user_id": 1,
+        "last_update_date": "2023-03-06T16:30:13.796273+00:00",
+        "last_update_user_id": 1,
+        "extra": {
+            "data": {
+                "type": "danger",
+                "active": true
+                }
+            },
+      "description": "Most of the CryoEM center instruments will undergo preventive maintenance during next week."
+    },
         {
         "date": "2023-03-01T16:30:13.796273+00:00",
         "type": "grids_preparation",