about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.dev/run-checks.sh2
-rw-r--r--.gitignore3
-rw-r--r--.guix-channel53
-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/entry.py2
-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.py44
-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__.py27
-rw-r--r--uploader/background_jobs.py135
-rw-r--r--uploader/base_routes.py11
-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/oauth2/views.py14
-rw-r--r--uploader/phenotypes/misc.py2
-rw-r--r--uploader/phenotypes/models.py220
-rw-r--r--uploader/phenotypes/views.py216
-rw-r--r--uploader/population/rqtl2.py6
-rw-r--r--uploader/population/views.py37
-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.css28
-rw-r--r--uploader/static/js/datatables.js74
-rw-r--r--uploader/static/js/upload_samples.js24
-rw-r--r--uploader/static/js/urls.js26
-rw-r--r--uploader/static/js/utils.js3
-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/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.html20
-rw-r--r--uploader/templates/genotypes/view-dataset.html16
-rw-r--r--uploader/templates/index.html229
-rw-r--r--uploader/templates/login.html12
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-base.html29
-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/job-status.html25
-rw-r--r--uploader/templates/phenotypes/load-phenotypes-success.html20
-rw-r--r--uploader/templates/phenotypes/macro-display-pheno-dataset-card.html28
-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/view-dataset.html95
-rw-r--r--uploader/templates/phenotypes/view-phenotype.html19
-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.html138
-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.html17
-rw-r--r--uploader/templates/publications/index.html57
-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.html19
-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
116 files changed, 2453 insertions, 1825 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/.gitignore b/.gitignore
index 91958fd..0d28418 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@
 /build
 /dist
 /bdist
-/*.egg-info
\ No newline at end of file
+/*.egg-info
+/flask_session/**
\ No newline at end of file
diff --git a/.guix-channel b/.guix-channel
index e8ea99d..f1a8fa6 100644
--- a/.guix-channel
+++ b/.guix-channel
@@ -4,18 +4,57 @@
  (keyring-reference "keyring")
  (dependencies
   (channel
+   (name gn-machines)
+   (url "https://git.genenetwork.org/gn-machines")
+   (branch "main"))
+  ;; Until https://issues.guix.gnu.org/68797 is resolved, we need to
+  ;; explicitly list guix-bioinformatics, guix-forge, guix-past and
+  ;; guix-rust-past-crates—the dependencies of the gn-machines channel—here.
+  (channel
+   (name guix)
+   (url "https://codeberg.org/guix/guix")
+   (branch "master")
+   (commit "0a4740705090acc4c8a10d4f53afc58c9f62e980")
+   (introduction
+    (channel-introduction
+     (version 0)
+     (commit "9edb3f66fd807b096b48283debdcddccfea34bad")
+     (signer
+      "BBB0 2DDF 2CEA F6A8 0D1D  E643 A2A0 6DF2 A33A 54FA"))))
+  (channel
+   (name guix-forge)
+   (url "https://git.systemreboot.net/guix-forge/")
+   (branch "main")
+   (commit "e43fd9a4d73654d3876e2c698af7da89f3408f89")
+   (introduction
+    (channel-introduction
+     (version 0)
+     (commit "0432e37b20dd678a02efee21adf0b9525a670310")
+     (signer
+      "7F73 0343 F2F0 9F3C 77BF  79D3 2E25 EE8B 6180 2BB3"))))
+  (channel
    (name guix-bioinformatics)
    (url "https://git.genenetwork.org/guix-bioinformatics")
-   (branch "master"))
-  ;; FIXME: guix-bioinformatics depends on guix-past. So, there
-  ;; should be no reason to explicitly depend on guix-past. But, the
-  ;; channel does not build otherwise. This is probably a guix bug.
+   (commit "9b0955f14ec725990abb1f6af3b9f171e4943f77"))
   (channel
    (name guix-past)
-   (url "https://gitlab.inria.fr/guix-hpc/guix-past")
+   (url "https://codeberg.org/guix-science/guix-past")
+   (branch "master")
+   (commit "473c942b509ab3ead35159d27dfbf2031a36cd4d")
+   (introduction
+    (channel-introduction
+     (version 0)
+     (commit "c3bc94ee752ec545e39c1b8a29f739405767b51c")
+     (signer
+      "3CE4 6455 8A84 FDC6 9DB4  0CFB 090B 1199 3D9A EBB5"))))
+  (channel
+   (name guix-rust-past-crates)
+   (url "https://codeberg.org/guix/guix-rust-past-crates.git")
+   (branch "trunk")
+   (commit "b8b7ffbd1cec9f56f93fae4da3a74163bbc9c570")
    (introduction
     (channel-introduction
      (version 0)
-     (commit "0c119db2ea86a389769f4d2b9c6f5c41c027e336")
+     (commit "1db24ca92c28255b28076792b93d533eabb3dc6a")
      (signer
-      "3CE4 6455 8A84 FDC6 9DB4  0CFB 090B 1199 3D9A EBB5"))))))
+      "F4C2 D1DF 3FDE EA63 D1D3 0776 ACC6 6D09 CA52 8292"))))))
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/entry.py b/scripts/rqtl2/entry.py
index e0e00e7..7423a4b 100644
--- a/scripts/rqtl2/entry.py
+++ b/scripts/rqtl2/entry.py
@@ -26,7 +26,7 @@ def build_main(
     def main():
         with (Redis.from_url(args.redisuri, decode_responses=True) as rconn,
               database_connection(args.databaseuri) as dbconn):
-            logger.setLevel(args.loglevel.upper())
+            logger.setLevel(args.log_level.upper())
             fqjobid = jobs.job_key(args.redisprefix, args.jobid)
 
             try:
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..54e5d45 100644
--- a/scripts/run_qtlreaper.py
+++ b/scripts/run_qtlreaper.py
@@ -4,11 +4,10 @@ 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 +56,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 +65,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 +79,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 +96,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 +131,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,7 +145,7 @@ 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",
@@ -157,26 +156,27 @@ def dispatch(args: Namespace) -> int:
                 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())
+                    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())
             save_qtl_values_to_db(conn, results)
             logger.debug("Cleaning up temporary files.")
             _traitsfile.unlink()
             _qtlreaper_main_output.unlink()
             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 +205,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..46689c5 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"]))
 
diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py
index d33c498..a71dd44 100644
--- a/uploader/background_jobs.py
+++ b/uploader/background_jobs.py
@@ -1,35 +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 (
-    url_for,
+    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 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."""
@@ -45,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(".")
@@ -61,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)
 
@@ -76,7 +92,8 @@ def handler(job: dict, handler_type: str) -> HandlerType:
     ).get(handler_type)
     if bool(_handler):
         return _handler(job)
-    return render_template("background-jobs/default-success-page.html", job=job)
+
+    return __default_handler__(job)
 
 
 error_handler = partial(handler, handler_type="error")
@@ -93,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("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(
-                "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>")
@@ -116,3 +133,93 @@ def job_error(job_id: uuid.UUID):
             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("jobs/job-not-found.html", job_id=job_id)
diff --git a/uploader/base_routes.py b/uploader/base_routes.py
index 80a15a0..72a8402 100644
--- a/uploader/base_routes.py
+++ b/uploader/base_routes.py
@@ -13,7 +13,6 @@ from flask import (flash,
 
 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__)
@@ -33,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-index.html",# TODO: Rename: sui-index.html, sui_base.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/oauth2/views.py b/uploader/oauth2/views.py
index 05f8542..b1b740f 100644
--- a/uploader/oauth2/views.py
+++ b/uploader/oauth2/views.py
@@ -33,16 +33,16 @@ def authorisation_code():
         app.logger.debug("ERROR: (%s)", error_response.content)
         flash("There was an error retrieving the authorisation token.",
               "alert alert-danger")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     def __fail_set_user_details__(_failure):
         app.logger.debug("Fetching user details fails: %s", _failure)
         flash("Could not retrieve the user details", "alert alert-danger")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     def __success_set_user_details__(_success):
         app.logger.debug("Session info: %s", _success)
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     def __success__(token):
         session.set_user_token(token)
@@ -53,7 +53,7 @@ def authorisation_code():
     code = request.args.get("code", "").strip()
     if not bool(code):
         flash("AuthorisationError: No code was provided.", "alert alert-danger")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     baseurl = urlparse(request.base_url, scheme=request.scheme)
     return request_token(
@@ -87,7 +87,7 @@ def logout():
         _user_str = f"{_user['name']} ({_user['email']})"
         session.clear_session_info()
         flash("Successfully signed out.", "alert alert-success")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     if user_logged_in():
         return session.user_token().then(
@@ -100,9 +100,9 @@ def logout():
                     "client_secret": oauth2_clientsecret()
                 })).either(
                     make_error_handler(
-                        redirect_to=redirect("/"),
+                        redirect_to=redirect(url_for("base.index")),
                         cleanup_thunk=lambda: __unset_session__(
                             session.session_info())),
                     lambda res: __unset_session__(session.session_info()))
     flash("There is no user that is currently logged in.", "alert alert-info")
-    return redirect("/")
+    return redirect(url_for("base.index"))
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 2afd8a3..ce73c89 100644
--- a/uploader/phenotypes/views.py
+++ b/uploader/phenotypes/views.py
@@ -31,10 +31,10 @@ from flask import (flash,
 from r_qtl import r_qtl2_qc as rqc
 from r_qtl import exceptions as rqe
 
-
 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
@@ -330,7 +330,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:
@@ -353,7 +353,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 (
@@ -371,7 +371,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:
@@ -383,9 +383,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]
 
@@ -419,7 +419,8 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
             today = datetime.date.today()
             return render_template(
                 ("phenotypes/add-phenotypes-with-rqtl2-bundle.html"
-                 if use_bundle else "phenotypes/add-phenotypes-raw-files.html"),
+                 if use_bundle
+                 else "phenotypes/add-phenotypes-raw-files.html"),
                 species=species,
                 population=population,
                 dataset=dataset,
@@ -464,7 +465,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",
@@ -505,6 +506,7 @@ def job_status(
             job = jobs.job(rconn, jobs.jobsnamespace(), str(job_id))
         except jobs.JobNotFound as _jnf:
             job = None
+
         return render_template("phenotypes/job-status.html",
                                species=species,
                                population=population,
@@ -610,6 +612,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",
@@ -652,11 +660,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"),
@@ -678,15 +681,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):
@@ -801,7 +805,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],
@@ -975,33 +979,34 @@ def load_data_success(
                               _publication["Authors"],
                               (_publication["Title"] or ""))
                              if item != "")
-            return render_template("phenotypes/load-phenotypes-success.html",
-                                   species=species,
-                                   population=population,
-                                   dataset=dataset,
-                                   job=job,
-                                   search_page_uri=urlunparse(ParseResult(
-                                       scheme=gn2_uri.scheme,
-                                       netloc=gn2_uri.netloc,
-                                       path="/search",
-                                       params="",
-                                       query=urlencode({
-                                           "species": species["Name"],
-                                           "group": population["Name"],
-                                           "type": "Phenotypes",
-                                           "dataset": dataset["Name"],
-                                           "search_terms_or": (
-                                               # Very long URLs will cause
-                                               # errors.
+            return render_template(
+                "phenotypes/load-phenotypes-success.html",
+                species=species,
+                population=population,
+                dataset=dataset,
+                job=job,
+                search_page_uri=urlunparse(ParseResult(
+                    scheme=gn2_uri.scheme,
+                    netloc=gn2_uri.netloc,
+                    path="/search",
+                    params="",
+                    query=urlencode({
+                        "species": species["Name"],
+                        "group": population["Name"],
+                        "type": "Phenotypes",
+                        "dataset": dataset["Name"],
+                        "search_terms_or": (
+                            # Very long URLs will cause
+                            # errors.
                                                " ".join(_xref_ids)
                                                if len(_xref_ids) <= 100
                                                else ""),
-                                           "search_terms_and": " ".join(
-                                               _search_terms).strip(),
-                                           "accession_id": "None",
-                                           "FormID": "searchResult"
-                                       }),
-                                       fragment="")))
+                        "search_terms_and": " ".join(
+                            _search_terms).strip(),
+                        "accession_id": "None",
+                        "FormID": "searchResult"
+                    }),
+                    fragment="")))
         except JobNotFound as _jnf:
             return render_template("jobs/job-not-found.html", job_id=job_id)
 
@@ -1057,9 +1062,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",
@@ -1067,6 +1073,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",
@@ -1100,7 +1107,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,
@@ -1135,9 +1142,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",
@@ -1149,3 +1157,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 8d4ceb7..795ce81 100644
--- a/uploader/population/views.py
+++ b/uploader/population/views.py
@@ -19,11 +19,12 @@ from uploader.authorisation import require_login
 from uploader.genotypes.views import genotypesbp
 from uploader.datautils import enumerate_sequence
 from uploader.phenotypes.views import phenotypesbp
-from uploader.phenotypes.models import datasets_by_population
 from uploader.expression_data.views import exprdatabp
 from uploader.species.models import all_species, species_by_id
 from uploader.monadic_requests import make_either_error_handler
 from uploader.input_validation import is_valid_representative_name
+from uploader.phenotypes.models import (dataset_phenotypes,
+                                        datasets_by_population)
 
 from .models import (save_population,
                      population_families,
@@ -154,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
         })
@@ -219,12 +220,26 @@ def view_population(species_id: int, population_id: int):
                                     species_id=species["SpeciesId"],
                                     streamlined_ui=streamlined_ui))
 
-        return render_template(("populations/sui-view-population.html"
-                                if bool(streamlined_ui)
-                                else "populations/view-population.html"),
-                               species=species,
-                               population=population,
-                               **({"dataset": datasets[0]} if len(datasets) == 1 else {}),
-                               activelink="view-population",
-                               streamlined_ui=streamlined_ui,
-                               view_under_construction=request.args.get("view_under_construction", False))
+        _datasets = datasets_by_population(
+            conn, species["SpeciesId"], population["Id"])
+        assert len(datasets) == 0 or len(datasets) == 1, (
+            "We expect only one phenotypes dataset per population.")
+        _kwargs = {
+            "species": species,
+            "population": population,
+            "activelink": "view-population",
+            "streamlined_ui": streamlined_ui,
+            "view_under_construction": request.args.get(
+                "view_under_construction", False)
+        }
+
+        if len(_datasets) == 1:
+            _dataset = _datasets[0]
+            _kwargs = {
+                **_kwargs,
+                "dataset": _dataset,
+                "phenotypes": enumerate_sequence(
+                    dataset_phenotypes(conn, population["Id"], _dataset["Id"]))
+            }
+
+        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 20acd01..4bfa7ae 100644
--- a/uploader/species/views.py
+++ b/uploader/species/views.py
@@ -54,9 +54,7 @@ def view_species(species_id: int):
                                         species_id=species_id,
                                         population_id=population["Id"]))
             return render_template(
-                ("species/sui-view-species.html"
-                 if bool(streamlined_ui)
-                 else "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..45e5d3d 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,12 @@ 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;
+}
diff --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/urls.js b/uploader/static/js/urls.js
new file mode 100644
index 0000000..e3fb7c6
--- /dev/null
+++ b/uploader/static/js/urls.js
@@ -0,0 +1,26 @@
+function baseURL() {
+    return new URL(`${window.location.protocol}//${window.location.host}`);
+};
+
+function buildURLFromCurrentURL(pathname, searchParams = new URLSearchParams()) {
+    var uri = baseURL();
+    uri.pathname=pathname;
+    var _search = new URLSearchParams(window.location.search);
+    searchParams.forEach(function(value, key) {
+        _search.set(key, value);
+    });
+    uri.search = _search.toString();
+    return uri
+};
+
+function deleteSearchParams(url, listOfParams = []) {
+    _params = new URLSearchParams(url.search);
+    listOfParams.forEach(function(paramName) {
+        _params.delete(paramName);
+    });
+
+    
+    newUrl = new URL(url.toString());
+    newUrl.search = _params.toString();
+    return newUrl;
+}
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/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/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..5f3dd6f 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%}
 
@@ -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..e426732 100644
--- a/uploader/templates/index.html
+++ b/uploader/templates/index.html
@@ -1,107 +1,162 @@
 {%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>
-
-    <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 extra_breadcrumbs%}{%endblock%}
 
-    <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>
-
-    <p><strong>Requirement</strong>: Samples must already have been registered
-      in the system before uploading genotype data.</p>
-
-    <h3 class="subheading">Phenotype Data</h3>
+{%block contents%}
 
-    <p>Phenotypes are the visible traits or features of a living thing. For
-      example, phenotypes include:</p>
+{%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%}
 
-    <ul>
-      <li>Weight</li>
-      <li>Height</li>
-      <li>Color (such as the color of fur or eyes)</li>
-    </ul>
+<div class="row">{{flash_all_messages()}}</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>
+{%if user_logged_in()%}
 
-    <!--
+<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>
 
-        <h3 class="subheading">Expression Data</h3>
+<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>View, edit and delete existing publications, and add new
+        publications by clicking on the button below.</p>
+
+      <a href="{{url_for('publications.index')}}"
+         title="Manage publications."
+         class="btn btn-primary">manage publications</a>
+    </div>
+  </div>
+</div>
 
-    <p class="text-danger">
-      <span class="glyphicon glyphicon-exclamation-sign"></span>
-      <strong>TODO</strong>: Document this &hellip;</p>
+  {%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%}
 
-    <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>
+  {%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>
+  <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%}
+
 
-{%endblock%}
+  {%block javascript%}
+  <script type="text/javascript" src="/static/js/species.js"></script>
+  {%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..b86cdcf 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 />
@@ -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..e6d67c7
--- /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>#</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/job-status.html b/uploader/templates/phenotypes/job-status.html
index 257f726..0bbe8e0 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%}
@@ -80,7 +69,7 @@
   {%endif%}
 </div>
 
-<h4 class="subheading">Errors</h4>
+<h3 class="subheading">upload errors</h3>
 <div class="row" style="max-height: 20em; overflow: scroll;">
   {%if errors | length == 0 %}
   <p class="text-info">
@@ -149,7 +138,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-pheno-dataset-card.html b/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html
index 11b108b..641421d 100644
--- a/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html
+++ b/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html
@@ -1,4 +1,4 @@
-{%from "populations/macro-display-population-card.html" import display_population_card%}
+{%from "populations/macro-display-population-card.html" import display_population_card, display_sui_population_card%}
 
 {%macro display_pheno_dataset_card(species, population, dataset)%}
 {{display_population_card(species, population)}}
@@ -29,3 +29,29 @@
   </div>
 </div>
 {%endmacro%}
+
+{%macro display_sui_pheno_dataset_card(species, population, dataset)%}
+{{display_sui_population_card(species, population)}}
+
+<div class="row">
+  <table class="table">
+    <caption>Current dataset</caption>
+    <tbody>
+      <tr>
+        <th>Name</th>
+        <td>{{dataset.Name}}</td>
+      </tr>
+
+      <tr>
+        <th>Full Name</th>
+        <td>{{dataset.FullName}}</td>
+      </tr>
+
+      <tr>
+        <th>Short Name</th>
+        <td>{{dataset.ShortName}}</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+{%endmacro%}
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/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..a69b024 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()}}
 
@@ -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 cc01c9e..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["FullName"]}}
-  </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 3bf8d0d..0000000
--- a/uploader/templates/populations/sui-view-population.html
+++ /dev/null
@@ -1,138 +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)
-        under consideration.</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>
-      <p>Please click the button below to provide the samples that will be used
-        in your data.</p>
-      <a href="{{url_for('species.populations.samples.list_samples',
-               species_id=species.SpeciesId,
-               population_id=population.Id)}}"
-         title="Upload samples for population '{{population['Name']}}'"
-         class="btn btn-primary">Upload Samples</a>
-    </div>
-
-    <div class="tab-pane fade show active"
-         id="phenotypes-content"
-         role="tabpanel"
-         aria-labelledby="phenotypes-content-tab">
-      <p>Upload and manage phenotypes and publications for population
-        "<em>{{population.FullName}} ({{population.Name}})</em>" of species
-        "<em>{{species.FullName}} ({{species.Name}})</em>".</p>
-
-      <p class="text-danger">Tabs will not work nicely here. Maybe present
-        options e.g.:
-      </p>
-      <div class="row">
-        <div class="col">
-          <a href="{{url_for('species.populations.phenotypes.view_dataset',
-                   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>
-          <!-- Go straight to upload form(s). -->
-        </div>
-        <div class="col">
-          <a href="#"
-             title="List all existing phenotypes for this population."
-             class="btn btn-info not-implemented">list existing phenotypes</a>
-          <!-- Means and QTLReaper will be computed in this page. -->
-        </div>
-        <div class="col">
-          <a href="#"
-             title="List all existing publications for this population."
-             class="btn btn-info not-implemented">list existing publications</a>
-          <!-- Maybe, actually filter publications by population? -->
-          <!-- Provide other features for publications on loaded page. -->
-        </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%}
-<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%}
-
-
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 540ecf1..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%}
@@ -12,7 +19,9 @@
 <div class="row">
   <form id="frm-create-publication"
         method="POST"
-        action="{{url_for('publications.edit_publication', publication_id=publication_id, **request.args)}}"
+        action="{{url_for('publications.edit_publication',
+                publication_id=publication_id,
+                next=request.args.get('next', ''))}}"
         class="form-horizontal">
 
     <div class="row mb-3">
diff --git a/uploader/templates/publications/index.html b/uploader/templates/publications/index.html
index 369812b..54d3fc0 100644
--- a/uploader/templates/publications/index.html
+++ b/uploader/templates/publications/index.html
@@ -3,16 +3,22 @@
 
 {%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 title to view more details and to edit details for that
+    publication.</p>
 </div>
 
 <div class="row">
@@ -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..a7c1a8f 100644
--- a/uploader/templates/species/base.html
+++ b/uploader/templates/species/base.html
@@ -1,17 +1,10 @@
 {%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()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.view_species', species_id=species['SpeciesId'])}}">
+    {{species["Name"]|title}}
+  </a>
 </li>
-{%block lvl2_breadcrumbs%}{%endblock%}
 {%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."""