diff --git a/CHANGELOG.md b/CHANGELOG.md
index af7186a241abbca849299410816b97e56be16de4..9f361607ef407170396c244a3fa98730fed4c46d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added (for new features, dependecies etc.)
 
+### Changed (for changes in existing functionality)
+
+### Deprecated (for soon-to-be removed features) 
+
+### Removed (for now removed features)
+
+### Security (in case of vulnerabilities)
+
+## v0.2-rc.1 (2020-04-10)
+
+### Added (for new features, dependecies etc.)
+
+* ext_bottom_line module (v0.1 - Experimental)
+    * A module which adds a preview section where any kind of summarizing
+      information (like plots, video players, thumbnails...) can be shown.
+    * Enable with build property `BUILD_MODULE_EXT_BOTTOM_LINE=ENABLED`.
+    * More documentation in `src/core/js/ext_bottom_line.js`.
+* ext_revisions (v0.1 - EXPERIMENTAL)
+    * Creates a backup copy of each entity which is updated via the edit_mode.
+    * Enable via the build property `BUILD_MODULE_EXT_REVISIONS=ENABLED`.
+    * Needs two special entities. A RecordType "Obsolete" and a reference
+      property "revisionOf" with data type "Obsolete".
 * Map (v0.3)
     * Adds a button to the navbar which toggles a map in the top of the main
       panel. The map currently shows all known entities which have geolocation
@@ -70,9 +92,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Deprecated (for soon-to-be removed features) 
 
+* Image Preview in the FileSystem. The functionality is to be replaced by real
+  thumbnails, which cover also non-image data-formats. The thumbnails resource
+  is part of the new file system API of the CaosDB Server which is currently
+  under development.
+
 ### Removed (for now removed features) 
 
 * Removed non-informative tests for webcaosdb.css
+* Hard-coded image and video preview in the entity panel. The preview of images
+  and videos is controlled by the `ext_bottom_line` module now.
 
 ### Fixed (for any bug fixes) 
 
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 0ce20ba28dc6fb31a427d0c6aed04ff100f31457..046375fa9718c1acdc0a30cf46d488a8beaf3a2b 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -60,7 +60,14 @@ BUILD_FOOTER_DATA_POLICY_HREF=https://indiscale.com/?page_id=156
 # Custom footer elements can be placed here (will be placed inside a <div>
 # element).
 BUILD_FOOTER_CUSTOM_ELEMENT_ONE=
+# BUILD_FOOTER_CUSTOM_ELEMENT_ONE='<p>Some content <img
+# src="/webinterface/${BUILD_NUMBER}/pics/some_image.png"/>'
+
+# Files from the `build.properties.files` directory can also be included here
+# These files will also have a second step of variable substitution with
+# ${BUILD_NUMBER}.
 BUILD_FOOTER_CUSTOM_ELEMENT_TWO=
+# BUILD_FOOTER_CUSTOM_ELEMENT_TWO=$(cat footer_element_2.html)
 BUILD_CUSTOM_IMPRINT='<p> Put an imprint note here </p>'
 
 
diff --git a/build.properties.files/footer_element_2.html b/build.properties.files/footer_element_2.html
new file mode 100644
index 0000000000000000000000000000000000000000..f0444d4291772d585049257631f779b3282fd4e1
--- /dev/null
+++ b/build.properties.files/footer_element_2.html
@@ -0,0 +1,7 @@
+<h1>Put your template content files into this directory</h1>
+
+<p>
+  Files in this directory can be substituted into the variables defined in
+  build.properties.d.  In a second step, parameter substitution takes place with
+  BUILD_NUMBER, if the word is preceded by a dollar sign: ${BUILD_NUMBER}
+</p>
diff --git a/conf/core/json/ext_bottom_line.json b/conf/core/json/ext_bottom_line.json
new file mode 100644
index 0000000000000000000000000000000000000000..528402f7b382ea40ad3ffe679606c5bb029ce7ea
--- /dev/null
+++ b/conf/core/json/ext_bottom_line.json
@@ -0,0 +1,20 @@
+{ "version": 0.1,
+  "creators": [
+    { "id": "test.success",
+      "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) === 'TestPreviewRecord-success'",
+      "create": "(entity) => 'SUCCESS'"
+    },
+    { "id": "test.error",
+      "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) === 'TestPreviewRecord-error'",
+      "create": "(entity) => new Promise((res,rej) => {rej('Test Error');})"
+    },
+    { "id": "test.load-forever",
+      "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) === 'TestPreviewRecord-load-forever'",
+      "create": "(entity) => new Promise((res,rej) => {})"
+    },
+    { "id": "test.success-2",
+      "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) !== 'TestPreviewRecord-fall-back'",
+      "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}]); }"
+    }
+  ]
+}
diff --git a/libs/plotly.js-1.52.2.zip b/libs/plotly.js-1.52.2.zip
new file mode 100644
index 0000000000000000000000000000000000000000..f7588bf63dd9e685fec3180fe3336829f34e566b
Binary files /dev/null and b/libs/plotly.js-1.52.2.zip differ
diff --git a/makefile b/makefile
index c7d7c91aba376d22b1e151102d84a7c2bb551278..d9091f32004cfe9c25e28c9e75a3cc18471ac73b 100644
--- a/makefile
+++ b/makefile
@@ -40,7 +40,7 @@ SRC_EXT_DIR = $(abspath src/ext)
 LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
-LIBS = fonts css/bootstrap.css js/bootstrap.js js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js
+LIBS = fonts css/bootstrap.css js/bootstrap.js js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
 
@@ -66,16 +66,20 @@ merge_xsl:
 
 build_properties:
 	@set -a -e ; \
-	for f in $$(ls build.properties.d/) ; do source "build.properties.d/$$f" ; done ; \
+	pushd build.properties.files ; \
+	for f in ../build.properties.d/* ; do source "$$f" ; done ; \
+	popd ; \
 	BUILD_NUMBER=$(BUILD_NUMBER) ; \
 	PROPS=$$(printenv | grep -e "^BUILD_") ; \
 	echo "SET BUILD PROPERTIES:" ; \
 	for p in $$PROPS ; do echo "  $$p" ; done; \
-	VARS=$$(printenv | grep -e "^BUILD_" | sed 's/=.*$$/}/' | sed 's/^/$${/') ; \
+	VARS=$$(printenv | grep -e "^BUILD_" | grep -v "^BUILD_NUMBER=" | \
+	        sed -E 's/(BUILD_[^=]+)=.*/$${\1}/') ; \
 	for f in $$(find $(PUBLIC_DIR) -type f) ; do \
 		echo "BUILD FILE: $$f" ; \
 		mv "$$f" "$${f}.tmp" ; \
-		envsubst "$$VARS" < "$${f}.tmp" > "$$f" ; \
+		envsubst "$$VARS" < "$${f}.tmp" | \
+		  envsubst "\$${BUILD_NUMBER}" > "$$f" ; \
 		rm "$${f}.tmp" ; \
 	done
 	@echo $(BUILD_NUMBER) > $(PUBLIC_DIR)/.build_number
@@ -252,9 +256,12 @@ $(LIBS_DIR)/js/leaflet-coordinates.js: unzip $(LIBS_DIR)/js
 $(LIBS_DIR)/css/leaflet-coordinates.css: unzip $(LIBS_DIR)/css
 	ln -s $(LIBS_DIR)/Leaflet.Coordinates-0.1.5/dist/Leaflet.Coordinates-0.1.5.css $@
 
-$(LIBS_DIR)/js/bootstrap-autocomplete.min.js: unzip $(LIBS_DIR)/css
+$(LIBS_DIR)/js/bootstrap-autocomplete.min.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.0/dist/latest/bootstrap-autocomplete.min.js $@
 
+$(LIBS_DIR)/js/plotly.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/plotly.js-1.52.2/dist/plotly.min.js $@
+
 
 $(addprefix $(LIBS_DIR)/, js css):
 	mkdir $@ || true
@@ -268,7 +275,7 @@ clean:
 
 .PHONY: unzip
 unzip:
-	for f in $(LIBS_ZIP); do unzip -o -d libs $$f; done
+	for f in $(LIBS_ZIP); do unzip -q -o -d libs $$f; done
 
 
 PYLINT = pylint3 -d all -e E,F
diff --git a/misc/ext_bottom_line_test_data.py b/misc/ext_bottom_line_test_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..1a43094eb6719e2b7b2a6a4ab42d47fcc415d513
--- /dev/null
+++ b/misc/ext_bottom_line_test_data.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+
+import os
+import caosdb
+import random
+
+filename = "small.webm"
+if not os.path.isfile(filename):
+    import wget #pylint: disable=E0401
+    filename = wget.download("http://techslides.com/demos/sample-videos/small.webm")
+caosdb.configure_connection(ssl_insecure=True)
+
+# clean
+old = caosdb.execute_query("FIND Test*")
+if len(old):
+    old.delete()
+
+# data model
+datamodel = caosdb.Container()
+datamodel.extend([
+    caosdb.RecordType("TestPreviewRecordType")
+])
+
+datamodel.insert()
+
+
+# test data
+testdata = caosdb.Container()
+
+## record with references
+refrec = caosdb.Record("TestPreviewRecord-references").add_parent("TestPreviewRecordType")
+testdata.append(refrec)
+
+video = caosdb.File(name="TestFileVideo", file=filename, path="testfile.webm")
+image = caosdb.File(name="TestFileImage",
+                    file="../src/core/pics/map_tile_caosdb_logo.png",
+                    path="testfile.png")
+for i in ["load-forever", "fall-back", "error", "success", "success-2"]:
+    rec = caosdb.Record("TestPreviewRecord-{}".format(i)).add_parent("TestPreviewRecordType")
+    testdata.append(rec)
+    refrec.add_property("TestPreviewRecordType", rec)
+
+testdata.append(image)
+testdata.append(video)
+
+
+testdata.insert();
+
+os.remove(filename)
+
+
diff --git a/misc/revision_test_data.py b/misc/revision_test_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..0f41fa9c1b748be0cbc6c5b917dc6739d3c21d89
--- /dev/null
+++ b/misc/revision_test_data.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Copyright 2020 IndiScale GmbH <info@indiscale.com>
+Copyright 2020 Timm Fitschen <t.fitschen@indiscale.com>
+"""
+
+import caosdb
+import random
+import os
+
+# data model
+c = caosdb.execute_query("FIND Test*")
+if len(c) > 0:
+    print(c)
+    delete = input("Delete these entities?\nType `yes`:")
+    if delete == "yes":
+        c.delete();
+    else:
+        print("You typed `{}`".format(delete))
+        print("[Canceled]")
+        exit(0)
+
+
+print("inserting test data")
+
+upload_file = open("test.dat", "w")
+upload_file.write("hello world\n")
+upload_file.close()
+
+testdata = caosdb.Container()
+testdata.extend([
+    caosdb.File("TestFile",
+        path="test.dat",
+        file="test.dat"),
+    caosdb.Property("TestRevisionOf", datatype="TestObsolete"),
+    caosdb.RecordType("TestObsolete"),
+    caosdb.RecordType("TestRecordType"),
+    caosdb.Property("TestProperty", datatype=caosdb.TEXT),
+    caosdb.Record("TestRecord"
+                 ).add_parent("TestRecordType"
+                 ).add_property("TestProperty", "this is a test"),
+])
+
+testdata.insert()
+os.remove("test.dat")
diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js
index 98d4feae395f47a0eaecb420ed99f231f4fbf70b..648e8249a258a9f723325ec34c230e07a5900514 100644
--- a/src/core/js/caosdb.js
+++ b/src/core/js/caosdb.js
@@ -183,10 +183,12 @@ function getEntityDatatype(element) {
  * @return {string} the data type
  */
 function getPropertyDatatype(element) {
-    var dt_elem = $(element).find(".caosdb-property-datatype");
+    var dt_elem = findElementByConditions(element,
+        x => x.classList.contains("caosdb-property-datatype"),
+        x => x.classList.contains("caosdb-preview-container"));
 
     if(dt_elem.length == 1){
-        return dt_elem.text();
+        return $(dt_elem[0]).text();
     } else if (dt_elem.length > 1){
         throw new Error("The datatype of this property could not uniquely be determined.");
     }
diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js
index 96ff91d2d9a2a6e153b56e8f6b66b6669f93a70b..b68d8ca4d9d5d309f3374c3717fe001806241173 100644
--- a/src/core/js/edit_mode.js
+++ b/src/core/js/edit_mode.js
@@ -151,8 +151,13 @@ var edit_mode = new function() {
         }
     }
 
-    this.property_drop_listener = function(e) { edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); }
-    this.parent_drop_listener = function(e) { edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_parent); }
+    this.property_drop_listener = function(e) {
+        edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); 
+    }
+
+    this.parent_drop_listener = function(e) {
+        edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_parent);
+    }
 
     this._drop_listener = function(e, add_cb) {
         e.preventDefault();
@@ -308,11 +313,6 @@ var edit_mode = new function() {
     }
 
 
-    this.update_entity_by_id = async function(ent_id) {
-        var ent_element = $("#" + ent_id)[0];
-        return this.update_entity(ent_element);
-    }
-
     /**
      * Insert entities.
      *
@@ -426,9 +426,11 @@ var edit_mode = new function() {
             getEntityDescription(ent_element),
             getEntityUnit(ent_element),
         );
-        return await update(xml);
+        return await edit_mode.update(xml);
     }
 
+    this.update = update;
+
     this.add_edit_mode_button = function(target, toggle_function) {
         var edit_mode_li = $('<li><button class="navbar-btn btn btn-link caosdb-f-btn-toggle-edit-mode">Edit Mode</button></li>');
         $(target).append(edit_mode_li);
@@ -1239,6 +1241,8 @@ var edit_mode = new function() {
             edit_mode.unhighlight();
             app.old = entity;
             app.entity = $(entity).clone(true)[0];
+            // remove preview stuff
+            $(app.entity).find(".caosdb-preview-container").remove();
             edit_mode.smooth_replace(app.old, app.entity);
 
             edit_mode.add_save_button(app.entity, () => app.update(app.entity));
diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js
new file mode 100644
index 0000000000000000000000000000000000000000..900263366e456c8f7a2945af6a24c7a10b345c0e
--- /dev/null
+++ b/src/core/js/ext_bottom_line.js
@@ -0,0 +1,512 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+
+/**
+ * Add a special section to each entity one the current page where a thumbnail,
+ * a video preview or any other kind of summary for the entity is shown.
+ *
+ * Apart from some defaults, the content for the bottom line has to be
+ * configured in the file `conf/ext/json/ext_bottom_line.json` which must
+ * contain a {@link BottomLineConfig}. An example is located at
+ * `conf/core/json/ext_bottom_line.json`.
+ *
+ * @module ext_bottom_line
+ * @version 0.1
+ *
+ * @requires jQuery (library)
+ * @requires log (singleton from loglevel library)
+ * @requires resolve_references.is_in_viewport_vertically (funtion from ext_references.js)
+ * @requires load_config (function from caosdb.js)
+ * @requires getEntityPath (function from caosdb.js)
+ * @requires connection (module from webcaosdb.js)
+ */
+var ext_bottom_line = function ($, logger, is_in_view_port, load_config, getEntityPath, connection) {
+
+    /**
+     * @property {string|function} create - a function with one parameter
+     *     (entity) Note: This property can as well be a
+     *     javascript string which evaluates to a function.
+     */
+    /**
+     * @type {BottomLineConfig}
+     * @property {string|HTMLElement} fallback - Fallback content if none of
+     *     the creators are applicable.
+     * @property {string} version - the version of the configuration which must
+     *     match this module's version.
+     * @property {CreatorConfig[]} creators - an array of creators.
+     */
+
+    /**
+     * @type {CreatorConfig}
+     * @property {string} [id] - a unique id for the creator. optional, for
+     *     debuggin purposes.
+     * @property {function|string} is_applicable - If this is a string this has
+     *     to be valid javascript! An asynchronous function which accepts one
+     *     parameter, an entity in html representation, and which returns true
+     *     iff this creator is applicable for the given entity.
+     * @property {string} create - This has to be valid javascript! An
+     *     asynchronous function which accepts one parameter, an entity in html
+     *     representation. It returns a HTMLElement or text node which will be
+     *     shown in the bottom line container iff the creator is applicable.
+     */
+
+    /**
+     * Check if an entity has a path attribute and one of a set of extensions.
+     *
+     * Note: the array of extensions must contain only lower-case strings.
+     *
+     * @param {HTMLElement} entity
+     * @param {string[]} extensions - an array of file extesions, e.g. `jpg`.
+     * @return {boolean} true iff the entity has a path with one of the
+     *     extensionss.
+     */
+    const _path_has_file_extension = function (entity, extensions) {
+        const path = getEntityPath(entity);
+        if (path) {
+            for (var ext of extensions) {
+                if(path.toLowerCase().endsWith(ext)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Create a preview for videos.
+     *
+     * @param {HTMLElement} entity
+     * @return {HTMLElement} a VIDEO element.
+     */
+    const _create_video_preview = function (entity) {
+        var path = connection.getFileSystemPath() + getEntityPath(entity);
+        return $(`<video controls="controls"><source src="${path}"/></video>`)[0];
+    }
+
+    /**
+     * Create a preview for pictures.
+     *
+     * @param {HTMLElement} entity
+     * @return {HTMLElement} an IMG element.
+     */
+    const _create_picture_preview = function (entity) {
+        var path = connection.getFileSystemPath() + getEntityPath(entity);
+        return $(`<img class="entity-image-preview" style="max-width: 200px; max-height=140px;" src="${path}"/>`)[0];
+    }
+
+    var fallback_preview = undefined;
+
+    /**
+     * Default creators.
+     *
+     * @member {Creator[]}
+     */
+    const _default_creators = [
+        { // pictures
+            id: "_default_creators.pictures",
+            is_applicable: (entity) => _path_has_file_extension(
+                entity, ["jpg", "png", "gif", "svg"]),
+            create: _create_picture_preview
+        },
+        { // videos
+            id: "_default_creators.videos",
+            is_applicable: (entity) => _path_has_file_extension(
+                entity, ["mp4", "mov", "webm"]),
+            create: _create_video_preview,
+        },
+        { // fallback
+            id: "_default_creators.fallback",
+            is_applicable: (entity) => true,
+            create: (entity) => fallback_preview,
+        },
+
+    ];
+
+
+    const previewShownEvent = new Event("ext_bottom_line.preview.shown");
+    const previewReadyEvent = new Event("ext_bottom_line.preview.ready");
+
+    const _css_class_preview_container = "caosdb-f-ext_bottom_line-container";
+    const _css_class_preview_container_resolvable = "caosdb-f-ext_bottom_line-container-resolvable";
+    const _css_class_preview_container_button = "caosdb-f-ext_bottom_line-container-button";
+
+    /**
+     * Store the list of creators.
+     *
+     * @member {Creator[]}
+     */
+    const _creators = [];
+
+    /**
+     * Return the preview container of the entity.
+     *
+     * @param {HTMLElement} entity - An entity in HTML representation.
+     * @returns {HTMLElement} the preview container or `undefined` if the entity
+     *     doesn't have any.
+     */
+    const get_preview_container = function (entity) {
+        return $(entity).children(`.${_css_class_preview_container}`)[0];
+    }
+
+    /**
+     * Add the element to the entity's preview container and remove all other
+     * content.
+     *
+     * This method appends the element (usually a preview, a waiting
+     * notification or an error message) to the HTMLElement with class
+     * `caosdb-f-ext_bottom_line-container` which was added to the entity by the
+     * {@link root_preview_handler}.
+     *
+     * If the preview container cannot be found an error is logged, but not
+     * thrown.
+     *
+     * @param {HTMLElement|String} element - A preview, a waiting notification,
+     *     an error message or similar.
+     * @param {HTMLElement} entity - An entity in HTML Representation which
+     *     must have a (deep) child with class `caosdb-f-ext_bottom_line-container`.
+     */
+    const set_preview_container = function (entity, element) {
+        const preview_container = $(get_preview_container(entity));
+        if (preview_container[0]) {
+            preview_container.empty();
+            var buttons = preview_container.siblings(`.${_css_class_preview_container_button}`);
+            if (element) {
+                buttons.css({"visibility": "initial"});
+                preview_container.append(element);
+            } else {
+                buttons.css({"visibility": "hidden"});
+            }
+        } else {
+            logger.error(new Error("Could not find the preview container."));
+        }
+    }
+
+    /**
+     * Append a preview to the entity and removes any pre-existing preview.
+     *
+     * If the preview is Promise for a preview a waiting notification is added
+     * to the entity instead and the actual preview is added after the Promise
+     * is resolved. If the Promise is rejected, a correspondig error is shown
+     * instead.
+     *
+     * @see root_preview_handler
+     *
+     * @async
+     * @param {HTMLElement} entity
+     * @param {string|HTMLElement|Promise} preview - A preview for an entity or
+     *     a Promise for a preview (which resolves as a string or an HTMLElement as well).
+     */
+    var set_preview = async function (entity, preview) {
+        try {
+            const wait = "Please wait...";
+            set_preview_container(entity, wait);
+            const result = await preview;
+            set_preview_container(entity, result);
+            if (result) {
+                entity.dispatchEvent(previewReadyEvent);
+            }
+        } catch (err) {
+            logger.error(err);
+            const err_msg = "An error occured while loading this preview";
+            set_preview_container(entity, err_msg);
+        }
+    }
+
+    /**
+     * Create and return a preview for a given entity.
+     *
+     * This root_preview_creator iterates over all the registered creators and
+     * uses the first match, i.e. the first creator object which return true
+     * for the `is_applicable(entity)` method of the creator object.
+     *
+     * If a creator throws an error during checking whether it `is_applicable`
+     * or during the `create` the error is logged and the creator is treated as
+     * if it were not applicable.
+     *
+     * @async
+     * @param {HTMLElement} entity - the entity for which the preview is to be
+     *     created.
+     * @returns {String|HTMLElement|Promise} A preview which can be added to
+     *     the entity DOM representation or a Promise for such a preview.
+     */
+    var root_preview_creator = async function (entity) {
+        for (let c of _creators) {
+            try {
+                if (await c.is_applicable(entity)) {
+                    return c.create(entity);
+                }
+            } catch (err) {
+                logger.error(err);
+            }
+        }
+        return undefined;
+    };
+
+    /**
+     * Add a preview container to the entity.
+     *
+     * The preview container is a HTMLElement with class {@link
+     * _css_class_preview_container}. It is intended to contain the preview
+     * after it has been created.
+     *
+     * The presence of a preview container is also the marker that an entity
+     * has been visited by the root_preview_handler yet and that a preview is
+     * loaded and added to the entity or on its way.
+     *
+     * Depending on the implementation the actual preview container might be
+     * wrapped into other elements which can be used for styling or other
+     * purposes.
+     *
+     * @param {HTMLElement} entity - An entity in HTML representation.
+     * @return {HTMLElement} the newly created container.
+     */
+    var add_preview_container = function (entity) {
+        const button_show = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-down"/> Show Preview</button>')
+            .css({width: "100%"})
+            .addClass(_css_class_preview_container_button);
+        const button_hide = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-up"/> Hide Preview</button>')
+            .css({width: "100%"})
+            .addClass(_css_class_preview_container_button)
+            .hide();
+        const style = { padding: "0px 10px" };
+        const container = $(`<div class="collapse"/>`)
+            .addClass(_css_class_preview_container)
+            .addClass(_css_class_preview_container_resolvable)
+            .css(style);
+        const show = function() {
+            button_show.hide();
+            button_hide.show();
+            container.collapse("show");
+        }
+        const hide = function() {
+            button_hide.hide();
+            button_show.show();
+            container.collapse("hide");
+        }
+        button_show.click(show);
+        button_hide.click(hide);
+        container.on("shown.bs.collapse", () => { container[0].dispatchEvent(previewShownEvent); });
+        $(entity).append(container);
+        $(entity).append(button_show);
+        $(entity).append(button_hide);
+
+        return container[0];
+    }
+
+    /**
+     * Create a preview for the entity and append it to the entity.
+     *
+     * The root_preview_handler calls the root_preview_creator which returns a
+     * preview which is to be added to the entity.
+     *
+     * @async
+     * @param {HTMLElement} entity - the entity for which the preview is to
+     *     created.
+     */
+    var root_preview_handler = async function (entity) {
+        var container = $(get_preview_container(entity) || add_preview_container(entity));
+        if (container.hasClass(_css_class_preview_container_resolvable)) {
+            container.removeClass(_css_class_preview_container_resolvable);
+            const preview = root_preview_creator(entity);
+            await set_preview(entity, preview);
+        }
+    }
+
+    /**
+     * Trigger the root_preview_handler for all entities within the view port
+     * when the view port.
+     */
+    var root_preview_handler_trigger = function () {
+        var entities = $(".caosdb-entity-panel,.caosdb-entity-preview");
+        for (let entity of entities) {
+
+            // TODO viewport + 1000 px for earlier loading
+            if (is_in_view_port(entity)) {
+                root_preview_handler(entity);
+            }
+        }
+    }
+
+    /**
+     * Intialize the scroll watcher which listens on the scroll event of the
+     * window and triggers the preview loading with a delay after the last
+     * scroll event.
+     *
+     * @param {integer} delay - timeout in milliseconds after the last scroll
+     *     event. After this timeout the trigger is called.
+     * @param {function} trigger - the callback which is called.
+     */
+    var init_watcher = function (delay, trigger) {
+        var scroll_timeout = undefined;
+        $(window).scroll(() => {
+            if (!scroll_timeout) {
+                clearTimeout(scroll_timeout);
+            }
+            scroll_timeout = setTimeout(trigger, delay);
+        });
+
+        var preview_timeout = undefined;
+        window.addEventListener(
+            preview.previewReadyEvent.type,
+            () => {
+                if (!preview_timeout) {
+                    clearTimeout(scroll_timeout);
+                }
+                scroll_timeout = setTimeout(trigger, 100);
+                return true;
+            },
+            true);
+    };
+
+
+    /**
+     * Configure the creators.
+     *
+     * @param {BottomLineConfig} config
+     */
+    var configure = async function (config) {
+        logger.debug("configure", config);
+        if(config.version != "0.1") {
+            throw new Error("Wrong version in config.");
+        }
+
+        fallback_preview = config.fallback || fallback_preview;
+
+        // append/load creators
+        _creators.splice(0, _creators.length);
+        for (let c of config.creators) {
+            _creators.push({
+                id: c.id,
+                is_applicable: typeof c.is_applicable === "function" ? c.is_applicable : eval(c.is_applicable),
+                create: typeof c.create === "function" ? c.create : eval(c.create)
+            });
+        }
+
+        // append default creators
+        for (let c of _default_creators) {
+            _creators.push(c);
+        }
+    };
+
+
+    /**
+     * Initialize this module.
+     *
+     * I.e. configure the list of creators and setup the scroll listener which
+     * triggers the root_preview_handler.
+     *
+     * @property {BottomLineConfig} [config] - an optional config. Per default, the
+     *     configuration is fetched from the server.
+     */
+    const init = async function (config) {
+        logger.info("ext_bottom_line initialized");
+
+        try {
+            let _config = config || await load_config("json/ext_bottom_line.json");
+            await configure(_config);
+
+            init_watcher(_config.delay || 500, root_preview_handler_trigger);
+
+            // trigger the whole thing for the first time
+            root_preview_handler_trigger();
+
+        } catch (err) {
+            logger.error(err);
+        }
+
+    }
+
+    return {
+        previewShownEvent: previewShownEvent,
+        previewReadyEvent: previewReadyEvent,
+        init: init,
+        init_watcher: init_watcher,
+        configure: configure,
+        add_preview_container: add_preview_container,
+        root_preview_handler: root_preview_handler,
+        _creators: _creators,
+        _css_class_preview_container,
+        _css_class_preview_container_button,
+        _css_class_preview_container_resolvable,
+    }
+}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection);
+
+
+/**
+ * Helper for plotly
+ */
+var plotly_preview = function (logger, ext_bottom_line, plotly) {
+
+    /**
+     * Create a plot with plotly.
+     *
+     * The layout and any other plotly options are set by this function. The
+     * only parameter `data` is the `data` parameter of the `Plotly.newPlot`
+     * factory.
+     *
+     * Hence the full documentation of the `data` parameter is available at
+     * https://plotly.com/javascript/plotlyjs-function-reference/#plotlynewplot
+     *
+     *
+     * @param {object[]} data - array of objects containing the data which is
+     *     to be plotted.
+     * @returns {HTMLElement} the element which contains the plot.
+     */
+    const create_plot = function (data) {
+        var div = $('<div/>')[0];
+        plotly.newPlot(div, data, { margin: { t: 0}, height: 400, widht: 400 }, {responsive: true});
+        return div;
+    }
+
+    const resize_plots_event_handler = function (e) {
+        var plots = $(e.target).find(".js-plotly-plot");
+        for (let plot of plots) {
+            plotly.Plots.resize(plot);
+        }
+    }
+
+    const init = function () {
+        window.addEventListener(ext_bottom_line.previewReadyEvent.type,
+            resize_plots_event_handler, true);
+        window.addEventListener(ext_bottom_line.previewShownEvent.type,
+            resize_plots_event_handler, true);
+    }
+
+    return {
+        create_plot: create_plot,
+        init: init,
+    };
+
+}(log.getLogger("plotly_preview"), ext_bottom_line, Plotly);
+
+
+// this will be replaced by require.js in the future.
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") {
+        caosdb_modules.register(plotly_preview);
+        caosdb_modules.register(ext_bottom_line);
+    }
+});
diff --git a/src/core/js/ext_revisions.js b/src/core/js/ext_revisions.js
new file mode 100644
index 0000000000000000000000000000000000000000..3ee086e60ed34ac659c5bb92de71d78b38f30e2b
--- /dev/null
+++ b/src/core/js/ext_revisions.js
@@ -0,0 +1,229 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+/**
+ * The ext_revisions module extends the edit_mode update functionality.
+ *
+ * The edit_mode.update_entity function is overridden by this module with a
+ * proxy pattern. That means, that the original function is still called, but a
+ * proxy (or wrapper) function adds further functionality.
+ *
+ * The extended update function creates a back-up version of the updated entity
+ * and adds a revisionOf property to the updated entity which references
+ * the back-up. The back-up entity loses all of its original parents and gets
+ * an "Obsolete" as only parent instead.
+ *
+ * Per default, the module assumes two Entities to be present in the
+ * database. A RecordType named "Obsolete" and a Property named
+ * "revisionOf". The initialization is aborted if these entities cannot be
+ * found and the module remains inactive.
+ *
+ * @module ext_revisions
+ * @version 0.1
+ *
+ * @requires jQuery
+ * @requires log
+ * @requires edit_mode
+ * @requires getEntityID
+ * @requires transaction
+ * @requires _createDocument
+ */
+var ext_revisions = function ($, logger, edit_mode, getEntityID, transaction, _createDocument) {
+
+
+    /**
+     * Default names for the two entities which are required by this module.
+     */
+    var _datamodel = { obsolete: "Obsolete", revisionOf: "revisionOf" };
+
+    /**
+     * Generate and insert the back-up entity which stores the old state of the
+     * entity which is to be updated.
+     *
+     * The obsolete entity has only one parent named "Obsolete".
+     * Apart from that, the obsolete entity has all the properties, name,
+     * description and so on from the original entity before the update.
+     *
+     * @param {string} id - the id of the entity which is to be updated.
+     * @returns {string} the id of the newly created obsolete entity.
+     */
+    var _insert_obsolete = async function (id) {
+        logger.debug("insert obsolete", id);
+
+        // create new obsolete entity from the original
+        const obsolete = await transaction.retrieveEntityById(id);
+        $(obsolete).attr("id", "-1");
+        $(obsolete).find("Permissions").remove();
+        $(obsolete).find("Parent").remove();
+        $(obsolete).append(`<Parent name="${_datamodel.obsolete}"/>`);
+
+        const doc = _createDocument("Request");
+        doc.firstElementChild.appendChild(obsolete);
+        const result = await transaction.insertEntitiesXml(doc);
+        const obsolete_id = $(result.firstElementChild).find("[id]").first().attr("id");
+        logger.trace("leave _insert_obsolete", obsolete_id);
+        return obsolete_id;
+    };
+
+    /**
+     * Generate a HTML string which represents a new "revisionOf" property
+     * which references the newly created obsolete entity. The property is
+     * meant to be appended to the property section of the entity which is to
+     * be updated.
+     *
+     * @param {string} obsolete_id - the id of the newly created obsolete
+     *     entity.
+     * @returns {string} A HTML represesentation of an entity property.
+     */
+    var _make_revision_of_property = async function (obsolete_id) {
+        logger.trace("enter _make_revision_of_property", obsolete_id);
+        const ret = (await transformation.transformProperty(str2xml(`<Response><Property id="${_datamodel._revisionOfId}" name="${_datamodel.revisionOf}" datatype="${_datamodel.obsolete}"></Property></Response>`))).firstElementChild;
+
+        $(ret).find(".caosdb-f-property-value").append(`<div class="caosdb-property-edit-value"><select><option value="${obsolete_id}" selected="selected"></option></select></div>`);
+
+        logger.trace("leave _make_revision_of_property", ret);
+        return ret;
+    }
+
+    /**
+     * Remove all properties from ent_element with the id of the "revisionOf"
+     * property.
+     *
+     * @param {HTMLElement} ent_element - entity in HTML representation.
+     */
+    var _remove_old_revision_of_property = function (ent_element) {
+        $(ent_element)
+            .find(".caosdb-f-entity-property")
+            .filter(function(index, property) {
+                if(_datamodel._revisionOfId === $(property)
+                    .find(".caosdb-property-id").text()) {
+                    return true;
+                }
+                return false;
+        }).remove();
+    }
+
+    /**
+     * Main functionality of this module is in here.
+     *
+     * This method is called by the (overridden) edit_mode.update_entity
+     * function before the actual update. It inserts a new obsolete entity
+     * which represesents the old state of the entity, deletes any revisionOf
+     * properties of the entity (if present) and adds a new revisionOf property
+     * which references the (newly inserted) obsolete entity.
+     *
+     * @param {HTMLElement} ent_element - The entity form which has been
+     * generated by the edit_mode with the changes by the user.
+     */
+    var _create_revision = async function (ent_element) {
+        logger.debug("create revision", ent_element);
+        var id = getEntityID(ent_element);
+
+        var obsolete_id = await _insert_obsolete(id);
+
+        // remove old revision of and add new one
+        _remove_old_revision_of_property(ent_element);
+        var revision_of_property = await _make_revision_of_property(obsolete_id);
+        var properties_section = ent_element.getElementsByClassName("caosdb-properties")[0];
+        properties_section.appendChild(revision_of_property);
+    };
+
+    /**
+     * Test whether the necessary entities exist ("revisionOf" and "Obsolete").
+     */
+    var _check_datamodel = async function() {
+        var results = Promise.all([
+            query(`FIND RecordType ${_datamodel.obsolete}`),
+            query(`FIND Property ${_datamodel.revisionOf}`)
+        ]);
+
+        for (let result of (await results)) {
+            if (result.length !== 1) {
+                throw new Error("Invalid datamodel");
+            }
+
+            var name = getEntityName(result[0]);
+            if (name && name.toLowerCase() === _datamodel.revisionOf.toLowerCase()) {
+                _datamodel._revisionOfId = getEntityID(result[0]);
+                _datamodel.revisionOf = name;
+            } else if (name && name.toLowerCase() === _datamodel.obsolete.toLowerCase()) {
+                _datamodel.obsolete = name;
+            }
+        }
+    };
+
+    /**
+     * Initialize the ext_revisions module.
+     *
+     * Per default, the module assumes two Entities to be present in the
+     * database. A RecordType named "Obsolete" and a Property named
+     * "revisionOf". The initialization is aborted if these entities cannot be
+     * found and the module remains inactive. For testing purposes the names of
+     * these entities can be set to different values via the respective
+     * parameters.
+     *
+     * @param {string} [obsolete] - The name of the obsolete RecordType.
+     * @param {string} [revisionOf] - The name of the revisionOf Property.
+     */
+    var init = async function (obsolete, revisionOf) {
+        if (typeof obsolete === "string") {
+            _datamodel.obsolete = obsolete;
+        }
+        if (typeof revisionOf === "string") {
+            _datamodel.revisionOf = revisionOf;
+        }
+
+        try {
+            await _check_datamodel();
+        } catch (err) {
+            logger.error("could not init ext_revisions", err);
+            return;
+        }
+
+        (function(proxied) {
+            edit_mode.update_entity = async function(ent_element) {
+                await _create_revision(ent_element);
+                return await proxied.apply(this, arguments)
+            };
+        })(edit_mode.update_entity);
+    }
+
+    return {
+        // public members, part of the API
+        init: init,
+        // private members, exposed for testing
+        _make_revision_of_property: _make_revision_of_property,
+        _datamodel: _datamodel,
+        _logger: logger,
+    }
+}($, log.getLogger("ext_revisions"), edit_mode, getEntityID, transaction, _createDocument);
+
+
+// this will be replaced by require.js in the future.
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_REVISIONS}" == "ENABLED") {
+        caosdb_modules.register(ext_revisions);
+    }
+});
diff --git a/src/core/js/preview.js b/src/core/js/preview.js
index a81b063e5c4b2e51f53335e7b86f43076ca8a0cb..04a7b699b2d1adae5143f7362e965828c3d61531 100644
--- a/src/core/js/preview.js
+++ b/src/core/js/preview.js
@@ -3,7 +3,9 @@
  */
 var preview = new function() {
 
-    this.previewReadyEvent = new Event("caosdb.preview.ready")
+    this.previewReadyEvent = new Event("caosdb.preview.ready");
+    this.showPreviewEvent = new Event("caosdb.preview.show");
+    this.hidePreviewEvent = new Event("caosdb.preview.hide");
 
     this.carouselId = 0;
     this.classNameEntityPreview = "caosdb-entity-preview";
@@ -99,6 +101,7 @@ var preview = new function() {
                 $(hidePreviewButton).hide();
                 $(refLinksContainer).show();
                 $(preview.getPreviewCarousel(ref_property_elem)).hide();
+                ref_property_elem.dispatchEvent(preview.hidePreviewEvent);
             });
         };
         app.onLeaveShowLinks = function(e) {
@@ -135,6 +138,7 @@ var preview = new function() {
                 $(preview.getPreviewCarousel(ref_property_elem)).show();
                 $(hidePreviewButton).show();
                 $(refLinksContainer).hide();
+                ref_property_elem.dispatchEvent(preview.showPreviewEvent);
             });
         }
         app.onResetApp = function(e, error) {
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index 56f00429e63b8454a88c952e39bb5666a57ca583..87b9e744c5e9d876347e0764c567df1a02f978bd 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -289,6 +289,13 @@ this.connection = new function() {
             return base;
         }
 
+        /**
+         * Return the root path of the file system resource.
+         */
+        this.getFileSystemPath = function () {
+            return connection.getBasePath() + "FileSystem/";
+        }
+
         /**
          * Return a full URI for these entities.
          *
diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl
index 2a4a803f95dd22f942d1f2d7ea9ee4b3303bf53a..7187c6b7d183b340a8eaee4cbdcead7ada44d433 100644
--- a/src/core/xsl/entity.xsl
+++ b/src/core/xsl/entity.xsl
@@ -181,15 +181,6 @@
             <xsl:apply-templates mode="entity-body" select="Property"/>
           </xsl:if>
         </ul>
-        <!-- Thumbnail -->
-        <xsl:if test="@path">
-          <xsl:call-template name="entity-body-thumbnail">
-            <xsl:with-param name="path" select="@path"/>
-          </xsl:call-template>
-          <xsl:call-template name="entity-body-video">
-            <xsl:with-param name="path" select="@path"/>
-          </xsl:call-template>
-        </xsl:if>
       </div>
       <!-- Annotations -->
       <xsl:call-template name="annotation-section">
@@ -198,38 +189,6 @@
       </xsl:call-template>
     </div>
   </xsl:template>
-  <!-- Thumbnails of images -->
-  <xsl:template name="entity-body-thumbnail">
-    <xsl:param name="path"/>
-    <xsl:if test="contains('.jpg.gif.png.svg',translate(substring($path, string-length($path) - 3), 'JPGIFNSV', 'jpgifnsv'))">
-      <div class="row">
-        <div class="col-sm-12">
-          <img class="entity-image-preview" style="max-width: 200px; max-height: 140px;">
-            <xsl:attribute name="src">
-              <xsl:value-of select="concat($filesystempath,$path)"/>
-            </xsl:attribute>
-          </img>
-        </div>
-      </div>
-    </xsl:if>
-  </xsl:template>
-  <!-- Video display -->
-  <xsl:template name="entity-body-video">
-    <xsl:param name="path"/>
-    <xsl:if test="contains('.mp4.mov.webm',translate(substring($path, string-length($path) - 3), 'MPOVWEB', 'mpovweb'))">
-      <div class="row">
-        <div class="col-sm-12">
-          <video controls="controls">
-            <source>
-              <xsl:attribute name="src">
-                <xsl:value-of select="concat($filesystempath,$path)"/>
-              </xsl:attribute>
-            </source>
-          </video>
-        </div>
-      </div>
-    </xsl:if>
-  </xsl:template>
   <!-- PROPERTIES -->
   <xsl:template match="Property" mode="entity-body">
     <li class="list-group-item caosdb-v-property-row caosdb-f-entity-property">
diff --git a/src/core/xsl/filesystem.xsl b/src/core/xsl/filesystem.xsl
index 1d9c189a029bf2459d67bd5cb0069f1fceb3149f..93028ed2f9667f5f7b673483cb87dacb2591d5ac 100644
--- a/src/core/xsl/filesystem.xsl
+++ b/src/core/xsl/filesystem.xsl
@@ -48,6 +48,21 @@
       </xsl:choose>
     </xsl:if>
   </xsl:template>
+  <!-- Thumbnails of images (Deprecated)-->
+  <xsl:template name="entity-body-thumbnail">
+    <xsl:param name="path"/>
+    <xsl:if test="contains('.jpg.gif.png.svg',translate(substring($path, string-length($path) - 3), 'JPGIFNSV', 'jpgifnsv'))">
+      <div class="row">
+        <div class="col-sm-12">
+          <img class="entity-image-preview" style="max-width: 200px; max-height: 140px;">
+            <xsl:attribute name="src">
+              <xsl:value-of select="concat($filesystempath,$path)"/>
+            </xsl:attribute>
+          </img>
+        </div>
+      </div>
+    </xsl:if>
+  </xsl:template>
   <xsl:template match="dir" mode="filesystem-item">
     <li class="list-group-item">
       <a class="caosdb-fs-dir">
diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl
index 40d9b24a4235e87f3b546d33808ff9372289d2f1..f4a128f63f52488657b32e8ff03043c276d4aaa3 100644
--- a/src/core/xsl/main.xsl
+++ b/src/core/xsl/main.xsl
@@ -132,6 +132,11 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/loglevel.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/plotly.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <xsl:element name="script">
       <xsl:attribute name="src">
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/webcaosdb.js')"/>
@@ -225,6 +230,16 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/tour.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bottom_line.js')"/>
+      </xsl:attribute>
+    </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_revisions.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <!--JS_EXTENSIONS-->
   </xsl:template>
   <xsl:template name="caosdb-data-container">
diff --git a/test/core/index.html b/test/core/index.html
index 762262ae2d2f0dbfc32a78f1e7aa004cb6b9a439..0d97a415334fd441319eec7e4db262c34d64ef07 100644
--- a/test/core/index.html
+++ b/test/core/index.html
@@ -38,6 +38,7 @@
   <script src="js/bootstrap-select.js"></script>
   <script src="js/bootstrap-autocomplete.min.js"></script>
   <script src="js/webcaosdb.js"></script>
+  <script src="js/plotly.js"></script>
   <script>
       caosdb_modules.auto_init = false;
       log.setLevel("trace");
@@ -63,6 +64,8 @@
   <script src="js/proj4.js"></script>
   <script src="js/proj4leaflet.js"></script>
   <script src="js/ext_map.js"></script>
+  <script src="js/ext_bottom_line.js"></script>
+  <script src="js/ext_revisions.js"></script>
   <script src="js/autocomplete.js"></script>
   <!--EXTENSIONS-->
   <script src="js/modules/webcaosdb.js.js"></script>
@@ -79,6 +82,8 @@
   <script src="js/modules/form_elements.js.js"></script>
   <script src="js/modules/ext_references.js.js"></script>
   <script src="js/modules/ext_map.js.js"></script>
+  <script src="js/modules/ext_bottom_line.js.js"></script>
+  <script src="js/modules/ext_revisions.js.js"></script>
   <script src="js/modules/autocomplete.js.js"></script>
 </body>
 </html>
diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js
index 61058a4301cc43c6f6da56999bcfccd320d92424..c6326e4400750f75ef0b939990a941c2f706da2e 100644
--- a/test/core/js/modules/edit_mode.js.js
+++ b/test/core/js/modules/edit_mode.js.js
@@ -169,10 +169,6 @@ QUnit.test("add_property_trash_button", function(assert){
     assert.ok(edit_mode.add_property_trash_button);
 });
 
-QUnit.test("update_entity_by_id", function(assert){
-    assert.ok(edit_mode.update_entity_by_id);
-});
-
 QUnit.test("insert_entity", function(assert){
     assert.ok(edit_mode.insert_entity);
 });
diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js
index a853c36556c5482e0bd0b1f751755e88b871d25e..9b790a007435d05d1e9410774bc8c898fe287a16 100644
--- a/test/core/js/modules/entity.xsl.js
+++ b/test/core/js/modules/entity.xsl.js
@@ -113,27 +113,6 @@ QUnit.test("Property with Permissions tag and type='ExperimentSeries' is recogni
     assert.equal(link_e.getAttribute("href"), "/entitypath/129950", "href location");
 });
 
-QUnit.test("File path is not converted to lower case.", function(assert) {
-    let xml_str = '<File path="UPPERCASE.JPG" id="1234"></File>';
-    let xml = str2xml(xml_str);
-    let html = applyTemplates(xml, this.entityXSL, 'entities');
-
-    // inject an entrance rule
-    //var xsl_tmp = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="File" mode="top-level-data"/></xsl:template>');
-
-
-    //var params = {
-    //    entitypath: "/entitypath/",
-    //     filesystempath: "/filesystempath/"
-    //};
-    //var html = xslt(xml, xsl, params);
-    assert.ok(html, "html is ok.");
-
-    var link_e = html.firstElementChild.getElementsByTagName("IMG")[0]
-    assert.ok(link_e, "<img> tag is there.");
-    assert.equal(link_e.getAttribute("src"), "/filesystempath/UPPERCASE.JPG", "src location is UPPERCASE.JPG");
-});
-
 QUnit.test("back references", function(assert) {
     // inject an entrance rule
     var xsl = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="*/@id" mode="backreference-link"/></xsl:template>');
diff --git a/test/core/js/modules/ext_bottom_line.js b/test/core/js/modules/ext_bottom_line.js
new file mode 100644
index 0000000000000000000000000000000000000000..46903ead3226c1cb6038e9320548284ce76b0672
--- /dev/null
+++ b/test/core/js/modules/ext_bottom_line.js
@@ -0,0 +1,124 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2019 IndiScale GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) {
+
+    const sleep = (ms) => {
+      return new Promise(res => setTimeout(res, ms))
+    }
+
+    var test_config = { "version": 0.1,
+      "fallback": "blablabla",
+      "creators": [
+        { "id": "test.success",
+          "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) === 'TestPreviewRecord-success'",
+          "create": "(entity) => 'SUCCESS'"
+        },
+        { "id": "test.error",
+          "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) === 'TestPreviewRecord-error'",
+          "create": "(entity) => new Promise((res,rej) => {rej('Test Error');})"
+        },
+        { "id": "test.load-forever",
+          "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) === 'TestPreviewRecord-load-forever'",
+          "create": "(entity) => new Promise((res,rej) => {})"
+        },
+        { "id": "test.success-2",
+          "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) !== 'TestPreviewRecord-fall-back'",
+          "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}]); }"
+        }
+      ]
+    };
+
+    QUnit.module("ext_bottom_line.js", {
+        before: async function (assert) {
+            // setup before module
+            await ext_bottom_line.configure(test_config);
+        },
+        beforeEach: function (assert) {
+            // setup before each test
+        },
+        afterEach: function (assert) {
+            // teardown after each test
+        },
+        after: function (assert) {
+            // teardown after module
+        }
+    });
+
+    QUnit.test("_creators", function (assert) {
+        assert.equal(ext_bottom_line._creators.length, 7, "seven creators");
+    });
+
+    QUnit.test("add_preview_container", function(assert) {
+        var entity = $("<div/>");
+        var container = $(ext_bottom_line.add_preview_container(entity[0]));
+        assert.ok(container.hasClass(ext_bottom_line._css_class_preview_container), `has class ${ext_bottom_line._css_class_preview_container}`);
+        assert.ok(container.hasClass(ext_bottom_line._css_class_preview_container_resolvable), `has class ${ext_bottom_line._css_class_preview_container_resolvable}`);
+    });
+
+    QUnit.test("root_preview_handler", async function(assert) {
+        for (let name_suffix of ["fall-back", "error", "load-forever", "success"]) {
+            let name = "TestPreviewRecord-" + name_suffix;
+            let entity_xml = `<Response><Record name="${name}"><Parent name='TestPreviewRecordType'/></Record></Response>`;
+            let entity = (await transformation.transformEntities(str2xml(entity_xml)))[0];
+            assert.equal(getEntityName(entity), name, "correct name");
+            var container = $(ext_bottom_line.add_preview_container(entity));
+
+            assert.ok(container.hasClass(ext_bottom_line._css_class_preview_container), `before has class ${ext_bottom_line._css_class_preview_container}`);
+            assert.ok(container.hasClass(ext_bottom_line._css_class_preview_container_resolvable), `before has class ${ext_bottom_line._css_class_preview_container_resolvable}`);
+
+            if (name_suffix === "load-forever"){
+                ext_bottom_line.root_preview_handler(entity);
+                await sleep(1000);
+            } else {
+                await ext_bottom_line.root_preview_handler(entity);
+            }
+
+            // ..._resolvable class removed
+            assert.ok(container.hasClass(ext_bottom_line._css_class_preview_container), `after has class ${ext_bottom_line._css_class_preview_container}`);
+            assert.notOk(container.hasClass(ext_bottom_line._css_class_preview_container_resolvable), `after missing class ${ext_bottom_line._css_class_preview_container_resolvable}`);
+
+
+            switch(name_suffix) {
+                case "fall-back":
+                    assert.equal(container.text(), "blablabla");
+                    break;
+                case "error":
+                    assert.equal(container.text(), "An error occured while loading this preview");
+                    break;
+                case "load-forever":
+                    assert.equal(container.text(), "Please wait...");
+                    break;
+                case "success":
+                    assert.equal(container.text(), "SUCCESS");
+                    break;
+                default:
+                    assert.ok(false);
+            }
+
+        }
+
+    });
+
+}($, ext_bottom_line, QUnit);
diff --git a/test/core/js/modules/ext_revisions.js.js b/test/core/js/modules/ext_revisions.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..e90fd7c97851e5f054690cb0d376be5e14d826e4
--- /dev/null
+++ b/test/core/js/modules/ext_revisions.js.js
@@ -0,0 +1,121 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+var ext_revisions_test_suite = function ($, ext_revisions, QUnit, edit_mode) {
+
+    var datamodel = ext_revisions._datamodel;
+
+    QUnit.module("ext_revisions.js", {
+        before: function (assert) {
+            // setup before module
+            this.original_update_entity = edit_mode.update_entity;
+            this.original_insert = transaction.insertEntitiesXml;
+            this.original_retrieve = transaction.retrieveEntityById;
+            this.original_query = query;
+            ext_revisions._logger.setLevel("trace");
+        },
+        beforeEach: function (assert) {
+            // setup before each test
+            datamodel.obsolete = "UNITTESTObsolete";
+            datamodel.revisionOf = "UNITTESTRevisionOf";
+        },
+        afterEach: function (assert) {
+            // teardown after each test
+            query = this.original_query;
+            edit_mode.update_entity = this.original_update_entity;
+            transaction.insertEntitiesXml = this.original_insert;
+            transaction.retrieveEntityById = this.original_retrieve;
+        },
+        after: function (assert) {
+            // teardown after module
+        }
+    });
+
+    QUnit.test("_make_revision_of_property", async function(assert) {
+        var p = await ext_revisions._make_revision_of_property("1234");
+        var editfield = $(p).find(".caosdb-property-edit-value");
+        var value = $(editfield).find("select").first()[0].selectedOptions[0].value;
+        assert.ok($(p).hasClass("caosdb-f-entity-property"), "is property");
+        assert.equal(value, "1234", "has value 1234");
+        assert.equal(getPropertyName(p), datamodel.revisionOf, "has revisionOf name");
+        assert.equal(getPropertyDatatype(p), datamodel.obsolete, "has Obsolete datatype");
+    });
+
+    /**
+     * This is a rather complete test, not a unit test.
+     */
+    QUnit.test("update calls update_entity through proxy", async function (assert) {
+        var done = assert.async(3);
+        var done_query = assert.async(2);
+        var ent_element = $('<div data-entity-id="15"><div class="caosdb-properties"/></div>')[0];
+
+        // mock server responses to several requests...
+        var retrieve_fun = async function(id) {
+            assert.equal(id, "15", "retrieve id 15");
+            done();
+            return $(`<Record id="15"><Parent name="ORIG_PARENT"/></Record>`)[0];
+        }
+        var insert_fun = async function(xml) {
+            var rec = xml.firstElementChild.firstElementChild;
+            assert.equal(rec.id, "-1", "insert with tmp id");
+            assert.equal($(rec).find("Parent").attr("name"), datamodel.obsolete, "Obsolete Parent");
+            xml.firstElementChild.firstElementChild.id = "2345";
+            console.log(xml2str(xml));
+            done();
+            return xml;
+        };
+        var update_fun = async function(ent_element) {
+            var prop = edit_mode.getProperties(ent_element)[0];
+            assert.equal(prop.name, datamodel.revisionOf, "has revisionOf");
+            assert.equal(prop.value, "2345", "revisionOf 2345");
+            done();
+        };
+        var query_fun = async function(query) {
+            assert.ok(query.startsWith("FIND") && ( query.endsWith(datamodel.obsolete) || query.endsWith(datamodel.revisionOf)), query);
+            done_query(); // called twice
+            return [$(`<div data-entity-name="${datamodel.revisionOf}" data-caosdb-id="3456"/>`)[0]];
+        }
+
+        // injecting the server mock-up responses.
+        transaction.retrieveEntityById = retrieve_fun;
+        transaction.insertEntitiesXml = insert_fun;
+        edit_mode.update_entity = update_fun;
+        query = query_fun;
+
+
+        // actual tests
+        assert.equal(update_fun, edit_mode.update_entity, "before init, the edit_mode.update_entity function has not been overridden.");
+
+        // call init which checks the datamodel and overwrites the
+        // edit_mode.update_entity function.
+        await ext_revisions.init();
+        assert.notEqual(update_fun, edit_mode.update_entity, "after init, the edit_mode.update_entity hab been overriden with a proxy calling the update_fun and the original function.");
+
+        // call edit_mode.update_entity which calls the insert_fun and the
+        // update_fun
+        await edit_mode.update_entity(ent_element);
+    });
+
+}($, ext_revisions, QUnit, edit_mode);