about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.dev/run-checks.sh2
-rw-r--r--.guix-channel4
-rw-r--r--README.org2
-rw-r--r--mypy.ini3
-rw-r--r--qc_app/default_settings.py2
-rw-r--r--r_qtl/r_qtl2.py8
-rw-r--r--scripts/cli/options.py2
-rw-r--r--scripts/compute_phenotype_means.py2
-rw-r--r--scripts/insert_samples.py16
-rw-r--r--scripts/load_phenotypes_to_db.py17
-rw-r--r--scripts/phenotypes/__init__.py1
-rw-r--r--scripts/phenotypes/delete_phenotypes.py173
-rw-r--r--scripts/qc_on_rqtl2_bundle.py9
-rw-r--r--scripts/rqtl2/install_genotypes.py6
-rw-r--r--scripts/rqtl2/install_phenos.py7
-rw-r--r--scripts/rqtl2/phenotypes_qc.py3
-rw-r--r--scripts/run_qtlreaper.py65
-rw-r--r--tests/conftest.py2
-rw-r--r--tests/test_instance_dir/config.py2
-rw-r--r--tests/uploader/test_parse.py2
-rw-r--r--uploader/__init__.py30
-rw-r--r--uploader/background_jobs.py143
-rw-r--r--uploader/base_routes.py14
-rw-r--r--uploader/configutils.py13
-rw-r--r--uploader/default_settings.py12
-rw-r--r--uploader/errors.py3
-rw-r--r--uploader/expression_data/dbinsert.py6
-rw-r--r--uploader/expression_data/views.py10
-rw-r--r--uploader/files/chunks.py4
-rw-r--r--uploader/files/functions.py4
-rw-r--r--uploader/files/views.py4
-rw-r--r--uploader/flask_extensions.py41
-rw-r--r--uploader/jobs.py8
-rw-r--r--uploader/oauth2/client.py20
-rw-r--r--uploader/phenotypes/misc.py2
-rw-r--r--uploader/phenotypes/models.py220
-rw-r--r--uploader/phenotypes/views.py248
-rw-r--r--uploader/population/rqtl2.py6
-rw-r--r--uploader/population/views.py7
-rw-r--r--uploader/publications/datatables.py2
-rw-r--r--uploader/publications/misc.py4
-rw-r--r--uploader/publications/pubmed.py3
-rw-r--r--uploader/publications/views.py33
-rw-r--r--uploader/route_utils.py22
-rw-r--r--uploader/samples/views.py167
-rw-r--r--uploader/session.py21
-rw-r--r--uploader/species/views.py4
-rw-r--r--uploader/static/css/layout-common.css18
-rw-r--r--uploader/static/css/layout-large.css6
-rw-r--r--uploader/static/css/layout-medium.css5
-rw-r--r--uploader/static/css/layout-small.css9
-rw-r--r--uploader/static/css/theme.css33
-rw-r--r--uploader/static/images/frontpage_banner.pngbin0 -> 122236 bytes
-rw-r--r--uploader/static/js/datatables.js74
-rw-r--r--uploader/static/js/upload_samples.js24
-rw-r--r--uploader/static/js/utils.js3
-rw-r--r--uploader/sui.py8
-rw-r--r--uploader/templates/background-jobs/base.html10
-rw-r--r--uploader/templates/background-jobs/default-success-page.html17
-rw-r--r--uploader/templates/background-jobs/delete-job.html61
-rw-r--r--uploader/templates/background-jobs/job-status.html45
-rw-r--r--uploader/templates/background-jobs/job-summary.html75
-rw-r--r--uploader/templates/background-jobs/list-jobs.html79
-rw-r--r--uploader/templates/background-jobs/macro-display-job-details.html29
-rw-r--r--uploader/templates/background-jobs/stop-job.html61
-rw-r--r--uploader/templates/background-jobs/sui-default-success-page.html17
-rw-r--r--uploader/templates/base.html120
-rw-r--r--uploader/templates/cli-output.html2
-rw-r--r--uploader/templates/genotypes/base.html31
-rw-r--r--uploader/templates/genotypes/list-genotypes.html17
-rw-r--r--uploader/templates/genotypes/list-markers.html22
-rw-r--r--uploader/templates/genotypes/view-dataset.html16
-rw-r--r--uploader/templates/index.html229
-rw-r--r--uploader/templates/jobs/sui-job-error.html17
-rw-r--r--uploader/templates/jobs/sui-job-not-found.html11
-rw-r--r--uploader/templates/jobs/sui-job-status.html24
-rw-r--r--uploader/templates/login.html12
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-base.html31
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-raw-files.html36
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html18
-rw-r--r--uploader/templates/phenotypes/base.html36
-rw-r--r--uploader/templates/phenotypes/confirm-delete-phenotypes.html196
-rw-r--r--uploader/templates/phenotypes/create-dataset.html5
-rw-r--r--uploader/templates/phenotypes/edit-phenotype.html2
-rw-r--r--uploader/templates/phenotypes/job-status.html55
-rw-r--r--uploader/templates/phenotypes/load-phenotypes-success.html20
-rw-r--r--uploader/templates/phenotypes/macro-display-preview-table.html24
-rw-r--r--uploader/templates/phenotypes/review-job-data.html4
-rw-r--r--uploader/templates/phenotypes/sui-add-phenotypes-base.html155
-rw-r--r--uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html829
-rw-r--r--uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html189
-rw-r--r--uploader/templates/phenotypes/sui-base.html25
-rw-r--r--uploader/templates/phenotypes/sui-job-status.html140
-rw-r--r--uploader/templates/phenotypes/sui-load-phenotypes-success.html26
-rw-r--r--uploader/templates/phenotypes/sui-review-job-data.html121
-rw-r--r--uploader/templates/phenotypes/view-dataset.html95
-rw-r--r--uploader/templates/phenotypes/view-phenotype.html21
-rw-r--r--uploader/templates/platforms/base.html22
-rw-r--r--uploader/templates/platforms/create-platform.html20
-rw-r--r--uploader/templates/platforms/list-platforms.html5
-rw-r--r--uploader/templates/populations/base.html28
-rw-r--r--uploader/templates/populations/create-population.html18
-rw-r--r--uploader/templates/populations/macro-display-population-card.html3
-rw-r--r--uploader/templates/populations/sui-base.html12
-rw-r--r--uploader/templates/populations/sui-view-population.html267
-rw-r--r--uploader/templates/populations/view-population.html201
-rw-r--r--uploader/templates/publications/base.html13
-rw-r--r--uploader/templates/publications/create-publication.html23
-rw-r--r--uploader/templates/publications/delete-publication.html11
-rw-r--r--uploader/templates/publications/edit-publication.html13
-rw-r--r--uploader/templates/publications/index.html59
-rw-r--r--uploader/templates/publications/view-publication.html2
-rw-r--r--uploader/templates/samples/base.html29
-rw-r--r--uploader/templates/samples/list-samples.html50
-rw-r--r--uploader/templates/samples/upload-failure.html5
-rw-r--r--uploader/templates/samples/upload-progress.html5
-rw-r--r--uploader/templates/samples/upload-samples.html97
-rw-r--r--uploader/templates/samples/upload-success.html5
-rw-r--r--uploader/templates/species/base.html21
-rw-r--r--uploader/templates/species/sui-base.html10
-rw-r--r--uploader/templates/species/sui-view-species.html127
-rw-r--r--uploader/templates/species/view-species.html171
-rw-r--r--uploader/templates/sui-base.html103
-rw-r--r--uploader/templates/sui-index.html123
-rw-r--r--uploader/ui.py2
125 files changed, 2421 insertions, 3506 deletions
diff --git a/.dev/run-checks.sh b/.dev/run-checks.sh
new file mode 100644
index 0000000..66ac681
--- /dev/null
+++ b/.dev/run-checks.sh
@@ -0,0 +1,2 @@
+pylint setup.py tests quality_control uploader r_qtl scripts && \
+    mypy --show-error-codes .
diff --git a/.guix-channel b/.guix-channel
index 54206b2..f1a8fa6 100644
--- a/.guix-channel
+++ b/.guix-channel
@@ -35,11 +35,12 @@
   (channel
    (name guix-bioinformatics)
    (url "https://git.genenetwork.org/guix-bioinformatics")
-   (commit "903465c85c9b2ae28480b236c3364da873ca8f51"))
+   (commit "9b0955f14ec725990abb1f6af3b9f171e4943f77"))
   (channel
    (name guix-past)
    (url "https://codeberg.org/guix-science/guix-past")
    (branch "master")
+   (commit "473c942b509ab3ead35159d27dfbf2031a36cd4d")
    (introduction
     (channel-introduction
      (version 0)
@@ -50,6 +51,7 @@
    (name guix-rust-past-crates)
    (url "https://codeberg.org/guix/guix-rust-past-crates.git")
    (branch "trunk")
+   (commit "b8b7ffbd1cec9f56f93fae4da3a74163bbc9c570")
    (introduction
     (channel-introduction
      (version 0)
diff --git a/README.org b/README.org
index ca77653..efa837b 100644
--- a/README.org
+++ b/README.org
@@ -219,7 +219,7 @@ To check for correct type usage in the application, run:
 Run unit tests with:
 #+BEGIN_SRC shell
   $ export UPLOADER_CONF=</path/to/configuration/file.py>
-  $ pytest -m unit_test
+  $ pytest -m unit_test -n auto
 #+END_SRC
 
 To run ALL tests (not just unit tests):
diff --git a/mypy.ini b/mypy.ini
index 7bed360..263460d 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,5 +1,8 @@
 [mypy]
 
+[mypy-lxml.*]
+ignore_missing_imports = True
+
 [mypy-flask.*]
 ignore_missing_imports = True
 
diff --git a/qc_app/default_settings.py b/qc_app/default_settings.py
index 7a9da0f..7bb0bf8 100644
--- a/qc_app/default_settings.py
+++ b/qc_app/default_settings.py
@@ -7,7 +7,7 @@ import os
 
 LOG_LEVEL = os.getenv("LOG_LEVEL", "WARNING")
 SECRET_KEY = b"<Please! Please! Please! Change This!>"
-UPLOAD_FOLDER = "/tmp/qc_app_files"
+UPLOADS_DIRECTORY = "/tmp/qc_app_files"
 REDIS_URL = "redis://"
 JOBS_TTL_SECONDS = 1209600 # 14 days
 GNQC_REDIS_PREFIX="GNQC"
diff --git a/r_qtl/r_qtl2.py b/r_qtl/r_qtl2.py
index 0ef487f..ce1dbf8 100644
--- a/r_qtl/r_qtl2.py
+++ b/r_qtl/r_qtl2.py
@@ -584,16 +584,16 @@ def read_csv_file_headers(
         comment_char: str = "#"
 ) -> tuple[str, ...]:
     """Read the 'true' headers of a CSV file."""
-    headers = tuple()
+    headers: tuple[str, ...] = tuple()
     for line in read_text_file(filepath):
         if line.startswith(comment_char):
             continue
 
-        line = tuple(field.strip() for field in line.split(separator))
+        row = tuple(field.strip() for field in line.split(separator))
         if not transposed:
-            return line
+            return row
 
-        headers = headers + (line[0],)
+        headers = headers + (row[0],)
         continue
 
     return headers
diff --git a/scripts/cli/options.py b/scripts/cli/options.py
index 67f35dc..70d2a27 100644
--- a/scripts/cli/options.py
+++ b/scripts/cli/options.py
@@ -13,7 +13,7 @@ def add_logging(parser: ArgumentParser) -> ArgumentParser:
         type=str,
         default="INFO",
         choices=loglevels,
-        help=(f"Controls the severity of events to log. Valid values are: " +
+        help=("Controls the severity of events to log. Valid values are: " +
               ", ".join(f"'{level}'" for level in loglevels)))
     return parser
 
diff --git a/scripts/compute_phenotype_means.py b/scripts/compute_phenotype_means.py
index ef2fabc..6d39ace 100644
--- a/scripts/compute_phenotype_means.py
+++ b/scripts/compute_phenotype_means.py
@@ -51,7 +51,7 @@ def run(args) -> int:
 
 
 T = TypeVar("T")
-def comma_separated_list(val: str, itemstype: T = str) -> tuple[T, ...]:
+def comma_separated_list(val: str, itemstype: type = str) -> tuple[T, ...]:
     """Convert val into a list of items of type 'itemstype'."""
     return tuple(itemstype(item.strip()) for item in val.split(","))
 
diff --git a/scripts/insert_samples.py b/scripts/insert_samples.py
index fc029f9..96ae8e2 100644
--- a/scripts/insert_samples.py
+++ b/scripts/insert_samples.py
@@ -6,10 +6,10 @@ import argparse
 import traceback
 
 import MySQLdb as mdb
-from redis import Redis
+
 from gn_libs.mysqldb import database_connection
 
-from uploader.check_connections import check_db, check_redis
+from uploader.check_connections import check_db
 from uploader.species.models import species_by_id
 from uploader.population.models import population_by_id
 from uploader.samples.models import (
@@ -35,7 +35,6 @@ class SeparatorAction(argparse.Action):
         setattr(namespace, self.dest, (chr(9) if values == "\\t" else values))
 
 def insert_samples(conn: mdb.Connection,# pylint: disable=[too-many-arguments, too-many-positional-arguments]
-                   rconn: Redis,# pylint: disable=[unused-argument]
                    speciesid: int,
                    populationid: int,
                    samplesfile: pathlib.Path,
@@ -119,11 +118,6 @@ if __name__ == "__main__":
             help=("The character used to delimit (surround?) the value in "
                   "each column."))
 
-        # == Script-specific extras ==
-        parser.add_argument("--redisuri",
-                            help="URL to initialise connection to redis",
-                            default="redis:///")
-
         args = parser.parse_args()
         return args
 
@@ -132,17 +126,13 @@ if __name__ == "__main__":
         status_code = 1 # Exit with an Exception
         args = cli_args()
         check_db(args.databaseuri)
-        check_redis(args.redisuri)
         if not args.samplesfile.exists():
             logging.error("File not found: '%s'.", args.samplesfile)
             return 2
 
-        with (Redis.from_url(args.redisuri, decode_responses=True) as rconn,
-              database_connection(args.databaseuri) as dbconn):
-
+        with database_connection(args.databaseuri) as dbconn:
             try:
                 status_code = insert_samples(dbconn,
-                                             rconn,
                                              args.speciesid,
                                              args.populationid,
                                              args.samplesfile,
diff --git a/scripts/load_phenotypes_to_db.py b/scripts/load_phenotypes_to_db.py
index e449b82..e303bb3 100644
--- a/scripts/load_phenotypes_to_db.py
+++ b/scripts/load_phenotypes_to_db.py
@@ -6,9 +6,9 @@ import time
 import logging
 import argparse
 import datetime
-from typing import Any
 from pathlib import Path
 from zipfile import ZipFile
+from typing import Any, Iterable
 from urllib.parse import urljoin
 from functools import reduce, partial
 
@@ -55,7 +55,7 @@ def save_phenotypes(
 
     if control_data["phenocovar_transposed"]:
         logger.info("Undoing transposition of the files rows and columns.")
-        phenofiles = (
+        phenofiles = tuple(
             rqtl2.transpose_csv_with_rename(
                 _file,
                 build_line_splitter(control_data),
@@ -86,7 +86,7 @@ def __row_to_dataitems__(
         dataidmap: dict,
         pheno_name2id: dict[str, int],
         samples: dict
-) -> tuple[dict, ...]:
+) -> Iterable[dict]:
     samplename = sample_row["id"]
 
     return ({
@@ -134,7 +134,7 @@ def save_numeric_data(# pylint: disable=[too-many-positional-arguments,too-many-
         conn: mysqldb.Connection,
         dataidmap: dict,
         pheno_name2id: dict[str, int],
-        samples: tuple[dict, ...],
+        samples: dict,
         control_data: dict,
         filesdir: Path,
         filetype: str,
@@ -311,7 +311,9 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
     ).either(__handle_error__, __handle_success__)
 
 
-def load_data(conn: mysqldb.Connection, job: dict) -> int:#pylint: disable=[too-many-locals]
+def load_data(# pylint: disable=[too-many-locals]
+        conn: mysqldb.Connection, job: dict
+) -> tuple[dict, dict, dict, tuple[int, ...]]:
     """Load the data attached in the given job."""
     _job_metadata = job["metadata"]
     # Steps
@@ -365,9 +367,8 @@ def load_data(conn: mysqldb.Connection, job: dict) -> int:#pylint: disable=[too-
                 "publication_id": row["publication_id"],
                 "data_id": row["data_id"]
             },)))
-    dataidmap, pheno_name2id, _xrefs = reduce(__build_phenos_maps__,
-                                      _phenos,
-                                      ({},{}, tuple()))
+    dataidmap, pheno_name2id, _xrefs = reduce(# type: ignore[var-annotated]
+        __build_phenos_maps__, _phenos, ({},{}, tuple()))
     # 3. a. Fetch the strain names and IDS: create name->ID map
     samples = {
         row["Name"]: row
diff --git a/scripts/phenotypes/__init__.py b/scripts/phenotypes/__init__.py
new file mode 100644
index 0000000..73ad839
--- /dev/null
+++ b/scripts/phenotypes/__init__.py
@@ -0,0 +1 @@
+"Scripts for dealing with phenotypes."
diff --git a/scripts/phenotypes/delete_phenotypes.py b/scripts/phenotypes/delete_phenotypes.py
new file mode 100644
index 0000000..461f3ec
--- /dev/null
+++ b/scripts/phenotypes/delete_phenotypes.py
@@ -0,0 +1,173 @@
+"""Delete phenotypes."""
+import sys
+import logging
+from pathlib import Path
+from typing import Optional
+from urllib.parse import urljoin
+from argparse import Namespace, ArgumentParser
+
+import requests
+from MySQLdb.cursors import DictCursor, BaseCursor
+
+from gn_libs.mysqldb import database_connection
+
+from uploader.phenotypes.models import delete_phenotypes
+from scripts.cli.logging import setup_logging
+from scripts.cli.options import (add_logging,
+                                 add_mariadb_uri,
+                                 add_population_id)
+
+logger = logging.getLogger(__name__)
+
+def read_xref_ids_file(filepath: Optional[Path]) -> tuple[int, ...]:
+    """Read the phenotypes' cross-reference IDS from file."""
+    if filepath is None:
+        return tuple()
+
+    logger.debug("Using file '%s' to retrieve XREF IDs for deletion.",
+                 filepath.name)
+    _ids: tuple[int, ...] = tuple()
+    with filepath.open(mode="r") as infile:
+        for line in infile.readlines():
+            try:
+                _ids += (int(line.strip()),)
+            except TypeError:
+                pass
+
+    return _ids
+
+
+def fetch_all_xref_ids(
+        cursor: BaseCursor, population_id: int) -> tuple[int, ...]:
+    """Fetch all cross-reference IDs."""
+    cursor.execute("SELECT Id FROM PublishXRef WHERE InbredSetId=%s",
+                   (population_id,))
+    return tuple(int(row["Id"]) for row in cursor.fetchall())
+
+
+def update_auth(
+        auth_details: tuple[str, str],
+        species_id: int,
+        population_id: int,
+        dataset_id: int,
+        xref_ids: tuple[int, ...] = tuple()
+):
+    """Update the authorisation server: remove items to delete."""
+    authserver, token = auth_details
+    resp = requests.post(
+        urljoin(authserver,
+                (f"/auth/data/phenotypes/{species_id}/{population_id}"
+                 f"/{dataset_id}/delete")),
+        timeout=(9.13, 20),
+        headers={
+            "Authorization": f"Bearer {token}",
+            "Content-Type": "application/json"
+        },
+        json={"xref_ids": xref_ids})
+    resp.raise_for_status()
+
+
+def delete_the_phenotypes(
+        cursor: BaseCursor,
+        population_id: int,
+        xref_ids: tuple[int, ...] = tuple()) -> int:
+    """Process and delete the phenotypes."""
+    delete_phenotypes(cursor, population_id, xref_ids)
+
+    return 0
+
+if __name__ == "__main__":
+    def parse_args() -> Namespace:
+        """Parse CLI arguments."""
+        parser = add_logging(
+            add_population_id(
+                add_mariadb_uri(
+                    ArgumentParser(
+                        prog="delete-phenotypes",
+                        description=(
+                            "Script to delete phenotypes from the database.")))))
+        parser.add_argument(
+            "dataset_id",
+            metavar="DATASET-ID",
+            type=int,
+            help="The dataset identifier for phenotypes to delete.")
+        parser.add_argument(
+            "auth_server_uri",
+            metavar="AUTH-SERVER-URI",
+            type=str,
+            help="URI to the authorisation server.")
+        parser.add_argument(
+            "auth_token",
+            metavar="AUTH-TOKEN",
+            type=str,
+            help=("Token to use to update the authorisation system with the "
+                  "deletions done."))
+        parser.add_argument(
+            "--xref_ids_file",
+            metavar="XREF-IDS-FILE",
+            type=Path,
+            help=("Path to a file with phenotypes cross-reference IDs to "
+                  "delete."))
+        parser.add_argument(
+            "--delete-all",
+            action="store_true",
+            help=("If no 'XREF-IDS-FILE' is provided, this flag determines "
+                  "whether or not all the phenotypes for the given population "
+                  "will be deleted."))
+        return parser.parse_args()
+
+
+    def main():
+        """The `delete-phenotypes` script's entry point."""
+        args = parse_args()
+        setup_logging(logger, args.log_level.upper(), tuple())
+        with (database_connection(args.db_uri) as conn,
+              conn.cursor(cursorclass=DictCursor) as cursor):
+            xref_ids = read_xref_ids_file(args.xref_ids_file)
+            try:
+                assert not (len(xref_ids) > 0 and args.delete_all)
+                xref_ids = (fetch_all_xref_ids(cursor, args.population_id)
+                            if args.delete_all else xref_ids)
+                logger.debug("Will delete %s phenotypes and related data",
+                             len(xref_ids))
+                if len(xref_ids) == 0:
+                    print("No cross-reference IDs were provided. Aborting.")
+                    return 0
+
+                print("Updating authorisations: ", end="")
+                update_auth((args.auth_server_uri, args.auth_token),
+                            args.species_id,
+                            args.population_id,
+                            args.dataset_id,
+                            xref_ids)
+                print("OK.")
+                print("Deleting the data: ", end="")
+                delete_phenotypes(cursor, args.population_id, xref_ids=xref_ids)
+                print("OK.")
+                if args.xref_ids_file is not None:
+                    print("Deleting temporary file: ", end="")
+                    args.xref_ids_file.unlink()
+                    print("OK.")
+
+                return 0
+            except AssertionError:
+                logger.error(
+                    "'DELETE-ALL' and 'XREF-IDS' are mutually exclusive. "
+                    "If you specify the list of XREF-IDS (in a file) to delete "
+                    "and also specify to 'DELETE-ALL' phenotypes in the "
+                    "population, we have no way of knowing what it is you want.")
+                return 1
+            except requests.exceptions.HTTPError as _exc:
+                resp = _exc.response
+                resp_data = resp.json()
+                logger.debug("%s: %s",
+                             resp_data["error"],
+                             resp_data["error_description"],
+                             exc_info=True)
+                return 1
+            except Exception as _exc:# pylint: disable=[broad-exception-caught]
+                logger.debug("Failed while attempting to delete phenotypes.",
+                             exc_info=True)
+                return 1
+
+    sys.exit(main())
diff --git a/scripts/qc_on_rqtl2_bundle.py b/scripts/qc_on_rqtl2_bundle.py
index 0207938..4e6ef00 100644
--- a/scripts/qc_on_rqtl2_bundle.py
+++ b/scripts/qc_on_rqtl2_bundle.py
@@ -40,7 +40,7 @@ def add_to_errors(rconn: Redis,
     """Add `errors` to a given list of errors"""
     errs = tuple(dict(item) for item in set(
         [dict2tuple(old) for old in
-         json.loads(rconn.hget(fqjobid, key) or "[]")] +
+         json.loads(rconn.hget(fqjobid, key) or "[]")] +# type: ignore[arg-type]
         [dict2tuple({"type": type(error).__name__, **error._asdict()})
          for error in errors]))
     rconn.hset(fqjobid, key, json.dumps(errs))
@@ -83,7 +83,8 @@ def retrieve_errors_with_progress(rconn: Redis,#pylint: disable=[too-many-locals
     count = 0
     checked = 0
     cdata = rqtl2.control_data(zfile)
-    rconn.hset(fqjobid, f"{filetype}-filesize", compute_filesize(zfile, filetype))
+    rconn.hset(
+        fqjobid, f"{filetype}-filesize", str(compute_filesize(zfile, filetype)))
     def __update_processed__(value):
         nonlocal checked
         checked = checked + len(value)
@@ -104,7 +105,7 @@ def retrieve_errors_with_progress(rconn: Redis,#pylint: disable=[too-many-locals
                             yield error
                         __update_processed__(value)
 
-        rconn.hset(fqjobid, f"{filetype}-linecount", count)
+        rconn.hset(fqjobid, f"{filetype}-linecount", count)# type: ignore[arg-type]
     except rqe.MissingFileException:
         fname = cdata.get(filetype)
         yield rqfe.MissingFile(filetype, fname, (
@@ -295,7 +296,7 @@ def run_qc(rconn: Redis,
         return 1
 
     def __fetch_errors__(rkey: str) -> tuple:
-        return tuple(json.loads(rconn.hget(fqjobid, rkey) or "[]"))
+        return tuple(json.loads(rconn.hget(fqjobid, rkey) or "[]")) # type: ignore[arg-type]
 
     return (1 if any((
         bool(__fetch_errors__(key))
diff --git a/scripts/rqtl2/install_genotypes.py b/scripts/rqtl2/install_genotypes.py
index 8762655..5e6abb0 100644
--- a/scripts/rqtl2/install_genotypes.py
+++ b/scripts/rqtl2/install_genotypes.py
@@ -20,7 +20,7 @@ from scripts.rqtl2.entry import build_main
 from scripts.rqtl2.cli_parser import add_common_arguments
 from scripts.cli_parser import init_cli_parser, add_global_data_arguments
 
-__MODULE__ = "scripts.rqtl2.install_genotypes"
+logger = getLogger(__name__)
 
 def insert_markers(
         dbconn: mdb.Connection,
@@ -191,7 +191,7 @@ def install_genotypes(#pylint: disable=[too-many-locals]
         dbconn: mdb.Connection,
         fullyqualifiedjobid: str,#pylint: disable=[unused-argument]
         args: argparse.Namespace,
-        logger: Logger = getLogger(__name__)
+        logger: Logger = logger # pylint: disable=[redefined-outer-name]
 ) -> int:
     """Load any existing genotypes into the database."""
     (speciesid, populationid, datasetid, rqtl2bundle) = (
@@ -257,5 +257,5 @@ if __name__ == "__main__":
 
         return parser.parse_args()
 
-    main = build_main(cli_args(), install_genotypes, __MODULE__)
+    main = build_main(cli_args(), install_genotypes, logger)
     sys.exit(main())
diff --git a/scripts/rqtl2/install_phenos.py b/scripts/rqtl2/install_phenos.py
index 9059cd6..11ac8a4 100644
--- a/scripts/rqtl2/install_phenos.py
+++ b/scripts/rqtl2/install_phenos.py
@@ -19,7 +19,7 @@ from r_qtl import r_qtl2_qc as rqc
 
 from functional_tools import take
 
-__MODULE__ = "scripts.rqtl2.install_phenos"
+logger = getLogger(__name__)
 
 def insert_probesets(dbconn: mdb.Connection,
                      platformid: int,
@@ -101,7 +101,8 @@ def install_pheno_files(#pylint: disable=[too-many-locals]
         dbconn: mdb.Connection,
         fullyqualifiedjobid: str,#pylint: disable=[unused-argument]
         args: argparse.Namespace,
-        logger: Logger = getLogger()) -> int:
+        logger: Logger = logger # pylint: disable=[redefined-outer-name]
+) -> int:
     """Load data in `pheno` files and other related files into the database."""
     (speciesid, platformid, datasetid, rqtl2bundle) = (
         args.speciesid, args.platformid, args.datasetid, args.rqtl2bundle)
@@ -159,5 +160,5 @@ if __name__ == "__main__":
 
         return parser.parse_args()
 
-    main = build_main(cli_args(), install_pheno_files, __MODULE__)
+    main = build_main(cli_args(), install_pheno_files, logger)
     sys.exit(main())
diff --git a/scripts/rqtl2/phenotypes_qc.py b/scripts/rqtl2/phenotypes_qc.py
index 9f11f57..72d6c83 100644
--- a/scripts/rqtl2/phenotypes_qc.py
+++ b/scripts/rqtl2/phenotypes_qc.py
@@ -376,7 +376,8 @@ def run_qc(# pylint: disable=[too-many-locals]
         rconn: Redis,
         dbconn: mdb.Connection,
         fullyqualifiedjobid: str,
-        args: Namespace
+        args: Namespace,
+        logger: Logger = logger # pylint: disable=[redefined-outer-name]
 ) -> int:
     """Run quality control checks on the bundle."""
     print("Beginning the quality assurance checks.")
diff --git a/scripts/run_qtlreaper.py b/scripts/run_qtlreaper.py
index ab58203..ab19da0 100644
--- a/scripts/run_qtlreaper.py
+++ b/scripts/run_qtlreaper.py
@@ -1,14 +1,14 @@
 """Script to run rust-qtlreaper and update database with results."""
+import os
 import sys
 import csv
 import time
 import secrets
 import logging
-import traceback
 import subprocess
 from pathlib import Path
-from typing import Union
 from functools import reduce
+from typing import Union, Iterator
 from argparse import Namespace, ArgumentParser
 
 from gn_libs import mysqldb
@@ -57,7 +57,7 @@ def reconcile_samples(
 def generate_qtlreaper_traits_file(
         outdir: Path,
         samples: tuple[str, ...],
-        traits_data: dict[str, Union[int, float]],
+        traits_data: tuple[dict[str, Union[int, float]], ...],
         filename_prefix: str = ""
 ) -> Path:
     """Generate a file for use with qtlreaper that contains the traits' data."""
@@ -66,7 +66,7 @@ def generate_qtlreaper_traits_file(
     _dialect.quoting=0
 
     _traitsfile = outdir.joinpath(
-        f"{filename_prefix}_{secrets.token_urlsafe(15)}.tsv")
+        f"{filename_prefix}_{secrets.token_urlsafe(15)}.tsv")#type: ignore[attr-defined]
     with _traitsfile.open(mode="w", encoding="utf-8") as outptr:
         writer = csv.DictWriter(
             outptr, fieldnames=("Trait",) + samples, dialect=_dialect)
@@ -80,14 +80,13 @@ def generate_qtlreaper_traits_file(
     return _traitsfile
 
 
-def parse_tsv_file(results_file: Path) -> list[dict]:
+def parse_tsv_file(results_file: Path) -> Iterator[dict]:
     """Parse the rust-qtlreaper output into usable python objects."""
     with results_file.open("r", encoding="utf-8") as readptr:
         _dialect = csv.unix_dialect()
         _dialect.delimiter = "\t"
         reader = csv.DictReader(readptr, dialect=_dialect)
-        for row in reader:
-            yield row
+        yield from reader
 
 
 def __qtls_by_trait__(qtls, current):
@@ -98,7 +97,8 @@ def __qtls_by_trait__(qtls, current):
     }
 
 
-def save_qtl_values_to_db(conn, qtls: dict):
+def save_qtl_values_to_db(conn, qtls: tuple[dict, ...]):
+    """Save computed QTLs to the database."""
     with conn.cursor() as cursor:
         cursor.executemany(
             "UPDATE PublishXRef SET "
@@ -132,11 +132,11 @@ def dispatch(args: Namespace) -> int:
                              ", ".join(_samples_not_in_genofile))
 
             # Fetch traits data: provided list, or all traits in db
-            _traitsdata = phenotypes_vector_data(
+            _traitsdata = tuple(phenotypes_vector_data(
                 conn,
                 args.species_id,
                 args.population_id,
-                xref_ids=tuple(args.xref_ids)).values()
+                xref_ids=tuple(args.xref_ids)).values())
             logger.debug("Successfully got traits data. Generating the QTLReaper's traits file…")
             _traitsfile = generate_qtlreaper_traits_file(
                 args.working_dir,
@@ -146,37 +146,50 @@ def dispatch(args: Namespace) -> int:
             logger.debug("QTLReaper's Traits file: %s", _traitsfile)
 
             _qtlreaper_main_output = args.working_dir.joinpath(
-                f"main-output-{secrets.token_urlsafe(15)}.tsv")
+                f"main-output-{secrets.token_urlsafe(15)}.tsv")#type: ignore[attr-defined]
             logger.debug("Main output filename: %s", _qtlreaper_main_output)
             with subprocess.Popen(
                     ("qtlreaper",
                      "--n_permutations", "1000",
                      "--geno", _genofile,
                      "--traits", _traitsfile,
-                     "--main_output", _qtlreaper_main_output)) as _qtlreaper:
+                     "--main_output", _qtlreaper_main_output),
+                    env=({**os.environ, "RUST_BACKTRACE": "full"}
+                         if logger.getEffectiveLevel() == logging.DEBUG
+                         else dict(os.environ))) as _qtlreaper:
                 while _qtlreaper.poll() is None:
                     logger.debug("QTLReaper process running…")
                     time.sleep(1)
-                    results = tuple(max(qtls, key=lambda qtl: qtl["LRS"])
-                                    for qtls in
-                                    reduce(__qtls_by_trait__,
-                                           parse_tsv_file(_qtlreaper_main_output),
-                                           {}).values())
-            save_qtl_values_to_db(conn, results)
+                    results = (
+                        tuple(#type: ignore[var-annotated]
+                            max(qtls, key=lambda qtl: qtl["LRS"])
+                            for qtls in
+                            reduce(__qtls_by_trait__,
+                                   parse_tsv_file(_qtlreaper_main_output),
+                                   {}).values())
+                        if _qtlreaper_main_output.exists()
+                        else tuple())
             logger.debug("Cleaning up temporary files.")
-            _traitsfile.unlink()
-            _qtlreaper_main_output.unlink()
+
+            # short-circuits to delete file if exists
+            _traitsfile.exists() and _traitsfile.unlink()
+            _qtlreaper_main_output.exists() and _qtlreaper_main_output.unlink()
+
+            if _qtlreaper.returncode != 0:
+                return _qtlreaper.returncode
+
+            save_qtl_values_to_db(conn, results)
             logger.info("Successfully computed p values for %s traits.", len(_traitsdata))
-            exitcode = 0
+            return 0
         except FileNotFoundError as fnf:
-            logger.error(", ".join(fnf.args), exc_info=False)
+            logger.error(", ".join(str(arg) for arg in fnf.args), exc_info=False)
         except AssertionError as aserr:
             logger.error(", ".join(aserr.args), exc_info=False)
-        except Exception as _exc:
+        except Exception as _exc:# pylint: disable=[broad-exception-caught]
             logger.debug("Type of exception: %s", type(_exc))
             logger.error("General exception!", exc_info=True)
-        finally:
-            return exitcode
+
+        return exitcode
 
 
 if __name__ == "__main__":
@@ -205,7 +218,7 @@ if __name__ == "__main__":
                   "in the population."))
         args = parser.parse_args()
         setup_logging(logger, args.log_level)
-        
+
         return dispatch(args)
 
     sys.exit(main())
diff --git a/tests/conftest.py b/tests/conftest.py
index a716c52..2009aab 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -183,7 +183,7 @@ def redis_conn_with_completed_job_some_errors(redis_url, redis_ttl, jobs_prefix,
 def uploads_dir(client): # pylint: disable=[redefined-outer-name]
     """Returns the configured, uploads directory, creating it if it does not
     exist."""
-    the_dir = client.application.config["UPLOAD_FOLDER"]
+    the_dir = client.application.config["UPLOADS_DIRECTORY"]
     if not os.path.exists(the_dir):
         os.mkdir(the_dir)
 
diff --git a/tests/test_instance_dir/config.py b/tests/test_instance_dir/config.py
index 2ee569b..f04b3df 100644
--- a/tests/test_instance_dir/config.py
+++ b/tests/test_instance_dir/config.py
@@ -6,6 +6,6 @@ import os
 
 LOG_LEVEL = os.getenv("LOG_LEVEL", "WARNING")
 SECRET_KEY = b"<Please! Please! Please! Change This!>"
-UPLOAD_FOLDER = "/tmp/qc_app_files"
+UPLOADS_DIRECTORY = "/tmp/qc_app_files"
 REDIS_URL = "redis://"
 JOBS_TTL_SECONDS = 600 # 10 minutes
diff --git a/tests/uploader/test_parse.py b/tests/uploader/test_parse.py
index 20c75b7..56e1b41 100644
--- a/tests/uploader/test_parse.py
+++ b/tests/uploader/test_parse.py
@@ -50,7 +50,7 @@ def test_parse_with_existing_uploaded_file(
     assert the_job["command"] == " ".join([
         sys.executable, "-m", "scripts.validate_file", db_url, redis_url,
         jobs_prefix, job_id, "--redisexpiry", str(redis_ttl), str(speciesid),
-        filetype, f"{client.application.config['UPLOAD_FOLDER']}/{filename}"])
+        filetype, f"{client.application.config['UPLOADS_DIRECTORY']}/{filename}"])
 
 @pytest.mark.parametrize(
     "filename,uri,error_msgs",
diff --git a/uploader/__init__.py b/uploader/__init__.py
index 7425b38..e00c726 100644
--- a/uploader/__init__.py
+++ b/uploader/__init__.py
@@ -11,7 +11,7 @@ from cachelib import FileSystemCache
 
 from gn_libs import jobs as gnlibs_jobs
 
-from flask_session import Session
+from flask_session import Session# type: ignore[attr-defined]
 
 
 from uploader.oauth2.client import user_logged_in, authserver_authorise_uri
@@ -73,6 +73,28 @@ def setup_modules_logging(app_logger, modules):
         _logger.setLevel(loglevel)
 
 
+def __setup_scratch_directory__(app: Flask) -> Flask:
+    app.config["SCRATCH_DIRECTORY"] = Path(
+        app.config["SCRATCH_DIRECTORY"]).absolute()
+    return app
+
+def __setup_upload_directory__(app: Flask) -> Flask:
+    if app.config.get("UPLOADS_DIRECTORY", "").strip() == "":
+        app.config["UPLOADS_DIRECTORY"] = app.config[
+            "SCRATCH_DIRECTORY"].joinpath("uploads")
+    else:
+        app.config["UPLOADS_DIRECTORY"] = Path(
+            app.config["UPLOADS_DIRECTORY"].strip()).absolute()
+
+    return app
+
+
+def update_unspecified_defaults(app: Flask) -> Flask:
+    """Setup the defaults for necessary configurations that do not have values
+    specified for them."""
+    return __setup_upload_directory__(__setup_scratch_directory__(app))
+
+
 def create_app(config: Optional[dict] = None):
     """The application factory.
 
@@ -100,10 +122,11 @@ def create_app(config: Optional[dict] = None):
             # Silently ignore secrets if the file does not exist.
             app.config.from_pyfile(secretsfile)
     app.config.update(config) # Override everything with passed in config
+    update_unspecified_defaults(app)
     ### END: Application configuration
 
     app.config["SESSION_CACHELIB"] = FileSystemCache(
-        cache_dir=Path(app.config["SESSION_FILESYSTEM_CACHE_PATH"]).absolute(),
+        cache_dir=str(Path(app.config["SESSION_FILESYSTEM_CACHE_PATH"]).absolute()),
         threshold=int(app.config["SESSION_FILESYSTEM_CACHE_THRESHOLD"]),
         default_timeout=int(app.config["SESSION_FILESYSTEM_CACHE_TIMEOUT"]))
 
@@ -113,7 +136,8 @@ def create_app(config: Optional[dict] = None):
         "uploader.flask_extensions",
         "uploader.publications.models",
         "uploader.publications.datatables",
-        "uploader.phenotypes.models"))
+        "uploader.phenotypes.models",
+        "uploader.phenotypes.views"))
 
     # setup jinja2 symbols
     app.add_template_global(user_logged_in)
diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py
index fc59ec7..a71dd44 100644
--- a/uploader/background_jobs.py
+++ b/uploader/background_jobs.py
@@ -1,39 +1,51 @@
 """Generic views and utilities to handle background jobs."""
 import uuid
+import datetime
 import importlib
 from typing import Callable
 from functools import partial
 
+from werkzeug.wrappers.response import Response
 from flask import (
+    flash,
     request,
     redirect,
-    Response,
     Blueprint,
-    render_template,
     current_app as app)
 
 from gn_libs import jobs
 from gn_libs import sqlite3
 from gn_libs.jobs.jobs import JobNotFound
 
-
-from uploader.sui import sui_template
-
-from uploader.flask_extensions import url_for
+from uploader import session
 from uploader.authorisation import require_login
+from uploader.flask_extensions import url_for, render_template
 
 background_jobs_bp = Blueprint("background-jobs", __name__)
 HandlerType = Callable[[dict], Response]
 
 
-def __default_error_handler__(job: dict) -> Response:
-    return redirect(url_for("background-jobs.job_error", job_id=job["job_id"]))
+def make_datetime_formatter(dtformat: str = "%A, %d %B %Y at %H:%M %Z") -> Callable[[str], str]:
+    """Make a datetime formatter with the provided `dtformat`"""
+    def __formatter__(val: str) -> str:
+        dt = datetime.datetime.fromisoformat(val)
+        return dt.strftime(dtformat.strip())
+
+    return __formatter__
+
+__default_datetime_formatter__ = make_datetime_formatter()
+
+
+def __default_handler__(_job):
+    return render_template("background-jobs/job-summary.html",
+                           job=_job,
+                           display_datetime=__default_datetime_formatter__)
 
 def register_handlers(
         job_type: str,
         success_handler: HandlerType,
         # pylint: disable=[redefined-outer-name]
-        error_handler: HandlerType = __default_error_handler__
+        error_handler: HandlerType = __default_handler__
         # pylint: disable=[redefined-outer-name]
 ) -> str:
     """Register success and error handlers for each job type."""
@@ -49,7 +61,7 @@ def register_handlers(
     return job_type
 
 
-def register_job_handlers(job: str):
+def register_job_handlers(job: dict):
     """Related to register handlers above."""
     def __load_handler__(absolute_function_path):
         _parts = absolute_function_path.split(".")
@@ -65,7 +77,7 @@ def register_job_handlers(job: str):
         try:
             _error_handler = __load_handler__(metadata["error_handler"])
         except Exception as _exc:# pylint: disable=[broad-exception-caught]
-            _error_handler = __default_error_handler__
+            _error_handler = __default_handler__
         register_handlers(
             metadata["job-type"], _success_handler, _error_handler)
 
@@ -80,8 +92,8 @@ def handler(job: dict, handler_type: str) -> HandlerType:
     ).get(handler_type)
     if bool(_handler):
         return _handler(job)
-    return render_template(sui_template("background-jobs/default-success-page.html"),
-                           job=job)
+
+    return __default_handler__(job)
 
 
 error_handler = partial(handler, handler_type="error")
@@ -98,17 +110,17 @@ def job_status(job_id: uuid.UUID):
             status = job["metadata"]["status"]
 
             register_job_handlers(job)
-            if status == "error":
+            if status in ("error", "stopped"):
                 return error_handler(job)
 
             if status == "completed":
                 return success_handler(job)
 
-            return render_template(sui_template("jobs/job-status.html"), job=job)
+            return render_template("background-jobs/job-status.html",
+                                   job=job,
+                                   display_datetime=__default_datetime_formatter__)
         except JobNotFound as _jnf:
-            return render_template(
-                sui_template("jobs/job-not-found.html"),
-                job_id=job_id)
+            return render_template("jobs/job-not-found.html", job_id=job_id)
 
 
 @background_jobs_bp.route("/error/<uuid:job_id>")
@@ -118,7 +130,96 @@ def job_error(job_id: uuid.UUID):
     with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
         try:
             job = jobs.job(conn, job_id, fulldetails=True)
-            return render_template(sui_template("jobs/job-error.html"), job=job)
+            return render_template("jobs/job-error.html", job=job)
+        except JobNotFound as _jnf:
+            return render_template("jobs/job-not-found.html", job_id=job_id)
+
+
+@background_jobs_bp.route("/list")
+@require_login
+def list_jobs():
+    """List background jobs."""
+    with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
+        return render_template(
+            "background-jobs/list-jobs.html",
+            jobs=jobs.jobs_by_external_id(
+                conn, session.user_details()["user_id"]),
+            display_datetime=__default_datetime_formatter__)
+
+
+@background_jobs_bp.route("/summary/<uuid:job_id>")
+@require_login
+def job_summary(job_id: uuid.UUID):
+    """Provide a summary for completed jobs."""
+    with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
+        try:
+            job = jobs.job(conn, job_id, fulldetails=True)
+            status = job["metadata"]["status"]
+
+            if status in ("completed", "error", "stopped"):
+                return render_template("background-jobs/job-summary.html",
+                                       job=job,
+                                       display_datetime=__default_datetime_formatter__)
+            return redirect(url_for(
+                "background-jobs.job_status", job_id=job["job_id"]))
+        except JobNotFound as _jnf:
+            return render_template("jobs/job-not-found.html", job_id=job_id)
+
+
+@background_jobs_bp.route("/delete/<uuid:job_id>", methods=["GET", "POST"])
+@require_login
+def delete_single(job_id: uuid.UUID):
+    """Delete a single job."""
+    with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
+        try:
+            job = jobs.job(conn, job_id, fulldetails=True)
+            status = job["metadata"]["status"]
+            if status not in ("completed", "error", "stopped"):
+                flash("We cannot delete a running job.", "alert alert-danger")
+                return redirect(url_for(
+                    "background-jobs.job_summary", job_id=job_id))
+
+            if request.method == "GET":
+                return render_template("background-jobs/delete-job.html",
+                                       job=job,
+                                       display_datetime=__default_datetime_formatter__)
+
+            if request.form["btn-confirm-delete"] == "delete":
+                jobs.delete_job(conn, job_id)
+                flash("Job was deleted successfully.", "alert alert-success")
+                return redirect(url_for("background-jobs.list_jobs"))
+            flash("Delete cancelled.", "alert alert-info")
+            return redirect(url_for(
+                "background-jobs.job_summary", job_id=job_id))
+        except JobNotFound as _jnf:
+            return render_template("jobs/job-not-found.html", job_id=job_id)
+
+
+@background_jobs_bp.route("/stop/<uuid:job_id>", methods=["GET", "POST"])
+@require_login
+def stop_job(job_id: uuid.UUID):
+    """Stop a running job."""
+    with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
+        try:
+            job = jobs.job(conn, job_id, fulldetails=True)
+            status = job["metadata"]["status"]
+            if status != "running":
+                flash("Cannot stop a job that is not running.", "alert alert-danger")
+                return redirect(url_for(
+                    "background-jobs.job_summary", job_id=job_id))
+
+            if request.method == "GET":
+                return render_template("background-jobs/stop-job.html",
+                                       job=job,
+                                       display_datetime=__default_datetime_formatter__)
+
+            if request.form["btn-confirm-stop"] == "stop":
+                jobs.kill_job(conn, job_id)
+                flash("Job was stopped successfully.", "alert alert-success")
+                return redirect(url_for(
+                    "background-jobs.job_summary", job_id=job_id))
+            flash("Stop cancelled.", "alert alert-info")
+            return redirect(url_for(
+                "background-jobs.job_summary", job_id=job_id))
         except JobNotFound as _jnf:
-            return render_template(sui_template("jobs/job-not-found.html"),
-                                   job_id=job_id)
+            return render_template("jobs/job-not-found.html", job_id=job_id)
diff --git a/uploader/base_routes.py b/uploader/base_routes.py
index cc2a270..72a8402 100644
--- a/uploader/base_routes.py
+++ b/uploader/base_routes.py
@@ -11,12 +11,8 @@ from flask import (flash,
                    current_app as app,
                    send_from_directory)
 
-
-from uploader.sui import sui_template
-
 from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
-from uploader.oauth2.client import user_logged_in
 from uploader.species.models import all_species, species_by_id
 
 base = Blueprint("base", __name__)
@@ -36,19 +32,15 @@ def favicon():
 def index():
     """Load the landing page"""
     streamlined_ui = request.args.get("streamlined_ui")
-    if not bool(streamlined_ui):# TODO: Remove this section
-        return render_template(
-            "index.html" if user_logged_in() else "login.html",
-            gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], "/intro"))
-
     with database_connection(app.config["SQL_URI"]) as conn:
         print("We found a species ID. Processing...")
         if not bool(request.args.get("species_id")):
             return render_template(
-                sui_template("index.html"),
+                "index.html",
                 gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], "/intro"),
                 species=all_species(conn),
-                streamlined_ui=streamlined_ui)
+                view_under_construction=request.args.get(
+                    "view_under_construction", False))
 
         species = species_by_id(conn, request.args.get("species_id"))
         if not bool(species):
diff --git a/uploader/configutils.py b/uploader/configutils.py
new file mode 100644
index 0000000..c5db50b
--- /dev/null
+++ b/uploader/configutils.py
@@ -0,0 +1,13 @@
+"""Functions to fetch settings."""
+from pathlib import Path
+
+def fetch_setting(app, setting):
+    """Fetch a specified configuration `setting` from the `app` object."""
+    return app.config[setting]
+
+def uploads_dir(app) -> Path:
+    """Fetch the uploads directory"""
+    _dir = Path(fetch_setting(app, "UPLOADS_DIRECTORY")).absolute()
+    assert _dir.exists() and _dir.is_dir(), (
+        f"'{_dir}' needs to be an existing directory.")
+    return _dir
diff --git a/uploader/default_settings.py b/uploader/default_settings.py
index bb3a967..6381a67 100644
--- a/uploader/default_settings.py
+++ b/uploader/default_settings.py
@@ -5,8 +5,14 @@ actual configuration file used for the production and staging systems.
 
 LOG_LEVEL = "WARNING"
 SECRET_KEY = b"<Please! Please! Please! Change This!>"
-UPLOAD_FOLDER = "/tmp/qc_app_files"
-TEMPORARY_DIRECTORY = "/tmp/gn-uploader-tmpdir"
+
+# Scratch directory and uploads:
+# *** The scratch directory ***
+# We avoid `/tmp` entirely for the scratch directory to avoid shared global
+# mutable state with other users/applications/processes.
+SCRATCH_DIRECTORY = "~/tmp/gn-uploader-scratchdir"
+UPLOADS_DIRECTORY = ""# If not set, will be under scratch directory.
+
 REDIS_URL = "redis://"
 JOBS_TTL_SECONDS = 1209600 # 14 days
 GNQC_REDIS_PREFIX="gn-uploader"
@@ -32,4 +38,4 @@ JWKS_DELETION_AGE_DAYS = 14 # Days (from creation) to keep a JWK around before d
 
 
 ## --- Feature flags ---
-FEATURE_FLAGS_HTTP = []
+FEATURE_FLAGS_HTTP: list[str] = []
diff --git a/uploader/errors.py b/uploader/errors.py
index 3e7c893..2ac48b8 100644
--- a/uploader/errors.py
+++ b/uploader/errors.py
@@ -3,7 +3,8 @@ import traceback
 from werkzeug.exceptions import HTTPException
 
 import MySQLdb as mdb
-from flask import Flask, request, render_template, current_app as app
+from flask import Flask, request, current_app as app
+from uploader.flask_extensions import render_template
 
 def handle_general_exception(exc: Exception):
     """Handle generic exceptions."""
diff --git a/uploader/expression_data/dbinsert.py b/uploader/expression_data/dbinsert.py
index 6d8ce80..7040698 100644
--- a/uploader/expression_data/dbinsert.py
+++ b/uploader/expression_data/dbinsert.py
@@ -94,7 +94,7 @@ def select_platform():
         job = jobs.job(rconn, jobs.jobsnamespace(), job_id)
         if job:
             filename = job["filename"]
-            filepath = f"{app.config['UPLOAD_FOLDER']}/{filename}"
+            filepath = f"{app.config['UPLOADS_DIRECTORY']}/{filename}"
             if os.path.exists(filepath):
                 default_species = 1
                 gchips = genechips()
@@ -367,7 +367,7 @@ def insert_data():
         assert form.get("datasetid"), "dataset"
 
         filename = form["filename"]
-        filepath = f"{app.config['UPLOAD_FOLDER']}/{filename}"
+        filepath = f"{app.config['UPLOADS_DIRECTORY']}/{filename}"
         redisurl = app.config["REDIS_URL"]
         if os.path.exists(filepath):
             with Redis.from_url(redisurl, decode_responses=True) as rconn:
@@ -377,7 +377,7 @@ def insert_data():
                         form["species"], form["genechipid"], form["datasetid"],
                         app.config["SQL_URI"], redisurl,
                         app.config["JOBS_TTL_SECONDS"]),
-                    redisurl, f"{app.config['UPLOAD_FOLDER']}/job_errors")
+                    redisurl, f"{app.config['UPLOADS_DIRECTORY']}/job_errors")
 
             return redirect(url_for("dbinsert.insert_status", job_id=job["jobid"]))
         return render_error(f"File '{filename}' no longer exists.")
diff --git a/uploader/expression_data/views.py b/uploader/expression_data/views.py
index 0b318b7..0e9b072 100644
--- a/uploader/expression_data/views.py
+++ b/uploader/expression_data/views.py
@@ -162,7 +162,7 @@ def upload_file(species_id: int, population_id: int):
                                    species=species,
                                    population=population)
 
-        upload_dir = app.config["UPLOAD_FOLDER"]
+        upload_dir = app.config["UPLOADS_DIRECTORY"]
         request_errors = errors(request)
         if request_errors:
             for error in request_errors:
@@ -225,7 +225,7 @@ def parse_file(species_id: int, population_id: int):
         _errors = True
 
     if filename:
-        filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
+        filepath = os.path.join(app.config["UPLOADS_DIRECTORY"], filename)
         if not os.path.exists(filepath):
             flash("Selected file does not exist (any longer)", "alert-danger")
             _errors = True
@@ -241,7 +241,7 @@ def parse_file(species_id: int, population_id: int):
                 species_id, filepath, filetype,# type: ignore[arg-type]
                 app.config["JOBS_TTL_SECONDS"]),
             redisurl,
-            f"{app.config['UPLOAD_FOLDER']}/job_errors")
+            f"{app.config['UPLOADS_DIRECTORY']}/job_errors")
 
     return redirect(url_for("species.populations.expression-data.parse_status",
                             species_id=species_id,
@@ -263,7 +263,7 @@ def parse_status(species_id: int, population_id: int, job_id: str):
             return render_template("no_such_job.html", job_id=job_id), 400
 
     error_filename = jobs.error_filename(
-        job_id, f"{app.config['UPLOAD_FOLDER']}/job_errors")
+        job_id, f"{app.config['UPLOADS_DIRECTORY']}/job_errors")
     if os.path.exists(error_filename):
         stat = os.stat(error_filename)
         if stat.st_size > 0:
@@ -345,7 +345,7 @@ def fail(species_id: int, population_id: int, job_id: str):
 
     if job:
         error_filename = jobs.error_filename(
-            job_id, f"{app.config['UPLOAD_FOLDER']}/job_errors")
+            job_id, f"{app.config['UPLOADS_DIRECTORY']}/job_errors")
         if os.path.exists(error_filename):
             stat = os.stat(error_filename)
             if stat.st_size > 0:
diff --git a/uploader/files/chunks.py b/uploader/files/chunks.py
index c4360b5..f63f32f 100644
--- a/uploader/files/chunks.py
+++ b/uploader/files/chunks.py
@@ -5,6 +5,8 @@ from typing import Iterator
 from flask import current_app as app
 from werkzeug.utils import secure_filename
 
+from uploader.configutils import uploads_dir
+
 
 def chunked_binary_read(filepath: Path, chunksize: int = 2048) -> Iterator:
     """Read a file in binary mode in chunks."""
@@ -29,4 +31,4 @@ def chunks_directory(uniqueidentifier: str) -> Path:
     """Compute the directory where chunks are temporarily stored."""
     if uniqueidentifier == "":
         raise ValueError("Unique identifier cannot be empty!")
-    return Path(app.config["UPLOAD_FOLDER"], f"tempdir_{uniqueidentifier}")
+    return Path(uploads_dir(app), f"tempdir_{uniqueidentifier}")
diff --git a/uploader/files/functions.py b/uploader/files/functions.py
index 7b9f06b..68f4e16 100644
--- a/uploader/files/functions.py
+++ b/uploader/files/functions.py
@@ -8,6 +8,8 @@ from flask import current_app
 from werkzeug.utils import secure_filename
 from werkzeug.datastructures import FileStorage
 
+from uploader.configutils import uploads_dir
+
 from .chunks import chunked_binary_read
 
 def save_file(fileobj: FileStorage, upload_dir: Path, hashed: bool = True) -> Path:
@@ -30,7 +32,7 @@ def save_file(fileobj: FileStorage, upload_dir: Path, hashed: bool = True) -> Pa
 
 def fullpath(filename: str):
     """Get a file's full path. This makes use of `flask.current_app`."""
-    return Path(current_app.config["UPLOAD_FOLDER"], filename).absolute()
+    return Path(uploads_dir(current_app), filename).absolute()
 
 
 def sha256_digest_over_file(filepath: Path) -> str:
diff --git a/uploader/files/views.py b/uploader/files/views.py
index 29059c7..ea0e827 100644
--- a/uploader/files/views.py
+++ b/uploader/files/views.py
@@ -6,13 +6,15 @@ from pathlib import Path
 
 from flask import request, jsonify, Blueprint, current_app as app
 
+from uploader.configutils import uploads_dir
+
 from .chunks import chunk_name, chunks_directory
 
 files = Blueprint("files", __name__)
 
 def target_file(fileid: str) -> Path:
     """Compute the full path for the target file."""
-    return Path(app.config["UPLOAD_FOLDER"], fileid)
+    return Path(uploads_dir(app), fileid)
 
 
 @files.route("/upload/resumable", methods=["GET"])
diff --git a/uploader/flask_extensions.py b/uploader/flask_extensions.py
index 30fbad7..0fc774a 100644
--- a/uploader/flask_extensions.py
+++ b/uploader/flask_extensions.py
@@ -2,19 +2,17 @@
 import logging
 from typing import Any, Optional
 
-from flask import (request, current_app as app, url_for as flask_url_for)
+from flask import (
+    request,
+    current_app as app,
+    url_for as flask_url_for,
+    render_template as flask_render_template)
 
 logger = logging.getLogger(__name__)
 
 
-def url_for(
-        endpoint: str,
-        _anchor: Optional[str] = None,
-        _method: Optional[str] = None,
-        _scheme: Optional[str] = None,
-        _external: Optional[bool] = None,
-        **values: Any) -> str:
-    """Extension to flask's `url_for` function."""
+def fetch_flags():
+    """Fetch get arguments that are defined as feature flags."""
     flags = {}
     for flag in app.config["FEATURE_FLAGS_HTTP"]:
         flag_value = (request.args.get(flag) or request.form.get(flag) or "").strip()
@@ -22,12 +20,33 @@ def url_for(
             flags[flag] = flag_value
             continue
         continue
+    logger.debug("HTTP FEATURE FLAGS: %s", flags)
+    return flags
 
-    logger.debug("HTTP FEATURE FLAGS: %s, other variables: %s", flags, values)
+
+def url_for(
+        endpoint: str,
+        _anchor: Optional[str] = None,
+        _method: Optional[str] = None,
+        _scheme: Optional[str] = None,
+        _external: Optional[bool] = None,
+        **values: Any) -> str:
+    """Extension to flask's `url_for` function."""
+    logger.debug("other variables: %s", values)
     return flask_url_for(endpoint=endpoint,
                          _anchor=_anchor,
                          _method=_method,
                          _scheme=_scheme,
                          _external=_external,
                          **values,
-                         **flags)
+                         **fetch_flags())
+
+
+def render_template(template_name_or_list, **context: Any) -> str:
+    """Extend flask's `render_template` function"""
+    return flask_render_template(
+        template_name_or_list,
+        **{
+            **context,
+            **fetch_flags() # override any flag values
+        })
diff --git a/uploader/jobs.py b/uploader/jobs.py
index 5968c03..b2de54b 100644
--- a/uploader/jobs.py
+++ b/uploader/jobs.py
@@ -147,8 +147,8 @@ def job_errors(
     return take(
         (
             json.loads(error)
-            for key in rconn.keys(f"{prefix}:{str(job_id)}:*:errors:*")
-            for error in rconn.lrange(key, 0, -1)),
+            for key in rconn.keys(f"{prefix}:{str(job_id)}:*:errors:*")# type: ignore[union-attr]
+            for error in rconn.lrange(key, 0, -1)),# type: ignore[union-attr]
         count)
 
 
@@ -160,8 +160,8 @@ def job_files_metadata(
     """Get the metadata for specific job file."""
     return {
         key.split(":")[-1]: {
-            **rconn.hgetall(key),
+            **rconn.hgetall(key),# type: ignore[dict-item]
             "filetype": key.split(":")[-3]
         }
-        for key in rconn.keys(f"{prefix}:{str(job_id)}:*:metadata*")
+        for key in rconn.keys(f"{prefix}:{str(job_id)}:*:metadata*")# type: ignore[union-attr]
     }
diff --git a/uploader/oauth2/client.py b/uploader/oauth2/client.py
index b94a044..e37816d 100644
--- a/uploader/oauth2/client.py
+++ b/uploader/oauth2/client.py
@@ -4,7 +4,7 @@ import time
 import uuid
 import random
 from datetime import datetime, timedelta
-from urllib.parse import urljoin, urlparse
+from urllib.parse import urljoin, urlparse, urlencode
 
 import requests
 from flask import request, current_app as app
@@ -18,6 +18,7 @@ from authlib.integrations.requests_client import OAuth2Session
 
 from uploader import session
 import uploader.monadic_requests as mrequests
+from uploader.flask_extensions import fetch_flags
 
 SCOPE = ("profile group role resource register-client user masquerade "
          "introspect migrate-data")
@@ -157,7 +158,10 @@ def fetch_user_details() -> Either:
                 "user_id": uuid.UUID(usrdets["user_id"]),
                 "name": usrdets["name"],
                 "email": usrdets["email"],
-                "token": session.user_token()}))
+                "token": session.user_token(),
+                "logged_in": session.user_token().either(
+                    lambda _e: False, lambda _t: True)
+            }))
         return udets
     return Right(suser)
 
@@ -173,11 +177,13 @@ def authserver_authorise_uri():
     """Build up the authorisation URI."""
     req_baseurl = urlparse(request.base_url, scheme=request.scheme)
     host_uri = f"{req_baseurl.scheme}://{req_baseurl.netloc}/"
-    return urljoin(
-        authserver_uri(),
-        "auth/authorise?response_type=code"
-        f"&client_id={oauth2_clientid()}"
-        f"&redirect_uri={urljoin(host_uri, 'oauth2/code')}")
+    args = {
+        "response_type": "code",
+        "client_id": oauth2_clientid(),
+        "redirect_uri": (
+            f"{urljoin(host_uri, 'oauth2/code')}?{urlencode(fetch_flags())}")
+    }
+    return f"{urljoin(authserver_uri(), 'auth/authorise')}?{urlencode(args)}"
 
 
 def __no_token__(_err) -> Left:
diff --git a/uploader/phenotypes/misc.py b/uploader/phenotypes/misc.py
index cbe3b7f..1924c07 100644
--- a/uploader/phenotypes/misc.py
+++ b/uploader/phenotypes/misc.py
@@ -8,7 +8,7 @@ def phenotypes_data_differences(
         filedata: tuple[dict, ...], dbdata: tuple[dict, ...]
 ) -> tuple[dict, ...]:
     """Compute differences between file data and db data"""
-    diff = tuple()
+    diff: tuple[dict, ...] = tuple()
     for filerow, dbrow in zip(
             sorted(filedata, key=lambda item: (item["phenotype_id"], item["xref_id"])),
             sorted(dbdata, key=lambda item: (item["PhenotypeId"], item["xref_id"]))):
diff --git a/uploader/phenotypes/models.py b/uploader/phenotypes/models.py
index af06376..3946a0f 100644
--- a/uploader/phenotypes/models.py
+++ b/uploader/phenotypes/models.py
@@ -1,4 +1,6 @@
 """Database and utility functions for phenotypes."""
+import time
+import random
 import logging
 import tempfile
 from pathlib import Path
@@ -6,8 +8,8 @@ from functools import reduce
 from datetime import datetime
 from typing import Union, Optional, Iterable
 
-import MySQLdb as mdb
-from MySQLdb.cursors import Cursor, DictCursor
+from MySQLdb.connections import Connection
+from MySQLdb.cursors import Cursor, DictCursor, BaseCursor
 
 from gn_libs.mysqldb import debug_query
 
@@ -27,7 +29,7 @@ __PHENO_DATA_TABLES__ = {
 
 
 def datasets_by_population(
-        conn: mdb.Connection,
+        conn: Connection,
         species_id: int,
         population_id: int
 ) -> tuple[dict, ...]:
@@ -42,7 +44,7 @@ def datasets_by_population(
         return tuple(dict(row) for row in cursor.fetchall())
 
 
-def dataset_by_id(conn: mdb.Connection,
+def dataset_by_id(conn: Connection,
                   species_id: int,
                   population_id: int,
                   dataset_id: int) -> dict:
@@ -57,7 +59,7 @@ def dataset_by_id(conn: mdb.Connection,
         return dict(cursor.fetchone())
 
 
-def phenotypes_count(conn: mdb.Connection,
+def phenotypes_count(conn: Connection,
                      population_id: int,
                      dataset_id: int) -> int:
     """Count the number of phenotypes in the dataset."""
@@ -85,11 +87,14 @@ def phenotype_publication_data(conn, phenotype_id) -> Optional[dict]:
         return dict(res)
 
 
-def dataset_phenotypes(conn: mdb.Connection,
-                       population_id: int,
-                       dataset_id: int,
-                       offset: int = 0,
-                       limit: Optional[int] = None) -> tuple[dict, ...]:
+def dataset_phenotypes(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        conn: Connection,
+        population_id: int,
+        dataset_id: int,
+        offset: int = 0,
+        limit: Optional[int] = None,
+        xref_ids: tuple[int, ...] = tuple()
+) -> tuple[dict, ...]:
     """Fetch the actual phenotypes."""
     _query = (
         "SELECT pheno.*, pxr.Id AS xref_id, pxr.InbredSetId, ist.InbredSetCode "
@@ -98,14 +103,16 @@ def dataset_phenotypes(conn: mdb.Connection,
         "INNER JOIN PublishFreeze AS pf ON pxr.InbredSetId=pf.InbredSetId "
         "INNER JOIN InbredSet AS ist ON pf.InbredSetId=ist.Id "
         "WHERE pxr.InbredSetId=%s AND pf.Id=%s") + (
+            f" AND pxr.Id IN ({', '.join(['%s'] * len(xref_ids))})"
+            if len(xref_ids) > 0 else "") + (
             f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")
     with conn.cursor(cursorclass=DictCursor) as cursor:
-        cursor.execute(_query, (population_id, dataset_id))
+        cursor.execute(_query, (population_id, dataset_id) + xref_ids)
         debug_query(cursor, logger)
         return tuple(dict(row) for row in cursor.fetchall())
 
 
-def __phenotype_se__(cursor: Cursor, xref_id, dataids_and_strainids):
+def __phenotype_se__(cursor: BaseCursor, xref_id, dataids_and_strainids):
     """Fetch standard-error values (if they exist) for a phenotype."""
     paramstr = ", ".join(["(%s, %s)"] * len(dataids_and_strainids))
     flat = tuple(item for sublist in dataids_and_strainids for item in sublist)
@@ -187,7 +194,7 @@ def __merge_pheno_data_and_se__(data, sedata) -> dict:
 
 
 def phenotype_by_id(
-        conn: mdb.Connection,
+        conn: Connection,
         species_id: int,
         population_id: int,
         dataset_id: int,
@@ -225,7 +232,7 @@ def phenotype_by_id(
     return None
 
 
-def phenotypes_data(conn: mdb.Connection,
+def phenotypes_data(conn: Connection,
                     population_id: int,
                     dataset_id: int,
                     offset: int = 0,
@@ -248,16 +255,16 @@ def phenotypes_data(conn: mdb.Connection,
         return tuple(dict(row) for row in cursor.fetchall())
 
 
-def phenotypes_vector_data(
-        conn: mdb.Connection,
+def phenotypes_vector_data(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        conn: Connection,
         species_id: int,
         population_id: int,
         xref_ids: tuple[int, ...] = tuple(),
         offset: int = 0,
         limit: Optional[int] = None
-) -> dict[tuple[int, int, int]: dict[str, Union[int,float]]]:
+) -> dict[tuple[int, int, int], dict[str, Union[int,float]]]:
     """Retrieve the vector data values for traits in the database."""
-    _params = (species_id, population_id)
+    _params: tuple[int, ...] = (species_id, population_id)
     _query = ("SELECT "
               "Species.Id AS SpeciesId, iset.Id AS InbredSetId, "
               "pxr.Id AS xref_id, pdata.*, Strain.Id AS StrainId, "
@@ -301,7 +308,7 @@ def phenotypes_vector_data(
         return reduce(__organise__, cursor.fetchall(), {})
 
 
-def save_new_dataset(cursor: Cursor,
+def save_new_dataset(cursor: BaseCursor,
                      population_id: int,
                      dataset_name: str,
                      dataset_fullname: str,
@@ -328,35 +335,6 @@ def save_new_dataset(cursor: Cursor,
     return {**params, "Id": cursor.lastrowid}
 
 
-def phenotypes_data_by_ids(
-        conn: mdb.Connection,
-        inbred_pheno_xref: dict[str, int]
-) -> tuple[dict, ...]:
-    """Fetch all phenotype data, filtered by the `inbred_pheno_xref` mapping."""
-    _paramstr = ",".join(["(%s, %s, %s)"] * len(inbred_pheno_xref))
-    _query = ("SELECT "
-              "pub.PubMed_ID, pheno.*, pxr.*, pd.*, str.*, iset.InbredSetCode "
-              "FROM Publication AS pub "
-              "RIGHT JOIN PublishXRef AS pxr0 ON pub.Id=pxr0.PublicationId "
-              "INNER JOIN Phenotype AS pheno ON pxr0.PhenotypeId=pheno.id "
-              "INNER JOIN PublishXRef AS pxr ON pheno.Id=pxr.PhenotypeId "
-              "INNER JOIN PublishData AS pd ON pxr.DataId=pd.Id "
-              "INNER JOIN Strain AS str ON pd.StrainId=str.Id "
-              "INNER JOIN StrainXRef AS sxr ON str.Id=sxr.StrainId "
-              "INNER JOIN PublishFreeze AS pf ON sxr.InbredSetId=pf.InbredSetId "
-              "INNER JOIN InbredSet AS iset ON pf.InbredSetId=iset.InbredSetId "
-              f"WHERE (pxr.InbredSetId, pheno.Id, pxr.Id) IN ({_paramstr}) "
-              "ORDER BY pheno.Id")
-    with conn.cursor(cursorclass=DictCursor) as cursor:
-        cursor.execute(_query, tuple(item for row in inbred_pheno_xref
-                                     for item in (row["population_id"],
-                                                  row["phenoid"],
-                                                  row["xref_id"])))
-        debug_query(cursor, logger)
-        return tuple(
-            reduce(__organise_by_phenotype__, cursor.fetchall(), {}).values())
-
-
 def __pre_process_phenotype_data__(row):
     _desc = row.get("description", "")
     _pre_pub_desc = row.get("pre_publication_description", _desc)
@@ -375,13 +353,13 @@ def __pre_process_phenotype_data__(row):
 
 
 def create_new_phenotypes(# pylint: disable=[too-many-locals]
-        conn: mdb.Connection,
+        conn: Connection,
         population_id: int,
         publication_id: int,
         phenotypes: Iterable[dict]
 ) -> tuple[dict, ...]:
     """Add entirely new phenotypes to the database. WARNING: Not thread-safe."""
-    _phenos = tuple()
+    _phenos: tuple[dict, ...] = tuple()
     with conn.cursor(cursorclass=DictCursor) as cursor:
         def make_next_id(idcol, table):
             cursor.execute(f"SELECT MAX({idcol}) AS last_id FROM {table}")
@@ -430,9 +408,10 @@ def create_new_phenotypes(# pylint: disable=[too-many-locals]
             if len(batch) == 0:
                 break
 
-            params, abbrevs = reduce(__build_params_and_prepubabbrevs__,
-                                     batch,
-                                     (tuple(), tuple()))
+            params, abbrevs = reduce(#type: ignore[var-annotated]
+                __build_params_and_prepubabbrevs__,
+                batch,
+                (tuple(), tuple()))
             # Check for uniqueness for all "Pre_publication_description" values
             abbrevs_paramsstr = ", ".join(["%s"] * len(abbrevs))
             _query = ("SELECT PublishXRef.PhenotypeId, Phenotype.* "
@@ -502,7 +481,7 @@ def create_new_phenotypes(# pylint: disable=[too-many-locals]
 
 
 def save_phenotypes_data(
-        conn: mdb.Connection,
+        conn: Connection,
         table: str,
         data: Iterable[dict]
 ) -> int:
@@ -532,7 +511,7 @@ def save_phenotypes_data(
 
 
 def quick_save_phenotypes_data(
-        conn: mdb.Connection,
+        conn: Connection,
         table: str,
         dataitems: Iterable[dict],
         tmpdir: Path
@@ -562,3 +541,134 @@ def quick_save_phenotypes_data(
             ")")
         debug_query(cursor, logger)
         return _count
+
+
+def __sleep_random__():
+    """Sleep a random amount of time chosen from 0.05s to 1s in increments of 0.05"""
+    time.sleep(random.choice(tuple(i / 20.0 for i in range(1, 21))))
+
+
+def delete_phenotypes_data(
+        cursor: BaseCursor,
+        data_ids: tuple[int, ...]
+) -> tuple[int, int, int]:
+    """Delete numeric data for phenotypes with the given data IDs."""
+    if len(data_ids) == 0:
+        return (0, 0, 0)
+
+    # Loop to handle big deletes i.e. ≥ 10000 rows
+    _dcount, _secount, _ncount = (0, 0, 0)# Count total rows deleted
+    while True:
+        _paramstr = ", ".join(["%s"] * len(data_ids))
+        cursor.execute(
+            "DELETE FROM PublishData "
+            f"WHERE Id IN ({_paramstr}) "
+            "ORDER BY Id ASC, StrainId ASC "# Make deletions deterministic
+            "LIMIT 1000",
+            data_ids)
+        _dcount_curr = cursor.rowcount
+        _dcount += _dcount_curr
+
+        cursor.execute(
+            "DELETE FROM PublishSE "
+            f"WHERE DataId IN ({_paramstr}) "
+            "ORDER BY DataId ASC, StrainId ASC "# Make deletions deterministic
+            "LIMIT 1000",
+            data_ids)
+        _secount_curr = cursor.rowcount
+        _secount += _secount_curr
+
+        cursor.execute(
+            "DELETE FROM NStrain "
+            f"WHERE DataId IN ({_paramstr}) "
+            "ORDER BY DataId ASC, StrainId ASC "# Make deletions deterministic
+            "LIMIT 1000",
+            data_ids)
+        _ncount_curr = cursor.rowcount
+        _ncount += _ncount_curr
+        __sleep_random__()
+
+        if all((_dcount_curr == 0, _secount_curr == 0, _ncount_curr == 0)):
+            # end loop if there are no more rows to delete.
+            break
+
+    return (_dcount, _secount, _ncount)
+
+
+def __linked_ids__(
+        cursor: BaseCursor,
+        population_id: int,
+        xref_ids: tuple[int, ...]
+) -> tuple[tuple[int, int, int], ...]:
+    """Retrieve `DataId` values from `PublishXRef` table."""
+    _paramstr = ", ".join(["%s"] * len(xref_ids))
+    cursor.execute("SELECT PhenotypeId, PublicationId, DataId "
+                   "FROM PublishXRef "
+                   f"WHERE InbredSetId=%s AND Id IN ({_paramstr})",
+                   (population_id,) + xref_ids)
+    return tuple(
+        (int(row["PhenotypeId"]), int(row["PublicationId"]), int(row["DataId"]))
+        for row in cursor.fetchall())
+
+
+def delete_phenotypes(
+        conn_or_cursor: Union[Connection, Cursor],
+        population_id: int,
+        xref_ids: tuple[int, ...]
+) -> tuple[int, int, int, int]:
+    """Delete phenotypes and all their data."""
+    def __delete_phenos__(cursor: BaseCursor, pheno_ids: tuple[int, ...]) -> int:
+        """Delete data from the `Phenotype` table."""
+        _paramstr = ", ".join(["%s"] * len(pheno_ids))
+
+        _pcount = 0
+        while True:
+            cursor.execute(
+                "DELETE FROM Phenotype "
+                f"WHERE Id IN ({_paramstr}) "
+                "ORDER BY Id "
+                "LIMIT 1000",
+                pheno_ids)
+            _pcount_curr = cursor.rowcount
+            _pcount += _pcount_curr
+            __sleep_random__()
+            if _pcount_curr == 0:
+                break
+
+        return cursor.rowcount
+
+    def __delete_xrefs__(cursor: BaseCursor) -> int:
+        _paramstr = ", ".join(["%s"] * len(xref_ids))
+
+        _xcount = 0
+        while True:
+            cursor.execute(
+                "DELETE FROM PublishXRef "
+                f"WHERE InbredSetId=%s AND Id IN ({_paramstr}) "
+                "ORDER BY Id "
+                "LIMIT 10000",
+                (population_id,) + xref_ids)
+            _xcount_curr = cursor.rowcount
+            _xcount += _xcount_curr
+            __sleep_random__()
+            if _xcount_curr == 0:
+                break
+
+        return _xcount
+
+    def __with_cursor__(cursor):
+        _phenoids, _pubids, _dataids = reduce(
+            lambda acc, curr: (acc[0] + (curr[0],),
+                               acc[1] + (curr[1],),
+                               acc[2] + (curr[2],)),
+            __linked_ids__(cursor, population_id, xref_ids),
+            (tuple(), tuple(), tuple()))
+        __delete_phenos__(cursor, _phenoids)
+        return (__delete_xrefs__(cursor),) + delete_phenotypes_data(
+            cursor, _dataids)
+
+    if isinstance(conn_or_cursor, BaseCursor):
+        return __with_cursor__(conn_or_cursor)
+
+    with conn_or_cursor.cursor(cursorclass=DictCursor) as cursor:
+        return __with_cursor__(cursor)
diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py
index 5b32fc0..23bc682 100644
--- a/uploader/phenotypes/views.py
+++ b/uploader/phenotypes/views.py
@@ -1,4 +1,6 @@
 """Views handling ('classical') phenotypes."""# pylint: disable=[too-many-lines]
+import io
+import csv
 import sys
 import uuid
 import json
@@ -21,22 +23,22 @@ from gn_libs import jobs as gnlibs_jobs
 from gn_libs.jobs.jobs import JobNotFound
 from gn_libs.mysqldb import database_connection
 
+from werkzeug.datastructures import Headers
 from flask import (flash,
                    request,
                    jsonify,
                    redirect,
                    Blueprint,
-                   current_app as app)
+                   current_app as app,
+                   Response as FlaskResponse)
 
 from r_qtl import r_qtl2_qc as rqc
 from r_qtl import exceptions as rqe
 
-
-from uploader.sui import sui_template
-
 from uploader import jobs
 from uploader import session
 from uploader.files import save_file
+from uploader.configutils import uploads_dir
 from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
 from uploader.oauth2.client import oauth2_post
@@ -185,12 +187,6 @@ def with_dataset(
 def view_dataset(# pylint: disable=[unused-argument]
         species: dict, population: dict, dataset: dict, **kwargs):
     """View a specific dataset"""
-    if bool(request.args.get("streamlined_ui")):
-        # Redirect back to the "View Population" page for the time being.
-        return redirect(url_for("species.populations.view_population",
-                                species_id=species["SpeciesId"],
-                                population_id=population["Id"]))
-
     with database_connection(app.config["SQL_URI"]) as conn:
         dataset = dataset_by_id(
             conn, species["SpeciesId"], population["Id"], dataset["Id"])
@@ -338,7 +334,7 @@ def process_phenotypes_rqtl2_bundle(error_uri):
     try:
         ## Handle huge files here...
         phenobundle = save_file(request.files["phenotypes-bundle"],
-                                Path(app.config["UPLOAD_FOLDER"]))
+                                uploads_dir(app))
         rqc.validate_bundle(phenobundle)
         return phenobundle
     except AssertionError as _aerr:
@@ -361,7 +357,7 @@ def process_phenotypes_individual_files(error_uri):
         "comment.char": form["file-comment-character"],
         "na.strings": form["file-na"].split(" "),
     }
-    bundlepath = Path(app.config["UPLOAD_FOLDER"],
+    bundlepath = Path(uploads_dir(app),
                       f"{str(uuid.uuid4()).replace('-', '')}.zip")
     with ZipFile(bundlepath,mode="w") as zfile:
         for rqtlkey, formkey, _type in (
@@ -379,7 +375,7 @@ def process_phenotypes_individual_files(error_uri):
                 # Chunked upload of large files was used
                 filedata = json.loads(form[formkey])
                 zfile.write(
-                    Path(app.config["UPLOAD_FOLDER"], filedata["uploaded-file"]),
+                    Path(uploads_dir(app), filedata["uploaded-file"]),
                     arcname=filedata["original-name"])
                 cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filedata["original-name"]]
             else:
@@ -391,9 +387,9 @@ def process_phenotypes_individual_files(error_uri):
                     return error_uri
 
                 filepath = save_file(
-                    _sentfile, Path(app.config["UPLOAD_FOLDER"]), hashed=False)
+                    _sentfile, uploads_dir(app), hashed=False)
                 zfile.write(
-                    Path(app.config["UPLOAD_FOLDER"], filepath),
+                    Path(uploads_dir(app), filepath),
                     arcname=filepath.name)
                 cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filepath.name]
 
@@ -426,9 +422,9 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
         if request.method == "GET":
             today = datetime.date.today()
             return render_template(
-                sui_template("phenotypes/add-phenotypes-with-rqtl2-bundle.html"
-                             if use_bundle
-                             else f"phenotypes/add-phenotypes-raw-files.html"),
+                ("phenotypes/add-phenotypes-with-rqtl2-bundle.html"
+                 if use_bundle
+                 else "phenotypes/add-phenotypes-raw-files.html"),
                 species=species,
                 population=population,
                 dataset=dataset,
@@ -473,7 +469,7 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
                     **({"publicationid": request.form["publication-id"]}
                        if request.form.get("publication-id") else {})})}),
             _redisuri,
-            f"{app.config['UPLOAD_FOLDER']}/job_errors")
+            f"{uploads_dir(app)}/job_errors")
 
         app.logger.debug("JOB DETAILS: %s", _job)
         jobstatusuri = url_for("species.populations.phenotypes.job_status",
@@ -515,7 +511,7 @@ def job_status(
         except jobs.JobNotFound as _jnf:
             job = None
 
-        return render_template(sui_template("phenotypes/job-status.html"),
+        return render_template("phenotypes/job-status.html",
                                species=species,
                                population=population,
                                dataset=dataset,
@@ -530,6 +526,65 @@ def job_status(
 
 @phenotypesbp.route(
     "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
+    "/<int:dataset_id>/job/<uuid:job_id>/download-errors",
+    methods=["GET"])
+@require_login
+@with_dataset(
+    species_redirect_uri="species.populations.phenotypes.index",
+    population_redirect_uri="species.populations.phenotypes.select_population",
+    redirect_uri="species.populations.phenotypes.list_datasets")
+def download_errors(
+        species: dict,
+        population: dict,
+        dataset: dict,
+        job_id: uuid.UUID,
+        **kwargs):# pylint: disable=[unused-argument]
+    """Download the list of errors as a CSV file."""
+    with Redis.from_url(app.config["REDIS_URL"], decode_responses=True) as rconn:
+        try:
+            job = jobs.job(rconn, jobs.jobsnamespace(), str(job_id))
+            _prefix_ = jobs.jobsnamespace()
+            _jobid_ = job['jobid']
+            def __generate_chunks__():
+                _errors_ = (
+                    json.loads(error)
+                    for key in rconn.keys(
+                            f"{_prefix_}:{str(_jobid_)}:*:errors:*")
+                    for error in rconn.lrange(key, 0, -1))
+                _chunk_no_ = 0
+                _all_errors_printed_ = False
+                while not _all_errors_printed_:
+                    _chunk_ = []
+                    try:
+                        for _ in range(0, 1000):
+                            _chunk_.append(next(_errors_))
+                    except StopIteration:
+                        _all_errors_printed_ = True
+                        if len(_chunk_) <= 0:
+                            raise
+
+                    _out_ = io.StringIO()
+                    _writer_ = csv.DictWriter(_out_, fieldnames=tuple(_chunk_[0].keys()))
+                    if _chunk_no_ == 0:
+                        _writer_.writeheader()
+                    _writer_.writerows(_chunk_)
+                    _chunk_no_ += 1
+                    yield _out_.getvalue()
+                    if _all_errors_printed_:
+                        return
+
+            headers = Headers()
+            headers.set("Content-Disposition",
+                        "attachment",
+                        filename=f"{job['job-type']}_{job['jobid']}.csv")
+            return FlaskResponse(
+                __generate_chunks__(), mimetype="text/csv", headers=headers)
+        except jobs.JobNotFound as _jnf:
+            return render_template("jobs/job-not-found.html", job_id=job_id)
+
+
+@phenotypesbp.route(
+    "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
     "/<int:dataset_id>/job/<uuid:job_id>/review",
     methods=["GET"])
 @require_login
@@ -595,7 +650,7 @@ def review_job_data(
             for filetype,meta in metadata.items()
         }
         _job_metadata = json.loads(job["job-metadata"])
-        return render_template(sui_template("phenotypes/review-job-data.html"),
+        return render_template("phenotypes/review-job-data.html",
                                species=species,
                                population=population,
                                dataset=dataset,
@@ -620,6 +675,12 @@ def load_phenotypes_success_handler(job):
         job_id=job["job_id"]))
 
 
+def proceed_to_job_status(job):
+    """A generic 'job success' handler for asynchronous phenotype jobs."""
+    app.logger.debug("The new job: %s", job)
+    return redirect(url_for("background-jobs.job_status", job_id=job["job_id"]))
+
+
 @phenotypesbp.route(
     "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
     "/<int:dataset_id>/load-data-to-database",
@@ -662,11 +723,6 @@ def load_data_to_database(
         def __handle_error__(resp):
             return render_template("http-error.html", *resp.json())
 
-        def __handle_success__(load_job):
-            app.logger.debug("The phenotypes loading job: %s", load_job)
-            return redirect(url_for(
-                "background-jobs.job_status", job_id=load_job["job_id"]))
-
 
         return request_token(
             token_uri=urljoin(oauth2client.authserver_uri(), "auth/token"),
@@ -688,15 +744,16 @@ def load_data_to_database(
                     "success_handler": (
                         "uploader.phenotypes.views"
                         ".load_phenotypes_success_handler")
-                })
+                },
+                external_id=session.logged_in_user_id())
         ).then(
             lambda job: gnlibs_jobs.launch_job(
                 job,
                 _jobs_db,
-                Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"),
+                Path(f"{uploads_dir(app)}/job_errors"),
                 worker_manager="gn_libs.jobs.launcher",
                 loglevel=_loglevel)
-        ).either(__handle_error__, __handle_success__)
+        ).either(__handle_error__, proceed_to_job_status)
 
 
 def update_phenotype_metadata(conn, metadata: dict):
@@ -811,7 +868,7 @@ def update_phenotype_data(conn, data: dict):
             }
         })
 
-    values, serrs, counts = tuple(
+    values, serrs, counts = tuple(# type: ignore[var-annotated]
         tuple({
             "data_id": row[0].split("::")[0],
             "strain_id": row[0].split("::")[1],
@@ -986,7 +1043,7 @@ def load_data_success(
                               (_publication["Title"] or ""))
                              if item != "")
             return render_template(
-                sui_template("phenotypes/load-phenotypes-success.html"),
+                "phenotypes/load-phenotypes-success.html",
                 species=species,
                 population=population,
                 dataset=dataset,
@@ -1068,9 +1125,10 @@ def recompute_means(# pylint: disable=[unused-argument]
                     "success_handler": (
                         "uploader.phenotypes.views."
                         "recompute_phenotype_means_success_handler")
-            }),
+                },
+                external_id=session.logged_in_user_id()),
             _jobs_db,
-            Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"),
+            Path(f"{uploads_dir(app)}/job_errors"),
             worker_manager="gn_libs.jobs.launcher",
             loglevel=_loglevel)
         return redirect(url_for("background-jobs.job_status",
@@ -1078,6 +1136,7 @@ def recompute_means(# pylint: disable=[unused-argument]
 
 
 def return_to_dataset_view_handler(job, msg: str):
+    """Handler for background jobs: Returns to `View Dataset` page."""
     flash(msg, "alert alert-success")
     return redirect(url_for(
         "species.populations.phenotypes.view_dataset",
@@ -1111,7 +1170,7 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument]
     _job_id = uuid.uuid4()
     _loglevel = logging.getLevelName(app.logger.getEffectiveLevel()).lower()
 
-    _workingdir = Path(app.config["TEMPORARY_DIRECTORY"]).joinpath("qtlreaper")
+    _workingdir = Path(app.config["SCRATCH_DIRECTORY"]).joinpath("qtlreaper")
     _workingdir.mkdir(exist_ok=True)
     command = [
         sys.executable,
@@ -1146,9 +1205,10 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument]
                     "success_handler": (
                         "uploader.phenotypes.views."
                         "rerun_qtlreaper_success_handler")
-            }),
+            },
+            external_id=session.logged_in_user_id()),
             _jobs_db,
-            Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"),
+            Path(f"{uploads_dir(app)}/job_errors"),
             worker_manager="gn_libs.jobs.launcher",
             loglevel=_loglevel)
         return redirect(url_for("background-jobs.job_status",
@@ -1160,3 +1220,119 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument]
 def rerun_qtlreaper_success_handler(job):
     """Handle success (re)running QTLReaper script."""
     return return_to_dataset_view_handler(job, "QTLReaper ran successfully!")
+
+
+def delete_phenotypes_success_handler(job):
+    """Handle success running the 'delete-phenotypes' script."""
+    return return_to_dataset_view_handler(
+        job, "Phenotypes deleted successfully.")
+
+
+@phenotypesbp.route(
+    "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
+    "/<int:dataset_id>/delete",
+    methods=["GET", "POST"])
+@require_login
+@with_dataset(
+    species_redirect_uri="species.populations.phenotypes.index",
+    population_redirect_uri="species.populations.phenotypes.select_population",
+    redirect_uri="species.populations.phenotypes.list_datasets")
+def delete_phenotypes(# pylint: disable=[unused-argument, too-many-locals]
+        species: dict,
+        population: dict,
+        dataset: dict,
+        **kwargs
+):
+    """Delete the specified phenotype data."""
+    _dataset_page = redirect(url_for(
+        "species.populations.phenotypes.view_dataset",
+        species_id=species["SpeciesId"],
+        population_id=population["Id"],
+        dataset_id=dataset["Id"]))
+
+    def __handle_error__(resp):
+        flash(
+            "Error retrieving authorisation token. Phenotype deletion "
+            "failed. Please try again later.",
+            "alert alert-danger")
+        return _dataset_page
+
+    _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
+    with (database_connection(app.config["SQL_URI"]) as conn,
+          sqlite3.connection(_jobs_db) as jobsconn):
+        form = request.form
+        xref_ids = tuple(int(item) for item in set(form.getlist("xref_ids")))
+
+        match form.get("action"):
+            case "cancel":
+                return redirect(url_for(
+                    "species.populations.phenotypes.view_dataset",
+                    species_id=species["SpeciesId"],
+                    population_id=population["Id"],
+                    dataset_id=dataset["Id"]))
+            case "delete":
+                _loglevel = logging.getLevelName(
+                    app.logger.getEffectiveLevel()).lower()
+                if form.get("confirm_delete_all_phenotypes", "") == "on":
+                    _cmd = ["--delete-all"]
+                else:
+                    # setup phenotypes xref_ids file
+                    _xref_ids_file = Path(
+                        app.config["SCRATCH_DIRECTORY"],
+                        f"delete-phenotypes-{uuid.uuid4()}.txt")
+                    with _xref_ids_file.open(mode="w", encoding="utf8") as ptr:
+                        ptr.write("\n".join(str(_id) for _id in xref_ids))
+
+                    _cmd = ["--xref_ids_file", str(_xref_ids_file)]
+
+                _job_id = uuid.uuid4()
+                return request_token(
+                    token_uri=urljoin(
+                        oauth2client.authserver_uri(), "auth/token"),
+                    user_id=session.user_details()["user_id"]
+                ).then(
+                    lambda token: gnlibs_jobs.initialise_job(
+                        jobsconn,
+                        _job_id,
+                        [
+                            sys.executable,
+                            "-u",
+                            "-m",
+                            "scripts.phenotypes.delete_phenotypes",
+                            "--log-level", _loglevel,
+                            app.config["SQL_URI"],
+                            str(species["SpeciesId"]),
+                            str(population["Id"]),
+                            str(dataset["Id"]),
+                            app.config["AUTH_SERVER_URL"],
+                            token["access_token"]] + _cmd,
+                        "delete-phenotypes",
+                        extra_meta={
+                            "species_id": species["SpeciesId"],
+                            "population_id": population["Id"],
+                            "dataset_id": dataset["Id"],
+                            "success_handler": (
+                                "uploader.phenotypes.views."
+                                "delete_phenotypes_success_handler")
+                        },
+                        external_id=session.logged_in_user_id())
+                ).then(
+                    lambda _job: gnlibs_jobs.launch_job(
+                        _job,
+                        _jobs_db,
+                        Path(f"{uploads_dir(app)}/job_errors"),
+                        worker_manager="gn_libs.jobs.launcher",
+                        loglevel=_loglevel)
+                ).either(__handle_error__, proceed_to_job_status)
+            case _:
+                _phenos: tuple[dict, ...] = tuple()
+                if len(xref_ids) > 0:
+                    _phenos = dataset_phenotypes(
+                        conn, population["Id"], dataset["Id"], xref_ids=xref_ids)
+
+                return render_template(
+                    "phenotypes/confirm-delete-phenotypes.html",
+                    species=species,
+                    population=population,
+                    dataset=dataset,
+                    phenotypes=_phenos)
diff --git a/uploader/population/rqtl2.py b/uploader/population/rqtl2.py
index 97d4854..bb5066e 100644
--- a/uploader/population/rqtl2.py
+++ b/uploader/population/rqtl2.py
@@ -134,7 +134,7 @@ def upload_rqtl2_bundle(species_id: int, population_id: int):
         try:
             app.logger.debug("Files in the form: %s", request.files)
             the_file = save_file(request.files["rqtl2_bundle_file"],
-                                 Path(app.config["UPLOAD_FOLDER"]))
+                                 Path(app.config["UPLOADS_DIRECTORY"]))
         except AssertionError:
             app.logger.debug(traceback.format_exc())
             flash("Please provide a valid R/qtl2 zip bundle.",
@@ -185,7 +185,7 @@ def trigger_rqtl2_bundle_qc(
                     "rqtl2-bundle-file": str(rqtl2bundle.absolute()),
                     "original-filename": originalfilename})}),
             redisuri,
-            f"{app.config['UPLOAD_FOLDER']}/job_errors")
+            f"{app.config['UPLOADS_DIRECTORY']}/job_errors")
         return jobid
 
 
@@ -895,7 +895,7 @@ def confirm_bundle_details(species_id: int, population_id: int):
                         })
                     }),
                 redisuri,
-                f"{app.config['UPLOAD_FOLDER']}/job_errors")
+                f"{app.config['UPLOADS_DIRECTORY']}/job_errors")
 
             return redirect(url_for("expression-data.rqtl2.rqtl2_processing_status",
                                     jobid=jobid))
diff --git a/uploader/population/views.py b/uploader/population/views.py
index a6e2358..795ce81 100644
--- a/uploader/population/views.py
+++ b/uploader/population/views.py
@@ -11,8 +11,6 @@ from flask import (flash,
                    Blueprint,
                    current_app as app)
 
-from uploader.sui import sui_template
-
 from uploader.samples.views import samplesbp
 from uploader.flask_extensions import url_for
 from uploader.oauth2.client import oauth2_post
@@ -157,7 +155,7 @@ def create_population(species_id: int):
             "FullName": population_fullname,
             "InbredSetCode": request.form.get("population_code") or None,
             "Description": request.form.get("population_description") or None,
-            "Family": request.form.get("population_family").strip() or None,
+            "Family": request.form.get("population_family", "").strip() or None,
             "MappingMethodId": request.form.get("population_mapping_method_id"),
             "GeneticType": request.form.get("population_genetic_type") or None
         })
@@ -244,5 +242,4 @@ def view_population(species_id: int, population_id: int):
                     dataset_phenotypes(conn, population["Id"], _dataset["Id"]))
             }
 
-        return render_template(sui_template("populations/view-population.html"),
-                               **_kwargs)
+        return render_template("populations/view-population.html", **_kwargs)
diff --git a/uploader/publications/datatables.py b/uploader/publications/datatables.py
index e07fafd..8b3d4a0 100644
--- a/uploader/publications/datatables.py
+++ b/uploader/publications/datatables.py
@@ -13,7 +13,7 @@ def fetch_publications(
         search: Optional[str] = None,
         offset: int = 0,
         limit: int = -1
-) -> tuple[dict, int, int, int]:
+) -> tuple[tuple[dict, ...], int, int, int]:
     """Fetch publications from the database."""
     _query = "SELECT * FROM Publication"
     _count_query = "SELECT COUNT(*) FROM Publication"
diff --git a/uploader/publications/misc.py b/uploader/publications/misc.py
index fca6f71..f0ff9c7 100644
--- a/uploader/publications/misc.py
+++ b/uploader/publications/misc.py
@@ -4,10 +4,10 @@
 def publications_differences(
         filedata: tuple[dict, ...],
         dbdata: tuple[dict, ...],
-        pubmedid2pubidmap: tuple[dict, ...]
+        pubmedid2pubidmap: dict[int, int]
 ) -> tuple[dict, ...]:
     """Compute the differences between file data and db data"""
-    diff = tuple()
+    diff: tuple[dict, ...] = tuple()
     for filerow, dbrow in zip(
             sorted(filedata, key=lambda item: (
                 item["phenotype_id"], item["xref_id"])),
diff --git a/uploader/publications/pubmed.py b/uploader/publications/pubmed.py
index 2531c4a..15bf701 100644
--- a/uploader/publications/pubmed.py
+++ b/uploader/publications/pubmed.py
@@ -1,5 +1,6 @@
 """Module to interact with NCBI's PubMed"""
 import logging
+from typing import Optional
 
 import requests
 from lxml import etree
@@ -40,7 +41,7 @@ def __pages__(pagination: etree.Element) -> str:
     )) if start is not None else ""
 
 
-def __abstract__(article: etree.Element) -> str:
+def __abstract__(article: etree.Element) -> Optional[str]:
     abstract = article.find("Abstract/AbstractText")
     return abstract.text if abstract is not None else None
 
diff --git a/uploader/publications/views.py b/uploader/publications/views.py
index 4ec832f..d9eb294 100644
--- a/uploader/publications/views.py
+++ b/uploader/publications/views.py
@@ -82,13 +82,21 @@ def view_publication(publication_id: int):
 @require_login
 def create_publication():
     """Create a new publication."""
+    _get_args = {
+        key: request.args[key]
+        for key in ("species_id", "population_id", "dataset_id", "return_to")
+        if bool(request.args.get(key))
+    }
+
     if request.method == "GET":
-        return render_template("publications/create-publication.html")
+        return render_template(
+            "publications/create-publication.html",
+            get_args=_get_args)
     form = request.form
     authors = form.get("publication-authors").encode("utf8")
     if authors is None or authors == "":
         flash("The publication's author(s) MUST be provided!", "alert alert-danger")
-        return redirect(url_for("publications.create", **request.args))
+        return redirect(url_for("publications.create"))
 
     with database_connection(app.config["SQL_URI"]) as conn:
         publications = create_new_publications(conn, ({
@@ -106,7 +114,7 @@ def create_publication():
         return redirect(url_for(
             request.args.get("return_to") or "publications.view_publication",
             publication_id=publications[0]["publication_id"],
-            **request.args))
+            **_get_args))
 
     flash("Publication creation failed!", "alert alert-danger")
     app.logger.debug("Failed to create the new publication.", exc_info=True)
@@ -130,14 +138,14 @@ def edit_publication(publication_id: int):
         _pub = update_publications(conn, ({
             "publication_id": publication_id,
             "pubmed_id": form.get("pubmed-id") or None,
-            "abstract": form.get("publication-abstract").encode("utf8") or None,
-            "authors": form.get("publication-authors").encode("utf8"),
-            "title":  form.get("publication-title").encode("utf8") or None,
-            "journal": form.get("publication-journal").encode("utf8") or None,
-            "volume": form.get("publication-volume").encode("utf8") or None,
-            "pages": form.get("publication-pages").encode("utf8") or None,
+            "abstract": (form.get("publication-abstract") or "").encode("utf8") or None,
+            "authors": (form.get("publication-authors") or "").encode("utf8"),
+            "title":  (form.get("publication-title") or "").encode("utf8") or None,
+            "journal": (form.get("publication-journal") or "").encode("utf8") or None,
+            "volume": (form.get("publication-volume") or "").encode("utf8") or None,
+            "pages": (form.get("publication-pages") or "").encode("utf8") or None,
             "month": (form.get("publication-month") or "").encode("utf8").capitalize() or None,
-            "year": form.get("publication-year").encode("utf8") or None
+            "year": (form.get("publication-year") or "").encode("utf8") or None
         },))
 
         if not _pub:
@@ -171,7 +179,8 @@ def delete_publication(publication_id: int):
             flash("Cannot delete publication with linked phenotypes!",
                   "alert-warning")
             return redirect(url_for(
-                "publications.view_publication", publication_id=publication_id))
+                "publications.view_publication",
+                publication_id=publication_id))
 
         if request.method == "GET":
             return render_template(
@@ -182,4 +191,4 @@ def delete_publication(publication_id: int):
 
         delete_publications(conn, (publication,))
         flash("Deleted the publication successfully.", "alert-success")
-        return render_template("publications/delete-publication-success.html")
+        return redirect(url_for("publications.index"))
diff --git a/uploader/route_utils.py b/uploader/route_utils.py
index 4449475..426d7eb 100644
--- a/uploader/route_utils.py
+++ b/uploader/route_utils.py
@@ -56,22 +56,24 @@ def generic_select_population(
 def redirect_to_next(default: dict):
     """Redirect to the next uri if specified, else redirect to default."""
     assert "uri" in default, "You must provide at least the 'uri' value."
-    try:
-        next_page = base64_decode_to_dict(request.args.get("next"))
-        _uri = next_page["uri"]
-        next_page.pop("uri")
-        return redirect(url_for(_uri, **next_page))
-    except (TypeError, JSONDecodeError) as _err:
-        logger.debug("We could not decode the next value '%s'",
-                     next_page,
-                     exc_info=True)
+    _next = request.args.get("next") or ""
+    if bool(_next):
+        try:
+            next_page = base64_decode_to_dict(_next)
+            _uri = next_page["uri"]
+            next_page.pop("uri")
+            return redirect(url_for(_uri, **next_page))
+        except (TypeError, JSONDecodeError) as _err:
+            logger.debug("We could not decode the next value '%s'",
+                         next_page,
+                         exc_info=True)
 
     return redirect(url_for(
         default["uri"],
         **{key:value for key,value in default.items() if key != "uri"}))
 
 
-def build_next_argument(uri: str, **kwargs) -> str:
+def build_next_argument(uri: str, **kwargs) -> bytes:
     """Build the `next` URI argument from provided details."""
     dumps_keywords = (
         "skipkeys", "ensure_ascii", "check_circular", "allow_nan", "cls",
diff --git a/uploader/samples/views.py b/uploader/samples/views.py
index f8baf7e..2a09f8e 100644
--- a/uploader/samples/views.py
+++ b/uploader/samples/views.py
@@ -1,17 +1,19 @@
 """Code regarding samples"""
-import os
 import sys
 import uuid
+import logging
 from pathlib import Path
 
-from redis import Redis
 from flask import (flash,
                    request,
                    redirect,
                    Blueprint,
                    current_app as app)
 
-from uploader import jobs
+from gn_libs import jobs
+from gn_libs import sqlite3
+
+from uploader import session
 from uploader.files import save_file
 from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
@@ -23,8 +25,7 @@ from uploader.datautils import safe_int, enumerate_sequence
 from uploader.species.models import all_species, species_by_id
 from uploader.request_checks import with_species, with_population
 from uploader.db_utils import (with_db_connection,
-                               database_connection,
-                               with_redis_connection)
+                               database_connection)
 
 from .models import samples_by_species_and_population
 
@@ -96,22 +97,6 @@ def list_samples(species: dict, population: dict, **kwargs):# pylint: disable=[u
                                activelink="list-samples")
 
 
-def build_sample_upload_job(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
-        speciesid: int,
-        populationid: int,
-        samplesfile: Path,
-        separator: str,
-        firstlineheading: bool,
-        quotechar: str):
-    """Define the async command to run the actual samples data upload."""
-    return [
-        sys.executable, "-m", "scripts.insert_samples", app.config["SQL_URI"],
-        str(speciesid), str(populationid), str(samplesfile.absolute()),
-        separator, f"--redisuri={app.config['REDIS_URL']}",
-        f"--quotechar={quotechar}"
-    ] + (["--firstlineheading"] if firstlineheading else [])
-
-
 @samplesbp.route("<int:species_id>/populations/<int:population_id>/upload-samples",
                methods=["GET", "POST"])
 @require_login
@@ -153,7 +138,7 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma
 
     try:
         samples_file = save_file(request.files["samples_file"],
-                                 Path(app.config["UPLOAD_FOLDER"]))
+                                 Path(app.config["UPLOADS_DIRECTORY"]))
     except AssertionError:
         flash("You need to provide a file with the samples data.",
               "alert-error")
@@ -170,102 +155,50 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma
 
     quotechar = (request.form.get("field_delimiter", '"') or '"')
 
-    redisuri = app.config["REDIS_URL"]
-    with Redis.from_url(redisuri, decode_responses=True) as rconn:
-        #T0DO: Add a QC step here — what do we check?
-        # 1. Does any sample in the uploaded file exist within the database?
-        #    If yes, what is/are its/their species and population?
-        # 2. If yes 1. above, provide error with notes on which species and
-        #    populations already own the samples.
-        the_job = jobs.launch_job(
+    _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
+    with sqlite3.connection(_jobs_db) as conn:
+        job = jobs.launch_job(
             jobs.initialise_job(
-                rconn,
-                jobs.jobsnamespace(),
+                conn,
                 str(uuid.uuid4()),
-                build_sample_upload_job(
-                    species["SpeciesId"],
-                    population["InbredSetId"],
-                    samples_file,
+                [
+                    sys.executable, "-m", "scripts.insert_samples",
+                    app.config["SQL_URI"],
+                    str(species["SpeciesId"]),
+                    str(population["InbredSetId"]),
+                    str(samples_file.absolute()),
                     separator,
-                    firstlineheading,
-                    quotechar),
+                    f"--quotechar={quotechar}"
+                ] + (["--firstlineheading"] if firstlineheading else []),
                 "samples_upload",
-                app.config["JOBS_TTL_SECONDS"],
-                {"job_name": f"Samples Upload: {samples_file.name}"}),
-            redisuri,
-            f"{app.config['UPLOAD_FOLDER']}/job_errors")
-        return redirect(url_for(
-            "species.populations.samples.upload_status",
-            species_id=species_id,
-            population_id=population_id,
-            job_id=the_job["jobid"]))
-
-
-@samplesbp.route("<int:species_id>/populations/<int:population_id>/"
-                 "upload-samples/status/<uuid:job_id>",
-                 methods=["GET"])
-@require_login
-@with_population(species_redirect_uri="species.populations.samples.index",
-                 redirect_uri="species.populations.samples.select_population")
-def upload_status(species: dict, population: dict, job_id: uuid.UUID, **kwargs):# pylint: disable=[unused-argument]
-    """Check on the status of a samples upload job."""
-    job = with_redis_connection(lambda rconn: jobs.job(
-        rconn, jobs.jobsnamespace(), job_id))
-    if job:
-        status = job["status"]
-        if status == "success":
-            return render_template("samples/upload-success.html",
-                                   job=job,
-                                   species=species,
-                                   population=population,)
-
-        if status == "error":
-            return redirect(url_for(
-                "species.populations.samples.upload_failure",
-                species_id=species["SpeciesId"],
-                population_id=population["Id"],
-                job_id=job_id))
-
-        error_filename = Path(jobs.error_filename(
-            job_id, f"{app.config['UPLOAD_FOLDER']}/job_errors"))
-        if error_filename.exists():
-            stat = os.stat(error_filename)
-            if stat.st_size > 0:
-                return redirect(url_for(
-                    "samples.upload_failure", job_id=job_id))
-
-        return render_template("samples/upload-progress.html",
-                               species=species,
-                               population=population,
-                               job=job) # maybe also handle this?
-
-    return render_template("no_such_job.html",
-                           job_id=job_id,
-                           species=species,
-                           population=population), 400
-
-
-@samplesbp.route("<int:species_id>/populations/<int:population_id>/"
-                 "upload-samples/failure/<uuid:job_id>",
-                 methods=["GET"])
-@require_login
-@with_population(species_redirect_uri="species.populations.samples.index",
-                 redirect_uri="species.populations.samples.select_population")
-def upload_failure(species: dict, population: dict, job_id: uuid.UUID, **kwargs):# pylint: disable=[unused-argument]
-    """Display the errors of the samples upload failure."""
-    job = with_redis_connection(lambda rconn: jobs.job(
-        rconn, jobs.jobsnamespace(), job_id))
-    if not bool(job):
-        return render_template("no_such_job.html", job_id=job_id), 400
-
-    error_filename = Path(jobs.error_filename(
-        job_id, f"{app.config['UPLOAD_FOLDER']}/job_errors"))
-    if error_filename.exists():
-        stat = os.stat(error_filename)
-        if stat.st_size > 0:
-            return render_template("worker_failure.html", job_id=job_id)
-
-    return render_template("samples/upload-failure.html",
-                           species=species,
-                           population=population,
-                           job=job)
+                extra_meta={
+                    "job_name": f"Samples Upload: {samples_file.name}",
+                    "species_id": species["SpeciesId"],
+                    "population_id": population["Id"],
+                    "success_handler": (
+                        "uploader.samples.views.samples_upload_success_handler")
+                },
+                external_id=session.logged_in_user_id()),
+            _jobs_db,
+            Path(f"{app.config['UPLOADS_DIRECTORY']}/job_errors").absolute(),
+            loglevel=logging.getLevelName(
+                app.logger.getEffectiveLevel()).lower())
+        return redirect(
+            url_for("background-jobs.job_status", job_id=job["job_id"]))
+
+
+def samples_upload_success_handler(job):
+    """Handler for background jobs: Successful upload of samples"""
+    return return_to_samples_list_view_handler(
+        job, "Samples uploaded successfully.")
+
+
+def return_to_samples_list_view_handler(job, msg):
+    """Handler for background jobs: Return to list_samples page."""
+    flash(msg, "alert alert-success")
+    return redirect(url_for(
+        "species.populations.samples."
+        "list_samples",
+        species_id=job["metadata"]["species_id"],
+        population_id=job["metadata"]["population_id"],
+        job_id=job["job_id"]))
diff --git a/uploader/session.py b/uploader/session.py
index 5af5827..9872ceb 100644
--- a/uploader/session.py
+++ b/uploader/session.py
@@ -1,12 +1,15 @@
 """Deal with user sessions"""
+import logging
 from uuid import UUID, uuid4
 from datetime import datetime
 from typing import Any, Optional, TypedDict
 
+from flask import session
 from authlib.jose import KeySet
-from flask import request, session
 from pymonad.either import Left, Right, Either
 
+logger = logging.getLogger(__name__)
+
 
 class UserDetails(TypedDict):
     """Session information relating specifically to the user."""
@@ -22,8 +25,6 @@ class SessionInfo(TypedDict):
     session_id: UUID
     user: UserDetails
     anon_id: UUID
-    user_agent: str
-    ip_addr: str
     masquerade: Optional[UserDetails]
     auth_server_jwks: Optional[dict[str, Any]]
 
@@ -66,9 +67,6 @@ def session_info() -> SessionInfo:
                 "logged_in": False
             },
             "anon_id": anon_id,
-            "user_agent": request.headers.get("User-Agent"),
-            "ip_addr": request.environ.get("HTTP_X_FORWARDED_FOR",
-                                           request.remote_addr),
             "masquerading": None
         }))
 
@@ -91,6 +89,17 @@ def user_details() -> UserDetails:
     """Retrieve user details."""
     return session_info()["user"]
 
+
+def logged_in_user_id() -> Optional[UUID]:
+    """Get user id for logged in user. If user has not logged in, return None."""
+    return user_token().then(
+        lambda _tok: user_details()
+    ).then(
+        lambda _user: Either(_user["user_id"],
+                             (None, _user["email"] != "anon@ymous.user"))
+    ).either(lambda _err: None, lambda uid: uid)
+
+
 def user_token() -> Either:
     """Retrieve the user token."""
     return session_info()["user"]["token"]
diff --git a/uploader/species/views.py b/uploader/species/views.py
index 9b14d01..4bfa7ae 100644
--- a/uploader/species/views.py
+++ b/uploader/species/views.py
@@ -8,8 +8,6 @@ from flask import (flash,
                    Blueprint,
                    current_app as app)
 
-from uploader.sui import sui_template
-
 from uploader.population import popbp
 from uploader.platforms import platformsbp
 from uploader.flask_extensions import url_for
@@ -56,7 +54,7 @@ def view_species(species_id: int):
                                         species_id=species_id,
                                         population_id=population["Id"]))
             return render_template(
-                sui_template("species/view-species.html"),
+                "species/view-species.html",
                 species=species,
                 activelink="view-species",
                 populations=populations_by_species(conn, species["SpeciesId"]))
diff --git a/uploader/static/css/layout-common.css b/uploader/static/css/layout-common.css
index 36a5735..9c9d034 100644
--- a/uploader/static/css/layout-common.css
+++ b/uploader/static/css/layout-common.css
@@ -1,3 +1,21 @@
 * {
     box-sizing: border-box;
 }
+
+body {
+    display: grid;
+    grid-gap: 1em;
+}
+
+#header {
+    margin: -0.7em; /* Fill entire length of screen */
+    /* Define layout for the children elements */
+    display: grid;
+}
+
+#header #header-nav {
+    /* Place it in the parent element */
+    grid-column-start: 1;
+    grid-column-end: 2;
+    display: flex;
+}
diff --git a/uploader/static/css/layout-large.css b/uploader/static/css/layout-large.css
index 8abd2dd..2d53627 100644
--- a/uploader/static/css/layout-large.css
+++ b/uploader/static/css/layout-large.css
@@ -1,8 +1,6 @@
 @media screen and (min-width: 20.1in) {
     body {
-        display: grid;
         grid-template-columns: 7fr 3fr;
-        grid-gap: 1em;
     }
 
     #header {
@@ -12,7 +10,7 @@
 
         /* Define layout for the children elements */
         display: grid;
-        grid-template-columns: 8fr 2fr;
+        grid-template-columns: 1fr 9fr;
     }
 
     #header #header-text {
@@ -45,6 +43,8 @@
         grid-column-start: 1;
         grid-column-end: 3;
         padding: 0 3px;
+
+        margin: -0.3em -0.7em 0 -0.7em;
     }
 
     #main #main-content {
diff --git a/uploader/static/css/layout-medium.css b/uploader/static/css/layout-medium.css
index 2cca711..50ceeb4 100644
--- a/uploader/static/css/layout-medium.css
+++ b/uploader/static/css/layout-medium.css
@@ -1,8 +1,6 @@
 @media screen and (width > 8in) and (max-width: 20in) {
     body {
-        display: grid;
         grid-template-columns: 65fr 35fr;
-        grid-gap: 1em;
     }
 
     #header {
@@ -12,7 +10,7 @@
 
         /* Define layout for the children elements */
         display: grid;
-        grid-template-columns: 8fr 2fr;
+        grid-template-columns: 2fr 8fr;
     }
 
     #header #header-text {
@@ -51,7 +49,6 @@
         /* Place it in the parent element */
         grid-column-start: 1;
         grid-column-end: 2;
-        grid-gap: 5px;
 
         /* Define layout for the children elements */
         max-width: 100%;
diff --git a/uploader/static/css/layout-small.css b/uploader/static/css/layout-small.css
index 80a3759..2e47217 100644
--- a/uploader/static/css/layout-small.css
+++ b/uploader/static/css/layout-small.css
@@ -2,7 +2,7 @@
     body {
         display: grid;
         grid-template-columns: 1fr;
-        grid-template-rows: 1fr 2fr 7fr;
+        grid-template-rows: 1fr 90fr;
         grid-gap: 1em;
     }
 
@@ -31,6 +31,11 @@
         grid-column-end: 2;
     }
 
+    #header #header-nav ul {
+        display: grid;
+        grid-template-columns: 1fr;
+    }
+
     #main {
         /* Place it in the parent element */
         grid-column-start: 1;
@@ -38,7 +43,7 @@
         display: grid;
 
         /* Define layout for the children elements */
-        grid-template-rows: 1.5em 80% 20%;
+        grid-template-rows: 1fr 80fr 20fr;
         grid-template-columns: 1fr;
     }
 
diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css
index 2acce5f..276978f 100644
--- a/uploader/static/css/theme.css
+++ b/uploader/static/css/theme.css
@@ -8,24 +8,27 @@ body {
 #header {
     background-color: #336699;
     color: #FFFFFF;
-    border-radius: 3px;
     min-height: 30px;
+    border-bottom: solid black 1px;
 }
 
 #header #header-nav .nav li a {
     /* Content styling */
     color: #FFFFFF;
-    background: #4477AA;
-    border: solid 5px #336699;
-    border-radius: 5px;
     font-size: 0.7em;
     text-align: center;
     padding: 1px 7px;
+    text-decoration: none;
 }
 
 #main #breadcrumbs {
-    border-radius:3px;
     text-align: center;
+    background-color: #D5D5D5;
+    padding: 0 1em 0 1em;
+}
+
+#main #breadcrumbs .breadcrumb {
+    padding-top: 0.5em;
 }
 
 #main #main-content {
@@ -34,7 +37,7 @@ body {
 }
 
 #main #sidebar-content {
-    background: #EEEEEE;
+    background: #FEFEFE;
 
     border-radius: 5px;
     padding: 10px 5px;
@@ -56,6 +59,10 @@ body {
     text-transform: capitalize;
 }
 
+label {
+    text-transform: Capitalize;
+}
+
 input[type="search"] {
     border-radius: 5px;
 }
@@ -79,3 +86,17 @@ table.dataTable tbody tr.selected td {
     padding-bottom: 0.2em;
     border-bottom: solid gray 1px;
 }
+
+
+.breadcrumb-item {
+    text-transform: Capitalize;
+}
+
+.breadcrumb-item a {
+    text-decoration: none;
+}
+
+.table thead tr th {
+    text-align: center;
+    vertical-align: middle;
+}
diff --git a/uploader/static/images/frontpage_banner.png b/uploader/static/images/frontpage_banner.png
new file mode 100644
index 0000000..d25e1c9
--- /dev/null
+++ b/uploader/static/images/frontpage_banner.png
Binary files differdiff --git a/uploader/static/js/datatables.js b/uploader/static/js/datatables.js
index 82fd696..bfcda2a 100644
--- a/uploader/static/js/datatables.js
+++ b/uploader/static/js/datatables.js
@@ -11,13 +11,36 @@ var addTableLength = (menuList, lengthToAdd, dataLength) => {
 
 var defaultLengthMenu = (data) => {
     menuList = []
-    var lengths = [10, 25, 50, 100, 1000, data.length];
+    var lengths = [10, 25, 50, 100, 1000];
+    if(data.length > 1000) {
+        lengths.push(data.length)
+    }
     lengths.forEach((len) => {
         menuList = addTableLength(menuList, len, data.length);
     });
     return menuList;
 };
 
+var setRowCheckableProperty = (node, state) => {
+    /**
+     * Set a row's (`node`) checkbox's or radio button's checked state to the
+     * boolean value `state`.
+     **/
+    if(typeof(state) == "boolean") {
+        var pseudoclass = state == false ? ":checked" : ":not(:checked)";
+        var checkable = (
+            $(node).find(`input[type="checkbox"]${pseudoclass}`)[0]
+                ||
+                $(node).find(`input[type="radio"]${pseudoclass}`)[0]);
+        $(checkable).prop("checked", state);
+    } else {
+        throw new Error("`state` *MUST* be a boolean value.")
+    }
+};
+
+var setRowChecked = (node) => {setRowCheckableProperty(node, true);};
+var setRowUnchecked = (node) => {setRowCheckableProperty(node, false);};
+
 var buildDataTable = (tableId, data = [], columns = [], userSettings = {}) => {
     var defaultSettings = {
         responsive: true,
@@ -35,35 +58,40 @@ var buildDataTable = (tableId, data = [], columns = [], userSettings = {}) => {
             lengthMenu: "",
             info: ""
         },
-        data: data,
-        columns: columns,
-        drawCallback: (settings) => {
-            $(this[0]).find("tbody tr").each((idx, row) => {
-                var arow = $(row);
-                var checkboxOrRadio = arow.find(".chk-row-select");
-                if (checkboxOrRadio) {
-                    if (arow.hasClass("selected")) {
-                        checkboxOrRadio.prop("checked", true);
-                    } else {
-                        checkboxOrRadio.prop("checked", false);
-                    }
-                }
+        drawCallback: function (settings) {
+            var api = this.api();
+            api.rows({selected: true}).nodes().each((node, index) => {
+                setRowChecked(node);
+            });
+            api.rows({selected: false}).nodes().each((node, index) => {
+                setRowUnchecked(node);
             });
         }
     }
     var theDataTable = $(tableId).DataTable({
         ...defaultSettings,
-        ...userSettings
+        ...userSettings,
+        ...(data.length == 0 ? {} : {data: data}),
+        ...(columns.length == 0 ? {} : {columns: columns})
     });
-    theDataTable.on("select", (event, datatable, type, cell, originalEvent) => {
-        datatable.rows({selected: true}).nodes().each((node, index) => {
-            $(node).find(".chk-row-select").prop("checked", true)
-        });
+    theDataTable.on("select", (event, datatable, type, indexes) => {
+        datatable
+            .rows(indexes)
+            .nodes()
+            .each((node, index) => {
+                setRowChecked(node);
+            });
     });
-    theDataTable.on("deselect", (event, datatable, type, cell, originalEvent) => {
-        datatable.rows({selected: false}).nodes().each((node, index) => {
-            $(node).find(".chk-row-select").prop("checked", false)
-        });
+    theDataTable.on("deselect", (event, datatable, type, indexes) => {
+        datatable
+            .rows(indexes)
+            .nodes()
+            .each(function(node, index) {
+                setRowUnchecked(node);
+            });
     });
+
+    theDataTable.selectAll = () => {theDataTable.rows().select()};
+    theDataTable.deselectAll = () => {theDataTable.rows().deselect()};
     return theDataTable;
 };
diff --git a/uploader/static/js/upload_samples.js b/uploader/static/js/upload_samples.js
index aed536f..1c25a1d 100644
--- a/uploader/static/js/upload_samples.js
+++ b/uploader/static/js/upload_samples.js
@@ -87,20 +87,20 @@ function display_preview(event) {
     var data_preview_table = document.getElementById("tbl:samples-preview");
     remove_rows(data_preview_table);
 
-    var separator = document.getElementById("select:separator").value;
+    var separator = document.getElementById("select-separator").value;
     if(separator === "other") {
-	separator = document.getElementById("txt:separator").value;
+	separator = document.getElementById("txt-separator").value;
     }
     if(separator == "") {
 	display_error_row(data_preview_table, "Please provide a separator.");
 	return false;
     }
 
-    var delimiter = document.getElementById("txt:delimiter").value;
+    var delimiter = document.getElementById("txt-delimiter").value;
 
-    var firstlineheading = document.getElementById("chk:heading").checked;
+    var firstlineheading = document.getElementById("chk-heading").checked;
 
-    var fileelement = document.getElementById("file:samples");
+    var fileelement = document.getElementById("file-samples");
     var preview_data = JSON.parse(
 	fileelement.getAttribute("data-preview-content") || "[]");
     if(preview_data.length == 0) {
@@ -115,18 +115,18 @@ function display_preview(event) {
 	delimiter));
 }
 
-document.getElementById("chk:heading").addEventListener(
+document.getElementById("chk-heading").addEventListener(
     "change", display_preview);
-document.getElementById("select:separator").addEventListener(
+document.getElementById("select-separator").addEventListener(
     "change", display_preview);
-document.getElementById("txt:separator").addEventListener(
+document.getElementById("txt-separator").addEventListener(
     "keyup", display_preview);
-document.getElementById("txt:delimiter").addEventListener(
+document.getElementById("txt-delimiter").addEventListener(
     "keyup", display_preview);
-document.getElementById("file:samples").addEventListener(
+document.getElementById("file-samples").addEventListener(
     "change", (event) => {
 	read_first_n_lines(event,
-			   document.getElementById("file:samples"),
+			   document.getElementById("file-samples"),
 			   30,
-			   document.getElementById("chk:heading").checked);
+			   document.getElementById("chk-heading").checked);
     });
diff --git a/uploader/static/js/utils.js b/uploader/static/js/utils.js
index 1b31661..62d3662 100644
--- a/uploader/static/js/utils.js
+++ b/uploader/static/js/utils.js
@@ -28,7 +28,8 @@ var remove_class = (element, classvalue) => {
 
 var add_class = (element, classvalue) => {
     remove_class(element, classvalue);
-    element.attr("class", (element.attr("class") || "") + " " + classvalue);
+    element.attr("class",
+                 ((element.attr("class") || "") + " " + classvalue).trim());
 };
 
 $(".not-implemented").click((event) => {
diff --git a/uploader/sui.py b/uploader/sui.py
deleted file mode 100644
index 8eb863d..0000000
--- a/uploader/sui.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Utilities for streamlined UI. This is a temporary module."""
-from flask import request
-
-def sui_template(template_path: str) -> str:
-    """Return the streamlined UI template for given template path."""
-    _sui="sui-" if request.args.get("streamlined_ui") else ""
-    _parts = template_path.split("/")
-    return "/".join(_parts[:-1] + [f"{_sui}{_parts[-1]}"])
diff --git a/uploader/templates/background-jobs/base.html b/uploader/templates/background-jobs/base.html
new file mode 100644
index 0000000..7201207
--- /dev/null
+++ b/uploader/templates/background-jobs/base.html
@@ -0,0 +1,10 @@
+{%extends "base.html"%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('background-jobs.list_jobs')}}">
+    background jobs
+  </a>
+</li>
+{%endblock%}
diff --git a/uploader/templates/background-jobs/default-success-page.html b/uploader/templates/background-jobs/default-success-page.html
deleted file mode 100644
index 5732456..0000000
--- a/uploader/templates/background-jobs/default-success-page.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{%extends "phenotypes/base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-
-{%block title%}Background Jobs: Success{%endblock%}
-
-{%block pagetitle%}Background Jobs: Success{%endblock%}
-
-{%block contents%}
-{{flash_all_messages()}}
-
-<div class="row">
-  <p>Job <strong>{{job.job_id}}</strong>,
-    {%if job.get("metadata", {}).get("job-type")%}
-    of type '<em>{{job.metadata["job-type"]}}</em>
-    {%endif%}' completed successfully.</p>
-</div>
-{%endblock%}
diff --git a/uploader/templates/background-jobs/delete-job.html b/uploader/templates/background-jobs/delete-job.html
new file mode 100644
index 0000000..242c775
--- /dev/null
+++ b/uploader/templates/background-jobs/delete-job.html
@@ -0,0 +1,61 @@
+{%extends "background-jobs/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "background-jobs/macro-display-job-details.html" import display_job_details%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}">
+    summary
+  </a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <h2 class="heading">background jobs: delete?</h2>
+
+  <p class="text-danger">Are you sure you want to delete the job below?</p>
+
+  {{display_job_details(job, display_datetime)}}
+</div>
+
+<div class="row">
+  <form id="frm-delete-job"
+        method="POST"
+        action="{{url_for('background-jobs.delete_single', job_id=job.job_id)}}">
+    <div class="row">
+      <div class="col">
+        <input type="submit"
+               class="btn btn-info"
+               value="cancel"
+               name="btn-confirm-delete" />
+      </div>
+      <div class="col">
+        <input type="submit"
+               class="btn btn-danger"
+               value="delete"
+               name="btn-confirm-delete" />
+      </div>
+    </div>
+  </form>
+</div>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+<div class="row">
+  <h6 class="subheading">What is this?</h6>
+</div>
+<div class="row">
+  <p>Confirm whether or not you want to delete job
+    <strong>{{job.job_id}}</strong>.</p>
+</div>
+{{super()}}
+{%endblock%}
diff --git a/uploader/templates/background-jobs/job-status.html b/uploader/templates/background-jobs/job-status.html
new file mode 100644
index 0000000..2e75c6d
--- /dev/null
+++ b/uploader/templates/background-jobs/job-status.html
@@ -0,0 +1,45 @@
+{%extends "background-jobs/base.html"%}
+{%from "background-jobs/macro-display-job-details.html" import display_job_details%}
+
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block extrameta%}
+<meta http-equiv="refresh" content="5" />
+{%endblock%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <h2 class="heading">job status</h2>
+
+  {{display_job_details(job, display_datetime)}}
+</div>
+
+<div class="row">
+  <div class="col">
+    <a href="{{url_for('background-jobs.stop_job', job_id=job.job_id)}}"
+       title="Stop/Kill this job."
+       class="btn btn-danger">stop job</a>
+  </div>
+</div>
+
+<div class="row">
+  <h3 class="subheading">STDOUT</h3>
+  <div style="max-width: 40em; overflow: scroll">
+    <pre>{{job["stdout"]}}</pre>
+  </div>
+</div>
+
+<div class="row">
+  <h3 class="subheading">STDERR</h3>
+  <div style="max-width: 40em; overflow: scroll">
+    <pre>{{job["stderr"]}}</pre>
+  </div>
+</div>
+
+{%endblock%}
diff --git a/uploader/templates/background-jobs/job-summary.html b/uploader/templates/background-jobs/job-summary.html
new file mode 100644
index 0000000..ef9ef6c
--- /dev/null
+++ b/uploader/templates/background-jobs/job-summary.html
@@ -0,0 +1,75 @@
+{%extends "background-jobs/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "background-jobs/macro-display-job-details.html" import display_job_details%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}">
+    summary
+  </a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <h2 class="heading">background jobs: summary</h2>
+
+  {{display_job_details(job, display_datetime)}}
+</div>
+
+<div class="row">
+  {%if view_under_construction%}
+  <div class="col">
+    <a href="#"
+       class="btn btn-info not-implemented"
+       title="Update the expiry date and time for this job.">update expiry</a>
+  </div>
+
+  {%if job.metadata.status in ("stopped",)%}
+  <div class="col">
+    <a href="#"
+       class="btn btn-warning not-implemented"
+       title="Create a new copy of this job, and run the copy.">Run Copy</a>
+  </div>
+  {%endif%}
+  {%endif%}
+
+  <div class="col">
+    <a href="{{url_for('background-jobs.delete_single', job_id=job.job_id)}}"
+       class="btn btn-danger"
+       title="Delete this job.">delete</a>
+  </div>
+</div>
+
+<div class="row">
+  <h3 class="subheading">Script Errors and Logging</h3>
+  <div style="max-width: 40em; overflow: scroll">
+    <pre>{{job["stderr"]}}</pre>
+  </div>
+</div>
+
+<div class="row">
+  <h3 class="subheading">Script Output</h3>
+  <div style="max-width: 40em; overflow: scroll">
+    <pre>{{job["stdout"]}}</pre>
+  </div>
+</div>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+<div class="row">
+  <h6 class="subheading">What is this?</h6>
+</div>
+<div class="row">
+  <p>This page shows the results of running job '{{job.job_id}}'.</p>
+</div>
+{{super()}}
+{%endblock%}
diff --git a/uploader/templates/background-jobs/list-jobs.html b/uploader/templates/background-jobs/list-jobs.html
new file mode 100644
index 0000000..c16b850
--- /dev/null
+++ b/uploader/templates/background-jobs/list-jobs.html
@@ -0,0 +1,79 @@
+{%extends "background-jobs/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row"><h2 class="heading">Background Jobs</h2></div>
+
+<div class="row">
+  <div class="table-responsive">
+    <table class="table">
+      <thead>
+        <tr class="table-primary">
+          <th>Type</th>
+          <th>Created</th>
+          <th title="Date and time past which the job's details will be deleted from the system.">
+            Expires</th>
+          <th>Status</th>
+          <th>Actions</th>
+        </tr>
+      </thead>
+
+      <tbody>
+        {%for job in jobs%}
+        <tr>
+          <td>{{job.metadata["job-type"]}}</td>
+          <td>{{display_datetime(job.created)}}</td>
+          <td title="Date and time past which the job's details will be deleted from the system.">
+            {{display_datetime(job.expires)}}
+          </td>
+          <td {%if job.metadata.status == "completed"%}
+              class="fw-bold text-capitalize text-success"
+              {%elif job.metadata.status == "error"%}
+              class="fw-bold text-capitalize text-danger"
+              {%elif job.metadata.status == "stopped"%}
+              class="fw-bold text-capitalize text-warning"
+              {%else%}
+              class="fw-bold text-capitalize text-info"
+              {%endif%}>
+            <div>
+              {{job.metadata.status}}
+            </div>
+          </td>
+          <td>
+            <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"
+               class="btn btn-info"
+               title="View more detailed information about this job.">
+              view summary</a>
+          </td>
+        </tr>
+        {%else%}
+        <tr>
+          <td colspan="5">
+            You do not have any jobs you have run in the background.</td>
+        </tr>
+        {%endfor%}
+      </tbody>
+    </table>
+  </div>
+</div>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+<div class="row">
+  <h6 class="subheading">What is this?</h6>
+</div>
+<div class="row">
+  <p>The table lists the jobs that are running in the background, that you
+    started.</p>
+  <p>You can use the tools provided on this page to manage the jobs, and to view
+    each job's details.</p>
+</div>
+{{super()}}
+{%endblock%}
diff --git a/uploader/templates/background-jobs/macro-display-job-details.html b/uploader/templates/background-jobs/macro-display-job-details.html
new file mode 100644
index 0000000..82e33c0
--- /dev/null
+++ b/uploader/templates/background-jobs/macro-display-job-details.html
@@ -0,0 +1,29 @@
+{%macro display_job_details(job, display_datetime)%}
+<table class="table">
+  <thead>
+  </thead>
+
+  <tbody>
+    <tr>
+      <th class="table-primary">Job ID</th>
+      <td>{{job.job_id}}</td>
+    </tr>
+    <tr>
+      <th class="table-primary">Type</th>
+      <td>{{job.metadata["job-type"]}}</td>
+    </tr>
+    <tr>
+      <th class="table-primary">Created</th>
+      <td>{{display_datetime(job.created)}}</td>
+    </tr>
+    <tr>
+      <th class="table-primary">Expires</th>
+      <td>{{display_datetime(job.expires)}}</td>
+    </tr>
+    <tr>
+      <th class="table-primary">Status</th>
+      <td>{{job.metadata.status}}</td>
+    </tr>
+  </tbody>
+</table>
+{%endmacro%}
diff --git a/uploader/templates/background-jobs/stop-job.html b/uploader/templates/background-jobs/stop-job.html
new file mode 100644
index 0000000..fc190ac
--- /dev/null
+++ b/uploader/templates/background-jobs/stop-job.html
@@ -0,0 +1,61 @@
+{%extends "background-jobs/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "background-jobs/macro-display-job-details.html" import display_job_details%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}">
+    summary
+  </a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <h2 class="heading">background jobs: stop?</h2>
+
+  <p class="text-danger">Are you sure you want to stop the job below?</p>
+
+  {{display_job_details(job, display_datetime)}}
+</div>
+
+<div class="row">
+  <form id="frm-stop-job"
+        method="POST"
+        action="{{url_for('background-jobs.stop_job', job_id=job.job_id)}}">
+    <div class="row">
+      <div class="col">
+        <input type="submit"
+               class="btn btn-info"
+               value="cancel"
+               name="btn-confirm-stop" />
+      </div>
+      <div class="col">
+        <input type="submit"
+               class="btn btn-danger"
+               value="stop"
+               name="btn-confirm-stop" />
+      </div>
+    </div>
+  </form>
+</div>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+<div class="row">
+  <h6 class="subheading">What is this?</h6>
+</div>
+<div class="row">
+  <p>Confirm whether or not you want to stop job
+    <strong>{{job.job_id}}</strong>.</p>
+</div>
+{{super()}}
+{%endblock%}
diff --git a/uploader/templates/background-jobs/sui-default-success-page.html b/uploader/templates/background-jobs/sui-default-success-page.html
deleted file mode 100644
index 5732456..0000000
--- a/uploader/templates/background-jobs/sui-default-success-page.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{%extends "phenotypes/base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-
-{%block title%}Background Jobs: Success{%endblock%}
-
-{%block pagetitle%}Background Jobs: Success{%endblock%}
-
-{%block contents%}
-{{flash_all_messages()}}
-
-<div class="row">
-  <p>Job <strong>{{job.job_id}}</strong>,
-    {%if job.get("metadata", {}).get("job-type")%}
-    of type '<em>{{job.metadata["job-type"]}}</em>
-    {%endif%}' completed successfully.</p>
-</div>
-{%endblock%}
diff --git a/uploader/templates/base.html b/uploader/templates/base.html
index d521ccb..ae4ecef 100644
--- a/uploader/templates/base.html
+++ b/uploader/templates/base.html
@@ -16,7 +16,11 @@
     <link rel="stylesheet" type="text/css"
           href="{{url_for('base.datatables',
                 filename='css/dataTables.bootstrap5.min.css')}}" />
-    <link rel="stylesheet" type="text/css" href="/static/css/styles.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-common.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-large.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-medium.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-small.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/theme.css" />
 
     {%block css%}{%endblock%}
 
@@ -26,14 +30,32 @@
     <header id="header">
       <span id="header-text">GeneNetwork</span>
       <nav id="header-nav">
-        <ul class="nav justify-content-end">
+        <ul class="nav">
+          {%if user_logged_in()%}
+          <li>
+            <a href="{{url_for('background-jobs.list_jobs')}}"
+               title="User's background jobs.">
+              <!-- https://icons.getbootstrap.com/icons/back/ -->
+              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-back" viewBox="0 0 16 16">
+                <path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/>
+              </svg>
+              Background jobs
+            </a>
+          </li>
+
           <li>
-            {%if user_logged_in()%}
             <a href="{{url_for('oauth2.logout')}}"
                title="Log out of the system">
+              <!-- https://icons.getbootstrap.com/icons/file-person/ -->
+              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-person" viewBox="0 0 16 16">
+                <path d="M12 1a1 1 0 0 1 1 1v10.755S12 11 8 11s-5 1.755-5 1.755V2a1 1 0 0 1 1-1zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
+                <path d="M8 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
+              </svg>
               <span class="glyphicon glyphicon-user"></span>
-              {{user_email()}} Sign Out</a>
-            {%else%}
+              Sign Out ({{user_email()}})</a>
+          </li>
+          {%else%}
+          <li>
             <a href="{{authserver_authorise_uri()}}"
                title="Log in to the system">Sign In</a>
             {%endif%}
@@ -42,91 +64,29 @@
       </nav>
     </header>
 
-    <aside id="nav-sidebar">
-      <ul class="nav flex-column">
-        <li {%if activemenu=="home"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('base.index')}}" >Home</a></li>
-        <li {%if activemenu=="publications"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('publications.index')}}"
-             title="View and manage publications.">Publications</a></li>
-        <li {%if activemenu=="species"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.list_species')}}"
-             title="View and manage species information.">Species</a></li>
-        <li {%if activemenu=="platforms"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.platforms.index')}}"
-             title="View and manage species platforms.">Sequencing Platforms</a></li>
-        <li {%if activemenu=="populations"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.populations.index')}}"
-             title="View and manage species populations.">Populations</a></li>
-        <li {%if activemenu=="samples"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.populations.samples.index')}}"
-             title="Upload population samples.">Samples</a></li>
-        <li {%if activemenu=="genotypes"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.populations.genotypes.index')}}"
-             title="Upload Genotype data.">Genotype Data</a></li>
-        <!--
-            TODO: Maybe include menus here for managing studies and dataset or
-            maybe have the studies/datasets managed under their respective
-            sections, e.g. "Publish*" studies/datasets under the "Phenotypes"
-            section, "ProbeSet*" studies/datasets under the "Expression Data"
-            sections, etc.
-          -->
-        <li {%if activemenu=="phenotypes"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.populations.phenotypes.index')}}"
-             title="Upload phenotype data.">Phenotype Data</a></li>
-        <!--
-        <li {%if activemenu=="expression-data"%}class="activemenu"{%endif%}>
-          <a href="{{url_for('species.populations.expression-data.index')}}"
-             title="Upload expression data."
-             class="not-implemented">Expression Data</a></li>
-        <li {%if activemenu=="individuals"%}class="activemenu"{%endif%}>
-          <a href="#"
-             class="not-implemented"
-             title="Upload individual data.">Individual Data</a></li>
-        <li {%if activemenu=="rna-seq"%}class="activemenu"{%endif%}>
-          <a href="#"
-             class="not-implemented"
-             title="Upload RNA-Seq data.">RNA-Seq Data</a></li>
-        <li {%if activemenu=="async-jobs"%}class="activemenu"{%endif%}>
-          <a href="#"
-             class="not-implemented"
-             title="View and manage the backgroud jobs you have running">
-            Background Jobs</a></li>
-        -->
-      </ul>
-    </aside>
 
     <main id="main" class="main">
+      <nav id="breadcrumbs" aria-label="breadcrumb">
+        <ol class="breadcrumb">
+          {%block breadcrumbs%}
+          <li class="breadcrumb-item">
+            <a href="{{url_for('base.index')}}">Home</a></li>
+          {%endblock%}
+        </ol>
+      </nav>
 
-      <div id="pagetitle" class="pagetitle">
-        <span class="title">Data Upload and Quality Control: {%block pagetitle%}{%endblock%}</span>
-        <!--
-            <nav>
-              <ol class="breadcrumb">
-                <li {%if activelink is not defined or activelink=="home"%}
-                    class="breadcrumb-item active"
-                    {%else%}
-                    class="breadcrumb-item"
-                    {%endif%}>
-                  <a href="{{url_for('base.index')}}">Home</a>
-                </li>
-                {%block lvl1_breadcrumbs%}{%endblock%}
-              </ol>
-            </nav>
-            -->
-      </div>
-
-      <div id="all-content">
-        <div id="main-content">
+      <div id="main-content">
           {%block contents%}{%endblock%}
         </div>
-        <div id="sidebar-content">
+
+      <div id="sidebar-content">
           {%block sidebarcontents%}{%endblock%}
         </div>
-      </div>
     </main>
 
 
+
+    <script type="text/javascript" src="/static/js/debug.js"></script>
     <!--
         Core dependencies
       -->
diff --git a/uploader/templates/cli-output.html b/uploader/templates/cli-output.html
index 64b1a9a..9cff09d 100644
--- a/uploader/templates/cli-output.html
+++ b/uploader/templates/cli-output.html
@@ -1,7 +1,7 @@
 {%macro cli_output(job, stream)%}
 
 <h4 class="subheading">{{stream | upper}} Output</h4>
-<div class="cli-output" style="max-height: 10em; overflow: auto;">
+<div class="cli-output" style="overflow: auto;">
   <pre>{{job.get(stream, "")}}</pre>
 </div>
 
diff --git a/uploader/templates/genotypes/base.html b/uploader/templates/genotypes/base.html
index 7d61312..8d1b951 100644
--- a/uploader/templates/genotypes/base.html
+++ b/uploader/templates/genotypes/base.html
@@ -1,23 +1,18 @@
 {%extends "populations/base.html"%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
 
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="genotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  {%if population is mapping%}
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.genotypes.list_genotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id)}}">
-    {%if dataset is defined and dataset is mapping%}
-    {{dataset.Name}}
-    {%else%}
-    Genotypes
-    {%endif%}</a>
-  {%else%}
-  <a href="{{url_for('species.populations.genotypes.index')}}">Genotypes</a>
-  {%endif%}
+           species_id=species['SpeciesId'],
+           population_id=population['Id'])}}">
+    genotype
+  </a>
 </li>
-{%block lvl4_breadcrumbs%}{%endblock%}
+{%endblock%}
+
+
+{%block sidebarcontents%}
+{{display_sui_population_card(species, population)}}
 {%endblock%}
diff --git a/uploader/templates/genotypes/list-genotypes.html b/uploader/templates/genotypes/list-genotypes.html
index 0f074fd..a2b98c8 100644
--- a/uploader/templates/genotypes/list-genotypes.html
+++ b/uploader/templates/genotypes/list-genotypes.html
@@ -1,23 +1,10 @@
 {%extends "genotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Genotypes{%endblock%}
 
 {%block pagetitle%}Genotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="list-genotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.genotypes.list_genotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id)}}">List genotypes</a>
-</li>
-{%endblock%}
-
 {%block contents%}
 {{flash_all_messages()}}
 
@@ -143,7 +130,3 @@
     all the rest.</p>
 </div>
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/genotypes/list-markers.html b/uploader/templates/genotypes/list-markers.html
index a705ae3..22189c7 100644
--- a/uploader/templates/genotypes/list-markers.html
+++ b/uploader/templates/genotypes/list-markers.html
@@ -1,20 +1,18 @@
 {%extends "genotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "species/macro-display-species-card.html" import display_species_card%}
 
 {%block title%}Genotypes: List Markers{%endblock%}
 
 {%block pagetitle%}Genotypes: List Markers{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="list-markers"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.genotypes.list_markers',
-           species_id=species.SpeciesId,
-           population_id=population.Id)}}">List markers</a>
+           species_id=species['SpeciesId'],
+           population_id=population['Id'])}}">
+    markers
+  </a>
 </li>
 {%endblock%}
 
@@ -59,7 +57,7 @@
   <table class="table">
     <thead>
       <tr>
-        <th title="">#</th>
+        <th title="">Index</th>
         <th title="">Marker Name</th>
         <th title="Chromosome">Chr</th>
         <th title="Physical location of the marker in megabasepairs">
@@ -99,7 +97,3 @@
 </div>
 {%endif%}
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_species_card(species)}}
-{%endblock%}
diff --git a/uploader/templates/genotypes/view-dataset.html b/uploader/templates/genotypes/view-dataset.html
index e7ceb36..1c4eccf 100644
--- a/uploader/templates/genotypes/view-dataset.html
+++ b/uploader/templates/genotypes/view-dataset.html
@@ -1,21 +1,17 @@
 {%extends "genotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Genotypes: View Dataset{%endblock%}
 
 {%block pagetitle%}Genotypes: View Dataset{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="view-dataset"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.genotypes.view_dataset',
            species_id=species.SpeciesId,
            population_id=population.Id,
-           dataset_id=dataset.Id)}}">view dataset</a>
+           dataset_id=dataset.Id)}}">dataset</a>
 </li>
 {%endblock%}
 
@@ -55,7 +51,3 @@
 </div>
 
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/index.html b/uploader/templates/index.html
index aa1414e..6e9c777 100644
--- a/uploader/templates/index.html
+++ b/uploader/templates/index.html
@@ -1,107 +1,170 @@
 {%extends "base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-forms.html" import add_http_feature_flags%}
 
 {%block title%}Home{%endblock%}
 
 {%block pagetitle%}Home{%endblock%}
 
-{%block contents%}
-
-<div class="row">
-  {{flash_all_messages()}}
-  <div class="explainer">
-    <p>Welcome to the <strong>GeneNetwork Data Upload and Quality Control
-        System</strong>.</p>
-    <p>This tool helps you prepare and upload research data to GeneNetwork for
-      analysis.</p>
-
-    <h2 class="heading">Getting Started</h2>
-    <p>The sections below explain the features of the system. Review this guide
-      to learn how to use the system.</p>
-
-    {%block extrapageinfo%}{%endblock%}
-
-    <h3 class="subheading">Species</h3>
-
-    <p>GeneNetwork supports genetic studies across multiple species (e.g. mice
-      [Mus musculus], human [homo sapiens], rats [Rattus norvegicus], etc.) .
-      Here you can:</p>
-    <ul>
-      <li>View all species that are currently supported</li>
-      <li>Add new species not yet in the system</li>
-    </ul>
-
-    <h3 class="subheading">Populations</h3>
-
-    <p>A "population" refers to a specific subgroup within a species that you’re
-      studying (e.g., BXD mice). Here you can:</p>
-    <ul>
-      <li>View the populations that exist for a selected species</li>
-      <li>Add new populations of study for a selected species</li>
-    </ul>
-
-    <h3 class="subheading">Samples</h3>
+{%block extra_breadcrumbs%}{%endblock%}
 
-    <p>Manage individual specimens or cases used in your experiments. These
-      include:</p>
-
-    <ul>
-      <li>Experimental subjects</li>
-      <li>Data sources (e.g., tissue samples, clinical cases)</li>
-      <li>Strain means (instead of entering multiple BXD1 individuals, for
-        example, the mean would be entered for a single BXD1 strain)</li>
-    </ul>
-
-
-    <h3 class="subheading">Genotype Data</h3>
-
-    <p>Upload and review genetic markers and allele encodings for your
-      population. Key details:</p>
+{%block contents%}
 
-    <ul>
-      <li>Markers are species-level (e.g., mouse SNP databases).</li>
-      <li>Allele data is population-specific (tied to your experimental
-        samples).</li>
-    </ul>
+{%macro add_form_buttons()%}
+<div class="row form-buttons">
+  <div class="col">
+    <input type="submit"
+           class="btn btn-primary"
+           value="use selected species" />
+  </div>
+  <div class="col">
+    <a href="{{url_for('species.create_species', return_to='base.index')}}"
+       class="btn btn-outline-primary"
+       title="Add a new species to Genenetwork.">add a new Species</a>
+  </div>
+</div>
+{%endmacro%}
 
-    <p><strong>Requirement</strong>: Samples must already have been registered
-      in the system before uploading genotype data.</p>
+<div class="row">{{flash_all_messages()}}</div>
 
-    <h3 class="subheading">Phenotype Data</h3>
+{%if user_logged_in()%}
 
-    <p>Phenotypes are the visible traits or features of a living thing. For
-      example, phenotypes include:</p>
+<div class="row">
+  <ul class="nav nav-tabs" id="index-actions">
+    <li class="nav-item presentation">
+      <button class="nav-link active"
+              id="upload-data-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#upload-data-content"
+              type="button"
+              role="tab"
+              aria-controls="upload-data-content"
+              aria-selected="false">Upload Data</button></li>
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="publications-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#publications-content"
+              type="button"
+              role="tab"
+              aria-controls="publications-content"
+              aria-selected="true">Publications</button></li>
+  </ul>
+</div>
 
-    <ul>
-      <li>Weight</li>
-      <li>Height</li>
-      <li>Color (such as the color of fur or eyes)</li>
-    </ul>
+<div class="row">
+  <div class="tab-content" id="upload-data-tabs-content">
+    <div class="tab-pane fade show active"
+         id="upload-data-content"
+         role="tabpanel"
+         aria-labelledby="upload-data-content-tab">
+      <h2 class="heading">Species</h2>
+
+      <p>Select the species you want to work with.</p>
+
+      <form method="GET" action="{{url_for('base.index')}}" class="form-horizontal">
+        {{add_http_feature_flags()}}
+
+        {{add_form_buttons()}}
+
+        {%if species | length != 0%}
+        <div style="margin-top:1em;">
+          <table id="tbl-select-species" class="table compact stripe"
+                 data-species-list='{{species | tojson}}'>
+            <thead>
+              <tr>
+                <th></th>
+                <th>Species Name</th>
+              </tr>
+            </thead>
+
+            <tbody></tbody>
+          </table>
+        </div>
+
+        {%else%}
+
+        <label class="control-label" for="rdo-cant-find-species">
+          <input id="rdo-cant-find-species" type="radio" name="species_id"
+                 value="CREATE-SPECIES" />
+          There are no species to select from. Create the first one.</label>
+
+        <div class="col-sm-offset-10 col-sm-2">
+          <input type="submit"
+                 class="btn btn-primary col-sm-offset-1"
+                 value="continue" />
+        </div>
+
+        {%endif%}
+
+        {{add_form_buttons()}}
+
+      </form>
+    </div>
+
+    <div class="tab-pane fade"
+         id="publications-content"
+         role="tabpanel"
+         aria-labelledby="publications-content-tab">
+      <p>You can view, edit, and delete existing publications, as well as add
+        new ones, by clicking the button below.</p>
+
+      <a href="{{url_for('publications.index')}}"
+         title="Manage publications."
+         class="btn btn-primary">manage publications</a>
+    </div>
+  </div>
+</div>
 
-    <p>This part of the system will allow you to upload and manage the values
-      for different phenotypes from various samples in your studies.</p>
+{%else%}
 
-    <!--
+<div class="row">
+  <img src="/static/images/frontpage_banner.png"
+       alt="Banner image showing the process flow a user would follow." />
+</div>
 
-        <h3 class="subheading">Expression Data</h3>
+<div class="row">
+  <p>The GeneNetwork Uploader (gn-uploader) lets you easily add new data to the
+    GeneNetwork System. It automatically checks your data for quality and walks
+    you through fixing any issues before submission.</p>
+</div>
 
-    <p class="text-danger">
-      <span class="glyphicon glyphicon-exclamation-sign"></span>
-      <strong>TODO</strong>: Document this &hellip;</p>
+<div class="row">
+  <div class="col">
+      <a href="{{authserver_authorise_uri()}}"
+         title="Sign in to the system"
+         class="btn btn-primary">Sign in</a>
+  </div>
+</div>
+{%endif%}
 
-    <h3 class="subheading">Individual Data</h3>
+{%endblock%}
 
-    <p class="text-danger">
-      <span class="glyphicon glyphicon-exclamation-sign"></span>
-      <strong>TODO</strong>: Document this &hellip;</p>
 
-    <h3 class="subheading">RNA-Seq Data</h3>
 
-    <p class="text-danger">
-      <span class="glyphicon glyphicon-exclamation-sign"></span>
-      <strong>TODO</strong>: Document this &hellip;</p>
-  </div>
-  -->
+{%block sidebarcontents%}
+{%if view_under_construction%}
+<div class="row">
+  <p>The data in Genenetwork is related to one species or another. Use the form
+    provided to select from existing species, or click on the
+    "Create a New Species" button if you cannot find the species you want to
+    work with.</p>
 </div>
+<div class="row">
+  <form id="frm-quick-navigation">
+    <legend>Quick Navigation</legend>
+    <div class="form-group">
+      <label for="fqn-species-id">Species</label>
+      <select name="species_id">
+        <option value="">Select species</option>
+      </select>
+    </div>
+  </form>
+</div>
+{%endif%}
+{%endblock%}
+
 
+{%block javascript%}
+<script type="text/javascript" src="/static/js/species.js"></script>
 {%endblock%}
diff --git a/uploader/templates/jobs/sui-job-error.html b/uploader/templates/jobs/sui-job-error.html
deleted file mode 100644
index 1a839a6..0000000
--- a/uploader/templates/jobs/sui-job-error.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{%extends "sui-base.html"%}
-
-{%from "flash_messages.html" import flash_all_messages%}
-
-{%block title%}Background Jobs: Error{%endblock%}
-
-{%block pagetitle%}Background Jobs: Error{%endblock%}
-
-{%block contents%}
-
-<h1>Background Jobs: Error</h1>
-<p>Job <strong>{{job["job_id"]}}</strong> failed!</p>
-<p>The error details are in the "STDERR" section below.</p>
-
-<h2>STDERR</h2>
-<pre>{{job["stderr"]}}</pre>
-{%endblock%}
diff --git a/uploader/templates/jobs/sui-job-not-found.html b/uploader/templates/jobs/sui-job-not-found.html
deleted file mode 100644
index 96c8586..0000000
--- a/uploader/templates/jobs/sui-job-not-found.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{%extends "sui-base.html"%}
-
-{%from "flash_messages.html" import flash_all_messages%}
-
-{%block title%}Background Jobs{%endblock%}
-
-{%block pagetitle%}Background Jobs{%endblock%}
-
-{%block contents%}
-<p>Could not find job with ID: {{job_id}}</p>
-{%endblock%}
diff --git a/uploader/templates/jobs/sui-job-status.html b/uploader/templates/jobs/sui-job-status.html
deleted file mode 100644
index fc5e532..0000000
--- a/uploader/templates/jobs/sui-job-status.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{%extends "sui-base.html"%}
-
-{%from "flash_messages.html" import flash_all_messages%}
-
-{%block extrameta%}
-<meta http-equiv="refresh" content="5" />
-{%endblock%}
-
-{%block title%}Background Jobs{%endblock%}
-
-{%block pagetitle%}Background Jobs{%endblock%}
-
-{%block contents%}
-
-<p>Status: {{job["metadata"]["status"]}}</p>
-<p>Job Type: {{job["metadata"]["job-type"]}}</p>
-
-<h2>STDOUT</h2>
-<pre>{{job["stdout"]}}</pre>
-
-<h2>STDERR</h2>
-<pre>{{job["stderr"]}}</pre>
-
-{%endblock%}
diff --git a/uploader/templates/login.html b/uploader/templates/login.html
deleted file mode 100644
index e76c644..0000000
--- a/uploader/templates/login.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{%extends "index.html"%}
-
-{%block title%}Data Upload{%endblock%}
-
-{%block pagetitle%}log in{%endblock%}
-
-{%block extrapageinfo%}
-<p class="text-dark">
-  You <strong>need to
-    <a href="{{authserver_authorise_uri()}}"
-       title="Sign in to the system">sign in</a></strong> to use this system.</p>
-{%endblock%}
diff --git a/uploader/templates/phenotypes/add-phenotypes-base.html b/uploader/templates/phenotypes/add-phenotypes-base.html
index 9909c20..3207129 100644
--- a/uploader/templates/phenotypes/add-phenotypes-base.html
+++ b/uploader/templates/phenotypes/add-phenotypes-base.html
@@ -1,26 +1,13 @@
 {%extends "phenotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
 
 {%block title%}Phenotypes{%endblock%}
 
 {%block pagetitle%}Phenotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="add-phenotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">Add Phenotypes</a>
-</li>
-{%endblock%}
-
 {%block contents%}
+{{super()}}
 {{flash_all_messages()}}
 
 <div class="row">
@@ -42,8 +29,7 @@
 
     {%block frm_add_phenotypes_elements%}{%endblock%}
 
-    <fieldset id="fldset-publication-info">
-      <legend>Publication Information</legend>
+      <h4>Publication Information</h4>
       <input type="hidden" name="publication-id" id="txt-publication-id" />
       <span class="form-text text-muted">
         Select a publication for your data. <br />
@@ -57,7 +43,7 @@
       <table id="tbl-select-publication" class="table compact stripe">
         <thead>
           <tr>
-            <th>#</th>
+            <th>Index</th>
             <th>PubMed ID</th>
             <th>Title</th>
             <th>Authors</th>
@@ -66,7 +52,6 @@
 
         <tbody></tbody>
       </table>
-    </fieldset>
 
     <div class="form-group">
       <input type="submit"
@@ -83,6 +68,8 @@
 {%endblock%}
 
 
+
+
 {%block javascript%}
 <script type="text/javascript">
   $(function() {
@@ -97,7 +84,8 @@
                       if(pub.PubMed_ID) {
                           return `<a href="https://pubmed.ncbi.nlm.nih.gov/` +
                               `${pub.PubMed_ID}/" target="_blank" ` +
-                              `title="Link to publication on NCBI.">` +
+                              `title="Link to publication on NCBI. This will ` +
+                              `open in a new tab.">` +
                               `${pub.PubMed_ID}</a>`;
                       }
                       return "";
@@ -110,10 +98,7 @@
                       if(pub.Title) {
                           title = pub.Title
                       }
-                      return `<a href="/publications/view/${pub.Id}" ` +
-                          `target="_blank" ` +
-                          `title="Link to view publication details">` +
-                          `${title}</a>`;
+                      return title;
                   }
               },
               {
diff --git a/uploader/templates/phenotypes/add-phenotypes-raw-files.html b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
index 67b56e3..b1322b2 100644
--- a/uploader/templates/phenotypes/add-phenotypes-raw-files.html
+++ b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
@@ -1,7 +1,6 @@
 {%extends "phenotypes/add-phenotypes-base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
 {%from "phenotypes/macro-display-preview-table.html" import display_preview_table%}
 {%from "phenotypes/macro-display-resumable-elements.html" import display_resumable_elements%}
 
@@ -9,19 +8,6 @@
 
 {%block pagetitle%}Phenotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="add-phenotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">Add Phenotypes</a>
-</li>
-{%endblock%}
-
 {%block frm_add_phenotypes_documentation%}
 <p>This page will allow you to upload all the separate files that make up your
   phenotypes. Here, you will have to upload each separate file individually. If
@@ -35,8 +21,7 @@
 {%endblock%}
 
 {%block frm_add_phenotypes_elements%}
-<fieldset id="fldset-file-metadata">
-  <legend>File(s) Metadata</legend>
+  <h4>File(s) Metadata</h4>
   <div class="form-group">
     <label for="txt-file-separator" class="form-label">File Separator</label>
     <div class="input-group">
@@ -103,12 +88,9 @@
       <a href="#docs-file-na" title="Documentation for no-value fields">
         documentation for more information</a>.</span>
   </div>
-</fieldset>
 
-<fieldset id="fldset-files">
   <legend>Data File(s)</legend>
 
-  <fieldset id="fldset-descriptions-file">
     <div class="form-group">
       <div class="form-check">
         <input id="chk-phenotype-descriptions-transposed"
@@ -159,10 +141,8 @@
       {{display_preview_table(
       "tbl-preview-pheno-desc", "phenotype descriptions")}}
     </div>
-  </fieldset>
-
 
-  <fieldset id="fldset-data-file">
+    
     <div class="form-group">
       <div class="form-check">
         <input id="chk-phenotype-data-transposed"
@@ -210,11 +190,9 @@
         on the expected format for the file provided here.</p>')}}
       {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}}
     </div>
-  </fieldset>
 
   
   {%if population.Family in families_with_se_and_n%}
-  <fieldset id="fldset-se-file">
     <div class="form-group">
       <div class="form-check">
         <input id="chk-phenotype-se-transposed"
@@ -261,10 +239,8 @@
 
       {{display_preview_table("tbl-preview-pheno-se", "standard errors")}}
     </div>
-  </fieldset>
 
 
-  <fieldset id="fldset-n-file">
     <div class="form-group">
       <div class="form-check">
         <input id="chk-phenotype-n-transposed"
@@ -311,8 +287,6 @@
 
       {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}}
     </div>
-  </fieldset>
-</fieldset>
 {%endif%}
 {%endblock%}
 
@@ -447,10 +421,6 @@
 
 {%endblock%}
 
-{%block sidebarcontents%}
-{{display_pheno_dataset_card(species, population, dataset)}}
-{%endblock%}
-
 
 {%block more_javascript%}
 <script src="{{url_for('base.node_modules',
@@ -495,7 +465,7 @@
               .map((field) => {
                   var value = field.trim();
                   if(navalues.includes(value)) {
-                      return "⋘NUL⋙";
+                      return "[NO-VALUE]";
                   }
                   return value;
               })
diff --git a/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html b/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html
index 898fc0c..4afd6ab 100644
--- a/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html
+++ b/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html
@@ -1,25 +1,11 @@
 {%extends "phenotypes/add-phenotypes-base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
 
 {%block title%}Phenotypes{%endblock%}
 
 {%block pagetitle%}Phenotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="add-phenotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">Add Phenotypes</a>
-</li>
-{%endblock%}
-
 {%block frm_add_phenotypes_documentation%}
 <p>Select the zip file bundle containing information on the phenotypes you
   wish to upload, then click the "Upload Phenotypes" button below to
@@ -201,7 +187,3 @@
     <em>phenotypes × individuals</em>.</p>
 </div>
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_pheno_dataset_card(species, population, dataset)}}
-{%endblock%}
diff --git a/uploader/templates/phenotypes/base.html b/uploader/templates/phenotypes/base.html
index adbc012..5959422 100644
--- a/uploader/templates/phenotypes/base.html
+++ b/uploader/templates/phenotypes/base.html
@@ -1,19 +1,27 @@
 {%extends "populations/base.html"%}
+{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_sui_pheno_dataset_card%}
 
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="phenotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  {%if dataset is mapping%}
+{%block breadcrumbs%}
+{{super()}}
+{%if dataset%}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.phenotypes.view_dataset',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">{{dataset.Name}}</a>
-  {%else%}
-  <a href="{{url_for('species.populations.phenotypes.index')}}">Phenotypes</a>
-  {%endif%}
+           species_id=species['SpeciesId'],
+           population_id=population['Id'],
+           dataset_id=dataset['Id'])}}">
+    {{dataset["Name"]}}
+  </a>
 </li>
-{%block lvl4_breadcrumbs%}{%endblock%}
+{%endif%}
+{%endblock%}
+
+{%block contents%}
+<div class="row">
+  <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2>
+</div>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+{{display_sui_pheno_dataset_card(species, population, dataset)}}
 {%endblock%}
diff --git a/uploader/templates/phenotypes/confirm-delete-phenotypes.html b/uploader/templates/phenotypes/confirm-delete-phenotypes.html
new file mode 100644
index 0000000..3cf6e65
--- /dev/null
+++ b/uploader/templates/phenotypes/confirm-delete-phenotypes.html
@@ -0,0 +1,196 @@
+{%extends "phenotypes/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Delete Phenotypes{%endblock%}
+
+{%block lvl4_breadcrumbs%}
+<li {%if activelink=="view-dataset"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.populations.phenotypes.view_dataset',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id)}}">View</a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row"><h2>Delete Phenotypes</h2></div>
+
+{%if phenotypes | length > 0%}
+<div class="row">
+  <p>You have requested to delete the following phenotypes:</p>
+</div>
+
+<div class="row">
+  <div class="col">
+    <a id="btn-select-all-phenotypes"
+       href="#"
+       class="btn btn-info"
+       title="Select all phenotypes">select all</a>
+  </div>
+  <div class="col">
+    <a id="btn-deselect-all-phenotypes"
+       href="#"
+       class="btn btn-warning"
+       title="Deselect all phenotypes">deselect all</a>
+  </div>
+</div>
+
+<div class="row">
+  <table id="tbl-delete-phenotypes" class="table">
+    <thead>
+      <tr>
+        <th>Index</th>
+        <th>Record ID</th>
+        <th>Description</th>
+      </tr>
+    </thead>
+    <tbody>
+      {%for phenotype in phenotypes%}
+      <tr>
+        <td>
+          <input id="chk-xref-id-{{phenotype.xref_id}}"
+                 name="xref_ids"
+                 type="checkbox"
+                 value="{{phenotype.xref_id}}"
+                 class="chk-row-select" />
+        </td>
+        <td>{{phenotype.xref_id}}</td>
+        <td>{{phenotype.Post_publication_description or
+          phenotype.Pre_publication_description or
+          phenotype.original_description}}</td>
+      </tr>
+      {%endfor%}
+    </tbody>
+  </table>
+</div>
+
+<div class="row">
+  <form id="frm-delete-phenotypes-selected"
+        method="POST"
+        action="{{url_for('species.populations.phenotypes.delete_phenotypes',
+                species_id=species.SpeciesId,
+                population_id=population.Id,
+                dataset_id=dataset.Id)}}">
+    <div class="row">
+      <div class="col">
+        <input class="btn btn-info"
+               type="submit"
+               title="Cancel delete and return to dataset page."
+               name="action"
+               value="cancel" /></div>
+      <div class="col">
+        <input id="btn-delete-phenotypes-selected"
+               class="btn btn-danger"
+               type="submit"
+               title="Delete the selected phenotypes from this dataset."
+               name="action"
+               value="delete" />
+      </div>
+    </div>
+  </form>
+</div>
+{%else%}
+<div class="row">
+  <p>You did not select any phenotypes to delete. Delete everything?</p>
+</div>
+
+<div class="row">
+  <form id="frm-delete-phenotypes-all"
+        method="POST"
+        action="{{url_for('species.populations.phenotypes.delete_phenotypes',
+                species_id=species.SpeciesId,
+                population_id=population.Id,
+                dataset_id=dataset.Id)}}">
+    <div class="form-check">
+      <input class="form-check-input"
+             type="checkbox"
+             name="confirm_delete_all_phenotypes"
+             id="chk-confirm-delete-all-phenotypes" />
+      <label class="form-check-label"
+             for="chk-confirm-delete-all-phenotypes">
+        delete all phenotypes?</label>
+    </div>
+
+    <div class="row">
+      <div class="col">
+        <input class="btn btn-info"
+               type="submit"
+               title="Cancel delete and return to dataset page."
+               name="action"
+               value="cancel" /></div>
+      <div class="col">
+        <input class="btn btn-danger"
+               type="submit"
+               title="Delete all phenotypes in this dataset."
+               name="action"
+               value="delete" />
+      </div>
+    </div>
+  </form>
+</div>
+{%endif%}
+
+{%endblock%}
+
+{%block javascript%}
+<script type="text/javascript">
+  $(function() {
+      var dt = buildDataTable(
+          "#tbl-delete-phenotypes",
+          data=[],
+          columns=[],
+          userSettings={
+              responsive: true,
+              select: {
+                  style: "os",
+                  info: false
+              },
+              initComplete: function(setting, json) {
+                  var api = this.api();
+                  api.rows().select();
+                  api.rows({selected: true}).nodes().each((node, index) => {
+                      setRowChecked(node);
+                  });
+              }
+          });
+
+      $("#btn-select-all-phenotypes").on("click", function(event) {
+          dt.selectAll();
+      });
+
+      $("#btn-deselect-all-phenotypes").on("click", function(event) {
+          dt.deselectAll();
+      });
+
+      $("#btn-delete-phenotypes-selected").on("click", function(event) {
+          event.preventDefault();
+          form = $("#frm-delete-phenotypes-selected");
+          form.find(".dynamically-added-element").remove();
+          dt.rows({selected: true}).nodes().each(function(node, index) {
+              var xref_id = $(node)
+                  .find('input[type="checkbox"]:checked')
+                  .val();
+              var chk = $('<input type="checkbox">');
+              chk.attr("class", "dynamically-added-element");
+              chk.attr("value", xref_id);
+              chk.attr("name", "xref_ids");
+              chk.attr("style", "display: none");
+              chk.prop("checked", true);
+              form.append(chk);
+          });
+          form.append(
+              $('<input type="hidden" name="action" value="delete" />'));
+          form.submit();
+      })
+  });
+</script>
+{%endblock%}
+
diff --git a/uploader/templates/phenotypes/create-dataset.html b/uploader/templates/phenotypes/create-dataset.html
index 19a2b34..6eced05 100644
--- a/uploader/templates/phenotypes/create-dataset.html
+++ b/uploader/templates/phenotypes/create-dataset.html
@@ -48,7 +48,8 @@
              {%else%}
              class="form-control"
              {%endif%}
-             required="required" />
+             required="required"
+             disabled="disabled" />
       <small class="form-text text-muted">
         <p>A short representative name for the dataset.</p>
         <p>Recommended: Use the population name and append "Publish" at the end.
@@ -66,7 +67,7 @@
       <input id="txt-dataset-fullname"
              name="dataset-fullname"
              type="text"
-             value="{{original_formdata.get('dataset-fullname', '')}}"
+             value="{{original_formdata.get('dataset-fullname', '') or population.Name + ' Phenotypes'}}"
              {%if errors["dataset-fullname"] is defined%}
              class="form-control danger"
              {%else%}
diff --git a/uploader/templates/phenotypes/edit-phenotype.html b/uploader/templates/phenotypes/edit-phenotype.html
index 115d6af..1b3ee9d 100644
--- a/uploader/templates/phenotypes/edit-phenotype.html
+++ b/uploader/templates/phenotypes/edit-phenotype.html
@@ -142,7 +142,7 @@
       <table class="table table-striped table-responsive table-form-table">
         <thead style="position: sticky; top: 0;">
           <tr>
-            <th>#</th>
+            <th>Index</th>
             <th>Sample</th>
             <th>Value</th>
             {%if population.Family in families_with_se_and_n%}
diff --git a/uploader/templates/phenotypes/job-status.html b/uploader/templates/phenotypes/job-status.html
index 257f726..951907f 100644
--- a/uploader/templates/phenotypes/job-status.html
+++ b/uploader/templates/phenotypes/job-status.html
@@ -2,7 +2,6 @@
 {%from "cli-output.html" import cli_output%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
 
 {%block extrameta%}
 {%if job and job.status not in ("success", "completed:success", "error", "completed:error")%}
@@ -14,23 +13,13 @@
 
 {%block pagetitle%}Phenotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="add-phenotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">View Datasets</a>
-</li>
-{%endblock%}
-
 {%block contents%}
 
 {%if job%}
-<h4 class="subheading">Progress</h4>
+<div class="row">
+  <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2>
+  <h3 class="subheading">upload progress</h3>
+</div>
 <div class="row" style="overflow:scroll;">
   <p><strong>Process Status:</strong> {{job.status}}</p>
   {%if metadata%}
@@ -63,10 +52,10 @@
   <p>
     {%if errors | length == 0%}
     <a href="{{url_for('species.populations.phenotypes.review_job_data',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id,
-           job_id=job_id)}}"
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             dataset_id=dataset.Id,
+             job_id=job_id)}}"
        class="btn btn-primary"
        title="Continue to process data">Continue</a>
     {%else%}
@@ -80,14 +69,29 @@
   {%endif%}
 </div>
 
-<h4 class="subheading">Errors</h4>
+<h3 class="subheading">upload errors</h3>
+{%if errors | length == 0 %}
 <div class="row" style="max-height: 20em; overflow: scroll;">
-  {%if errors | length == 0 %}
   <p class="text-info">
     <span class="glyphicon glyphicon-info-sign"></span>
     No errors found so far
   </p>
-  {%else%}
+</div>
+{%else%}
+{%if errors | length > 0%}
+<div class="row">
+  <div class="col">
+    <a href="{{url_for('species.populations.phenotypes.download_errors',
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             dataset_id=dataset.Id,
+             job_id=job_id)}}"
+       class="btn btn-info"
+       title="Download the errors as a CSV file.">download errors CSV</a>
+  </div>
+</div>
+{%endif%}
+<div class="row" style="max-height: 20em; overflow: scroll;">
   <table class="table table-responsive">
     <thead style="position: sticky; top: 0; background: white;">
       <tr>
@@ -122,7 +126,8 @@
       {%endfor%}
     </tbody>
   </table>
-  {%endif%}
+</div>
+{%endif%}
 </div>
 
 <div class="row">
@@ -149,7 +154,3 @@
 </div>
 {%endif%}
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_pheno_dataset_card(species, population, dataset)}}
-{%endblock%}
diff --git a/uploader/templates/phenotypes/load-phenotypes-success.html b/uploader/templates/phenotypes/load-phenotypes-success.html
index 645be16..1fb0e61 100644
--- a/uploader/templates/phenotypes/load-phenotypes-success.html
+++ b/uploader/templates/phenotypes/load-phenotypes-success.html
@@ -1,26 +1,14 @@
 {%extends "phenotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
 
 {%block title%}Phenotypes{%endblock%}
 
 {%block pagetitle%}Phenotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="load-phenotypes-success"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">Add Phenotypes</a>
-</li>
-{%endblock%}
-
 {%block contents%}
+{{super()}}
+
 <div class="row">
   <p>You have successfully loaded
     <!-- maybe indicate the number of phenotypes here? -->your
@@ -34,9 +22,5 @@
 </div>
 {%endblock%}
 
-{%block sidebarcontents%}
-{{display_pheno_dataset_card(species, population, dataset)}}
-{%endblock%}
-
 
 {%block more_javascript%}{%endblock%}
diff --git a/uploader/templates/phenotypes/macro-display-preview-table.html b/uploader/templates/phenotypes/macro-display-preview-table.html
index 5a4c422..6dffe9f 100644
--- a/uploader/templates/phenotypes/macro-display-preview-table.html
+++ b/uploader/templates/phenotypes/macro-display-preview-table.html
@@ -1,19 +1,11 @@
 {%macro display_preview_table(tableid, filetype)%}
-<div class="card">
-  <div class="card-body">
-    <h5 class="card-title">{{filetype | title}}: File Preview</h5>
-    <div class="card-text" style="overflow: scroll;">
-      <table id="{{tableid}}" class="table table-condensed table-responsive">
-        <thead>
-          <tr>
-          </tr>
-        <tbody>
-          <tr>
-            <td class="data-row-template text-info"></td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-  </div>
+<div class="table-responsive"
+     style="max-width:39.2em;border-radius:5px;border: solid 1px;overflow-x: scroll;">
+  <h5>{{filetype | title}}: File Preview</h5>
+  <table id="{{tableid}}" class="table">
+    <thead><tr></tr></thead>
+
+    <tbody></tbody>
+  </table>
 </div>
 {%endmacro%}
diff --git a/uploader/templates/phenotypes/review-job-data.html b/uploader/templates/phenotypes/review-job-data.html
index 859df74..c8355b2 100644
--- a/uploader/templates/phenotypes/review-job-data.html
+++ b/uploader/templates/phenotypes/review-job-data.html
@@ -104,10 +104,6 @@
 {%endif%}
 {%endblock%}
 
-{%block sidebarcontents%}
-{{display_pheno_dataset_card(species, population, dataset)}}
-{%endblock%}
-
 
 {%block javascript%}
 <script type="text/javascript">
diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-base.html b/uploader/templates/phenotypes/sui-add-phenotypes-base.html
deleted file mode 100644
index 1e71267..0000000
--- a/uploader/templates/phenotypes/sui-add-phenotypes-base.html
+++ /dev/null
@@ -1,155 +0,0 @@
-{%extends "phenotypes/sui-base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-table-pagination.html" import table_pagination%}
-
-{%block title%}Phenotypes{%endblock%}
-
-{%block pagetitle%}Phenotypes{%endblock%}
-
-{%block contents%}
-{{super()}}
-{{flash_all_messages()}}
-
-<div class="row">
-  <form id="frm-add-phenotypes"
-        method="POST"
-        enctype="multipart/form-data"
-        action="{{url_for('species.populations.phenotypes.add_phenotypes',
-                species_id=species.SpeciesId,
-                population_id=population.Id,
-                dataset_id=dataset.Id,
-                use_bundle=use_bundle)}}"
-        data-resumable-target="{{url_for('files.resumable_upload_post')}}">
-    <legend>Add New Phenotypes</legend>
-
-    <div class="form-text help-block">
-      {%block frm_add_phenotypes_documentation%}{%endblock%}
-      <p><strong class="text-warning">This will not update any existing phenotypes!</strong></p>
-    </div>
-
-    {%block frm_add_phenotypes_elements%}{%endblock%}
-
-    <fieldset id="fldset-publication-info">
-      <legend>Publication Information</legend>
-      <input type="hidden" name="publication-id" id="txt-publication-id" />
-      <span class="form-text text-muted">
-        Select a publication for your data. <br />
-        Can't find a publication you can use? Go ahead and
-        <a href="{{url_for(
-                 'publications.create_publication',
-                 return_to='species.populations.phenotypes.add_phenotypes',
-                 species_id=species.SpeciesId,
-                 population_id=population.Id,
-                 dataset_id=dataset.Id)}}">create a new publication</a>.</span>
-      <table id="tbl-select-publication" class="table compact stripe">
-        <thead>
-          <tr>
-            <th>#</th>
-            <th>PubMed ID</th>
-            <th>Title</th>
-            <th>Authors</th>
-          </tr>
-        </thead>
-
-        <tbody></tbody>
-      </table>
-    </fieldset>
-
-    <div class="form-group">
-      <input type="submit"
-             value="upload phenotypes"
-             class="btn btn-primary" />
-    </div>
-  </form>
-</div>
-
-<div class="row">
-  {%block page_documentation%}{%endblock%}
-</div>
-
-{%endblock%}
-
-
-
-
-{%block javascript%}
-<script type="text/javascript">
-  $(function() {
-      var publicationsDataTable = buildDataTable(
-          "#tbl-select-publication",
-          [],
-          [
-              {data: "index"},
-              {
-                  searchable: true,
-                  data: (pub) => {
-                      if(pub.PubMed_ID) {
-                          return `<a href="https://pubmed.ncbi.nlm.nih.gov/` +
-                              `${pub.PubMed_ID}/" target="_blank" ` +
-                              `title="Link to publication on NCBI.">` +
-                              `${pub.PubMed_ID}</a>`;
-                      }
-                      return "";
-                  }
-              },
-              {
-                  searchable: true,
-                  data: (pub) => {
-                      var title = "⸻";
-                      if(pub.Title) {
-                          title = pub.Title
-                      }
-                      return `<a href="/publications/view/${pub.Id}" ` +
-                          `target="_blank" ` +
-                          `title="Link to view publication details">` +
-                          `${title}</a>`;
-                  }
-              },
-              {
-                  searchable: true,
-                  data: (pub) => {
-                      authors = pub.Authors.split(",").map(
-                          (item) => {return item.trim();});
-                      if(authors.length > 1) {
-                          return authors[0] + ", et. al.";
-                      }
-                      return authors[0];
-                  }
-              }
-          ],
-          {
-              serverSide: true,
-              ajax: {
-                  url: "/publications/list",
-                  dataSrc: "publications"
-              },
-              select: "single",
-              paging: true,
-              scrollY: 700,
-              deferRender: true,
-              scroller: true,
-              scrollCollapse: true,
-              layout: {
-                  topStart: "info",
-                  topEnd: "search"
-              }
-          });
-      publicationsDataTable.on("select", (event, datatable, type, indexes) => {
-          indexes.forEach((element, index, thearray) => {
-              let row = datatable.row(element).node();
-              console.debug(datatable.row(element).data());
-              $("#frm-add-phenotypes #txt-publication-id").val(
-                  datatable.row(element).data().Id);
-          });
-      });
-      publicationsDataTable.on("deselect", (event, datatable, type, indexes) => {
-          indexes.forEach((element, index, thearray) => {
-              let row = datatable.row(element).node();
-              $("#frm-add-phenotypes #txt-publication-id").val(null);
-          });
-      });
-  });
-</script>
-
-{%block more_javascript%}{%endblock%}
-{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html b/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html
deleted file mode 100644
index 6038617..0000000
--- a/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html
+++ /dev/null
@@ -1,829 +0,0 @@
-{%extends "phenotypes/sui-add-phenotypes-base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-preview-table.html" import display_preview_table%}
-{%from "phenotypes/macro-display-resumable-elements.html" import display_resumable_elements%}
-
-{%block title%}Phenotypes{%endblock%}
-
-{%block pagetitle%}Phenotypes{%endblock%}
-
-{%block frm_add_phenotypes_documentation%}
-<p>This page will allow you to upload all the separate files that make up your
-  phenotypes. Here, you will have to upload each separate file individually. If
-  you want instead to upload all your files as a single ZIP file,
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id,
-           use_bundle=true)}}"
-     title="">click here</a>.</p>
-{%endblock%}
-
-{%block frm_add_phenotypes_elements%}
-<fieldset id="fldset-file-metadata">
-  <legend>File(s) Metadata</legend>
-  <div class="form-group">
-    <label for="txt-file-separator" class="form-label">File Separator</label>
-    <div class="input-group">
-      <input id="txt-file-separator"
-             name="file-separator"
-             type="text"
-             value="&#9;"
-             class="form-control"
-             maxlength="1" />
-      <span class="input-group-btn">
-        <button id="btn-reset-file-separator" class="btn btn-info">Reset Default</button>
-      </span>
-    </div>
-    <span class="form-text text-muted">
-      Provide the character that separates the fields in your file(s). It should
-      be the same character for all files (if more than one is provided).<br />
-      A tab character will be assumed if you leave this field blank. See
-      <a href="#docs-file-separator"
-         title="Documentation for file-separator characters">
-        documentation for more information</a>.
-    </span>
-  </div>
-
-  <div class="form-group">
-    <label for="txt-file-comment-character" class="form-label">File Comment-Character</label>
-    <div class="input-group">
-      <input id="txt-file-comment-character"
-             name="file-comment-character"
-             type="text"
-             value="#"
-             class="form-control"
-             maxlength="1" />
-      <span class="input-group-btn">
-        <button id="btn-reset-file-comment-character" class="btn btn-info">
-          Reset Default</button>
-      </span>
-    </div>
-    <span class="form-text text-muted">
-      This specifies that lines that begin with the character provided will be
-      considered comment lines and ignored in their entirety. See
-      <a href="#docs-file-comment-character"
-         title="Documentation for comment characters">
-        documentation for more information</a>.
-    </span>
-  </div>
-
-  <div class="form-group">
-    <label for="txt-file-na" class="form-label">File "No-Value" Indicators</label>
-    <div class="input-group">
-      <input id="txt-file-na"
-             name="file-na"
-             type="text"
-             value="- NA N/A"
-             class="form-control" />
-      <span class="input-group-btn">
-        <button id="btn-reset-file-na" class="btn btn-info">Reset Default</button>
-      </span>
-    </div>
-    <span class="form-text text-muted">
-      This specifies strings in your file indicate that there is no value for a
-      particular cell (a cell is where a column and row intersect). Provide a
-      space-separated list of strings if you have more than one way of
-      indicating no values. See
-      <a href="#docs-file-na" title="Documentation for no-value fields">
-        documentation for more information</a>.</span>
-  </div>
-</fieldset>
-
-<fieldset id="fldset-files">
-  <legend>Data File(s)</legend>
-
-  <fieldset id="fldset-descriptions-file">
-    <div class="form-group">
-      <div class="form-check">
-        <input id="chk-phenotype-descriptions-transposed"
-               name="phenotype-descriptions-transposed"
-               type="checkbox"
-               class="form-check-input"
-               style="border: solid #8EABF0" />
-        <label for="chk-phenotype-descriptions-transposed"
-               class="form-check-label">
-          Description file transposed?</label>
-      </div>
-
-      <div class="non-resumable-elements">
-        <label for="finput-phenotype-descriptions" class="form-label">
-          Phenotype Descriptions</label>
-        <input id="finput-phenotype-descriptions"
-               name="phenotype-descriptions"
-               class="form-control"
-               type="file"
-               data-preview-table="tbl-preview-pheno-desc"
-               required="required"  />
-        <span class="form-text text-muted">
-          Provide a file that contains only the phenotype descriptions,
-          <a href="#docs-file-phenotype-description"
-             title="Documentation of the phenotype data file format.">
-            the documentation for the expected format of the file</a>.</span>
-      </div>
-      {{display_resumable_elements(
-      "resumable-phenotype-descriptions",
-      "phenotype descriptions",
-      '<p>Drag and drop the CSV file that contains the descriptions of your
-        phenotypes here.</p>
-
-      <p>The CSV file should be a matrix of
-        <strong>phenotypes × descriptions</strong> i.e. The first column
-        contains the phenotype names/identifiers whereas the first row is a list
-        of metadata fields like, "description", "units", etc.</p>
-
-      <p>If the format is transposed (i.e.
-        <strong>descriptions × phenotypes</strong>) select the checkbox above.
-      </p>
-
-      <p>Please see the
-        <a href="#docs-file-phenotype-description"
-           title="Documentation of the phenotype data file format.">
-          "Phenotypes Descriptions" documentation</a> section below for more
-        information on the expected format of the file provided here.</p>')}}
-      {{display_preview_table(
-      "tbl-preview-pheno-desc", "phenotype descriptions")}}
-    </div>
-  </fieldset>
-
-
-  <fieldset id="fldset-data-file">
-    <div class="form-group">
-      <div class="form-check">
-        <input id="chk-phenotype-data-transposed"
-               name="phenotype-data-transposed"
-               type="checkbox"
-               class="form-check-input"
-               style="border: solid #8EABF0" />
-        <label for="chk-phenotype-data-transposed" class="form-check-label">
-          Data file transposed?</label>
-      </div>
-
-      <div class="non-resumable-elements">
-        <label for="finput-phenotype-data" class="form-label">Phenotype Data</label>
-        <input id="finput-phenotype-data"
-               name="phenotype-data"
-               class="form-control"
-               type="file"
-               data-preview-table="tbl-preview-pheno-data"
-               required="required"  />
-        <span class="form-text text-muted">
-          Provide a file that contains only the phenotype data. See
-          <a href="#docs-file-phenotype-data"
-             title="Documentation of the phenotype data file format.">
-            the documentation for the expected format of the file</a>.</span>
-      </div>
-
-      {{display_resumable_elements(
-      "resumable-phenotype-data",
-      "phenotype data",
-      '<p>Drag and drop a CSV file that contains the phenotypes numerical data
-        here. You can click the "Browse" button (below and to the right) to
-        select the file from your computer.</p>
-
-      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
-        i.e. The first column contains the samples identifiers while the first
-        row is the list of phenotypes identifiers occurring in the phenotypes
-        descriptions file.</p>
-
-      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
-        select the checkbox above.</p>
-      <p>Please see the
-        <a href="#docs-file-phenotype-data"
-           title="Documentation of the phenotype data file format.">
-          "Phenotypes Data" documentation</a> section below for more information
-        on the expected format for the file provided here.</p>')}}
-      {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}}
-    </div>
-  </fieldset>
-
-  
-  {%if population.Family in families_with_se_and_n%}
-  <fieldset id="fldset-se-file">
-    <div class="form-group">
-      <div class="form-check">
-        <input id="chk-phenotype-se-transposed"
-               name="phenotype-se-transposed"
-               type="checkbox"
-               class="form-check-input"
-               style="border: solid #8EABF0" />
-        <label for="chk-phenotype-se-transposed" class="form-check-label">
-          Standard-Errors file transposed?</label>
-      </div>
-      <div class="group non-resumable-elements">
-        <label for="finput-phenotype-se" class="form-label">Phenotype: Standard Errors</label>
-        <input id="finput-phenotype-se"
-               name="phenotype-se"
-               class="form-control"
-               type="file"
-               data-preview-table="tbl-preview-pheno-se"
-               required="required"  />
-        <span class="form-text text-muted">
-          Provide a file that contains only the standard errors for the phenotypes,
-          computed from the data above.</span>
-      </div>
-
-      {{display_resumable_elements(
-      "resumable-phenotype-se",
-      "standard errors",
-      '<p>Drag and drop a CSV file that contains the phenotypes standard-errors
-        data here. You can click the "Browse" button (below and to the right) to
-        select the file from your computer.</p>
-
-      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
-        i.e. The first column contains the samples identifiers while the first
-        row is the list of phenotypes identifiers occurring in the phenotypes
-        descriptions file.</p>
-
-      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
-        select the checkbox above.</p>
-
-      <p>Please see the
-        <a href="#docs-file-phenotype-se"
-           title="Documentation of the phenotype data file format.">
-          "Phenotypes Data" documentation</a> section below for more information
-        on the expected format of the file provided here.</p>')}}
-
-      {{display_preview_table("tbl-preview-pheno-se", "standard errors")}}
-    </div>
-  </fieldset>
-
-
-  <fieldset id="fldset-n-file">
-    <div class="form-group">
-      <div class="form-check">
-        <input id="chk-phenotype-n-transposed"
-               name="phenotype-n-transposed"
-               type="checkbox"
-               class="form-check-input"
-               style="border: solid #8EABF0" />
-        <label for="chk-phenotype-n-transposed" class="form-check-label">
-          Counts file transposed?</label>
-      </div>
-      <div class="non-resumable-elements">
-        <label for="finput-phenotype-n" class="form-label">Phenotype: Number of Samples/Individuals</label>
-        <input id="finput-phenotype-n"
-               name="phenotype-n"
-               class="form-control"
-               type="file"
-               data-preview-table="tbl-preview-pheno-n"
-               required="required"  />
-        <span class="form-text text-muted">
-          Provide a file that contains only the number of samples/individuals used in
-          the computation of the standard errors above.</span>
-      </div>
-
-      {{display_resumable_elements(
-      "resumable-phenotype-n",
-      "number of samples/individuals",
-      '<p>Drag and drop a CSV file that contains the samples\' phenotypes counts
-        data here. You can click the "Browse" button (below and to the right) to
-        select the file from your computer.</p>
-
-      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
-        i.e. The first column contains the samples identifiers while the first
-        row is the list of phenotypes identifiers occurring in the phenotypes
-        descriptions file.</p>
-
-      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
-        select the checkbox above.</p>
-
-      <p>Please see the
-        <a href="#docs-file-phenotype-se"
-           title="Documentation of the phenotype data file format.">
-          "Phenotypes Data" documentation</a> section below for more information
-        on the expected format of the file provided here.</p>')}}
-
-      {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}}
-    </div>
-  </fieldset>
-</fieldset>
-{%endif%}
-{%endblock%}
-
-
-{%block page_documentation%}
-<div class="row">
-  <h2 class="heading" id="docs-help">Help</h2>
-  <h3 class="subheading">Common Features</h3>
-  <p>The following are the common expectations for <strong>ALL</strong> the
-    files provided in the form above:
-    <ul>
-      <li>The file <strong>MUST</strong> be character-separated values (CSV)
-        text file</li>
-      <li>The first row in the file <strong>MUST</strong> be a heading row, and
-        will be composed of the list identifiers for all of
-        samples/individuals/cases involved in your study.</li>
-      <li>The first column of data in the file <strong>MUST</strong> be the
-        identifiers for all of the phenotypes you wish to upload.</li>
-    </ul>
-  </p>
-
-  <p>If you do not specify the separator character, then we will assume a
-    <strong>TAB</strong> character was used as your separator.</p>
-
-  <p>We also assume you might include comments lines in your files. In that
-    case, if you do not specify what character denotes that a line in your files
-    is a comment line, we will assume the <strong>#</strong> character.<br />
-    A comment <strong>MUST ALWAYS</strong> begin at the start of the line marked
-    with the comment character specified.</p>
-
-  <h3 class="subheading" id="docs-file-metadata">File Metadata</h3>
-  <p>We request some details about your files to help us parse and process the
-    files correctly. The details we collect are:</p>
-  <dl>
-    <dt id="docs-file-separator">File separator</dt>
-    <dd>The files you provide should be character-separated value (CSV) files.
-      We need to know what character you used to separate the values in your
-      file. Some common ones are the Tab character, the comma, etc.<br />
-      Providing that information makes it possible for the system to parse and
-      process your files correctly.<br>
-      <strong>NOTE:</strong> All the files you upload MUST use the same
-      separator.</dd>
-
-    <dt id="docs-file-comment-character">Comment character</dt>
-    <dd>We support use of comment lines in your files. We only support one type
-      of comment style, the <em>line comment</em>.<br />
-      This mean the comment begins at the start of the line, and the end of that
-      line indicates the end of that comment. If you have a really long comment,
-      then you need to break it across multiple lines, marking each line a
-      comment line.<br />
-      The "comment character" is the character at the start of the line that
-      indicates that the line is a line comment.</dd>
-
-    <dt id="docs-file-na">No-Value indicator(s)</dt>
-    <dd>Data in the real world is messy, and in some cases, entirely absent. You
-      need to indicate, in your files, that a particular field did not have a
-      value, and once you do that, you then need to let the system know how you
-      mark such fields. Common ways of indicating "empty values" are, leaving
-      the field blank, using a character such as '-', or using strings like
-      "NA", "N/A", "NULL", etc.<br />
-      Providing this information will help with parsing and processing such
-      no-value fields the correct way.</dd>
-  </dl>
-
-  <h3 class="subheading" id="docs-file-phenotype-description">
-    file: Phenotypes Descriptions</h3>
-  <p>The data in this file is a matrix of <em>phenotypes × metadata-fields</em>.
-    Please note we use the term "metadata-fields" above loosely, due to lack of
-    a good word for this.</p>
-  <p>The file <strong>MUST</strong> have columns in this order:
-    <dl>
-      <dt>Phenotype Identifiers</dt>
-      <dd>These are the names/identifiers for your phenotypes. These
-        names/identifiers are the same ones you will have in all the other files you are
-        uploading.</dd>
-
-      <dt>Descriptions</dt>
-      <dd>Each phenotype will need a description. Good description are necessary
-        to inform other people of what the data is about. Good description are
-        hard to construct, so we provide
-        <a href="https://info.genenetwork.org/faq.php#q-22"
-           title="How to write phenotype descriptions">
-          advice on describing your phenotypes.</a></dd>
-
-      <dt>Units</dt>
-      <dd>Each phenotype will need units for the measurements taken. If there are
-        none, then indicate the field is a no-value field.</dd>
-  </dl></p>
-  <p>You can add more columns after those three if you want to, but these 3
-    <strong>MUST</strong> be present.</p>
-  <p>The file would, for example, look like the following:</p>
-  <code>id,description,units,…<br />
-    pheno10001|Central nervous system, behavior, cognition; …|mg|…<br />
-    pheno10002|Aging, metabolism, central nervous system: …|mg|…<br />
-    ⋮<br /></code>
-
-  <p><strong>Note 01</strong>: The first usable row is the heading row.</p>
-  <p><strong>Note 02: </strong>This example demonstrates a subtle issue that
-    could make your CSV file invalid &mdash; the choice of your field separator
-    character.<br >
-    In the example above, we use the pipe character (<code>|</code>) as our
-    field separator. This is because, if we follow the advice on how to write
-    good descriptions, then we cannot use the comma as our separator &ndash; if
-    we did, then our CSV file would be invalid because the system would have no
-    way to tell the difference between the comma as a field separator, and the
-    comma as a way to separate the "general category and ontology terms".</p>
-
-  <h3 class="subheading">file: Phenotype Data, Standard Errors and/or Sample Counts</h3>
-  <span id="docs-file-phenotype-data"></span>
-  <span id="docs-file-phenotype-se"></span>
-  <span id="docs-file-phenotype-n"></span>
-  <p>The data is a matrix of <em>samples(or individuals) × phenotypes</em>, e.g.</p>
-  <code>
-    # num-cases: 2549
-    # num-phenos: 13
-    id,pheno10001,pheno10002,pheno10003,pheno10004,53.099998,…<br />
-    IND001,61.400002,49,62.5,55.099998,…<br />
-    IND002,54.099998,50.099998,53.299999,55.099998,…<br />
-    IND003,483,403,501,403,…<br />
-    IND004,49.799999,45.5,62.900002,NA,…<br />
-    ⋮<br /></code>
-
-  <p>where <code>IND001,IND002,IND003,IND004,…</code> are the
-    samples/individuals/cases in your study, and
-    <code>pheno10001,pheno10002,pheno10004,pheno10004,…</code> are the
-    identifiers for your phenotypes.</p>
-  <p>The lines beginning with the "<em>#</em>" symbol (i.e.
-    <code># num-cases: 2549</code> and <code># num-phenos: 13</code> are comment
-    lines and will be ignored</p>
-  <p>In this example, the comma (,) is used as the file separator.</p>
-</div>
-
-{%endblock%}
-
-
-{%block more_javascript%}
-<script src="{{url_for('base.node_modules',
-             filename='resumablejs/resumable.js')}}"></script>
-<script type="text/javascript" src="/static/js/files.js"></script>
-
-<script type="text/javascript">
-  $("#btn-reset-file-separator").on("click", (event) => {
-      event.preventDefault();
-      $("#txt-file-separator").val("\t");
-      $("#txt-file-separator").trigger("change");
-  });
-  $("#btn-reset-file-comment-character").on("click", (event) => {
-      event.preventDefault();
-      $("#txt-file-comment-character").val("#");
-      $("#txt-file-comment-character").trigger("change");
-  });
-  $("#btn-reset-file-na").on("click", (event) => {
-      event.preventDefault();
-      $("#txt-file-na").val("- NA N/A");
-      $("#txt-file-na").trigger("change");
-  });
-
-  var update_preview = (table, filedata, formdata, numrows) => {
-      table.find("thead tr").remove()
-      table.find(".data-row").remove();
-      var linenum = 0;
-      var tableheader = table.find("thead");
-      var tablebody = table.find("tbody");
-      var numheadings = 0;
-      var navalues = formdata
-          .na_strings
-          .split(" ")
-          .map((v) => {return v.trim();})
-          .filter((v) => {return Boolean(v);});
-      filedata.forEach((line) => {
-          if(line.startsWith(formdata.comment_char) || linenum >= numrows) {
-              return false;
-          }
-          var row = $("<tr></tr>");
-          line.split(formdata.separator)
-              .map((field) => {
-                  var value = field.trim();
-                  if(navalues.includes(value)) {
-                      return "⋘NUL⋙";
-                  }
-                  return value;
-              })
-              .filter((field) => {
-                  return (field !== "" && field != undefined && field != null);
-              })
-              .forEach((field) => {
-                  if(linenum == 0) {
-                      numheadings += 1;
-                      var tablefield = $("<th></th>");
-                      tablefield.text(field);
-                      row.append(tablefield);
-                  } else {
-                      add_class(row, "data-row");
-                      var tablefield = $("<td></td>");
-                      tablefield.text(field);
-                      row.append(tablefield);
-                  }
-              });
-
-          if(linenum == 0) {
-              tableheader.append(row);
-          } else {
-              tablebody.append(row);
-          }
-          linenum += 1;
-      });
-
-      if(table.find("tbody tr.data-row").length > 0) {
-          add_class(table.find(".data-row-template"), "visually-hidden");
-      } else {
-          remove_class(table.find(".data-row-template"), "visually-hidden");
-      }
-  };
-
-  var makePreviewUpdater = (preview_table) => {
-      return (data) => {
-          update_preview(
-              preview_table,
-              data,
-              filesMetadata(),
-              PREVIEW_ROWS);
-      };
-  };
-
-  var preview_tables_to_elements_map = {
-      "#tbl-preview-pheno-desc": "#finput-phenotype-descriptions",
-      "#tbl-preview-pheno-data": "#finput-phenotype-data",
-      "#tbl-preview-pheno-se": "#finput-phenotype-se",
-      "#tbl-preview-pheno-n": "#finput-phenotype-n"
-  };
-
-  var filesMetadata = () => {
-      return {
-          "separator": $("#txt-file-separator").val(),
-          "comment_char": $(
-              "#txt-file-comment-character").val(),
-          "na_strings": $("#txt-file-na").val()
-      }
-  };
-
-  var PREVIEW_ROWS = 5;
-
-  var handler_update_previews = (event) => {
-      Object.entries(preview_tables_to_elements_map).forEach((mapentry) => {
-          var preview_table = $(mapentry[0]);
-          var file_input = $(mapentry[1]);
-          if(file_input[0].files.length > 0) {
-              readFirstNLines(
-                  file_input[0].files[0],
-                  10,
-                  [makePreviewUpdater(preview_table)]);
-          }
-      });
-
-      if(typeof(resumables) !== "undefined") {
-          resumables.forEach((resumable) => {
-              if(resumable.files.length > 0) {
-                  readFirstNLines(
-                      resumable.files[0].file,
-                      10,
-                      [makePreviewUpdater(resumable.preview_table)]);
-              }
-          });
-      }
-  };
-
-  [
-      "#txt-file-separator",
-      "#txt-file-comment-character",
-      "#txt-file-na"
-  ].forEach((elementid) => {
-      $(elementid).on("change", handler_update_previews);
-  });
-
-  [
-      "#finput-phenotype-descriptions",
-      "#finput-phenotype-data",
-      "#finput-phenotype-se",
-      "#finput-phenotype-n"
-  ].forEach((elementid) => {
-      $(elementid).on("change", (event) => {
-          readFirstNLines(
-              event.target.files[0],
-              10,
-              [makePreviewUpdater(
-                  $("#" + event.target.getAttribute("data-preview-table")))]);
-      });
-  });
-
-
-  var resumableDisplayFiles = (display_area, files) => {
-      files.forEach((file) => {
-          display_area.find(".file-display").remove();
-          var display_element = display_area
-              .find(".file-display-template")
-              .clone();
-          remove_class(display_element, "visually-hidden");
-          remove_class(display_element, "file-display-template");
-          add_class(display_element, "file-display");
-          display_element.find(".filename").text(file.name
-                                                || file.fileName
-                                                || file.relativePath
-                                                || file.webkitRelativePath);
-          display_element.find(".filesize").text(
-              (file.size / (1024*1024)).toFixed(2) + "MB");
-          display_element.find(".fileuniqueid").text(file.uniqueIdentifier);
-          display_element.find(".filemimetype").text(file.file.type);
-          display_area.append(display_element);
-      });
-  };
-
-
-  var indicateProgress = (resumable, progress_bar) => {
-      return () => {/*Has no event!*/
-          var progress = (resumable.progress() * 100).toFixed(2);
-          var pbar = progress_bar.find(".progress-bar");
-          remove_class(progress_bar, "visually-hidden");
-          pbar.css("width", progress+"%");
-          pbar.attr("aria-valuenow", progress);
-          pbar.text("Uploading: " + progress + "%");
-      };
-  };
-
-  var retryUpload = (retry_button, cancel_button) => {
-      retry_button.on("click", (event) => {
-          resumable.files.forEach((file) => {file.retry();});
-          add_class(retry_button, "visually-hidden");
-          remove_class(cancel_button, "visually-hidden");
-          add_class(browse_button, "visually-hidden");
-      });
-  };
-
-  var cancelUpload = (cancel_button, retry_button) => {
-      cancel_button.on("click", (event) => {
-          resumable.files.forEach((file) => {
-              if(file.isUploading()) {
-                  file.abort();
-              }
-          });
-          add_class(cancel_button, "visually-hidden");
-          remove_class(retry_button, "visually-hidden");
-          remove_class(browse_button, "visually-hidden");
-      });
-  };
-
-
-  var startUpload = (browse_button, retry_button, cancel_button) => {
-      return (event) => {
-          remove_class(cancel_button, "visually-hidden");
-          add_class(retry_button, "visually-hidden");
-          add_class(browse_button, "visually-hidden");
-      };
-  };
-
-  var processForm = (form) => {
-      var formdata = new FormData(form);
-      uploaded_files.forEach((msg) => {
-          formdata.delete(msg["file-input-name"]);
-          formdata.append(msg["file-input-name"], JSON.stringify({
-              "uploaded-file": msg["uploaded-file"],
-              "original-name": msg["original-name"]
-          }));
-      });
-      formdata.append("resumable-upload", "true");
-      formdata.append("publication-id", $("#txt-publication-id").val());
-      return formdata;
-  }
-
-  var uploaded_files = new Set();
-  var submitForm = (new_file) => {
-      uploaded_files.add(new_file);
-      if(uploaded_files.size === resumables.length) {
-          var form = $("#frm-add-phenotypes");
-          if(form.length !== 1) {
-              // TODO: Handle error somehow?
-              alert("Could not find form!!!");
-              return false;
-          }
-
-          $.ajax({
-              "url": form.attr("action"),
-              "type": "POST",
-              "data": processForm(form[0]),
-              "processData": false,
-              "contentType": false,
-              "success": (data, textstatus, jqxhr) => {
-                  // TODO: Redirect to endpoint that should come as part of the
-                  //       success/error message.
-                  console.log("SUCCESS DATA: ", data);
-                  console.log("SUCCESS STATUS: ", textstatus);
-                  console.log("SUCCESS jqXHR: ", jqxhr);
-                  window.location.assign(window.location.origin + data["redirect-to"]);
-              },
-          });
-          return false;
-      }
-      return false;
-  };
-
-  var uploadSuccess = (file_input_name) => {
-      return (file, message) => {
-          submitForm({...JSON.parse(message), "file-input-name": file_input_name});
-      };
-  };
-
-
-  var uploadError = () => {
-      return (message, file) => {
-          $("#frm-add-phenotypes input[type=submit]").removeAttr("disabled");
-          console.log("THE FILE:", file);
-          console.log("THE ERROR MESSAGE:", message);
-      };
-  };
-
-
-
-  var makeResumableObject = (form_id, file_input_id, resumable_element_id, preview_table_id) => {
-      var the_form = $("#" + form_id);
-      var file_input = $("#" + file_input_id);
-      var submit_button = the_form.find("input[type=submit]");
-      if(file_input.length != 1) {
-          return false;
-      }
-      var r = errorHandler(
-          fileSuccessHandler(
-              uploadStartHandler(
-                  filesAddedHandler(
-                      markResumableDragAndDropElement(
-                          makeResumableElement(
-                              the_form.attr("data-resumable-target"),
-                              file_input.parent(),
-                              $("#" + resumable_element_id),
-                              submit_button,
-                              ["csv", "tsv", "txt"]),
-                          file_input.parent(),
-                          $("#" + resumable_element_id),
-                          $("#" + resumable_element_id + "-browse-button")),
-                      (files) => {
-                          // TODO: Also trigger preview!
-                          resumableDisplayFiles(
-                              $("#" + resumable_element_id + "-selected-files"), files);
-                          files.forEach((file) => {
-                              readFirstNLines(
-                                  file.file,
-                                  10,
-                                  [makePreviewUpdater(
-                                      $("#" + preview_table_id))])
-                          });
-                      }),
-                  startUpload($("#" + resumable_element_id + "-browse-button"),
-                              $("#" + resumable_element_id + "-retry-button"),
-                              $("#" + resumable_element_id + "-cancel-button"))),
-              uploadSuccess(file_input.attr("name"))),
-          uploadError());
-
-      /** Setup progress indicator **/
-      progressHandler(
-          r,
-          indicateProgress(r, $("#" + resumable_element_id + "-progress-bar")));
-
-      return r;
-  };
-
-  var resumables = [
-      ["frm-add-phenotypes", "finput-phenotype-descriptions", "resumable-phenotype-descriptions", "tbl-preview-pheno-desc"],
-      ["frm-add-phenotypes", "finput-phenotype-data", "resumable-phenotype-data", "tbl-preview-pheno-data"],
-      ["frm-add-phenotypes", "finput-phenotype-se", "resumable-phenotype-se", "tbl-preview-pheno-se"],
-      ["frm-add-phenotypes", "finput-phenotype-n", "resumable-phenotype-n", "tbl-preview-pheno-n"],
-  ].map((row) => {
-      r = makeResumableObject(row[0], row[1], row[2], row[3]);
-      r.preview_table = $("#" + row[3]);
-      return r;
-  }).filter((val) => {
-      return Boolean(val);
-  });
-
-  $("#frm-add-phenotypes input[type=submit]").on("click", (event) => {
-      event.preventDefault();
-      console.debug();
-      if ($("#txt-publication-id").val() == "") {
-          alert("You MUST provide a publication for the phenotypes.");
-          return false;
-      }
-      // TODO: Check all the relevant files exist
-      // TODO: Verify that files are not duplicated
-      var filenames = [];
-      var nondupfiles = [];
-      resumables.forEach((r) => {
-          var fname = r.files[0].file.name;
-          filenames.push(fname);
-          if(!nondupfiles.includes(fname)) {
-              nondupfiles.push(fname);
-          }
-      });
-
-      // Check that all files were provided
-      if(resumables.length !== filenames.length) {
-          window.alert("You MUST provide all the files requested.");
-          event.target.removeAttribute("disabled");
-          return false;
-      }
-
-      // Check that there are no duplicate files
-      var duplicates = Object.entries(filenames.reduce(
-          (acc, curr, idx, arr) => {
-              acc[curr] = (acc[curr] || 0) + 1;
-              return acc;
-          },
-          {})).filter((entry) => {return entry[1] !== 1;});
-      if(duplicates.length > 0) {
-          var msg = "The file(s):\r\n";
-          msg = msg + duplicates.reduce(
-              (msgstr, afile) => {
-                  return msgstr + "  • " + afile[0] + "\r\n";
-              },
-              "");
-          msg = msg + "is(are) duplicated. Please fix and try again.";
-          window.alert(msg);
-          event.target.removeAttribute("disabled");
-          return false;
-      }
-      // TODO: Check all fields
-      // Start the uploads.
-      event.target.setAttribute("disabled", "disabled");
-      resumables.forEach((r) => {r.upload();});
-  });
-</script>
-{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html b/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html
deleted file mode 100644
index 29a8dea..0000000
--- a/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html
+++ /dev/null
@@ -1,189 +0,0 @@
-{%extends "phenotypes/sui-add-phenotypes-base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-table-pagination.html" import table_pagination%}
-
-{%block title%}Phenotypes{%endblock%}
-
-{%block pagetitle%}Phenotypes{%endblock%}
-
-{%block frm_add_phenotypes_documentation%}
-<p>Select the zip file bundle containing information on the phenotypes you
-  wish to upload, then click the "Upload Phenotypes" button below to
-  upload the data.</p>
-<p>If you wish to upload the files individually instead,
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}"
-     title="">click here</a>.</p>
-<p>See the <a href="#section-file-formats">File Formats</a> section below
-  to get an understanding of what is expected of the bundle files you
-  upload.</p>
-{%endblock%}
-
-{%block frm_add_phenotypes_elements%}
-<div class="form-group">
-  <label for="finput-phenotypes-bundle" class="form-label">
-    Phenotypes Bundle</label>
-  <input type="file"
-         id="finput-phenotypes-bundle"
-         name="phenotypes-bundle"
-         accept="application/zip, .zip"
-	 required="required"
-         class="form-control" />
-</div>
-{%endblock%}
-
-{%block page_documentation%}
-<div class="row">
-  <h2 class="heading" id="section-file-formats">File Formats</h2>
-  <p>We accept an extended form of the
-    <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html#format-of-the-data-files"
-       title="R/qtl2 software input file format documentation">
-      input files' format used with the R/qtl2 software</a> as a single ZIP
-    file</p>
-  <p>The files that are used for this feature are:
-    <ul>
-      <li>the <em>control</em> file</li>
-      <li><em>pheno</em> file(s)</li>
-      <li><em>phenocovar</em> file(s)</li>
-      <li><em>phenose</em> files(s)</li>
-    </ul>
-  </p>
-  <p>Other files within the bundle will be ignored, for this feature.</p>
-  <p>The following section will detail the expectations for each of the
-    different file types within the uploaded ZIP file bundle for phenotypes:</p>
-
-  <h3 class="subheading">Control File</h3>
-  <p>There <strong>MUST be <em>one, and only one</em></strong> file that acts
-    as the control file. This file can be:
-    <ul>
-      <li>a <em>JSON</em> file, or</li>
-      <li>a <em>YAML</em> file.</li>
-    </ul>
-  </p>
-
-  <p>The control file is useful for defining things about the bundle such as:</p>
-  <ul>
-    <li>The field separator value (default: <code>sep: ','</code>). There can
-      only ever be one field separator and it <strong>MUST</strong> be the same
-      one for <strong>ALL</strong> files in the bundle.</li>
-    <li>The comment character (default: <code>comment.char: '#'</code>). Any
-      line that starts with this character will be considered a comment line and
-      be ignored in its entirety.</li>
-    <li>Code for missing values (default: <code>na.strings: 'NA'</code>). You
-      can specify more than one code to indicate missing values, e.g.
-      <code>{…, "na.strings": ["NA", "N/A", "-"], …}</code></li>
-  </ul>
-
-  <h3 class="subheading"><em>pheno</em> File(s)</h3>
-  <p>These files are the main data files. You must have at least one of these
-    files in your bundle for it to be valid for this step.</p>
-  <p>The data is a matrix of <em>individuals × phenotypes</em> by default, as
-    below:<br />
-    <code>
-      id,10001,10002,10003,10004,…<br />
-      BXD1,61.400002,54.099998,483,49.799999,…<br />
-      BXD2,49,50.099998,403,45.5,…<br />
-      BXD5,62.5,53.299999,501,62.900002,…<br />
-      BXD6,53.099998,55.099998,403,NA,…<br />
-      ⋮<br /></code>
-  </p>
-  <p>If the <code>pheno_transposed</code> value is set to <code>True</code>,
-    then the data will be a <em>phenotypes × individuals</em> matrix as in the
-    example below:<br />
-    <code>
-      id,BXD1,BXD2,BXD5,BXD6,…<br />
-      10001,61.400002,49,62.5,53.099998,…<br />
-      10002,54.099998,50.099998,53.299999,55.099998,…<br />
-      10003,483,403,501,403,…<br />
-      10004,49.799999,45.5,62.900002,NA,…<br />
-      ⋮
-    </code>
-  </p>
-
-
-  <h3 class="subheading"><em>phenocovar</em> File(s)</h3>
-  <p>At least one phenotypes metadata file with the metadata values such as
-    descriptions, PubMed Identifier, publication titles (if present), etc.</p>
-  <p>The data in this/these file(s) is a matrix of
-    <em>phenotypes × phenotypes-covariates</em>. The first column is always the
-    phenotype names/identifiers — same as in the R/qtl2 format.</p>
-  <p><em>phenocovar</em> files <strong>should never be transposed</strong>!</p>
-  <p>This file <strong>MUST</strong> be present in the bundle, and have data for
-    the bundle to be considered valid by our system for this step.<br />
-    In addition to that, the following are the fields that <strong>must be
-      present</strong>, and
-    have values, in the file before the file is considered valid:
-    <ul>
-      <li><em>description</em>: A description for each phenotype. Useful
-        for users to know what the phenotype is about.</li>
-      <li><em>units</em>: The units of measurement for the phenotype,
-        e.g. milligrams for brain weight, centimetres/millimetres for
-        tail-length, etc.</li>
-  </ul></p>
-
-  <p>The following <em>optional</em> fields can also be provided:
-    <ul>
-      <li><em>pubmedid</em>: A PubMed Identifier for the publication where
-        the phenotype is published. If this field is not provided, the system will
-        assume your phenotype is not published.</li>
-    </ul>
-  </p>
-  <p>These files will be marked up in the control file with the
-    <code>phenocovar</code> key, as in the examples below:
-    <ol>
-      <li>JSON: single file<br />
-        <code>{<br />
-          &nbsp;&nbsp;⋮,<br />
-          &nbsp;&nbsp;"phenocovar": "your_covariates_file.csv",<br />
-          &nbsp;&nbsp;⋮<br />
-          }
-        </code>
-      </li>
-      <li>JSON: multiple files<br />
-        <code>{<br />
-          &nbsp;&nbsp;⋮,<br />
-          &nbsp;&nbsp;"phenocovar": [<br />
-          &nbsp;&nbsp;&nbsp;&nbsp;"covariates_file_01.csv",<br />
-          &nbsp;&nbsp;&nbsp;&nbsp;"covariates_file_01.csv",<br />
-          &nbsp;&nbsp;&nbsp;&nbsp;⋮<br />
-          &nbsp;&nbsp;],<br />
-          &nbsp;&nbsp;⋮<br />
-          }
-        </code>
-      </li>
-      <li>YAML: single file or<br />
-        <code>
-          ⋮<br />
-          phenocovar: your_covariates_file.csv<br />
-          ⋮
-        </code>
-      </li>
-      <li>YAML: multiple files<br />
-        <code>
-          ⋮<br />
-          phenocovar:<br />
-          - covariates_file_01.csv<br />
-          - covariates_file_02.csv<br />
-          - covariates_file_03.csv<br />
-          …<br />
-          ⋮
-        </code>
-      </li>
-    </ol>
-  </p>
-
-  <h3 class="subheading"><em>phenose</em> and <em>phenonum</em> File(s)</h3>
-  <p>These are extensions to the R/qtl2 standard, i.e. these types ofs file are
-    not supported by the original R/qtl2 file format</p>
-  <p>We use these files to upload the standard errors (<em>phenose</em>) when
-    the data file (<em>pheno</em>) is average data. In that case, the
-    <em>phenonum</em> file(s) contains the number of individuals that were
-    involved when computing the averages.</p>
-  <p>Both types of files are matrices of <em>individuals × phenotypes</em> by
-    default. Like the related <em>pheno</em> files, if
-    <code>pheno_transposed: True</code>, then the file will be a matrix of
-    <em>phenotypes × individuals</em>.</p>
-</div>
-{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-base.html b/uploader/templates/phenotypes/sui-base.html
deleted file mode 100644
index d7d980f..0000000
--- a/uploader/templates/phenotypes/sui-base.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{%extends "populations/sui-base.html"%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_sui_pheno_dataset_card%}
-
-{%block breadcrumbs%}
-{{super()}}
-<li class="breadcrumb-item">
-  <a href="{{url_for('species.populations.phenotypes.view_dataset',
-           species_id=species['SpeciesId'],
-           population_id=population['Id'],
-           dataset_id=dataset['Id'])}}">
-    {{dataset["Name"]}}
-  </a>
-</li>
-{%endblock%}
-
-{%block contents%}
-<div class="row">
-  <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2>
-</div>
-{%endblock%}
-
-
-{%block sidebarcontents%}
-{{display_sui_pheno_dataset_card(species, population, dataset)}}
-{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-job-status.html b/uploader/templates/phenotypes/sui-job-status.html
deleted file mode 100644
index bca87d5..0000000
--- a/uploader/templates/phenotypes/sui-job-status.html
+++ /dev/null
@@ -1,140 +0,0 @@
-{%extends "phenotypes/sui-base.html"%}
-{%from "cli-output.html" import cli_output%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-table-pagination.html" import table_pagination%}
-
-{%block extrameta%}
-{%if job and job.status not in ("success", "completed:success", "error", "completed:error")%}
-<meta http-equiv="refresh" content="5" />
-{%endif%}
-{%endblock%}
-
-{%block title%}Phenotypes{%endblock%}
-
-{%block pagetitle%}Phenotypes{%endblock%}
-
-{%block contents%}
-
-{%if job%}
-<div class="row">
-  <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2>
-  <h3 class="subheading">upload progress</h3>
-</div>
-<div class="row" style="overflow:scroll;">
-  <p><strong>Process Status:</strong> {{job.status}}</p>
-  {%if metadata%}
-  <table class="table table-responsive">
-    <thead>
-      <tr>
-        <th>File</th>
-        <th>Status</th>
-        <th>Lines Processed</th>
-        <th>Total Errors</th>
-      </tr>
-    </thead>
-
-    <tbody>
-      {%for file,meta in metadata.items()%}
-      <tr>
-        <td>{{file}}</td>
-        <td>{{meta.status}}</td>
-        <td>{{meta.linecount}}</td>
-        <td>{{meta["total-errors"]}}</td>
-      </tr>
-      {%endfor%}
-    </tbody>
-  </table>
-  {%endif%}
-</div>
-
-<div class="row">
-  {%if  job.status in ("completed:success", "success")%}
-  <p>
-    {%if errors | length == 0%}
-    <a href="{{url_for('species.populations.phenotypes.review_job_data',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id,
-           job_id=job_id)}}"
-       class="btn btn-primary"
-       title="Continue to process data">Continue</a>
-    {%else%}
-    <span class="text-muted"
-          disabled="disabled"
-          style="border: solid 2px;border-radius: 5px;padding: 0.3em;">
-      Cannot continue due to errors. Please fix the errors first.
-    </span>
-    {%endif%}
-  </p>
-  {%endif%}
-</div>
-
-<h3 class="subheading">upload errors</h3>
-<div class="row" style="max-height: 20em; overflow: scroll;">
-  {%if errors | length == 0 %}
-  <p class="text-info">
-    <span class="glyphicon glyphicon-info-sign"></span>
-    No errors found so far
-  </p>
-  {%else%}
-  <table class="table table-responsive">
-    <thead style="position: sticky; top: 0; background: white;">
-      <tr>
-        <th>File</th>
-        <th>Row</th>
-        <th>Column</th>
-        <th>Value</th>
-        <th>Message</th>
-      </tr>
-    </thead>
-
-    <tbody style="font-size: 0.9em;">
-      {%for error in errors%}
-      <tr>
-        <td>{{error.filename}}</td>
-        <td>{{error.rowtitle}}</td>
-        <td>{{error.coltitle}}</td>
-        <td>{%if error.cellvalue is not none and error.cellvalue | length > 25%}
-          {{error.cellvalue[0:24]}}&hellip;
-          {%else%}
-          {{error.cellvalue}}
-          {%endif%}
-        </td>
-        <td>
-          {%if error.message | length > 250 %}
-          {{error.message[0:249]}}&hellip;
-          {%else%}
-          {{error.message}}
-          {%endif%}
-        </td>
-      </tr>
-      {%endfor%}
-    </tbody>
-  </table>
-  {%endif%}
-</div>
-
-<div class="row">
-  {{cli_output(job, "stdout")}}
-</div>
-
-<div class="row">
-  {{cli_output(job, "stderr")}}
-</div>
-
-{%else%}
-<div class="row">
-  <h3 class="text-danger">No Such Job</h3>
-  <p>Could not find a job with the ID: {{job_id}}</p>
-  <p>
-    Please go back to
-    <a href="{{url_for('species.populations.phenotypes.view_dataset',
-             species_id=species.SpeciesId,
-             population_id=population.Id,
-             dataset_id=dataset.Id)}}"
-       title="'{{dataset.Name}}' dataset page">
-      the '{{dataset.Name}}' dataset page</a>
-    to upload new phenotypes or edit existing ones.</p>
-</div>
-{%endif%}
-{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-load-phenotypes-success.html b/uploader/templates/phenotypes/sui-load-phenotypes-success.html
deleted file mode 100644
index dff0682..0000000
--- a/uploader/templates/phenotypes/sui-load-phenotypes-success.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{%extends "phenotypes/sui-base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-table-pagination.html" import table_pagination%}
-
-{%block title%}Phenotypes{%endblock%}
-
-{%block pagetitle%}Phenotypes{%endblock%}
-
-{%block contents%}
-{{super()}}
-
-<div class="row">
-  <p>You have successfully loaded
-    <!-- maybe indicate the number of phenotypes here? -->your
-    new phenotypes into the database.</p>
-  <!-- TODO: Maybe notify user that they have sole access. -->
-  <!-- TODO: Maybe provide a link to go to GeneNetwork to view the data. -->
-  <p>View your data
-    <a href="{{search_page_uri}}"
-       target="_blank">on GeneNetwork2</a>.
-    You might need to login to GeneNetwork2 to view specific traits.</p>
-</div>
-{%endblock%}
-
-
-{%block more_javascript%}{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-review-job-data.html b/uploader/templates/phenotypes/sui-review-job-data.html
deleted file mode 100644
index ea4183d..0000000
--- a/uploader/templates/phenotypes/sui-review-job-data.html
+++ /dev/null
@@ -1,121 +0,0 @@
-{%extends "phenotypes/sui-base.html"%}
-{%from "cli-output.html" import cli_output%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-table-pagination.html" import table_pagination%}
-{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
-
-{%block extrameta%}
-{%if not job%}
-<meta http-equiv="refresh"
-      content="20; url={{url_for('species.populations.phenotypes.view_dataset', species_id=species.SpeciesId,
-               population_id=population.Id,
-               dataset_id=dataset.Id)}}" />
-{%endif%}
-{%endblock%}
-
-{%block title%}Phenotypes{%endblock%}
-
-{%block pagetitle%}Phenotypes{%endblock%}
-
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="add-phenotypes"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id)}}">View Datasets</a>
-</li>
-{%endblock%}
-
-{%block contents%}
-
-{%if job%}
-<div class="row">
-  <h3 class="heading">Data Review</h3>
-  <p class="text-info"><strong>
-      The data has <em>NOT</em> been added/saved yet. Review the details below
-      and click "Continue" to save the data.</strong></p>
-  <p>The &#x201C;<strong>{{dataset.FullName}}</strong>&#x201D; dataset from the
-    &#x201C;<strong>{{population.FullName}}</strong>&#x201D; population of the
-    species &#x201C;<strong>{{species.SpeciesName}} ({{species.FullName}})</strong>&#x201D;
-    will be updated as follows:</p>
-
-  <ul>
-    {%if publication%}
-    <li>All {{summary.get("pheno", {}).get("total-data-rows", "0")}} phenotypes
-      are linked to the following publication:
-      <ul>
-        <li><strong>Publication Title:</strong>
-          {{publication.Title or "—"}}</li>
-        <li><strong>Author(s):</strong>
-          {{publication.Authors or "—"}}</li>
-      </ul>
-    </li>
-    {%endif%}
-  {%for ftype in ("phenocovar", "pheno", "phenose", "phenonum")%}
-  {%if summary.get(ftype, False)%}
-    <li>A total of {{summary[ftype]["number-of-files"]}} files will be processed
-      adding {%if ftype == "phenocovar"%}(possibly){%endif%}
-      {{summary[ftype]["total-data-rows"]}} new
-      {%if ftype == "phenocovar"%}
-      phenotypes
-      {%else%}
-      {{summary[ftype]["description"]}} rows
-      {%endif%}
-      to the database.
-    </li>
-  {%endif%}
-  {%endfor%}
-  </ul>
-
-  <form id="frm-review-phenotype-data"
-        method="POST"
-        action="{{url_for('species.populations.phenotypes.load_data_to_database',
-                species_id=species.SpeciesId,
-                population_id=population.Id,
-                dataset_id=dataset.Id)}}">
-    <input type="hidden" name="data-qc-job-id" value="{{job.jobid}}" />
-    <input type="submit"
-           value="continue"
-           class="btn btn-primary" />
-  </form>
-</div>
-{%else%}
-<div class="row">
-  <h4 class="subheading">Invalid Job</h3>
-  <p class="text-danger">
-    Could not find a job with the ID: <strong>{{job_id}}.</p>
-  <p>You will be redirected in
-    <span id="countdown-element" class="text-info">20</span> second(s)</p>
-  <p class="text-muted">
-    <small>
-      If you are not redirected, please
-      <a href="{{url_for(
-               'species.populations.phenotypes.view_dataset',
-               species_id=species.SpeciesId,
-               population_id=population.Id,
-               dataset_id=dataset.Id)}}">click here</a> to continue
-    </small>
-  </p>
-</div>
-{%endif%}
-{%endblock%}
-
-
-{%block javascript%}
-<script type="text/javascript">
-  $(document).ready(function() {
-      var countdown = 20;
-      var countdown_element = $("#countdown-element");
-      if(countdown_element.length === 1) {
-          intv = window.setInterval(function() {
-              countdown = countdown - 1;
-              countdown_element.html(countdown);
-          }, 1000);
-      }
-  });
-</script>
-{%endblock%}
diff --git a/uploader/templates/phenotypes/view-dataset.html b/uploader/templates/phenotypes/view-dataset.html
index c634a48..3bb2586 100644
--- a/uploader/templates/phenotypes/view-dataset.html
+++ b/uploader/templates/phenotypes/view-dataset.html
@@ -1,7 +1,6 @@
 {%extends "phenotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "macro-table-pagination.html" import table_pagination%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Phenotypes{%endblock%}
 
@@ -24,25 +23,12 @@
 {{flash_all_messages()}}
 
 <div class="row">
-  <p>The basic dataset details are:</p>
-
-  <table class="table">
-    <thead>
-      <tr>
-        <th>Name</th>
-        <th>Full Name</th>
-        <th>Short Name</th>
-      </tr>
-    </thead>
+  <h2>Phenotype Data</h2>
 
-    <tbody>
-      <tr>
-        <td>{{dataset.Name}}</td>
-        <td>{{dataset.FullName}}</td>
-        <td>{{dataset.ShortName}}</td>
-      </tr>
-    </tbody>
-  </table>
+  <p>Click on any of the phenotypes in the table below to view and edit that
+    phenotype's data.</p>
+  <p>Use the search to filter through all the phenotypes and find specific
+    phenotypes of interest.</p>
 </div>
 
 <div class="row">
@@ -68,7 +54,7 @@
       <input type="submit"
              title="Compute/Recompute the means for all phenotypes."
              class="btn btn-info"
-             value="(rec/c)ompute means"
+             value="compute means"
              id="submit-frm-recompute-phenotype-means" />
     </form>
   </div>
@@ -86,24 +72,29 @@
       <input type="submit"
              title="Run/Rerun QTLReaper."
              class="btn btn-info"
-             value="(re)run QTLReaper"
+             value="run QTLReaper"
              id="submit-frm-rerun-qtlreaper" />
     </form>
   </div>
-</div>
-
-<div class="row">
-  <h2>Phenotype Data</h2>
 
-  <p>Click on any of the phenotypes in the table below to view and edit that
-    phenotype's data.</p>
-  <p>Use the search to filter through all the phenotypes and find specific
-    phenotypes of interest.</p>
+  <div class="col">
+    <form id="frm-delete-phenotypes"
+          method="POST"
+          action="{{url_for(
+                  'species.populations.phenotypes.delete_phenotypes',
+                  species_id=species['SpeciesId'],
+                  population_id=population['Id'],
+                  dataset_id=dataset['Id'])}}">
+      <input type="submit"
+             class="btn btn-danger"
+             id="btn-delete-phenotypes"
+             title="Delete phenotypes from this dataset. If no phenotypes are selected in the table, this will delete ALL the phenotypes."
+             value="delete phenotypes" />
+    </form>
+  </div>
 </div>
 
-
-<div class="row">
-
+<div class="row" style="margin-top: 0.5em;">
   <table id="tbl-phenotypes-list" class="table compact stripe cell-border">
     <thead>
       <tr>
@@ -119,12 +110,10 @@
 </div>
 {%endblock%}
 
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
-
 
 {%block javascript%}
+<script type="text/javascript" src="/static/js/urls.js"></script>
+
 <script type="text/javascript">
   $(function() {
       var species_id = {{species.SpeciesId}};
@@ -151,11 +140,12 @@
                       var spcs_id = {{species.SpeciesId}};
                       var pop_id = {{population.Id}};
                       var dtst_id = {{dataset.Id}};
-                      return `<a href="/species/${spcs_id}` +
+                      var url = buildURLFromCurrentURL(
+                          (`/species/${spcs_id}` +
                           `/populations/${pop_id}` +
                           `/phenotypes/datasets/${dtst_id}` +
-                          `/phenotype/${pheno.xref_id}` +
-                          `" target="_blank">` +
+                           `/phenotype/${pheno.xref_id}`));
+                      return `<a href="${url.toString()}" target="_blank">` +
                           `${pheno.InbredSetCode}_${pheno.xref_id}` +
                           `</a>`;
                   }
@@ -204,6 +194,33 @@
               });
               form.submit();
           });
+
+      $("#btn-delete-phenotypes").on(
+          "click",
+          function(event) {
+              // Collect selected phenotypes for deletion, if any.
+              event.preventDefault();
+              form = $("#frm-delete-phenotypes");
+              form.find(".dynamically-added-element").remove();
+              $("#tbl-phenotypes-list")
+                  .DataTable()
+                  .rows({selected: true}).
+                  nodes().each(function(node, index) {
+                      var parts = $(node)
+                          .find(".chk-row-select")
+                          .val()
+                          .split("_");
+                      var xref_id = parts[parts.length - 1].trim();
+                      var chk = $('<input type="checkbox">');
+                      chk.attr("class", "dynamically-added-element");
+                      chk.attr("value", xref_id);
+                      chk.attr("name", "xref_ids");
+                      chk.attr("style", "display: none");
+                      chk.prop("checked", true);
+                      form.append(chk);
+                  });
+              form.submit();
+          });
   });
 </script>
 {%endblock%}
diff --git a/uploader/templates/phenotypes/view-phenotype.html b/uploader/templates/phenotypes/view-phenotype.html
index 75e3c1e..a59949e 100644
--- a/uploader/templates/phenotypes/view-phenotype.html
+++ b/uploader/templates/phenotypes/view-phenotype.html
@@ -1,25 +1,10 @@
 {%extends "phenotypes/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Phenotypes{%endblock%}
 
 {%block pagetitle%}Phenotypes{%endblock%}
 
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="view-phenotype"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.phenotypes.view_phenotype',
-           species_id=species.SpeciesId,
-           population_id=population.Id,
-           dataset_id=dataset.Id,
-           xref_id=xref_id)}}">View Phenotype</a>
-</li>
-{%endblock%}
-
 {%block contents%}
 {{flash_all_messages()}}
 
@@ -118,7 +103,7 @@ or "group:resource:delete-resource" in privileges%}
     <table class="table">
       <thead>
         <tr>
-          <th>#</th>
+          <th>Index</th>
           <th>Sample</th>
           <th>Value</th>
           {%if has_se%}
@@ -153,7 +138,3 @@ or "group:resource:delete-resource" in privileges%}
 </div>
 
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/platforms/base.html b/uploader/templates/platforms/base.html
index dac965f..d4f3686 100644
--- a/uploader/templates/platforms/base.html
+++ b/uploader/templates/platforms/base.html
@@ -1,13 +1,17 @@
 {%extends "species/base.html"%}
+{%from "species/macro-display-species-card.html" import display_sui_species_card%}
 
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="platforms"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.platforms.index')}}">
-    Sequencing Platforms</a>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.platforms.list_platforms',
+           species_id=species['SpeciesId'])}}">
+    Platforms
+  </a>
 </li>
-{%block lvl4_breadcrumbs%}{%endblock%}
+{%endblock%}
+
+
+{%block sidebarcontents%}
+{{display_sui_species_card(species)}}
 {%endblock%}
diff --git a/uploader/templates/platforms/create-platform.html b/uploader/templates/platforms/create-platform.html
index 0866d5e..3a62472 100644
--- a/uploader/templates/platforms/create-platform.html
+++ b/uploader/templates/platforms/create-platform.html
@@ -1,19 +1,15 @@
 {%extends "platforms/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "species/macro-display-species-card.html" import display_species_card%}
 
 {%block title%}Platforms &mdash; Create Platforms{%endblock%}
 
-{%block pagetitle%}Platforms &mdash; Create Platforms{%endblock%}
-
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="create-platform"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.platforms.create_platform',
-           species_id=species.SpeciesId)}}">create platform</a>
+           species_id=species['SpeciesId'])}}">
+    Create
+  </a>
 </li>
 {%endblock%}
 
@@ -118,7 +114,3 @@
   </form>
 </div>
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_species_card(species)}}
-{%endblock%}
diff --git a/uploader/templates/platforms/list-platforms.html b/uploader/templates/platforms/list-platforms.html
index a6bcfdc..db14745 100644
--- a/uploader/templates/platforms/list-platforms.html
+++ b/uploader/templates/platforms/list-platforms.html
@@ -1,6 +1,5 @@
 {%extends "platforms/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "species/macro-display-species-card.html" import display_species_card%}
 
 {%block title%}Platforms &mdash; List Platforms{%endblock%}
 
@@ -87,7 +86,3 @@
   {%endif%}
 </div>
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_species_card(species)}}
-{%endblock%}
diff --git a/uploader/templates/populations/base.html b/uploader/templates/populations/base.html
index 9db8083..24cacc2 100644
--- a/uploader/templates/populations/base.html
+++ b/uploader/templates/populations/base.html
@@ -1,18 +1,20 @@
 {%extends "species/base.html"%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
 
-{%block lvl2_breadcrumbs%}
-<li {%if activelink=="populations"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  {%if population is mapping%}
+{%block breadcrumbs%}
+{{super()}}
+{%if population%}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.view_population',
-           species_id=species.SpeciesId,
-           population_id=population.Id)}}">{{population.Name}}</a>
-  {%else%}
-  <a href="{{url_for('species.populations.index')}}">Populations</a>
-  {%endif%}
+           species_id=species['SpeciesId'],
+           population_id=population['Id'])}}">
+    {{population["Name"]}}
+  </a>
 </li>
-{%block lvl3_breadcrumbs%}{%endblock%}
+{%endif%}
+{%endblock%}
+
+
+{%block sidebarcontents%}
+{{display_sui_population_card(species, population)}}
 {%endblock%}
diff --git a/uploader/templates/populations/create-population.html b/uploader/templates/populations/create-population.html
index 007b6bf..d5359f5 100644
--- a/uploader/templates/populations/create-population.html
+++ b/uploader/templates/populations/create-population.html
@@ -1,20 +1,16 @@
 {%extends "populations/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "species/macro-select-species.html" import select_species_form%}
-{%from "species/macro-display-species-card.html" import display_species_card%}
 
 {%block title%}Create Population{%endblock%}
 
 {%block pagetitle%}Create Population{%endblock%}
 
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="create-population"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.create_population',
-           species_id=species.SpeciesId)}}">create population</a>
+           species_id=species['SpeciesId'])}}">
+    create population</a>
 </li>
 {%endblock%}
 
@@ -263,7 +259,3 @@
   </form>
 </div>
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_species_card(species)}}
-{%endblock%}
diff --git a/uploader/templates/populations/macro-display-population-card.html b/uploader/templates/populations/macro-display-population-card.html
index 6b5f1e0..f3040ea 100644
--- a/uploader/templates/populations/macro-display-population-card.html
+++ b/uploader/templates/populations/macro-display-population-card.html
@@ -43,7 +43,7 @@
 
 {%macro display_sui_population_card(species, population)%}
 {{display_sui_species_card(species)}}
-
+{%if population%}
 <div class="row">
   <table class="table">
     <caption>Current population</caption>
@@ -75,4 +75,5 @@
     </tbody>
   </table>
 </div>
+{%endif%}
 {%endmacro%}
diff --git a/uploader/templates/populations/sui-base.html b/uploader/templates/populations/sui-base.html
deleted file mode 100644
index 0ca5c59..0000000
--- a/uploader/templates/populations/sui-base.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{%extends "species/sui-base.html"%}
-
-{%block breadcrumbs%}
-{{super()}}
-<li class="breadcrumb-item">
-  <a href="{{url_for('species.populations.view_population',
-           species_id=species['SpeciesId'],
-           population_id=population['Id'])}}">
-    {{population["Name"]}}
-  </a>
-</li>
-{%endblock%}
diff --git a/uploader/templates/populations/sui-view-population.html b/uploader/templates/populations/sui-view-population.html
deleted file mode 100644
index 6244f4d..0000000
--- a/uploader/templates/populations/sui-view-population.html
+++ /dev/null
@@ -1,267 +0,0 @@
-{%extends "populations/sui-base.html"%}
-{%from "macro-step-indicator.html" import step_indicator%}
-{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
-
-{%block contents%}
-<div class="row">
-  <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2>
-</div>
-
-<div class="row">
-  <ul class="nav nav-tabs" id="population-actions">
-    <li class="nav-item presentation">
-      <button class="nav-link"
-              id="samples-tab"
-              data-bs-toggle="tab"
-              data-bs-target="#samples-content"
-              type="button"
-              role="tab"
-              aria-controls="samples-content"
-              aria-selected="true">Samples</button></li>
-    <li class="nav-item presentation">
-      <button class="nav-link active"
-              id="phenotypes-tab"
-              data-bs-toggle="tab"
-              data-bs-target="#phenotypes-content"
-              type="button"
-              role="tab"
-              aria-controls="phenotypes-content"
-              aria-selected="false">Phenotypes</button></li>
-    {%if view_under_construction%}
-    <li class="nav-item presentation">
-      <button class="nav-link"
-              id="genotypes-tab"
-              data-bs-toggle="tab"
-              data-bs-target="#genotypes-content"
-              type="button"
-              role="tab"
-              aria-controls="genotypes-content"
-              aria-selected="false">Genotypes</button></li>
-    <li class="nav-item presentation">
-      <button class="nav-link"
-              id="expression-data-tab"
-              data-bs-toggle="tab"
-              data-bs-target="#expression-data-content"
-              type="button"
-              role="tab"
-              aria-controls="expression-data-content"
-              aria-selected="false">Expression-Data</button></li>
-    {%endif%}
-  </ul>
-</div>
-
-<div class="row">
-  <div class="tab-content" id="populations-tabs-content">
-    <div class="tab-pane fade"
-         id="samples-content"
-         role="tabpanel"
-         aria-labelledby="samples-content-tab">
-      <p>Think of a <strong>"sample"</strong> as say a single case or individual
-        in the experiment. It could even be a single strain (where applicable).
-      </p>
-      <p>This is a convenience feature for when you want to upload phenotypes to
-        the system, but do not have the genotypes data ready yet.</p>
-      <a href="{{url_for('species.populations.samples.list_samples',
-               species_id=species.SpeciesId,
-               population_id=population.Id)}}"
-         title="View and upload samples for population '{{population['Name']}}'"
-         class="btn btn-primary">Manage Samples</a>
-    </div>
-
-    <div class="tab-pane fade show active"
-         id="phenotypes-content"
-         role="tabpanel"
-         aria-labelledby="phenotypes-content-tab">
-
-      <div class="row" style="margin-top: 0.3em;">
-        <div class="col">
-          <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-                   species_id=species.SpeciesId,
-                   population_id=population.Id,
-                   dataset_id=dataset.Id)}}"
-             title="Upload phenotype data for population '{{population['Name']}}'"
-             class="btn btn-primary">Upload new Phenotypes</a>
-        </div>
-        <div class="col">
-          <a href="#"
-             title="List all existing publications for this population."
-             class="btn btn-primary not-implemented">view publications</a>
-          <!-- Maybe, actually filter publications by population? -->
-          <!-- Provide other features for publications on loaded page. -->
-        </div>
-      </div>
-
-      <div class="row" style="margin-top: 1em;">
-        <h3> Phenotypes in  Population "{{population.FullName}} ({{population.Name}})"</h3>
-
-        <p>The table below lists the phenotypes that already exist for
-          population "<em>{{population.FullName}} ({{population.Name}})</em>" of
-          species "<em>{{species.FullName}} ({{species.Name}})</em>".</p>
-
-        <div class="row phenotypes-list-actions">
-          <div class="col">
-            <form id="frm-recompute-phenotype-means"
-                  method="POST"
-                  action="{{url_for(
-                          'species.populations.phenotypes.recompute_means',
-                          species_id=species['SpeciesId'],
-                          population_id=population['Id'],
-                          dataset_id=dataset['Id'])}}">
-              <input id="submit-frm-recompute-phenotype-means"
-                     class="btn btn-info"
-                     type="submit"
-                     title="Compute/Recompute the means for selected phenotypes (or all phenotypes if none selected)."
-                     value="(Rec/C)ompute means" />
-            </form>
-          </div>
-          <div class="col">
-            <form id="frm-rerun-qtlreaper"
-                  method="POST"
-                  action="{{url_for(
-                          'species.populations.phenotypes.rerun_qtlreaper',
-                          species_id=species['SpeciesId'],
-                          population_id=population['Id'],
-                          dataset_id=dataset['Id'])}}">
-              <input id="submit-frm-rerun-qtlreaper"
-                     class="btn btn-info"
-                     type="submit"
-                     title="Run/Rerun QTLReaper for selected phenotypes (or all phenotypes if none selected)."
-                     value="(rer/r)un QTLReaper" />
-            </form>
-          </div>
-        </div>
-
-        <table id="tbl-phenotypes-list" class="table compact stripe cell-border">
-          <thead>
-            <tr>
-              <th></th>
-              <th>Index</th>
-              <th>Record</th>
-              <th>Description</th>
-            </tr>
-          </thead>
-
-          <tbody></tbody>
-        </table>
-      </div>
-    </div>
-
-    <div class="tab-pane fade"
-         id="genotypes-content"
-         role="tabpanel"
-         aria-labelledby="genotypes-content-tab">
-      <p>This allows you to upload the data that concerns your genotypes.</p>
-      <p>Any samples/individuals/cases/strains that do not already exist in the
-        system will be added. This does not delete any existing data.</p>
-      <a href="{{url_for('species.populations.genotypes.list_genotypes',
-               species_id=species.SpeciesId,
-               population_id=population.Id)}}"
-         title="Upload genotype information for the '{{population.FullName}}' population of the '{{species.FullName}}' species."
-         class="btn btn-primary">upload genotypes</a>
-    </div>
-    <div class="tab-pane fade" id="expression-data-content" role="tabpanel" aria-labelledby="expression-data-content-tab">
-      <p>Upload expression data (mRNA data) for this population.</p>
-      <a href="#" title="" class="btn btn-primary">upload genotypes</a>
-    </div>
-  </div>
-</div>
-{%endblock%}
-
-{%block sidebarcontents%}
-<div class="row">
-  <p>Each tab presents a feature that's available at the population level.
-    Select the tab that allows you to continue with your task.</p>
-</div>
-{{display_sui_population_card(species, population)}}
-{%endblock%}
-
-
-
-
-{%block javascript%}
-<script type="text/javascript" src="/static/js/urls.js"></script>
-
-<script type="text/javascript">
-  $(function() {
-      /** JS to build list of phenotypes table. **/
-      var species_id = {{species.SpeciesId}};
-      var population_id = {{population.Id}};
-      var dataset_id = {{dataset.Id}};
-      var dataset_name = "{{dataset.Name}}";
-      var data = {{phenotypes | tojson}};
-
-      var dtPhenotypesList = buildDataTable(
-          "#tbl-phenotypes-list",
-          data,
-          [
-              {
-                  data: function(pheno) {
-                      return `<input type="checkbox" name="selected-phenotypes" `
-                          + `id="chk-selected-phenotypes-${pheno.InbredSetCode}_${pheno.xref_id}" `
-                          + `value="${pheno.InbredSetCode}_${pheno.xref_id}" `
-                          + `class="chk-row-select" />`
-                  }
-              },
-              {data: "sequence_number"},
-              {
-                  data: function(pheno, type, set, meta) {
-                      var spcs_id = {{species.SpeciesId}};
-                      var pop_id = {{population.Id}};
-                      var dtst_id = {{dataset.Id}};
-                      var url = buildURLFromCurrentURL(
-                          (`/species/${spcs_id}` +
-                          `/populations/${pop_id}` +
-                          `/phenotypes/datasets/${dtst_id}` +
-                           `/phenotype/${pheno.xref_id}`));
-                      return `<a href="${url.toString()}" target="_blank">` +
-                          `${pheno.InbredSetCode}_${pheno.xref_id}` +
-                          `</a>`;
-                  }
-              },
-              {
-                  data: function(pheno) {
-                      return (pheno.Post_publication_description ||
-                              pheno.Original_description ||
-                              pheno.Pre_publication_description);
-                  }
-              }
-          ],
-          {
-              select: "multi+shift",
-              layout: {
-                  top1Start: {
-                      pageLength: {
-                          text: "Show _MENU_ of _TOTAL_"
-                      }
-                  },
-                  topStart: "info",
-                  top1End: null
-              },
-              rowId: function(pheno) {
-                  return `${pheno.InbredSetCode}_${pheno.xref_id}`;
-              }
-          });
-
-
-      $("#submit-frm-rerun-qtlreaper").on(
-          "click",
-          function(event) {
-              // (Re)run the QTLReaper script for selected phenotypes.
-              event.preventDefault();
-              var form = $("#frm-rerun-qtlreaper");
-              form.find(".dynamically-added-element").remove();
-              dtPhenotypesList.rows({selected: true}).nodes().each((node, index) => {
-                  _cloned = $(node).find(".chk-row-select").clone();
-                  _cloned.removeAttr("id");
-                  _cloned.removeAttr("class");
-                  _cloned.attr("style", "display: none;");
-                  _cloned.attr("data-type", "dynamically-added-element");
-                  _cloned.attr("class", "dynamically-added-element checkbox");
-                  _cloned.prop("checked", true);
-                  form.append(_cloned);
-              });
-              form.submit();
-          });
-  });
-</script>
-{%endblock%}
diff --git a/uploader/templates/populations/view-population.html b/uploader/templates/populations/view-population.html
index 3b9661b..ac89bc7 100644
--- a/uploader/templates/populations/view-population.html
+++ b/uploader/templates/populations/view-population.html
@@ -1,104 +1,127 @@
 {%extends "populations/base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "species/macro-select-species.html" import select_species_form%}
-{%from "species/macro-display-species-card.html" import display_species_card%}
-
-{%block title%}Populations{%endblock%}
-
-{%block pagetitle%}Populations{%endblock%}
-
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="view-population"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.view_population',
-           species_id=species.SpeciesId,
-           population_id=population.InbredSetId)}}">view</a>
-</li>
-{%endblock%}
-
+{%from "macro-step-indicator.html" import step_indicator%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
 
 {%block contents%}
 <div class="row">
-  <h2>Population Details</h2>
-
-  {{flash_all_messages()}}
-
-  <dl>
-    <dt>Name</dt>
-    <dd>{{population.Name}}</dd>
-
-    <dt>FullName</dt>
-    <dd>{{population.FullName}}</dd>
-
-    <dt>Code</dt>
-    <dd>{{population.InbredSetCode}}</dd>
-
-    <dt>Genetic Type</dt>
-    <dd>{{population.GeneticType}}</dd>
-
-    <dt>Family</dt>
-    <dd>{{population.Family}}</dd>
-
-    <dt>Information</dt>
-    <dd><a href="https://info.genenetwork.org/species/source.php?SpeciesName={{species.Name}}&InbredSetName={{population.Name}}"
-           title="Link to detailed information on this population."
-           target="_blank">Population Information</a></dd>
-  </dl>
+  <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2>
 </div>
 
 <div class="row">
-  … maybe provide a way to organise populations in the same family here …
+  <ul class="nav nav-tabs" id="population-actions">
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="samples-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#samples-content"
+              type="button"
+              role="tab"
+              aria-controls="samples-content"
+              aria-selected="true">Samples</button></li>
+    <li class="nav-item presentation">
+      <button class="nav-link active"
+              id="phenotypes-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#phenotypes-content"
+              type="button"
+              role="tab"
+              aria-controls="phenotypes-content"
+              aria-selected="false">Phenotypes</button></li>
+    {%if view_under_construction%}
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="genotypes-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#genotypes-content"
+              type="button"
+              role="tab"
+              aria-controls="genotypes-content"
+              aria-selected="false">Genotypes</button></li>
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="expression-data-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#expression-data-content"
+              type="button"
+              role="tab"
+              aria-controls="expression-data-content"
+              aria-selected="false">Expression-Data</button></li>
+    {%endif%}
+  </ul>
 </div>
 
 <div class="row">
-  <h3>Actions</h3>
-
-  <p>
-    Click any of the following links to use this population in performing the
-    subsequent operations.
-  </p>
-
-  <nav class="nav">
-    <ul>
-      <li>
-        <a href="{{url_for('species.populations.samples.list_samples',
-                 species_id=species.SpeciesId,
-                 population_id=population.Id)}}"
-           title="Manage samples: Add new or delete existing.">
-          manage samples</a>
-      </li>
-      <li>
-        <a href="{{url_for('species.populations.genotypes.list_genotypes',
-                 species_id=species.SpeciesId,
-                 population_id=population.Id)}}"
-           title="Manage genotypes for {{species.FullName}}">Manage Genotypes</a>
-      </li>
-      <li>
-        <a href="{{url_for('species.populations.phenotypes.list_datasets',
-                 species_id=species.SpeciesId,
-                 population_id=population.Id)}}"
-           title="Manage phenotype data.">manage phenotype data</a>
-      </li>
-      <li>
-        <a href="#" title="Manage expression data"
-           class="not-implemented">manage expression data</a>
-      </li>
-      <li>
-        <a href="#" title="Manage individual data"
-           class="not-implemented">manage individual data</a>
-      </li>
-      <li>
-        <a href="#" title="Manage RNA-Seq data"
-           class="not-implemented">manage RNA-Seq data</a>
-      </li>
-    </ul>
-  </nav>
+  <div class="tab-content" id="populations-tabs-content">
+    <div class="tab-pane fade"
+         id="samples-content"
+         role="tabpanel"
+         aria-labelledby="samples-content-tab">
+      <p>Think of a <strong>"sample"</strong> as say a single case or individual
+        in the experiment. It could even be a single strain (where applicable).
+      </p>
+      <p>This is a convenience feature for when you want to upload phenotypes to
+        the system, but do not have the genotypes data ready yet.</p>
+      <a href="{{url_for('species.populations.samples.list_samples',
+               species_id=species.SpeciesId,
+               population_id=population.Id)}}"
+         title="View and upload samples for population '{{population['Name']}}'"
+         class="btn btn-primary">Manage Samples</a>
+    </div>
+
+    <div class="tab-pane fade show active"
+         id="phenotypes-content"
+         role="tabpanel"
+         aria-labelledby="phenotypes-content-tab">
+
+      <div class="row" style="margin-top: 1em;">
+        <h3> Phenotypes in  Population "{{population.FullName}} ({{population.Name}})"</h3>
+
+        <p>To view existing phenotype traits, or upload new ones, click the button below:</p>
+
+        <div class="row">
+          <div class="col">
+            <a href="{{url_for(
+                     'species.populations.phenotypes.list_datasets',
+                     species_id=species.SpeciesId,
+                     population_id=population.Id)}}"
+               title="View and upload phenotype traits"
+               class="btn btn-primary">Phenotypes</a>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="tab-pane fade"
+         id="genotypes-content"
+         role="tabpanel"
+         aria-labelledby="genotypes-content-tab">
+      <p>This allows you to upload the data that concerns your genotypes.</p>
+      <p>Any samples/individuals/cases/strains that do not already exist in the
+        system will be added. This does not delete any existing data.</p>
+      <a href="{{url_for('species.populations.genotypes.list_genotypes',
+               species_id=species.SpeciesId,
+               population_id=population.Id)}}"
+         title="Upload genotype information for the '{{population.FullName}}' population of the '{{species.FullName}}' species."
+         class="btn btn-primary">upload genotypes</a>
+    </div>
+    <div class="tab-pane fade" id="expression-data-content" role="tabpanel" aria-labelledby="expression-data-content-tab">
+      <p>Upload expression data (mRNA data) for this population.</p>
+      <a href="#" title="" class="btn btn-primary">upload genotypes</a>
+    </div>
+  </div>
 </div>
 {%endblock%}
 
 {%block sidebarcontents%}
-{{display_species_card(species)}}
+<div class="row">
+  <p>Each tab presents a feature that's available at the population level.
+    Select the tab that allows you to continue with your task.</p>
+</div>
+{{super()}}
+{%endblock%}
+
+
+
+
+{%block javascript%}
 {%endblock%}
diff --git a/uploader/templates/publications/base.html b/uploader/templates/publications/base.html
index db80bfa..de0a350 100644
--- a/uploader/templates/publications/base.html
+++ b/uploader/templates/publications/base.html
@@ -1,12 +1,9 @@
 {%extends "base.html"%}
 
-{%block lvl1_breadcrumbs%}
-<li {%if activelink=="publications"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('publications.index')}}">Publications</a>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.index')}}"
+     title="Manage publications">Publications</a>
 </li>
-{%block lvl2_breadcrumbs%}{%endblock%}
 {%endblock%}
diff --git a/uploader/templates/publications/create-publication.html b/uploader/templates/publications/create-publication.html
index 3f828a9..fb0127d 100644
--- a/uploader/templates/publications/create-publication.html
+++ b/uploader/templates/publications/create-publication.html
@@ -3,7 +3,13 @@
 
 {%block title%}View Publication{%endblock%}
 
-{%block pagetitle%}View Publication{%endblock%}
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.create_publication', **get_args)}}"
+     title="Manage publications">create publication</a>
+</li>
+{%endblock%}
 
 
 {%block contents%}
@@ -12,7 +18,7 @@
 <div class="row">
   <form id="frm-create-publication"
         method="POST"
-        action="{{url_for('publications.create_publication', **request.args)}}"
+        action="{{url_for('publications.create_publication', **get_args)}}"
         class="form-horizontal">
 
     <div class="row mb-3">
@@ -152,11 +158,14 @@
       </div>
     </div>
 
-    <div class="row mb-3">
-      <div class="col-sm-2"></div>
-      <div class="col-sm-8">
-        <input type="submit" class="btn btn-primary" value="Add" />
-        <input type="reset" class="btn btn-danger" />
+    <div class="row">
+      <div class="col">
+        <input type="submit"
+               class="btn btn-primary"
+               value="create publication" />
+      </div>
+      <div class="col">
+        <input type="reset" class="btn btn-danger" value="reset form" />
       </div>
     </div>
 
diff --git a/uploader/templates/publications/delete-publication.html b/uploader/templates/publications/delete-publication.html
index 0ac93ec..a9c8c7c 100644
--- a/uploader/templates/publications/delete-publication.html
+++ b/uploader/templates/publications/delete-publication.html
@@ -1,9 +1,16 @@
 {%extends "publications/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 
-{%block title%}View Publication{%endblock%}
+{%block title%}Delete Publication{%endblock%}
 
-{%block pagetitle%}View Publication{%endblock%}
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.delete_publication',
+           publication_id=publication.Id)}}"
+     title="Manage publications">delete publication</a>
+</li>
+{%endblock%}
 
 
 {%block contents%}
diff --git a/uploader/templates/publications/edit-publication.html b/uploader/templates/publications/edit-publication.html
index 97fa134..314a78c 100644
--- a/uploader/templates/publications/edit-publication.html
+++ b/uploader/templates/publications/edit-publication.html
@@ -1,9 +1,16 @@
 {%extends "publications/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 
-{%block title%}View Publication{%endblock%}
-
-{%block pagetitle%}View Publication{%endblock%}
+{%block title%}Edit Publication{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.edit_publication',
+           publication_id=publication.Id)}}"
+     title="Edit the publication's details">edit publication</a>
+</li>
+{%endblock%}
 
 
 {%block contents%}
diff --git a/uploader/templates/publications/index.html b/uploader/templates/publications/index.html
index 369812b..eb2e81b 100644
--- a/uploader/templates/publications/index.html
+++ b/uploader/templates/publications/index.html
@@ -3,23 +3,29 @@
 
 {%block title%}Publications{%endblock%}
 
-{%block pagetitle%}Publications{%endblock%}
-
 
 {%block contents%}
 {{flash_all_messages()}}
 
 <div class="row" style="padding-bottom: 1em;">
-  <a href="{{url_for('publications.create_publication')}}"
-     class="btn btn-primary">
-    add new publication</a>
+  <div class="col">
+    <a href="{{url_for('publications.create_publication')}}"
+       class="btn btn-primary"
+       title="Create a new publication.">
+      add new publication</a>
+  </div>
+</div>
+
+<div class="row">
+  <p>Click on the title to view more details or to edit the information for that
+    publication.</p>
 </div>
 
 <div class="row">
   <table id="tbl-list-publications" class="table compact stripe">
     <thead>
       <tr>
-        <th>#</th>
+        <th>Index</th>
         <th>PubMed ID</th>
         <th>Title</th>
         <th>Authors</th>
@@ -33,6 +39,8 @@
 
 
 {%block javascript%}
+<script type="text/javascript" src="/static/js/urls.js"></script>
+
 <script type="text/javascript">
   $(function() {
       var publicationsDataTable = buildDataTable(
@@ -43,24 +51,25 @@
               {
                   searchable: true,
                   data: (pub) => {
-                  if(pub.PubMed_ID) {
-                      return `<a href="https://pubmed.ncbi.nlm.nih.gov/` +
-                          `${pub.PubMed_ID}/" target="_blank" ` +
-                          `title="Link to publication on NCBI.">` +
-                          `${pub.PubMed_ID}</a>`;
-                  }
-                  return "";
+                      if(pub.PubMed_ID) {
+                          return `<a href="https://pubmed.ncbi.nlm.nih.gov/` +
+                              `${pub.PubMed_ID}/" target="_blank" ` +
+                              `title="Link to publication on NCBI.">` +
+                              `${pub.PubMed_ID}</a>`;
+                      }
+                      return "";
                   }
               },
               {
                   searchable: true,
                   data: (pub) => {
-                  var title = "⸻";
-                  if(pub.Title) {
-                      title = pub.Title
-                  }
-                  return `<a href="/publications/view/${pub.Id}" ` +
-                          `target="_blank" ` +
+                      var title = "⸻";
+                      if(pub.Title) {
+                          title = pub.Title
+                      }
+                      url=buildURLFromCurrentURL(
+                          `/publications/view/${pub.Id}`);
+                      return `<a href="${url}" target="_blank" ` +
                           `title="Link to view publication details">` +
                           `${title}</a>`;
                   }
@@ -68,12 +77,12 @@
               {
                   searchable: true,
                   data: (pub) => {
-                  authors = pub.Authors.split(",").map(
-                      (item) => {return item.trim();});
-                  if(authors.length > 1) {
-                      return authors[0] + ", et. al.";
-                  }
-                  return authors[0];
+                      authors = pub.Authors.split(",").map(
+                          (item) => {return item.trim();});
+                      if(authors.length > 1) {
+                          return authors[0] + ", et. al.";
+                      }
+                      return authors[0];
                   }
               }
           ],
diff --git a/uploader/templates/publications/view-publication.html b/uploader/templates/publications/view-publication.html
index 0bd7bc5..01ccf1e 100644
--- a/uploader/templates/publications/view-publication.html
+++ b/uploader/templates/publications/view-publication.html
@@ -3,8 +3,6 @@
 
 {%block title%}View Publication{%endblock%}
 
-{%block pagetitle%}View Publication{%endblock%}
-
 
 {%block contents%}
 {{flash_all_messages()}}
diff --git a/uploader/templates/samples/base.html b/uploader/templates/samples/base.html
index 291782b..7fd5020 100644
--- a/uploader/templates/samples/base.html
+++ b/uploader/templates/samples/base.html
@@ -1,12 +1,25 @@
 {%extends "populations/base.html"%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
 
-{%block lvl3_breadcrumbs%}
-<li {%if activelink=="samples"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.samples.index')}}">Samples</a>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.populations.samples.list_samples',
+           species_id=species['SpeciesId'],
+           population_id=population.Id)}}">
+    Samples
+  </a>
 </li>
-{%block lvl4_breadcrumbs%}{%endblock%}
+{%endblock%}
+
+{%block contents%}
+<div class="row">
+  <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2>
+</div>
+{%endblock%}
+
+
+
+{%block sidebarcontents%}
+{{display_sui_population_card(species, population)}}
 {%endblock%}
diff --git a/uploader/templates/samples/list-samples.html b/uploader/templates/samples/list-samples.html
index aed27c3..3aac984 100644
--- a/uploader/templates/samples/list-samples.html
+++ b/uploader/templates/samples/list-samples.html
@@ -1,53 +1,34 @@
 {%extends "samples/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
 {%from "populations/macro-select-population.html" import select_population_form%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Samples &mdash; List Samples{%endblock%}
 
-{%block pagetitle%}Samples &mdash; List Samples{%endblock%}
-
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="list-samples"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.populations.samples.list_samples',
-           species_id=species.SpeciesId,
-           population_id=population.Id)}}">List</a>
-</li>
-{%endblock%}
-
 {%block contents%}
-{{flash_all_messages()}}
+{{super()}}
 
 <div class="row">
-  <p>
-    You selected the population "{{population.FullName}}" from the
-    "{{species.FullName}}" species.
-  </p>
+  <h3 class="subheading">manage samples</h3>
+  {{flash_all_messages()}}
 </div>
 
 <div class="row">
-  <p>
+  <div class="col">
     <a href="{{url_for('species.populations.samples.upload_samples',
              species_id=species.SpeciesId,
              population_id=population.Id)}}"
        title="Add samples for population '{{population.FullName}}' from species
               '{{species.FullName}}'."
-       class="btn btn-primary">
-      add samples
-    </a>
-  </p>
+       class="btn btn-primary">add new samples</a>
+  </div>
 </div>
 
 {%if samples | length > 0%}
 <div class="row">
   <p>
-    This population already has <strong>{{total_samples}}</strong>
-    samples/individuals entered. You can explore the list of samples in this
-    population in the table below.
+    Population "{{population.FullName}} ({{population.Name}})" already has
+    <strong>{{total_samples}}</strong> samples/individuals entered. You can
+    explore the list of samples in the table below.
   </p>
 </div>
 
@@ -106,15 +87,6 @@
       {%endfor%}
     </tbody>
   </table>
-
-  <p>
-    <a href="#"
-       title="Delete samples from population '{{population.FullName}}' from species
-              '{{species.FullName}}'."
-       class="btn btn-danger not-implemented">
-      delete all samples
-    </a>
-  </p>
 </div>
 {%else%}
 <div class="row">
@@ -124,7 +96,3 @@
 {%endif%}
 
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/samples/upload-failure.html b/uploader/templates/samples/upload-failure.html
index 2cf8053..75192ec 100644
--- a/uploader/templates/samples/upload-failure.html
+++ b/uploader/templates/samples/upload-failure.html
@@ -1,6 +1,5 @@
 {%extends "base.html"%}
 {%from "cli-output.html" import cli_output%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Samples Upload Failure{%endblock%}
 
@@ -31,7 +30,3 @@
 {{cli_output(job, "stderr")}}
 </div>
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/samples/upload-progress.html b/uploader/templates/samples/upload-progress.html
index 677d457..38f931b 100644
--- a/uploader/templates/samples/upload-progress.html
+++ b/uploader/templates/samples/upload-progress.html
@@ -1,6 +1,5 @@
 {%extends "samples/base.html"%}
 {%from "cli-output.html" import cli_output%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block extrameta%}
 <meta http-equiv="refresh" content="5">
@@ -25,7 +24,3 @@
 </div>
 
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/samples/upload-samples.html b/uploader/templates/samples/upload-samples.html
index 6422094..1f665a3 100644
--- a/uploader/templates/samples/upload-samples.html
+++ b/uploader/templates/samples/upload-samples.html
@@ -1,21 +1,16 @@
 {%extends "samples/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
-{%from "populations/macro-select-population.html" import select_population_form%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Samples &mdash; Upload Samples{%endblock%}
 
-{%block pagetitle%}Samples &mdash; Upload Samples{%endblock%}
-
-{%block lvl4_breadcrumbs%}
-<li {%if activelink=="uploade-samples"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
   <a href="{{url_for('species.populations.samples.upload_samples',
-           species_id=species.SpeciesId,
-           population_id=population.Id)}}">List</a>
+           species_id=species['SpeciesId'],
+           population_id=population.Id)}}">
+    Upload
+  </a>
 </li>
 {%endblock%}
 
@@ -23,35 +18,6 @@
 {{flash_all_messages()}}
 
 <div class="row">
-  <p>
-    You can now upload the samples for the "{{population.FullName}}" population
-    from the "{{species.FullName}}" species here.
-  </p>
-  <p>
-    Upload a <strong>character-separated value (CSV)</strong> file that contains
-    details about your samples. The CSV file should have the following fields:
-    <dl>
-      <dt>Name</dt>
-      <dd>The primary name/identifier for the sample/individual.</dd>
-
-      <dt>Name2</dt>
-      <dd>A secondary name for the sample. This can simply be the same as
-        <strong>Name</strong> above. This field <strong>MUST</strong> contain a
-        value.</dd>
-
-      <dt>Symbol</dt>
-      <dd>A symbol for the sample. This can be a strain name, e.g. 'BXD60' for
-        species that have strains. This field can be left empty for species like
-        Humans that do not have strains..</dd>
-
-      <dt>Alias</dt>
-      <dd>An alias for the sample. Can be an empty field, or take on the same
-        value as that of the Symbol.</dd>
-    </dl>
-  </p>
-</div>
-
-<div class="row">
   <form id="form-samples"
         method="POST"
         action="{{url_for('species.populations.samples.upload_samples',
@@ -65,14 +31,17 @@
 
     <div class="form-group">
       <label for="file-samples" class="form-label">select file</label>
-      <input type="file" name="samples_file" id="file:samples"
+      <input type="file" name="samples_file" id="file-samples"
 	     accept="text/csv, text/tab-separated-values, text/plain"
 	     class="form-control" />
+      <small class="form-text text-muted">
+        See the <a href="#docs-samples-upload">documentation below</a> for
+        details on expected file format.</small>
     </div>
 
     <div class="form-group">
-      <label for="select:separator" class="form-label">field separator</label>
-      <select id="select:separator"
+      <label for="select-separator" class="form-label">field separator</label>
+      <select id="select-separator"
 	      name="separator"
 	      required="required"
 	      class="form-control">
@@ -83,7 +52,7 @@
         <option value=";">Semicolon</option>
         <option value="other">Other</option>
       </select>
-      <input id="txt:separator"
+      <input id="txt-separator"
 	     type="text"
 	     name="other_separator"
 	     class="form-control" />
@@ -95,11 +64,11 @@
     </div>
 
     <div class="form-group form-check">
-      <input id="chk:heading"
+      <input id="chk-heading"
 	     type="checkbox"
 	     name="first_line_heading"
 	     class="form-check-input" />
-      <label for="chk:heading" class="form-check-label">
+      <label for="chk-heading" class="form-check-label">
         first line is a heading?</label>
       <small class="form-text text-muted">
         Select this if the first line in your file contains headings for the
@@ -108,8 +77,8 @@
     </div>
 
     <div class="form-group">
-      <label for="txt:delimiter" class="form-label">field delimiter</label>
-      <input id="txt:delimiter"
+      <label for="txt-delimiter" class="form-label">field delimiter</label>
+      <input id="txt-delimiter"
 	     type="text"
 	     name="field_delimiter"
 	     maxlength="1"
@@ -149,10 +118,34 @@
     </tbody>
   </table>
 </div>
-{%endblock%}
 
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
+
+
+<div class="row" id="docs-samples-upload">
+  <h3 class="subheading">File Format</h3>
+  <p>
+    Upload a <strong>character-separated value (CSV)</strong> file that contains
+    details about your samples. The CSV file should have the following fields:
+    <dl>
+      <dt>Name</dt>
+      <dd>The primary name/identifier for the sample/individual.</dd>
+
+      <dt>Name2</dt>
+      <dd>A secondary name for the sample. This can simply be the same as
+        <strong>Name</strong> above. This field <strong>MUST</strong> contain a
+        value.</dd>
+
+      <dt>Symbol</dt>
+      <dd>A symbol for the sample. This can be a strain name, e.g. 'BXD60' for
+        species that have strains. This field can be left empty for species like
+        Humans that do not have strains..</dd>
+
+      <dt>Alias</dt>
+      <dd>An alias for the sample. Can be an empty field, or take on the same
+        value as that of the Symbol.</dd>
+    </dl>
+  </p>
+</div>
 {%endblock%}
 
 {%block javascript%}
diff --git a/uploader/templates/samples/upload-success.html b/uploader/templates/samples/upload-success.html
index 881d466..d6318e9 100644
--- a/uploader/templates/samples/upload-success.html
+++ b/uploader/templates/samples/upload-success.html
@@ -1,6 +1,5 @@
 {%extends "samples/base.html"%}
 {%from "cli-output.html" import cli_output%}
-{%from "populations/macro-display-population-card.html" import display_population_card%}
 
 {%block title%}Job Status{%endblock%}
 
@@ -30,7 +29,3 @@
 </div>
 
 {%endblock%}
-
-{%block sidebarcontents%}
-{{display_population_card(species, population)}}
-{%endblock%}
diff --git a/uploader/templates/species/base.html b/uploader/templates/species/base.html
index f64f72b..3be79f0 100644
--- a/uploader/templates/species/base.html
+++ b/uploader/templates/species/base.html
@@ -1,17 +1,12 @@
 {%extends "base.html"%}
 
-{%block lvl1_breadcrumbs%}
-<li {%if activelink=="species"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  {%if species is mapping%}
-  <a href="{{url_for('species.view_species', species_id=species.SpeciesId)}}">
-    {{species.Name}}</a>
-  {%else%}
-  <a href="{{url_for('species.list_species')}}">Species</a>
-  {%endif%}
+{%block breadcrumbs%}
+{{super()}}
+{%if species%}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.view_species', species_id=species['SpeciesId'])}}">
+    {{species["Name"]|title}}
+  </a>
 </li>
-{%block lvl2_breadcrumbs%}{%endblock%}
+{%endif%}
 {%endblock%}
diff --git a/uploader/templates/species/sui-base.html b/uploader/templates/species/sui-base.html
deleted file mode 100644
index f7b4fef..0000000
--- a/uploader/templates/species/sui-base.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{%extends "sui-base.html"%}
-
-{%block breadcrumbs%}
-{{super()}}
-<li class="breadcrumb-item">
-  <a href="{{url_for('species.view_species', species_id=species['SpeciesId'])}}">
-    {{species["Name"]|title}}
-  </a>
-</li>
-{%endblock%}
diff --git a/uploader/templates/species/sui-view-species.html b/uploader/templates/species/sui-view-species.html
deleted file mode 100644
index 4b6402e..0000000
--- a/uploader/templates/species/sui-view-species.html
+++ /dev/null
@@ -1,127 +0,0 @@
-{%extends "species/sui-base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-forms.html" import add_http_feature_flags%}
-{%from "macro-step-indicator.html" import step_indicator%}
-{%from "species/macro-display-species-card.html" import display_sui_species_card%}
-
-{%block title%}View Species{%endblock%}
-
-{%macro add_form_buttons()%}
-<div class="row form-buttons">
-  <div class="col">
-    <input type="submit"
-           value="use selected population"
-           class="btn btn-primary" />
-  </div>
-
-  <div class="col">
-    <a href="url_for('species.population.create_population',
-             species_id=species.SpeciesId,
-             return_to='species.view_species')"
-       title="Create a new population for species '{{species.Name}}'."
-       class="btn btn-outline-info">
-      Create a new population
-    </a>
-  </div>
-</div>
-{%endmacro%}
-
-
-{%block contents%}
-<div class="row">
-  <h2 class="heading">{{species.FullName}} ({{species.Name}})</h2>
-</div>
-
-<div class "row">
-  <ul class="nav nav-tabs" id="species-actions">
-    <li class="nav-item presentation">
-      <button class="nav-link active"
-              id="populations-tab"
-              data-bs-toggle="tab"
-              data-bs-target="#populations-content"
-              type="button"
-              role="tab"
-              aria-controls="populations-content"
-              aria-selected="true">Populations</button>
-    </li>
-    <li class="nav-item presentation">
-      <button class="nav-link"
-              id="sequencing-platforms-tab"
-              data-bs-toggle="tab"
-              data-bs-target="#sequencing-platforms-content"
-              type="button"
-              role="tab"
-              aria-controls="sequencing-platforms-content"
-              aria-selected="true">Sequencing Platforms</button>
-    </li>
-  </ul>
-</div>
-
-<div class="row">
-  <div class="tab-content" id="species-tabs-content">
-    <div class="tab-pane fade show active"
-         id="populations-content"
-         role="tabpanel"
-         aria-labelledby="populations-content-tab">
-      <p>Data belonging to a particular species is further divided into one or more
-        populations for easier handling. Please select the population you want to work
-        with.</p>
-
-      <form method="GET"
-            action="{{url_for('species.view_species', species_id=species.SpeciesId)}}"
-            class="form-horizontal">
-        {{add_http_feature_flags()}}
-        {{add_form_buttons()}}
-
-        {%if populations | length != 0%}
-        <div style="margin-top:0.3em;">
-          <table id="tbl-select-population" class="table compact stripe"
-                 data-populations-list='{{populations | tojson}}'>
-            <thead>
-              <tr>
-                <th></th>
-                <th>Population</th>
-              </tr>
-            </thead>
-
-            <tbody></tbody>
-          </table>
-        </div>
-
-        {%else%}
-        <p class="form-text">
-          There are no populations currently defined for {{species['FullName']}}
-          ({{species['SpeciesName']}}).</p>
-        {%endif%}
-
-        {{add_form_buttons()}}
-
-      </form>
-    </div>
-    <div class="tab-pane fade"
-         id="sequencing-platforms-content"
-         role="tabpanel"
-         aria-labelledby="sequencing-platforms-content-tab">
-      <p>Upload and manage the sequencing platforms for species
-        '{{species.Name | title}} ({{species.FullName}})'
-        <a href="{{url_for('species.platforms.list_platforms',
-                 species_id=species.SpeciesId)}}"
-           title="Manage sequencing platforms for {{species.Name}}">here</a>.
-      </p>
-    </div>
-  </div>
-</div>
-{%endblock%}
-
-{%block sidebarcontents%}
-<div class="row">
-  <p>You can manage species' populations and sequencing platforms here. Select
-    the tab for the feature you wish to continue working on.</p>
-</div>
-{{display_sui_species_card(species)}}
-{%endblock%}
-
-
-{%block javascript%}
-<script type="text/javascript" src="/static/js/populations.js"></script>
-{%endblock%}
diff --git a/uploader/templates/species/view-species.html b/uploader/templates/species/view-species.html
index 2d02f7e..81608fc 100644
--- a/uploader/templates/species/view-species.html
+++ b/uploader/templates/species/view-species.html
@@ -1,90 +1,127 @@
 {%extends "species/base.html"%}
 {%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-forms.html" import add_http_feature_flags%}
+{%from "macro-step-indicator.html" import step_indicator%}
+{%from "species/macro-display-species-card.html" import display_sui_species_card%}
 
 {%block title%}View Species{%endblock%}
 
-{%block pagetitle%}View Species{%endblock%}
+{%macro add_form_buttons()%}
+<div class="row form-buttons">
+  <div class="col">
+    <input type="submit"
+           value="use selected population"
+           class="btn btn-primary" />
+  </div>
+
+  <div class="col">
+    <a href="{{url_for('species.populations.create_population',
+             species_id=species.SpeciesId,
+             return_to='species.view_species')}}"
+       title="Create a new population for species '{{species.Name}}'."
+       class="btn btn-outline-info">
+      Create a new population
+    </a>
+  </div>
+</div>
+{%endmacro%}
 
-{%block lvl2_breadcrumbs%}
-<li {%if activelink=="view-species"%}
-    class="breadcrumb-item active"
-    {%else%}
-    class="breadcrumb-item"
-    {%endif%}>
-  <a href="{{url_for('species.view_species', species_id=species.SpeciesId)}}">View</a>
-</li>
-{%endblock%}
 
 {%block contents%}
-{{flash_all_messages()}}
 <div class="row">
-  <h2>Details on species {{species.FullName}}</h2>
+  <h2 class="heading">{{species.FullName}} ({{species.Name}})</h2>
+</div>
 
-  <dl>
-    <dt>Common Name</dt>
-    <dd>{{species.SpeciesName}}</dd>
+<div class "row">
+  <ul class="nav nav-tabs" id="species-actions">
+    <li class="nav-item presentation">
+      <button class="nav-link active"
+              id="populations-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#populations-content"
+              type="button"
+              role="tab"
+              aria-controls="populations-content"
+              aria-selected="true">Populations</button>
+    </li>
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="sequencing-platforms-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#sequencing-platforms-content"
+              type="button"
+              role="tab"
+              aria-controls="sequencing-platforms-content"
+              aria-selected="true">Sequencing Platforms</button>
+    </li>
+  </ul>
+</div>
 
-    <dt>Scientific Name</dt>
-    <dd>{{species.FullName}}</dd>
+<div class="row">
+  <div class="tab-content" id="species-tabs-content">
+    <div class="tab-pane fade show active"
+         id="populations-content"
+         role="tabpanel"
+         aria-labelledby="populations-content-tab">
+      <p>Data belonging to a particular species is further divided into one or more
+        populations for easier handling. Please select the population you want to work
+        with.</p>
 
-    <dt>Taxonomy ID</dt>
-    <dd>{{species.TaxonomyId}}</dd>
-  </dl>
+      <form method="GET"
+            action="{{url_for('species.view_species', species_id=species.SpeciesId)}}"
+            class="form-horizontal">
+        {{add_http_feature_flags()}}
+        {{add_form_buttons()}}
 
-  <h3>Actions</h3>
+        {%if populations | length != 0%}
+        <div style="margin-top:0.3em;">
+          <table id="tbl-select-population" class="table compact stripe"
+                 data-populations-list='{{populations | tojson}}'>
+            <thead>
+              <tr>
+                <th></th>
+                <th>Population</th>
+              </tr>
+            </thead>
 
-  <p>
-    You can proceed to perform any of the following actions for species
-    {{species.FullName}}
-  </p>
+            <tbody></tbody>
+          </table>
+        </div>
 
-  <ol>
-    <li>
-      <a href="{{url_for('species.populations.list_species_populations',
-               species_id=species.SpeciesId)}}"
-         title="Create/Edit populations for {{species.FullName}}">
-        Manage populations</a>
-    </li>
-    <li>
-      <a href="{{url_for('species.platforms.list_platforms',
-               species_id=species.SpeciesId)}}"
-         title="Create/Edit sequencing platforms for {{species.FullName}}">
-        Manage sequencing platforms</a>
-    </li>
-  </ol>
+        {%else%}
+        <p class="form-text">
+          There are no populations currently defined for {{species['FullName']}}
+          ({{species['SpeciesName']}}).</p>
+        {%endif%}
 
-  
+        {{add_form_buttons()}}
+
+      </form>
+    </div>
+    <div class="tab-pane fade"
+         id="sequencing-platforms-content"
+         role="tabpanel"
+         aria-labelledby="sequencing-platforms-content-tab">
+      <p>Upload and manage the sequencing platforms for species
+        '{{species.Name | title}} ({{species.FullName}})'
+        <a href="{{url_for('species.platforms.list_platforms',
+                 species_id=species.SpeciesId)}}"
+           title="Manage sequencing platforms for {{species.Name}}">here</a>.
+      </p>
+    </div>
+  </div>
 </div>
 {%endblock%}
 
 {%block sidebarcontents%}
-<div class="card">
-  <div class="card-body">
-    <h5 class="card-title">Species Extras</h5>
-    <div class="card-text">
-      <p>Some extra internal-use details (mostly for UI concerns on GeneNetwork)</p>
-      <p>
-        <small>
-          If you do not understand what the following are about, simply ignore them
-          &mdash;
-          They have no bearing whatsoever on your data, or its analysis.
-        </small>
-      </p>
-      <dl>
-        <dt>Family</dt>
-        <dd>{{species.Family}}</dd>
+<div class="row">
+  <p>You can manage species' populations and sequencing platforms here. Select
+    the tab for the feature you wish to continue working on.</p>
+</div>
+{{display_sui_species_card(species)}}
+{%endblock%}
 
-        <dt>FamilyOrderId</dt>
-        <dd>{{species.FamilyOrderId}}</dd>
 
-        <dt>OrderId</dt>
-        <dd>{{species.OrderId}}</dd>
-      </dl>
-    </div>
-    <a href="{{url_for('species.edit_species_extra',
-             species_id=species.SpeciesId)}}"
-       class="card-link"
-       title="Edit the species' internal-use details.">Edit</a>
-  </div>
-</div>
+{%block javascript%}
+<script type="text/javascript" src="/static/js/populations.js"></script>
 {%endblock%}
diff --git a/uploader/templates/sui-base.html b/uploader/templates/sui-base.html
deleted file mode 100644
index 719a646..0000000
--- a/uploader/templates/sui-base.html
+++ /dev/null
@@ -1,103 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-  <head>
-
-    <meta charset="UTF-8" />
-    <meta application-name="GeneNetwork Quality-Control Application" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    {%block extrameta%}{%endblock%}
-
-    <title>Data Upload and Quality Control: {%block title%}{%endblock%}</title>
-
-    <link rel="stylesheet" type="text/css"
-	  href="{{url_for('base.bootstrap',
-                filename='css/bootstrap.min.css')}}" />
-    <link rel="stylesheet" type="text/css"
-          href="{{url_for('base.datatables',
-                filename='css/dataTables.bootstrap5.min.css')}}" />
-    <link rel="stylesheet" type="text/css" href="/static/css/layout-common.css" />
-    <link rel="stylesheet" type="text/css" href="/static/css/layout-large.css" />
-    <link rel="stylesheet" type="text/css" href="/static/css/layout-medium.css" />
-    <link rel="stylesheet" type="text/css" href="/static/css/layout-small.css" />
-    <link rel="stylesheet" type="text/css" href="/static/css/theme.css" />
-
-    {%block css%}{%endblock%}
-
-  </head>
-
-  <body>
-    <header id="header">
-      <span id="header-text">GeneNetwork</span>
-      <nav id="header-nav">
-        <ul class="nav justify-content-end">
-          <li>
-            {%if user_logged_in()%}
-            <a href="{{url_for('oauth2.logout')}}"
-               title="Log out of the system">
-              <span class="glyphicon glyphicon-user"></span>
-              {{user_email()}} Sign Out</a>
-            {%else%}
-            <a href="{{authserver_authorise_uri()}}"
-               title="Log in to the system">Sign In</a>
-            {%endif%}
-          </li>
-        </ul>
-      </nav>
-    </header>
-
-
-    <main id="main" class="main">
-      <nav id="breadcrumbs" aria-label="breadcrumb">
-        <ol class="breadcrumb">
-          {%block breadcrumbs%}
-          <li class="breadcrumb-item">
-            <a href="{{url_for('base.index')}}">Home</a></li>
-          {%endblock%}
-        </ol>
-      </nav>
-
-      <div id="main-content">
-          {%block contents%}{%endblock%}
-        </div>
-
-      <div id="sidebar-content">
-          {%block sidebarcontents%}{%endblock%}
-        </div>
-    </main>
-
-
-
-    <script type="text/javascript" src="/static/js/debug.js"></script>
-    <!--
-        Core dependencies
-      -->
-    <script src="{{url_for('base.jquery',
-                 filename='jquery.min.js')}}"></script>
-    <script src="{{url_for('base.bootstrap',
-                 filename='js/bootstrap.min.js')}}"></script>
-
-    <!--
-        DataTables dependencies
-      -->
-    <script type="text/javascript"
-            src="{{url_for('base.datatables',
-                 filename='js/dataTables.min.js')}}"></script>
-    <script type="text/javascript"
-        src="{{url_for('base.datatables_extensions',
-             filename='scroller/js/dataTables.scroller.min.js')}}"></script>
-    <script type="text/javascript"
-            src="{{url_for('base.datatables_extensions',
-                 filename='buttons/js/dataTables.buttons.min.js')}}"></script>
-    <script type="text/javascript"
-            src="{{url_for('base.datatables_extensions',
-                 filename='select/js/dataTables.select.min.js')}}"></script>
-
-    <!--
-        local dependencies
-      -->
-    <script type="text/javascript" src="/static/js/utils.js"></script>
-    <script type="text/javascript" src="/static/js/datatables.js"></script>
-    {%block javascript%}{%endblock%}
-  </body>
-</html>
diff --git a/uploader/templates/sui-index.html b/uploader/templates/sui-index.html
deleted file mode 100644
index 888823f..0000000
--- a/uploader/templates/sui-index.html
+++ /dev/null
@@ -1,123 +0,0 @@
-{%extends "sui-base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "macro-forms.html" import add_http_feature_flags%}
-{%from "macro-step-indicator.html" import step_indicator%}
-
-{%block title%}Home{%endblock%}
-
-{%block pagetitle%}Home{%endblock%}
-
-{%block extra_breadcrumbs%}{%endblock%}
-
-{%block contents%}
-
-{%macro add_form_buttons()%}
-<div class="row form-buttons">
-      <div class="col">
-        <input type="submit"
-               class="btn btn-primary"
-               value="use selected species" />
-      </div>
-      <div class="col">
-        <a href="{{url_for('species.create_species', return_to='base.index')}}"
-           class="btn btn-outline-primary"
-           title="Create a new species.">Create a new Species</a>
-      </div>
-</div>
-{%endmacro%}
-
-<div class="row">{{flash_all_messages()}}</div>
-
-{%if user_logged_in()%}
-
-<div class="row">
-  <div class="row">
-    <h2 class="heading">Species</h2>
-
-    <p>Select the species you want to work with.</p>
-  </div>
-</div>
-
-<div class="row">
-  <form method="GET" action="{{url_for('base.index')}}" class="form-horizontal">
-    {{add_http_feature_flags()}}
-
-    {{add_form_buttons()}}
-
-    {%if species | length != 0%}
-    <div style="margin-top:1em;">
-      <table id="tbl-select-species" class="table compact stripe"
-             data-species-list='{{species | tojson}}'>
-        <thead>
-          <tr>
-            <th></th>
-            <th>Species Name</th>
-          </tr>
-        </thead>
-
-        <tbody></tbody>
-      </table>
-    </div>
-
-    {%else%}
-
-    <label class="control-label" for="rdo-cant-find-species">
-      <input id="rdo-cant-find-species" type="radio" name="species_id"
-             value="CREATE-SPECIES" />
-      There are no species to select from. Create the first one.</label>
-
-    <div class="col-sm-offset-10 col-sm-2">
-      <input type="submit"
-             class="btn btn-primary col-sm-offset-1"
-             value="continue" />
-    </div>
-
-    {%endif%}
-
-    {{add_form_buttons()}}
-
-  </form>
-</div>
-
-{%else%}
-
-<div class="row">
-  <p>The Genenetwork Uploader (<em>gn-uploader</em>) enables upload of new data
-    into the Genenetwork System. It provides Quality Control over data, and
-    guidance in case you data does not meet the standards for acceptance.</p>
-  <p>
-    <a href="{{authserver_authorise_uri()}}"
-       title="Sign in to the system"
-       class="btn btn-primary">Sign in</a>
-    to get started.</p>
-</div>
-{%endif%}
-
-{%endblock%}
-
-
-
-{%block sidebarcontents%}
-<div class="row">
-  <p>The data in Genenetwork is related to one species or another. Use the form
-    provided to select from existing species, or click on the
-    "Create a New Species" button if you cannot find the species you want to
-    work with.</p>
-</div>
-<div class="row">
-  <form id="frm-quick-navigation">
-    <legend>Quick Navigation</legend>
-    <div class="form-group">
-      <label for="fqn-species-id">Species</label>
-      <select name="species_id">
-        <option value="">Select species</option>
-      </select>
-    </div>
-  </form>
-</div>
-{%endblock%}
-
-
-{%block javascript%}
-<script type="text/javascript" src="/static/js/species.js"></script>
-{%endblock%}
diff --git a/uploader/ui.py b/uploader/ui.py
index 1994056..41791c7 100644
--- a/uploader/ui.py
+++ b/uploader/ui.py
@@ -1,5 +1,5 @@
 """Utilities to handle the UI"""
-from flask import render_template as flask_render_template
+from uploader.flask_extensions import render_template as flask_render_template
 
 def make_template_renderer(default):
     """Render template for species."""