From 1a21e0d3baca3870611a1fe712027a559d9a4b93 Mon Sep 17 00:00:00 2001
From: Kai Reinhard <K.Reinhard@micromata.de>
Date: Sun, 09 Dec 2018 21:43:05 +0000
Subject: [PATCH] Web server started.

---
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java              |   58 
 borgbutler-webapp/public/manifest.json                                                          |   46 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/Languages.java                   |   20 
 borgbutler-webapp/public/favicon/apple-icon-72x72.png                                           |    0 
 borgbutler-webapp/src/components/general/loading/LoadingOverlay.jsx                             |   28 
 borgbutler-webapp/public/favicon/apple-icon-144x144.png                                         |    0 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java         |   69 
 borgbutler-webapp/src/components/general/translation/I18n.jsx                                   |   20 
 borgbutler-webapp/src/components/views/footer/Footer.jsx                                        |   25 
 borgbutler-webapp/public/favicon/ms-icon-310x310.png                                            |    0 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfigurationHandler.java  |  112 +
 borgbutler-webapp/public/favicon/apple-icon.png                                                 |    0 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java            |   88 +
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java                 |   85 +
 borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx                        |  215 ++
 borgbutler-webapp/src/components/general/forms/FormComponents.jsx                               |  298 +++
 borgbutler-webapp/src/actions/log.js                                                            |   54 
 borgbutler-webapp/src/components/views/logging/LogViewer.css                                    |   12 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/Version.java                     |  107 +
 borgbutler-webapp/public/favicon/favicon-96x96.png                                              |    0 
 borgbutler-webapp/public/favicon/ms-icon-70x70.png                                              |    0 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java            |   70 
 borgbutler-webapp/public/favicon.ico                                                            |    0 
 borgbutler-webapp/src/components/general/forms/EditableTextField.jsx                            |  136 +
 borgbutler-webapp/src/components/views/develop/RestServices.jsx                                 |  146 +
 borgbutler-webapp/public/images/merlin-icon.png                                                 |    0 
 borgbutler-webapp/public/favicon/apple-icon-120x120.png                                         |    0 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java |  136 +
 borgbutler-webapp/src/components/general/forms/FormButton.jsx                                   |   55 
 borgbutler-webapp/src/components/views/logging/LogEntry.jsx                                     |   24 
 borgbutler-webapp/package.json                                                                  |   40 
 borgbutler-webapp/public/browserconfig.xml                                                      |    2 
 borgbutler-webapp/public/favicon/apple-icon-76x76.png                                           |    0 
 borgbutler-webapp/public/css/bootstrap.min.css                                                  |    7 
 borgbutler-webapp/public/index.html                                                             |   32 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java               |   41 
 borgbutler-webapp/public/favicon/android-icon-96x96.png                                         |    0 
 borgbutler-webapp/public/favicon/apple-icon-precomposed.png                                     |    0 
 borgbutler-webapp/src/components/views/logging/LogEmbeddedPanel.jsx                             |  106 +
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java      |   87 +
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java      |   55 
 borgbutler-webapp/build.gradle                                                                  |    8 
 borgbutler-webapp/public/favicon/apple-icon-57x57.png                                           |    0 
 borgbutler-webapp/src/components/general/IconComponents.jsx                                     |  127 +
 borgbutler-webapp/public/favicon/favicon-32x32.png                                              |    0 
 borgbutler-webapp/src/components/views/config/UpdatePage.jsx                                    |  126 +
 borgbutler-webapp/public/favicon/favicon-16x16.png                                              |    0 
 borgbutler-webapp/src/containers/WebApp.test.js                                                 |    9 
 borgbutler-webapp/src/css/my-style.css                                                          |  406 ++++
 borgbutler-webapp/src/utilities/global.js                                                       |   49 
 borgbutler-webapp/src/components/general/Loading.jsx                                            |    8 
 borgbutler-webapp/src/index.js                                                                  |   47 
 borgbutler-webapp/src/components/general/forms/EditableTextField.css                            |   33 
 borgbutler-webapp/public/favicon/ms-icon-144x144.png                                            |    0 
 borgbutler-webapp/src/components/general/OpenLocalFile.jsx                                      |   47 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserUtils.java              |   53 
 borgbutler-webapp/public/favicon/apple-icon-60x60.png                                           |    0 
 borgbutler-server/build.gradle                                                                  |   93 +
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java    |  121 +
 borgbutler-webapp/src/components/views/logging/LogTable.jsx                                     |   72 
 borgbutler-webapp/src/actions/index.js                                                          |    6 
 borgbutler-webapp/src/actions/types.js                                                          |    8 
 borgbutler-webapp/src/actions/version.js                                                        |   38 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserManager.java            |   24 
 borgbutler-webapp/public/favicon/apple-icon-152x152.png                                         |    0 
 borgbutler-webapp/src/components/general/ErrorAlertGenericRestFailure.jsx                       |   13 
 borgbutler-webapp/src/components/general/forms/FormSelect.jsx                                   |   71 
 borgbutler-server/src/main/resources/log4j.properties                                           |   19 
 borgbutler-webapp/src/components/general/LinkFile.jsx                                           |   41 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java           |  177 ++
 borgbutler-webapp/src/reducers/index.js                                                         |   12 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java            |   42 
 borgbutler-webapp/public/favicon/ms-icon-150x150.png                                            |    0 
 borgbutler-webapp/public/favicon/android-icon-144x144.png                                       |    0 
 borgbutler-webapp/public/favicon/android-icon-48x48.png                                         |    0 
 borgbutler-webapp/public/favicon/apple-icon-114x114.png                                         |    0 
 borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx                             |  120 +
 .gitignore                                                                                      |    1 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java                        |  113 +
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogFilter.java           |   71 
 borgbutler-webapp/src/components/views/footer/style.css                                         |   12 
 borgbutler-webapp/public/favicon/apple-icon-180x180.png                                         |    0 
 borgbutler-webapp/src/components/views/config/ConfigurationAccountTab.jsx                       |  135 +
 borgbutler-webapp/public/favicon/android-icon-36x36.png                                         |    0 
 borgbutler-webapp/src/components/general/loading/failed/Overlay.jsx                             |   15 
 borgbutler-webapp/src/containers/WebApp.jsx                                                     |   77 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java             |   53 
 borgbutler-webapp/src/components/general/loading/LoadingOverlay.module.css                      |   18 
 borgbutler-webapp/public/favicon/android-icon-192x192.png                                       |    0 
 borgbutler-webapp/src/components/general/ErrorAlert.jsx                                         |   20 
 borgbutler-webapp/src/reducers/version.js                                                       |   44 
 settings.gradle                                                                                 |    7 
 borgbutler-webapp/src/components/views/logging/LogPage.jsx                                      |   89 +
 borgbutler-webapp/src/utilities/i18n.js                                                         |   73 
 borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserData.java               |   59 
 borgbutler-webapp/public/favicon/android-icon-72x72.png                                         |    0 
 borgbutler-webapp/src/components/general/BootstrapComponents.jsx                                |   23 
 borgbutler-webapp/src/components/general/forms/FormCheckbox.jsx                                 |   62 
 borgbutler-webapp/src/components/general/Menu.jsx                                               |   89 +
 borgbutler-webapp/src/components/views/logging/LogFilters.jsx                                   |   91 +
 borgbutler-webapp/src/reducers/log.js                                                           |   52 
 borgbutler-webapp/src/components/views/Start.jsx                                                |   20 
 102 files changed, 5,068 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3fc88b8..853fd5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@
 *.iws
 borgbutler-core/build
 borgbutler-core/out
+borgbutler-server/out
diff --git a/borgbutler-server/build.gradle b/borgbutler-server/build.gradle
new file mode 100644
index 0000000..1a3c1cb
--- /dev/null
+++ b/borgbutler-server/build.gradle
@@ -0,0 +1,93 @@
+description = 'borgbutler-server'
+
+buildscript {
+    repositories {
+        mavenCentral()
+    }
+}
+
+dependencies {
+    compile project(':borgbutler-core')
+    // https://mvnrepository.com/artifact/org.apache.commons/commons-text
+    compile group: 'org.apache.commons', name: 'commons-text', version: '1.6'
+    compile group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.4.12.v20180830'
+    compile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '9.4.12.v20180830'
+    compile group: 'org.eclipse.jetty', name: 'jetty-servlets', version: '9.4.12.v20180830'
+    compile group: 'org.glassfish.jaxb', name: 'jaxb-core', version: '2.3.0.1'
+    compile group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.1'
+    compile group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet', version: '2.27'
+    compile group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.27'
+    compile group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.27'
+    compile group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.27'
+    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'
+    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6'
+    compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
+    compile group: 'javax.xml.ws', name: 'jaxws-api', version: '2.3.1'
+    compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
+
+    // https://mvnrepository.com/artifact/commons-cli/commons-cli
+    compile group: 'commons-cli', name: 'commons-cli', version: '1.4'
+    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.21.0'
+}
+
+apply plugin: 'application'
+mainClassName = "de.micromata.borgbutler.server.Main"
+
+run() {
+    doFirst {
+        jvmArgs = [
+                "-DapplicationHome=${rootDir}"
+        ]
+    }
+}
+
+run.dependsOn ':borgbutler-webapp:npmBuild'
+// run.dependsOn ':borgbutler-docs:buildWebDoc'
+
+apply plugin: 'distribution'
+
+task createVersionProperties(dependsOn: processResources) {
+    doLast {
+        new File("$buildDir/resources/main/version.properties").withWriter { w ->
+            Properties p = new Properties()
+            p['version'] = project.version.toString()
+            p['name'] = project.name
+            p['build.date.millis'] = '' + System.currentTimeMillis()
+            p.store w, null
+        }
+    }
+}
+
+classes {
+    dependsOn createVersionProperties
+}
+
+// Ugly work arround for getting the applications home dir:
+applicationDefaultJvmArgs = ["-DapplicationHome=MY_APPLICATION_HOME"]
+
+startScripts {
+    doLast {
+        unixScript.text = unixScript.text.replace('MY_APPLICATION_HOME', '\$APP_HOME')
+        windowsScript.text = windowsScript.text.replace('MY_APPLICATION_HOME', '%~dp0..')
+    }
+}
+
+// Builds the distribution
+distributions {
+    main {
+        contents {
+            // Prepared by nbmBuild:
+            from ("${project(':borgbutler-webapp').projectDir}/build") {
+                into 'web'
+            }
+            // Containing test templates and other stuff:
+            from ("${rootProject.projectDir}/examples") {
+                into 'examples'
+            }
+        }
+    }
+}
+
+distZip.dependsOn ':borgbutler-webapp:npmBuild'
+//distZip.dependsOn ':borgbutler-docs:buildWebDoc'
+task(dist).dependsOn distZip
\ No newline at end of file
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Languages.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Languages.java
new file mode 100644
index 0000000..c535a43
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Languages.java
@@ -0,0 +1,20 @@
+package de.micromata.borgbutler.server;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Locale;
+
+public class Languages {
+    public static Locale asLocale(String language) {
+        return asLocale(language, false);
+    }
+
+    public static Locale asLocale(String language, boolean rootAsDefault) {
+        Locale locale = StringUtils.isNotBlank(language) ? Locale.forLanguageTag(language) : null;
+        return (locale != null || !rootAsDefault) ? locale : Locale.ROOT;
+    }
+
+    public static String asString(Locale locale) {
+        return locale != null ? locale.getLanguage() : null;
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
new file mode 100644
index 0000000..2bcd91d
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Main.java
@@ -0,0 +1,113 @@
+package de.micromata.borgbutler.server;
+
+import de.micromata.borgbutler.server.jetty.JettyServer;
+import de.micromata.borgbutler.server.user.SingleUserManager;
+import de.micromata.borgbutler.server.user.UserManager;
+import org.apache.commons.cli.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Main {
+    private static Logger log = LoggerFactory.getLogger(Main.class);
+
+    private static final Main main = new Main();
+
+    private JettyServer server;
+    private boolean shutdownInProgress;
+
+    private Main() {
+    }
+
+    public static void main(String[] args) {
+        main._start(args);
+    }
+
+    public static JettyServer startUp(String... restPackageNames) {
+        return main._startUp(restPackageNames);
+    }
+
+    public static void shutdown() {
+        main._shutdown();
+    }
+
+
+    private void _start(String[] args) {
+        // create Options object
+        Options options = new Options();
+        options.addOption("p", "port", true, "The default port for the web server.");
+        options.addOption("q", "quiet", false, "Don't open browser automatically.");
+        options.addOption("h", "help", false, "Print this help screen.");
+        CommandLineParser parser = new DefaultParser();
+        try {
+            // parse the command line arguments
+            CommandLine line = parser.parse(options, args);
+            if (line.hasOption('h')) {
+                printHelp(options);
+                return;
+            }
+            if (line.hasOption('p')) {
+                // initialise the member variable
+                String portString = line.getOptionValue("p");
+                try {
+                    int port = Integer.parseInt(portString);
+                    if (port < 1 || port > 65535) {
+                        System.err.println("Port outside range.");
+                        return;
+                    }
+                    ServerConfigurationHandler.getDefaultConfiguration().setPort(port);
+                } catch (NumberFormatException ex) {
+                    printHelp(options);
+                    return;
+                }
+            }
+            RunningMode.setServerType(RunningMode.ServerType.SERVER);
+            RunningMode.logMode();
+            Runtime.getRuntime().addShutdownHook(new Thread() {
+                @Override
+                public void run() {
+                    main._shutdown();
+                }
+            });
+
+            JettyServer server = startUp();
+            if (!line.hasOption('q')) {
+
+                try {
+                    java.awt.Desktop.getDesktop().browse(java.net.URI.create(server.getUrl()));
+                } catch (Exception ex) {
+                    log.info("Can't open web browser: " + ex.getMessage());
+                }
+            }
+        } catch (ParseException ex) {
+            // oops, something went wrong
+            System.err.println("Parsing failed.  Reason: " + ex.getMessage());
+            printHelp(options);
+        }
+    }
+
+    private JettyServer _startUp(String... restPackageNames) {
+        server = new JettyServer();
+        server.start(restPackageNames);
+
+        UserManager.setUserManager(new SingleUserManager());
+
+        return server;
+    }
+
+    private void _shutdown() {
+        synchronized (this) {
+            if (shutdownInProgress == true) {
+                // Another thread already called this method. There is nothing further to do.
+                return;
+            }
+            shutdownInProgress = true;
+        }
+        log.info("Shutting down BorgButler web server...");
+        server.stop();
+    }
+
+    private static void printHelp(Options options) {
+        HelpFormatter formatter = new HelpFormatter();
+        formatter.printHelp("borgbutler-server", options);
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java
new file mode 100644
index 0000000..74af5ec
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/RunningMode.java
@@ -0,0 +1,85 @@
+package de.micromata.borgbutler.server;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.TimeZone;
+
+public class RunningMode {
+    private static Logger log = LoggerFactory.getLogger(RunningMode.class);
+    private static OSType osType;
+
+    public enum Mode {PRODUCTION, DEVELOPMENT}
+
+    public enum ServerType {DESKTOP, SERVER}
+
+    public enum UserManagement {SINGLE}
+
+    public enum OSType {MAC_OS, WINDOWS, LINUX, OTHER}
+
+    private static boolean running;
+    private static File baseDir;
+    private static Boolean development;
+    private static ServerType serverType;
+    private static UserManagement userManagement = UserManagement.SINGLE;
+
+    public static Mode getMode() {
+        return isDevelopmentMode() ? Mode.DEVELOPMENT : Mode.PRODUCTION;
+    }
+
+    public static boolean isDevelopmentMode() {
+        if (development == null) {
+            development = new File(ServerConfiguration.getApplicationHome(), "merlin-core").exists();
+            if (development) {
+                log.warn("*** Starting Merlin server in development mode. This mode shouldn't be used in production environments. ***");
+            }
+        }
+        return development;
+    }
+
+    public static OSType getOSType() {
+        if (osType == null) {
+            String osTypeString = System.getProperty("os.name");
+            if (osTypeString == null) {
+                osType = OSType.OTHER;
+            } else if (osTypeString.toLowerCase().contains("mac")) {
+                osType = OSType.MAC_OS;
+            } else if (osTypeString.toLowerCase().contains("win")) {
+                osType = OSType.WINDOWS;
+            } else if (osTypeString.toLowerCase().contains("linux")) {
+                osType = OSType.LINUX;
+            } else {
+                osType = OSType.OTHER;
+            }
+        }
+        return osType;
+    }
+
+    public static ServerType getServerType() {
+        return serverType;
+    }
+
+    public static void setServerType(ServerType serverType) {
+        if (RunningMode.serverType != null && serverType != RunningMode.serverType) {
+            throw new IllegalArgumentException("Can't set server-type twice with different values: new='"
+                    + serverType + "', old='" + RunningMode.serverType + "'.");
+        }
+        RunningMode.serverType = serverType;
+    }
+
+    public static UserManagement getUserManagement() {
+        return userManagement;
+    }
+
+    /**
+     * After setting all values you should call this method for a logging output with all current settings.
+     */
+    public static void logMode() {
+        log.info("Starting " + Version.getInstance().getAppName() + " " + Version.getInstance().getVersion()
+                + " (" + Version.getInstance().formatBuildDateISO(TimeZone.getDefault())
+                + ") with: mode='" + RunningMode.getMode() + "', serverType='" + RunningMode.serverType
+                + "', home dir='" + ServerConfiguration.getApplicationHome() + "', javaVersion='"
+                + System.getProperty("java.version") + "'.");
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
new file mode 100644
index 0000000..3d2023b
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfiguration.java
@@ -0,0 +1,69 @@
+package de.micromata.borgbutler.server;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.beans.Transient;
+
+public class ServerConfiguration {
+    private static Logger log = LoggerFactory.getLogger(ServerConfiguration.class);
+    private final static String[] SUPPORTED_LANGUAGES = {"en"};
+    private static String applicationHome;
+
+    private int port;
+    private boolean webDevelopmentMode = false;
+    private boolean templatesDirModified = false;
+
+    public static ServerConfiguration getDefault() {
+        return ServerConfigurationHandler.getDefaultConfiguration();
+    }
+
+    public static String[] getSupportedLanguages() {
+        return SUPPORTED_LANGUAGES;
+    }
+
+    public static String getApplicationHome() {
+        if (applicationHome == null) {
+            applicationHome = System.getProperty("applicationHome");
+            if (StringUtils.isBlank(applicationHome)) {
+                applicationHome = System.getProperty("user.dir");
+                log.info("applicationHome is not given as JVM   parameter. Using current working dir (OK for start in IDE): " + applicationHome);
+            }
+        }
+        return applicationHome;
+    }
+
+    public void resetModifiedFlag() {
+        templatesDirModified = false;
+    }
+
+    @Transient
+    public boolean isTemplatesDirModified() {
+        return templatesDirModified;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    /**
+     * If true, CrossOriginFilter will be set.
+     */
+    public boolean isWebDevelopmentMode() {
+        return webDevelopmentMode;
+    }
+
+    public void setWebDevelopmentMode(boolean webDevelopmentMode) {
+        this.webDevelopmentMode = webDevelopmentMode;
+    }
+
+    public void copyFrom(ServerConfiguration other) {
+        this.port = other.port;
+        this.webDevelopmentMode = other.webDevelopmentMode;
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfigurationHandler.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfigurationHandler.java
new file mode 100644
index 0000000..ff58c65
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/ServerConfigurationHandler.java
@@ -0,0 +1,112 @@
+package de.micromata.borgbutler.server;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+public class ServerConfigurationHandler {
+    private Logger log = LoggerFactory.getLogger(ServerConfigurationHandler.class);
+    private static final ServerConfigurationHandler instance = new ServerConfigurationHandler();
+    private static final String WEBSERVER_PORT_PREF = "webserver-port";
+    public static final int WEBSERVER_PORT_DEFAULT = 8042;
+    private static final String LANGUAGE_PREF = "language";
+    private static final String LANGUAGE_DEFAULT = null;
+    private static final String WEB_DEVELOPMENT_MODE_PREF = "web-development-mode";
+    private static final boolean WEB_DEVELOPMENT_MODE_PREF_DEFAULT = false;
+
+    private Preferences preferences;
+    private ServerConfiguration configuration = new ServerConfiguration();
+    private Set<String> extraPreferences = new HashSet<>();
+
+    /**
+     * Only for test case.
+     *
+     * @param preferences
+     */
+    ServerConfigurationHandler(Preferences preferences) {
+        this.preferences = preferences;
+    }
+
+    private ServerConfigurationHandler() {
+        preferences = Preferences.userRoot().node("de").node("micromata").node("merlin");
+        load();
+    }
+
+    public static ServerConfigurationHandler getInstance() {
+        return instance;
+    }
+
+    public static ServerConfiguration getDefaultConfiguration() {
+        return instance.getConfiguration();
+    }
+
+    public ServerConfiguration getConfiguration() {
+        return configuration;
+    }
+
+    public void load() {
+        configuration.setPort(preferences.getInt(WEBSERVER_PORT_PREF, WEBSERVER_PORT_DEFAULT));
+        configuration.setWebDevelopmentMode(preferences.getBoolean(WEB_DEVELOPMENT_MODE_PREF, WEB_DEVELOPMENT_MODE_PREF_DEFAULT));
+        configuration.resetModifiedFlag();
+    }
+
+    public void save() {
+        log.info("Saving configuration to user prefs.");
+        preferences.putInt(WEBSERVER_PORT_PREF, configuration.getPort());
+        preferences.putBoolean(WEB_DEVELOPMENT_MODE_PREF, configuration.isWebDevelopmentMode());
+        try {
+            preferences.flush();
+        } catch (BackingStoreException ex) {
+            log.error("Couldn't flush user preferences: " + ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * For saving own properties.
+     *
+     * @param key   The key under which to save the given value.
+     * @param value The value to store. If null, any previous stored value under the given key will be removed.
+     */
+    public void save(String key, String value) {
+        if (StringUtils.isEmpty(value)) {
+            preferences.remove(key);
+        } else {
+            preferences.put(key, value);
+            extraPreferences.add(key);
+        }
+        try {
+            preferences.flush();
+        } catch (BackingStoreException ex) {
+            log.error("Couldn't flush user preferences: " + ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * @param key Gets own property saved with {@link #save()}.
+     */
+    public String get(String key, String defaultValue) {
+        extraPreferences.add(key);
+        return preferences.get(key, defaultValue);
+    }
+
+    public void removeAllSettings() {
+        log.warn("Removes all configuration settings from user prefs.");
+        preferences.remove(WEBSERVER_PORT_PREF);
+        preferences.remove(LANGUAGE_PREF);
+        preferences.remove(WEB_DEVELOPMENT_MODE_PREF);
+        for(String extraKey : extraPreferences) {
+            preferences.remove(extraKey);
+        }
+        try {
+            preferences.flush();
+        } catch (BackingStoreException ex) {
+            log.error("Couldn't flush user preferences: " + ex.getMessage(), ex);
+        }
+        load();
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Version.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Version.java
new file mode 100644
index 0000000..7afb727
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/Version.java
@@ -0,0 +1,107 @@
+package de.micromata.borgbutler.server;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Properties;
+import java.util.TimeZone;
+
+public class Version {
+    private Logger log = LoggerFactory.getLogger(Version.class);
+    private String appName;
+    private String version;
+    private String buildDateUTC;
+    private Date buildDate;
+    private String updateVersion;
+
+    private static final Version instance = new Version();
+
+    public static Version getInstance() {
+        instance.init();
+        return instance;
+    }
+
+    private void init() {
+        synchronized (this) {
+            if (appName != null) {
+                return;
+            }
+            try (InputStream inputStream = ClassLoader.getSystemResourceAsStream("version.properties")) {
+                if (inputStream == null) {
+                    log.warn("version.properties not found (OK, if started e. g. in IDE");
+                    version = "99.0";
+                    appName = "BorgButler";
+                    buildDate = new Date();
+                } else {
+                    Properties props = new Properties();
+                    props.load(inputStream);
+                    appName = props.getProperty("name");
+                    version = props.getProperty("version");
+                    String buildDateMillisString = props.getProperty("build.date.millis");
+                    long buildDateMillis = 0;
+                    if (buildDateMillisString != null) {
+                        try {
+                            buildDateMillis = Long.parseLong(buildDateMillisString);
+                        } catch (NumberFormatException ex) {
+                            log.error("Can't parse build date (millis expected): " + buildDateMillisString + ": " + ex.getMessage(), ex);
+                        }
+                    }
+                    buildDate = new Date(buildDateMillis);
+                }
+            } catch (Exception ex) {
+                log.error("Can't load version information from classpath. File 'version.properties' not found: " + ex.getMessage(), ex);
+                appName = "BorgButler";
+                version = "?.?";
+                buildDateUTC = "1970-01-01 00:00:00";
+                return;
+            }
+            buildDateUTC = formatBuildDateISO(TimeZone.getTimeZone("UTC"));
+            log.debug("appName=" + appName + ", version=" + version + ", buildDateUTC=" + buildDateUTC);
+        }
+    }
+
+    public String formatBuildDateISO(TimeZone timeZone) {
+        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss");
+        formatter.setTimeZone(timeZone);
+        return formatter.format(buildDate);
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Replaces -SNAPSHOT by dev for snapshot versions. For none snapshot releases the returned value is equal to the version.
+     *
+     * @return the version as string.
+     */
+    public String getShortVersion() {
+        return version.replace("-SNAPSHOT", "dev");
+    }
+
+    public String getBuildDateUTC() {
+        return buildDateUTC;
+    }
+
+    public Date getBuildDate() {
+        return buildDate;
+    }
+
+    /**
+     * @return Version of the available update, if exist. Otherwise null.
+     */
+    public String getUpdateVersion() {
+        return updateVersion;
+    }
+
+    public void setUpdateVersion(String updateVersion) {
+        this.updateVersion = updateVersion;
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java
new file mode 100644
index 0000000..1f153e8
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/jetty/JettyServer.java
@@ -0,0 +1,177 @@
+package de.micromata.borgbutler.server.jetty;
+
+import de.micromata.borgbutler.server.ServerConfiguration;
+import de.micromata.borgbutler.server.ServerConfigurationHandler;
+import de.micromata.borgbutler.server.RunningMode;
+import de.micromata.borgbutler.server.rest.ConfigurationRest;
+import de.micromata.borgbutler.server.user.UserFilter;
+import org.apache.commons.lang3.ArrayUtils;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.*;
+import org.eclipse.jetty.servlets.CrossOriginFilter;
+import org.eclipse.jetty.util.resource.Resource;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.DispatcherType;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.EnumSet;
+
+public class JettyServer {
+    private Logger log = LoggerFactory.getLogger(JettyServer.class);
+    private static final String HOST = "127.0.0.1";
+    private static final int MAX_PORT_NUMBER = 65535;
+    private Server server;
+    private int port;
+
+    public void start(String... restPackageNames) {
+        port = findFreePort();
+        if (port == -1) {
+            return;
+        }
+        log.info("Starting web server on port " + port);
+        server = new Server();
+
+        ServerConnector connector = new ServerConnector(server);
+        connector.setHost(HOST);
+        connector.setPort(port);
+        server.setConnectors(new Connector[]{connector});
+
+        ServletContextHandler ctx =
+                new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+        ctx.setContextPath("/");
+
+        ResourceConfig resourceConfig = new ResourceConfig();
+        String[] packageNames = {ConfigurationRest.class.getPackage().getName()};
+        if (restPackageNames != null && restPackageNames.length > 0) {
+            packageNames = (String[]) ArrayUtils.addAll(packageNames, restPackageNames);
+        }
+        resourceConfig.packages(packageNames);
+        resourceConfig.register(MultiPartFeature.class)
+                .register(JacksonFeature.class);
+        //   .register(LoggingFilter.class)
+        //   .property("jersey.config.server.tracing.type", "ALL")
+        //   .property("jersey.config.server.tracing.threshold", "VERBOSE"))
+        ServletHolder jerseyServlet = new ServletHolder(
+                new ServletContainer(resourceConfig));
+        jerseyServlet.setInitOrder(1);
+        ctx.addServlet(jerseyServlet, "/rest/*");
+        ctx.addFilter(UserFilter.class, "/rest/*", EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST));
+        // Following code doesn't work:
+        // jerseyServlet.setInitParameter("useFileMappedBuffer", "false");
+        // jerseyServlet.setInitParameter("cacheControl","max-age=0,public");
+
+        try {
+            Path path;
+            if (RunningMode.isDevelopmentMode()) {
+                path = Paths.get(ServerConfiguration.getApplicationHome(), "merlin-webapp", "build");
+            } else {
+                path = Paths.get(ServerConfiguration.getApplicationHome(), "web");
+            }
+            if (!Files.exists(path)) {
+                log.error("********** Fatal: Can't find web archive: " + path.toAbsolutePath());
+            }
+            URL url = path.toUri().toURL();
+            log.debug("Using web directory: " + url);
+            ctx.setBaseResource(Resource.newResource(url));
+        } catch (IOException ex) {
+            log.error(ex.getMessage(), ex);
+            return;
+        }
+        ctx.setWelcomeFiles(new String[]{"index.html"});
+        ctx.setInitParameter(DefaultServlet.CONTEXT_INIT + "cacheControl", "no-store,no-cache,must-revalidate");//"max-age=5,public");
+        ctx.setInitParameter(DefaultServlet.CONTEXT_INIT + "useFileMappedBuffer", "false");
+        ctx.addServlet(DefaultServlet.class, "/");
+
+        ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler();
+        errorHandler.addErrorPage(404, "/");
+        ctx.setErrorHandler(errorHandler);
+
+        if (RunningMode.isDevelopmentMode() || ServerConfigurationHandler.getDefaultConfiguration().isWebDevelopmentMode()) {
+            log.warn("*********************************");
+            log.warn("***********            **********");
+            log.warn("*********** ATTENTION! **********");
+            log.warn("***********            **********");
+            log.warn("*********** Running in **********");
+            log.warn("*********** dev mode!  **********");
+            log.warn("***********            **********");
+            log.warn("*********************************");
+            log.warn("Don't deliver this app in dev mode due to security reasons (CrossOriginFilter is set)!");
+
+            FilterHolder filterHolder = ctx.addFilter(CrossOriginFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
+            filterHolder.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
+            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,HEAD");
+            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin");
+        }
+
+        server.setHandler(ctx);
+
+        try {
+            server.start();
+        } catch (Exception ex) {
+            log.error("Can't start jetty: " + ex.getMessage(), ex);
+        }
+    }
+
+    public void stop() {
+        log.info("Stopping web server.");
+        try {
+            server.stop();
+        } catch (Exception ex) {
+            log.error("Can't stop web server: " + ex.getMessage(), ex);
+        }
+        if (server != null) {
+            server.destroy();
+        }
+    }
+
+    private int findFreePort() {
+        int port = ServerConfigurationHandler.getInstance().getConfiguration().getPort();
+        return findFreePort(port);
+    }
+
+    private int findFreePort(int startPort) {
+        int port = startPort > 0 ? startPort : 1;
+        if (port > MAX_PORT_NUMBER) {
+            log.warn("Port can't be higher than " + MAX_PORT_NUMBER + ": " + port + ". It's a possible mis-configuration.");
+            port = ServerConfigurationHandler.WEBSERVER_PORT_DEFAULT;
+        }
+        for (int i = port; i < port + 10; i++) {
+            try (ServerSocket socket = new ServerSocket()) {
+                socket.bind(new InetSocketAddress(HOST, i));
+                return i;
+            } catch (Exception ex) {
+                log.info("Port " + i + " already in use or not available. Trying next port.");
+                continue; // try next port
+            }
+        }
+        if (startPort != ServerConfigurationHandler.WEBSERVER_PORT_DEFAULT) {
+            log.info("Trying to fix port due to a possible mis-configuration.");
+            return findFreePort(ServerConfigurationHandler.WEBSERVER_PORT_DEFAULT);
+        }
+        log.error("No free port found! Giving up.");
+        return -1;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public String getUrl() {
+        return "http://" + HOST + ":" + port + "/";
+    }
+}
+
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java
new file mode 100644
index 0000000..3fa1eb5
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/Log4jMemoryAppender.java
@@ -0,0 +1,136 @@
+package de.micromata.borgbutler.server.logging;
+
+import org.apache.commons.collections4.queue.CircularFifoQueue;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class Log4jMemoryAppender extends AppenderSkeleton {
+    private static final int MAX_RESULT_SIZE = 1000;
+    private static final int QUEUE_SIZE = 10000;
+    private static Log4jMemoryAppender instance;
+
+    private int lastLogEntryOrderNumber = -1;
+
+    public static Log4jMemoryAppender getInstance() {
+        return instance;
+    }
+
+    public Log4jMemoryAppender() {
+        if (instance != null) {
+            throw new IllegalArgumentException("Log4jMemoryAppender shouldn't be instantiated twice!");
+        }
+        instance = this;
+    }
+
+    /**
+     * For test purposes.
+     */
+    Log4jMemoryAppender(boolean ignoreMultipleInstance) {
+
+    }
+
+    CircularFifoQueue<LoggingEventData> queue = new CircularFifoQueue<>(QUEUE_SIZE);
+
+    @Override
+    protected void append(LoggingEvent event) {
+        LoggingEventData eventData = new LoggingEventData(event);
+        eventData.orderNumber = ++lastLogEntryOrderNumber;
+        queue.add(eventData);
+    }
+
+    /**
+     * For testing purposes.
+     *
+     * @param event
+     */
+    void append(LoggingEventData event) {
+        queue.add(event);
+    }
+
+    public List<LoggingEventData> query(LogFilter filter, Locale locale) {
+        List<LoggingEventData> result = new ArrayList<>();
+        if (filter == null) {
+            return result;
+        }
+        int maxSize = filter.getMaxSize() != null ? filter.getMaxSize() : MAX_RESULT_SIZE;
+        if (maxSize > MAX_RESULT_SIZE) {
+            maxSize = MAX_RESULT_SIZE;
+        }
+        int counter = 0;
+        //I18n i18n = CoreI18n.getDefault().get(locale);
+        for (LoggingEventData event : queue) {
+            if (!event.getLevel().matches(filter.getThreshold())) {
+                continue;
+            }
+            if (filter.getLastReceivedLogOrderNumber() != null) {
+                if (event.getOrderNumber() <= filter.getLastReceivedLogOrderNumber()) {
+                    continue;
+                }
+            }
+            String logString = null;
+            String message = event.getMessage();
+            boolean localizedMessage = false;
+            /*if (message != null && message.startsWith("i18n=")) {
+                I18nLogEntry i18nLogEntry = I18nLogEntry.parse(message);
+                message = i18n.formatMessage(i18nLogEntry.getI18nKey(), (Object[])i18nLogEntry.getArgs());
+                localizedMessage = true;
+            }*/
+
+            if (StringUtils.isNotBlank(filter.getSearch())) {
+                StringBuilder sb = new StringBuilder();
+                sb.append(event.getLogDate());
+                append(sb, event.getLevel(), true);
+                append(sb, message, true);
+                append(sb, event.getJavaClass(), true);
+                append(sb, event.getStackTrace(), filter.isShowStackTraces());
+                logString = sb.toString();
+            }
+            if (logString == null || matches(logString, filter.getSearch())) {
+                LoggingEventData resultEvent = event;
+                if (localizedMessage) {
+                    // Need a clone
+                    resultEvent = (LoggingEventData)event.clone();
+                    resultEvent.setMessage(message);
+                }
+                if (filter.isAscendingOrder()) {
+                    result.add(resultEvent);
+                } else {
+                    result.add(0, resultEvent);
+                }
+                if (counter++ > maxSize) {
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    private void append(StringBuilder sb, Object value, boolean append) {
+        if (!append || value == null) {
+            return;
+        }
+        sb.append("|#|").append(value);
+    }
+
+    public void close() {
+    }
+
+    public boolean requiresLayout() {
+        return false;
+    }
+
+    private boolean matches(String str, String searchString) {
+        if (StringUtils.isBlank(str)) {
+            return StringUtils.isBlank(searchString);
+        }
+        if (StringUtils.isBlank(searchString)) {
+            return true;
+        }
+        return str.toLowerCase().contains(searchString.toLowerCase());
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogFilter.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogFilter.java
new file mode 100644
index 0000000..0e1155f
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogFilter.java
@@ -0,0 +1,71 @@
+package de.micromata.borgbutler.server.logging;
+
+/**
+ * For filtering log messages.
+ */
+public class LogFilter {
+    private String search;
+    private LogLevel threshold;
+    private Integer maxSize;
+    private boolean ascendingOrder;
+    private boolean showStackTraces;
+    private Integer lastReceivedLogOrderNumber;
+
+    /**
+     * @return Search string for all fields.
+     */
+    public String getSearch() {
+        return search;
+    }
+
+    public void setSearch(String search) {
+        this.search = search;
+    }
+
+    public LogLevel getThreshold() {
+        return threshold;
+    }
+
+    public void setThreshold(LogLevel threshold) {
+        this.threshold = threshold;
+    }
+
+    public Integer getMaxSize() {
+        return maxSize;
+    }
+
+    public void setMaxSize(Integer maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    public void setAscendingOrder(boolean ascendingOrder) {
+        this.ascendingOrder = ascendingOrder;
+    }
+
+    /**
+     * @return false at default (default is descending order of the result).
+     */
+    public boolean isAscendingOrder() {
+        return ascendingOrder;
+    }
+
+
+    public boolean isShowStackTraces() {
+        return showStackTraces;
+    }
+
+    public void setShowStackTraces(boolean showStackTraces) {
+        this.showStackTraces = showStackTraces;
+    }
+
+    /**
+     * @return If given, all log entries with order orderNumber higher than this orderNumber will be queried.
+     */
+    public Integer getLastReceivedLogOrderNumber() {
+        return lastReceivedLogOrderNumber;
+    }
+
+    public void setLastReceivedLogOrderNumber(Integer lastReceivedLogOrderNumber) {
+        this.lastReceivedLogOrderNumber = lastReceivedLogOrderNumber;
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java
new file mode 100644
index 0000000..a4f8b69
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LogLevel.java
@@ -0,0 +1,42 @@
+package de.micromata.borgbutler.server.logging;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Level;
+import org.apache.log4j.spi.LoggingEvent;
+
+public enum LogLevel {
+    ERROR, WARN, INFO, DEBUG, TRACE;
+
+    /**
+     * @param treshold
+     * @return True, if this log level is equals or higher than given treshold. ERROR is the highest and TRACE the lowest.
+     */
+    public boolean matches(LogLevel treshold) {
+        if (treshold == null) {
+            return true;
+        }
+        return this.ordinal() <= treshold.ordinal();
+    }
+
+    public static LogLevel getLevel(LoggingEvent event) {
+        switch (event.getLevel().toInt()) {
+            case Level.ERROR_INT:
+                return LogLevel.ERROR;
+            case Level.INFO_INT:
+                return LogLevel.INFO;
+            case Level.DEBUG_INT:
+                return LogLevel.DEBUG;
+            case Level.WARN_INT:
+                return LogLevel.WARN;
+            case Level.TRACE_INT:
+                return LogLevel.TRACE;
+            default:
+                return LogLevel.ERROR;
+        }
+
+    }
+
+    public static String getSupportedValues() {
+        return StringUtils.join(LogLevel.values(), ", ");
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java
new file mode 100644
index 0000000..c0d8c1b
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/logging/LoggingEventData.java
@@ -0,0 +1,121 @@
+package de.micromata.borgbutler.server.logging;
+
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * For easier serialization: JSON
+ */
+public class LoggingEventData implements Cloneable {
+    private SimpleDateFormat ISO_DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+    int orderNumber;
+    LogLevel level;
+    String message;
+    private String messageObjectClass;
+    private String loggerName;
+    private String logDate;
+    String javaClass;
+    private String javaClassSimpleName;
+    private String lineNumber;
+    private String methodName;
+    private String stackTrace;
+
+    LoggingEventData() {
+
+    }
+
+    public LoggingEventData(LoggingEvent event) {
+        level = LogLevel.getLevel(event);
+        message = event.getRenderedMessage();
+        messageObjectClass = event.getMessage().getClass().toString();
+        loggerName = event.getLoggerName();
+        logDate = getIsoLogDate(event.timeStamp);
+        LocationInfo info = event.getLocationInformation();
+        Throwable throwable = event.getThrowableInformation() != null ? event.getThrowableInformation().getThrowable() : null;
+        if (throwable != null) {
+            StringWriter writer = new StringWriter();
+            PrintWriter printWriter = new PrintWriter(writer);
+            throwable.printStackTrace(printWriter);
+            stackTrace = writer.toString();
+        }
+        if (info != null) {
+            javaClass = info.getClassName();
+            javaClassSimpleName = ClassUtils.getShortClassName(info.getClassName());
+            lineNumber = info.getLineNumber();
+            methodName = info.getMethodName();
+        }
+    }
+
+    public LogLevel getLevel() {
+        return level;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public String getMessageObjectClass() {
+        return messageObjectClass;
+    }
+
+    public String getLoggerName() {
+        return loggerName;
+    }
+
+    public String getLogDate() {
+        return logDate;
+    }
+
+    public String getJavaClass() {
+        return javaClass;
+    }
+
+    public String getJavaClassSimpleName() {
+        return javaClassSimpleName;
+    }
+
+    public String getLineNumber() {
+        return lineNumber;
+    }
+
+    public String getMethodName() {
+        return methodName;
+    }
+
+    public int getOrderNumber() {
+        return orderNumber;
+    }
+
+    public String getStackTrace() {
+        return stackTrace;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    private String getIsoLogDate(long millis) {
+        synchronized (ISO_DATEFORMAT) {
+            return ISO_DATEFORMAT.format(new Date(millis));
+        }
+    }
+
+    @Override
+    public Object clone() {
+        LoggingEventData clone = null;
+        try {
+            clone = (LoggingEventData) super.clone();
+        } catch (CloneNotSupportedException ex) {
+            throw new UnsupportedOperationException(this.getClass().getCanonicalName() + " isn't cloneable: " + ex.getMessage(), ex);
+        }
+        return clone;
+    }
+
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
new file mode 100644
index 0000000..325ccb4
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/ConfigurationRest.java
@@ -0,0 +1,87 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.json.JsonUtils;
+import de.micromata.borgbutler.server.ServerConfiguration;
+import de.micromata.borgbutler.server.ServerConfigurationHandler;
+import de.micromata.borgbutler.server.user.UserData;
+import de.micromata.borgbutler.server.user.UserManager;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+
+@Path("/configuration")
+public class ConfigurationRest {
+    private Logger log = LoggerFactory.getLogger(ConfigurationRest.class);
+
+    @GET
+    @Path("config")
+    @Produces(MediaType.APPLICATION_JSON)
+    /**
+     *
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils#toJson(Object, boolean)
+     */
+    public String getConfig(@QueryParam("prettyPrinter") boolean prettyPrinter) {
+        ServerConfiguration config = new ServerConfiguration();
+        config.copyFrom(ServerConfigurationHandler.getInstance().getConfiguration());
+        String json = JsonUtils.toJson(config, prettyPrinter);
+        return json;
+    }
+
+    @POST
+    @Path("config")
+    @Produces(MediaType.TEXT_PLAIN)
+    public void setConfig(String jsonConfig) {
+        ServerConfigurationHandler configurationHandler = ServerConfigurationHandler.getInstance();
+        ServerConfiguration config = configurationHandler.getConfiguration();
+        ServerConfiguration srcConfig = JsonUtils.fromJson(ServerConfiguration.class, jsonConfig);
+        config.copyFrom(srcConfig);
+        configurationHandler.save();
+    }
+
+    @GET
+    @Path("user")
+    @Produces(MediaType.APPLICATION_JSON)
+    /**
+     *
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils#toJson(Object, boolean)
+     */
+    public String getUser(@QueryParam("prettyPrinter") boolean prettyPrinter) {
+        UserData user = RestUtils.getUser();
+        String json = JsonUtils.toJson(user, prettyPrinter);
+        return json;
+    }
+
+    @POST
+    @Path("user")
+    @Produces(MediaType.TEXT_PLAIN)
+    public void setUser(String jsonConfig) {
+        UserData user = JsonUtils.fromJson(UserData.class, jsonConfig);
+        if (user.getLocale() != null && StringUtils.isBlank(user.getLocale().getLanguage())) {
+            // Don't set locale with "" as language.
+            user.setLocale(null);
+        }
+        if (StringUtils.isBlank(user.getDateFormat())) {
+            // Don't set dateFormat as "".
+            user.setDateFormat(null);
+        }
+        UserManager.instance().saveUser(user);
+    }
+
+    /**
+     * Resets the settings to default values (deletes all settings).
+     */
+    @GET
+    @Path("reset")
+    @Produces(MediaType.APPLICATION_JSON)
+    public String resetConfig(@QueryParam("IKnowWhatImDoing") boolean securityQuestion) {
+        if (securityQuestion) {
+            ServerConfigurationHandler.getInstance().removeAllSettings();
+        }
+        return getConfig(false);
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
new file mode 100644
index 0000000..f71cb84
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/I18nRest.java
@@ -0,0 +1,41 @@
+package de.micromata.borgbutler.server.rest;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import java.util.Locale;
+
+@Path("/i18n")
+public class I18nRest {
+    private Logger log = LoggerFactory.getLogger(I18nRest.class);
+
+    @GET
+    @Path("list")
+    @Produces(MediaType.APPLICATION_JSON)
+    /**
+     *
+     * @param requestContext For detecting the user's client locale.
+     * @param locale If not given, the client's language (browser) will be used.
+     * @param keysOnly If true, only the keys will be returned. Default is false.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils#toJson(Object, boolean)
+     */
+    public String getList(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter,
+                          @QueryParam("keysOnly") boolean keysOnly, @QueryParam("locale") String locale) {
+        Locale localeObject;
+        if (StringUtils.isNotBlank(locale)) {
+            localeObject = new Locale(locale);
+        } else {
+            localeObject = RestUtils.getUserLocale(requestContext);
+        }
+        return ""; // i18n not yet supported.
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java
new file mode 100644
index 0000000..8cef74f
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/LoggingRest.java
@@ -0,0 +1,70 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.json.JsonUtils;
+import de.micromata.borgbutler.server.logging.Log4jMemoryAppender;
+import de.micromata.borgbutler.server.logging.LogFilter;
+import de.micromata.borgbutler.server.logging.LogLevel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+
+@Path("/logging")
+public class LoggingRest {
+    private Logger log = LoggerFactory.getLogger(LoggingRest.class);
+
+    /**
+     * @param requestContext
+     * @param search
+     * @param logLevelTreshold fatal, error, warn, info, debug or trace (case insensitive).
+     * @param maxSize          Max size of the result list.
+     * @param ascendingOrder   Default is false (default is descending order).
+     * @param lastReceivedOrderNumber The last received order number for updating log entries (preventing querying all entries again).
+     * @param mdcTemplatePk
+     * @param mdcTemplateDefinitionPk
+     * @param prettyPrinter
+     * @return
+     */
+    @GET
+    @Path("query")
+    @Produces(MediaType.APPLICATION_JSON)
+    public String query(@Context HttpServletRequest requestContext,
+                        @QueryParam("search") String search, @QueryParam("treshold") String logLevelTreshold,
+                        @QueryParam("maxSize") Integer maxSize, @QueryParam("ascendingOrder") Boolean ascendingOrder,
+                        @QueryParam("lastReceivedOrderNumber") Integer lastReceivedOrderNumber,
+                        @QueryParam("mdcTemplatePk") String mdcTemplatePk,
+                        @QueryParam("mdcTemplateDefinitionPk") String mdcTemplateDefinitionPk,
+                        @QueryParam("prettyPrinter") boolean prettyPrinter) {
+        LogFilter filter = new LogFilter();
+        filter.setSearch(search);
+        if (logLevelTreshold != null) {
+            try {
+                LogLevel treshold = LogLevel.valueOf(logLevelTreshold.trim().toUpperCase());
+                filter.setThreshold(treshold);
+            } catch (IllegalArgumentException ex) {
+                log.error("Can't parse log level treshold: " + logLevelTreshold + ". Supported values (case insensitive): " + LogLevel.getSupportedValues());
+            }
+        }
+        if (filter.getThreshold() == null) {
+            filter.setThreshold(LogLevel.INFO);
+        }
+        if (maxSize != null) {
+            filter.setMaxSize(maxSize);
+        }
+        if (ascendingOrder != null && ascendingOrder == true) {
+            filter.setAscendingOrder(true);
+        }
+        if (lastReceivedOrderNumber != null) {
+            filter.setLastReceivedLogOrderNumber(lastReceivedOrderNumber);
+        }
+        Log4jMemoryAppender appender = Log4jMemoryAppender.getInstance();
+        String json = JsonUtils.toJson(appender.query(filter, RestUtils.getUserLocale(requestContext)), prettyPrinter);
+        return json;
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java
new file mode 100644
index 0000000..a69231d
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/RestUtils.java
@@ -0,0 +1,58 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.server.RunningMode;
+import de.micromata.borgbutler.server.user.UserData;
+import de.micromata.borgbutler.server.user.UserUtils;
+import org.slf4j.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+import java.util.Locale;
+
+public class RestUtils {
+    /**
+     * @return null, if the local app (JavaFX) is running and the request is from localhost. Otherwise message, why local
+     * service isn't available.
+     */
+    public static String checkLocalDesktopAvailable(HttpServletRequest requestContext) {
+        if (RunningMode.getServerType() != RunningMode.ServerType.DESKTOP) {
+            return "Service unavailable. No desktop app on localhost available.";
+        }
+        String remoteAddr = requestContext.getRemoteAddr();
+        if (remoteAddr == null || !remoteAddr.equals("127.0.0.1")) {
+            return "Service not available. Can't call this service remote. Run this service on localhost of the running desktop app.";
+        }
+        return null;
+    }
+
+    /**
+     * @return Returns the user put by the UserFilter.
+     * @see UserUtils#getUser()
+     * @see de.micromata.borgbutler.server.user.UserFilter
+     */
+    static UserData getUser() {
+        UserData user = UserUtils.getUser();
+        if (user == null) {
+            throw new IllegalStateException("No user given in rest call.");
+        }
+        return UserUtils.getUser();
+    }
+
+    static Locale getUserLocale(HttpServletRequest requestContext) {
+        UserData user = RestUtils.getUser();
+        Locale locale = user.getLocale();
+        if (locale == null) {
+            locale = requestContext.getLocale();
+        }
+        return locale;
+    }
+
+    static Response get404Response(Logger log, String errorMessage) {
+        log.error(errorMessage);
+        Response response = Response.status(404).
+                entity(errorMessage).
+                type("text/plain").
+                build();
+        return response;
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
new file mode 100644
index 0000000..efd277f
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/rest/VersionRest.java
@@ -0,0 +1,88 @@
+package de.micromata.borgbutler.server.rest;
+
+import de.micromata.borgbutler.json.JsonUtils;
+import de.micromata.borgbutler.server.Languages;
+import de.micromata.borgbutler.server.Version;
+import de.micromata.borgbutler.server.user.UserData;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import java.util.Date;
+import java.util.Locale;
+
+@Path("/")
+public class VersionRest {
+    private Logger log = LoggerFactory.getLogger(VersionRest.class);
+
+    @GET
+    @Path("version")
+    @Produces(MediaType.APPLICATION_JSON)
+    /**
+     *
+     * @param requestContext For detecting the user's client locale.
+     * @param prettyPrinter If true then the json output will be in pretty format.
+     * @see JsonUtils#toJson(Object, boolean)
+     */
+    public String getVersion(@Context HttpServletRequest requestContext, @QueryParam("prettyPrinter") boolean prettyPrinter) {
+        UserData user = RestUtils.getUser();
+        String language = Languages.asString(user.getLocale());
+        if (StringUtils.isBlank(language)) {
+            Locale locale = requestContext.getLocale();
+            language = locale.getLanguage();
+        }
+        MyVersion version = new MyVersion(language, RestUtils.checkLocalDesktopAvailable(requestContext) == null);
+        String json = JsonUtils.toJson(version, prettyPrinter);
+        return json;
+    }
+
+    public class MyVersion {
+        private Version version;
+        private String language;
+        private boolean localDesktopAvailable;
+
+        private MyVersion(String language, boolean localDesktopAvailable) {
+            this.version = Version.getInstance();
+            this.language = language;
+            this.localDesktopAvailable = localDesktopAvailable;
+        }
+
+        public String getAppName() {
+            return version.getAppName();
+        }
+
+        public String getVersion() {
+            return version.getVersion();
+        }
+
+        public String getBuildDateUTC() {
+            return version.getBuildDateUTC();
+        }
+
+        public Date getBuildDate() {
+            return version.getBuildDate();
+        }
+
+        /**
+         * @return Version of the available update, if exist. Otherwise null.
+         */
+        public String getUpdateVersion() {
+            return version.getUpdateVersion();
+        }
+
+        public String getLanguage() {
+            return language;
+        }
+
+        public boolean isLocalDesktopAvailable() {
+            return localDesktopAvailable;
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java
new file mode 100644
index 0000000..b7f2528
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/SingleUserManager.java
@@ -0,0 +1,55 @@
+package de.micromata.borgbutler.server.user;
+
+import de.micromata.borgbutler.server.Languages;
+import de.micromata.borgbutler.server.RunningMode;
+import de.micromata.borgbutler.server.ServerConfigurationHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+/**
+ * Contains only one (dummy) user (for desktop version).
+ */
+public class SingleUserManager extends UserManager {
+    private static final String USER_LOCAL_PREF_KEY = "userLocale";
+    private static Logger log = LoggerFactory.getLogger(SingleUserManager.class);
+    private UserData singleUser;
+
+    public SingleUserManager() {
+        if (RunningMode.getUserManagement() != RunningMode.UserManagement.SINGLE) {
+            throw new IllegalStateException("Can't use SingleUserManager in user management mode '" + RunningMode.getUserManagement()
+                    + "'. Only allowed in '" + RunningMode.UserManagement.SINGLE + "'.");
+        }
+        log.info("Using SingleUserManger as user manager.");
+        singleUser = new UserData();
+        singleUser.setUsername("admin");
+        singleUser.setAdmin(true);
+        String language = ServerConfigurationHandler.getInstance().get("userLocale", null);
+        Locale locale = Languages.asLocale(language);
+        singleUser.setLocale(locale);
+        String dateFormat = ServerConfigurationHandler.getInstance().get("userDateFormat", null);
+        singleUser.setDateFormat(dateFormat);
+    }
+
+    public UserData getUser(String id) {
+        return singleUser;
+    }
+
+    /**
+     * Stores only the user's configured locale.
+     *
+     * @param userData
+     * @see ServerConfigurationHandler#save(String, String)
+     */
+    @Override
+    public void saveUser(UserData userData) {
+        Locale locale = userData.getLocale();
+        this.singleUser.setLocale(locale);
+        String dateFormat = userData.getDateFormat();
+        this.singleUser.setDateFormat(dateFormat);
+        String lang = Languages.asString(locale);
+        ServerConfigurationHandler.getInstance().save("userLocale", lang);
+        ServerConfigurationHandler.getInstance().save("userDateFormat", dateFormat);
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserData.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserData.java
new file mode 100644
index 0000000..1e77970
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserData.java
@@ -0,0 +1,59 @@
+package de.micromata.borgbutler.server.user;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Locale;
+
+/**
+ * Stores the user data in Thread local for accessing everywhere inside the rest thread.
+ * It's only a dummy and simple implementation.
+ */
+public class UserData {
+    private Locale locale;
+    private String username;
+    private String dateFormat;
+    private boolean admin;
+
+    public Locale getLocale() {
+        return locale;
+    }
+
+    public void setLocale(Locale locale) {
+        this.locale = locale;
+    }
+
+    public String getDateFormat() {
+        return dateFormat;
+    }
+
+    public void setDateFormat(String dateFormat) {
+        this.dateFormat = dateFormat;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    void setUsername(String username) {
+        this.username = username;
+    }
+
+    public boolean isAdmin() {
+        return admin;
+    }
+
+    public void setAdmin(boolean admin) {
+        this.admin = admin;
+    }
+
+    @Override
+    public String toString() {
+        ToStringBuilder tos = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
+        tos.append("username", username);
+        tos.append("dateFormat", dateFormat);
+        tos.append("admin", admin);
+        tos.append("locale", locale);
+        return tos.toString();
+    }
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java
new file mode 100644
index 0000000..36f0583
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserFilter.java
@@ -0,0 +1,53 @@
+package de.micromata.borgbutler.server.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * Ensuring the user data inside request threads. For now, it's only a simple implementation (no login required).
+ * Only the user's (client's) locale is used.
+ */
+public class UserFilter implements Filter {
+    private Logger log = LoggerFactory.getLogger(UserFilter.class);
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+        try {
+            UserData userData = UserUtils.getUser();
+            if (userData != null) {
+                log.warn("****************************************");
+                log.warn("***********                   **********");
+                log.warn("*********** SECURITY WARNING! **********");
+                log.warn("***********                   **********");
+                log.warn("*********** Internal error:   **********");
+                log.warn("*********** User already set! **********");
+                log.warn("***********                   **********");
+                log.warn("****************************************");
+                log.warn("Don't deliver this app in dev mode due to security reasons!");
+                String message = "User already given for this request. Rejecting request due to security reasons. Given user: " + userData;
+                log.error(message);
+                throw new IllegalArgumentException(message);
+            }
+            userData = UserManager.instance().getUser("dummy");
+            UserUtils.setUser(userData, request.getLocale());
+            if (log.isDebugEnabled()) log.debug("Request for user: " + userData);
+            chain.doFilter(request, response);
+        } finally {
+            UserUtils.removeUser();
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserManager.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserManager.java
new file mode 100644
index 0000000..5ae62e4
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserManager.java
@@ -0,0 +1,24 @@
+package de.micromata.borgbutler.server.user;
+
+/**
+ * Handles all user data.
+ */
+public abstract class UserManager {
+    private static UserManager instance;
+
+    public static void setUserManager(UserManager userManager) {
+        instance = userManager;
+    }
+
+    public static UserManager instance() {
+        return instance;
+    }
+
+    public abstract UserData getUser(String id);
+
+    /**
+     * The userData was modified. Persists the given user.
+     * @param userData
+     */
+    public abstract void saveUser(UserData userData);
+}
diff --git a/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserUtils.java b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserUtils.java
new file mode 100644
index 0000000..17e5115
--- /dev/null
+++ b/borgbutler-server/src/main/java/de/micromata/borgbutler/server/user/UserUtils.java
@@ -0,0 +1,53 @@
+package de.micromata.borgbutler.server.user;
+
+import java.util.Locale;
+
+public class UserUtils {
+    private static final ThreadLocal<UserInfo> threadUserInfo = new ThreadLocal<UserInfo>();
+
+    /**
+     * Gets the current user from ThreadLocal.
+     * @return
+     */
+    public static UserData getUser() {
+        UserInfo user = threadUserInfo.get();
+        if (user == null) {
+            return null;
+        }
+        return user.userData;
+    }
+
+    public static Locale getUserLocale() {
+        UserInfo userInfo = threadUserInfo.get();
+        if (userInfo == null) return null;
+        UserData user = userInfo.userData;
+        Locale locale = user.getLocale();
+        if (locale == null) {
+            locale = userInfo.requestLocale;
+        }
+        return locale;
+    }
+
+    public static String getUserDateFormat() {
+        UserData user = getUser();
+        return user != null ? user.getDateFormat() : null;
+    }
+
+    static void setUser(UserData user, Locale requestLocale) {
+        threadUserInfo.set(new UserInfo(user, requestLocale));
+    }
+
+    static void removeUser() {
+        threadUserInfo.remove();
+    }
+
+    private static class UserInfo {
+        private UserData userData;
+        private Locale requestLocale;
+
+        private UserInfo(UserData userData, Locale requestLocale) {
+            this.userData = userData;
+            this.requestLocale = requestLocale;
+        }
+    }
+}
diff --git a/borgbutler-server/src/main/resources/log4j.properties b/borgbutler-server/src/main/resources/log4j.properties
new file mode 100644
index 0000000..5b3dfdf
--- /dev/null
+++ b/borgbutler-server/src/main/resources/log4j.properties
@@ -0,0 +1,19 @@
+log4j.rootLogger=info, stdout, memory, file
+#log4j.logger.de.micromata.borgbutler.persistency=debug
+#log4j.logger.de.micromata.borgbutler.main.jetty=debug
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+
+# Pattern to output the caller's file name and line number.
+log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
+
+log4j.appender.memory=de.micromata.borgbutler.server.logging.Log4jMemoryAppender
+
+log4j.appender.file=org.apache.log4j.RollingFileAppender
+
+log4j.appender.file.File=merlin.log
+log4j.appender.file.MaxFileSize=10MB
+log4j.appender.file.MaxBackupIndex=5
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
diff --git a/borgbutler-webapp/build.gradle b/borgbutler-webapp/build.gradle
new file mode 100644
index 0000000..434caf4
--- /dev/null
+++ b/borgbutler-webapp/build.gradle
@@ -0,0 +1,8 @@
+description = 'borgbutler-webapp'
+
+task npmBuild(type: Exec) {
+    workingDir '.'
+
+    executable 'sh'
+    commandLine 'npm', 'run', 'build'
+}
\ No newline at end of file
diff --git a/borgbutler-webapp/package.json b/borgbutler-webapp/package.json
new file mode 100644
index 0000000..8db8cf3
--- /dev/null
+++ b/borgbutler-webapp/package.json
@@ -0,0 +1,40 @@
+{
+  "name": "borgbuttler-webapp",
+  "version": "0.1.0",
+  "private": false,
+  "dependencies": {
+    "@fortawesome/fontawesome-svg-core": "^1.2.6",
+    "@fortawesome/free-solid-svg-icons": "^5.4.1",
+    "@fortawesome/react-fontawesome": "^0.1.3",
+    "bootstrap": "^4.1.3",
+    "classnames": "^2.2.6",
+    "history": "^4.7.2",
+    "i": "^0.3.6",
+    "js-file-download": "^0.4.4",
+    "npm": "^6.4.1",
+    "prop-types": "^15.5.7",
+    "react": "^16.4.2",
+    "react-dom": "^16.4.2",
+    "react-highlighter": "^0.4.3",
+    "react-redux": "^5.0.7",
+    "react-router": "^4.3.1",
+    "react-router-bootstrap": "^0.24.4",
+    "react-router-dom": "^4.3.1",
+    "react-scripts": "^2.0.5",
+    "reactstrap": "^6.5.0",
+    "redux": "^4.0.0",
+    "redux-thunk": "^2.3.0"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test --env=jsdom",
+    "eject": "react-scripts eject"
+  },
+  "browserslist": [
+    ">0.2%",
+    "not dead",
+    "not ie <= 11",
+    "not op_mini all"
+  ]
+}
diff --git a/borgbutler-webapp/public/browserconfig.xml b/borgbutler-webapp/public/browserconfig.xml
new file mode 100644
index 0000000..c554148
--- /dev/null
+++ b/borgbutler-webapp/public/browserconfig.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
\ No newline at end of file
diff --git a/borgbutler-webapp/public/css/bootstrap.min.css b/borgbutler-webapp/public/css/bootstrap.min.css
new file mode 100644
index 0000000..8826912
--- /dev/null
+++ b/borgbutler-webapp/public/css/bootstrap.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v4.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2018 The Bootstrap Authors
+ * Copyright 2011-2018 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::after,.was-validated .custom-file-input:valid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::after,.was-validated .custom-file-input:invalid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-label::after{border-color:#80bdff}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:not(:disabled):not(.disabled){cursor:pointer}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{color:#000;text-decoration:none;opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-dialog-centered::before{display:block;height:calc(100vh - (.5rem * 2));content:""}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-dialog-centered::before{height:calc(100vh - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-item-next,.carousel-item-prev,.carousel-item.active{transition:none}}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-fade .carousel-item{opacity:0;transition-duration:.6s;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/borgbutler-webapp/public/favicon.ico b/borgbutler-webapp/public/favicon.ico
new file mode 100644
index 0000000..5f80464
--- /dev/null
+++ b/borgbutler-webapp/public/favicon.ico
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/android-icon-144x144.png b/borgbutler-webapp/public/favicon/android-icon-144x144.png
new file mode 100644
index 0000000..6552cf1
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/android-icon-144x144.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/android-icon-192x192.png b/borgbutler-webapp/public/favicon/android-icon-192x192.png
new file mode 100644
index 0000000..82daf5d
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/android-icon-192x192.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/android-icon-36x36.png b/borgbutler-webapp/public/favicon/android-icon-36x36.png
new file mode 100644
index 0000000..f78ad13
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/android-icon-36x36.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/android-icon-48x48.png b/borgbutler-webapp/public/favicon/android-icon-48x48.png
new file mode 100644
index 0000000..0a0cb20
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/android-icon-48x48.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/android-icon-72x72.png b/borgbutler-webapp/public/favicon/android-icon-72x72.png
new file mode 100644
index 0000000..230ee04
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/android-icon-72x72.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/android-icon-96x96.png b/borgbutler-webapp/public/favicon/android-icon-96x96.png
new file mode 100644
index 0000000..b322cf3
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/android-icon-96x96.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-114x114.png b/borgbutler-webapp/public/favicon/apple-icon-114x114.png
new file mode 100644
index 0000000..a3cbef0
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-114x114.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-120x120.png b/borgbutler-webapp/public/favicon/apple-icon-120x120.png
new file mode 100644
index 0000000..9ea1607
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-120x120.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-144x144.png b/borgbutler-webapp/public/favicon/apple-icon-144x144.png
new file mode 100644
index 0000000..6552cf1
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-144x144.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-152x152.png b/borgbutler-webapp/public/favicon/apple-icon-152x152.png
new file mode 100644
index 0000000..d3e1598
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-152x152.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-180x180.png b/borgbutler-webapp/public/favicon/apple-icon-180x180.png
new file mode 100644
index 0000000..7399513
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-180x180.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-57x57.png b/borgbutler-webapp/public/favicon/apple-icon-57x57.png
new file mode 100644
index 0000000..1f904bb
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-57x57.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-60x60.png b/borgbutler-webapp/public/favicon/apple-icon-60x60.png
new file mode 100644
index 0000000..174af47
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-60x60.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-72x72.png b/borgbutler-webapp/public/favicon/apple-icon-72x72.png
new file mode 100644
index 0000000..230ee04
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-72x72.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-76x76.png b/borgbutler-webapp/public/favicon/apple-icon-76x76.png
new file mode 100644
index 0000000..8af3652
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-76x76.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon-precomposed.png b/borgbutler-webapp/public/favicon/apple-icon-precomposed.png
new file mode 100644
index 0000000..5b4cf89
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon-precomposed.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/apple-icon.png b/borgbutler-webapp/public/favicon/apple-icon.png
new file mode 100644
index 0000000..5b4cf89
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/apple-icon.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/favicon-16x16.png b/borgbutler-webapp/public/favicon/favicon-16x16.png
new file mode 100644
index 0000000..56613f8
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/favicon-16x16.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/favicon-32x32.png b/borgbutler-webapp/public/favicon/favicon-32x32.png
new file mode 100644
index 0000000..19c9653
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/favicon-32x32.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/favicon-96x96.png b/borgbutler-webapp/public/favicon/favicon-96x96.png
new file mode 100644
index 0000000..b322cf3
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/favicon-96x96.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/ms-icon-144x144.png b/borgbutler-webapp/public/favicon/ms-icon-144x144.png
new file mode 100644
index 0000000..6552cf1
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/ms-icon-144x144.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/ms-icon-150x150.png b/borgbutler-webapp/public/favicon/ms-icon-150x150.png
new file mode 100644
index 0000000..9dd6c2a
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/ms-icon-150x150.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/ms-icon-310x310.png b/borgbutler-webapp/public/favicon/ms-icon-310x310.png
new file mode 100644
index 0000000..cdb7022
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/ms-icon-310x310.png
Binary files differ
diff --git a/borgbutler-webapp/public/favicon/ms-icon-70x70.png b/borgbutler-webapp/public/favicon/ms-icon-70x70.png
new file mode 100644
index 0000000..2ca7b38
--- /dev/null
+++ b/borgbutler-webapp/public/favicon/ms-icon-70x70.png
Binary files differ
diff --git a/borgbutler-webapp/public/images/merlin-icon.png b/borgbutler-webapp/public/images/merlin-icon.png
new file mode 100644
index 0000000..32b8ced
--- /dev/null
+++ b/borgbutler-webapp/public/images/merlin-icon.png
Binary files differ
diff --git a/borgbutler-webapp/public/index.html b/borgbutler-webapp/public/index.html
new file mode 100644
index 0000000..fdf380b
--- /dev/null
+++ b/borgbutler-webapp/public/index.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta name="theme-color" content="#000000">
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
+    <link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
+    <link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
+    <link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
+    <link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
+    <link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
+    <link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
+    <link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
+    <link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
+    <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
+    <link rel="icon" type="image/png" sizes="192x192"  href="/favicon/android-icon-192x192.png">
+    <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
+    <link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
+    <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
+    <link rel="manifest" href="/manifest.json">
+    <meta name="msapplication-TileColor" content="#ffffff">
+    <meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
+    <meta name="theme-color" content="#ffffff">
+    <title>Merlin Webapp</title>
+<body>
+<noscript>
+    You need to enable JavaScript to run this app.
+</noscript>
+<div id="root"></div>
+</body>
+</html>
diff --git a/borgbutler-webapp/public/manifest.json b/borgbutler-webapp/public/manifest.json
new file mode 100644
index 0000000..5ae3b93
--- /dev/null
+++ b/borgbutler-webapp/public/manifest.json
@@ -0,0 +1,46 @@
+{
+  "short_name": "Merlin Webapp",
+  "name": "Control Merlin with this webapp.",
+  "start_url": "./index.html",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+  "icons": [
+    {
+      "src": "\/android-icon-36x36.png",
+      "sizes": "36x36",
+      "type": "image\/png",
+      "density": "0.75"
+    },
+    {
+      "src": "\/android-icon-48x48.png",
+      "sizes": "48x48",
+      "type": "image\/png",
+      "density": "1.0"
+    },
+    {
+      "src": "\/android-icon-72x72.png",
+      "sizes": "72x72",
+      "type": "image\/png",
+      "density": "1.5"
+    },
+    {
+      "src": "\/android-icon-96x96.png",
+      "sizes": "96x96",
+      "type": "image\/png",
+      "density": "2.0"
+    },
+    {
+      "src": "\/android-icon-144x144.png",
+      "sizes": "144x144",
+      "type": "image\/png",
+      "density": "3.0"
+    },
+    {
+      "src": "\/android-icon-192x192.png",
+      "sizes": "192x192",
+      "type": "image\/png",
+      "density": "4.0"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/borgbutler-webapp/src/actions/index.js b/borgbutler-webapp/src/actions/index.js
new file mode 100644
index 0000000..12428a6
--- /dev/null
+++ b/borgbutler-webapp/src/actions/index.js
@@ -0,0 +1,6 @@
+import {changeFilter, requestLogReload} from './log';
+import {loadVersion} from './version';
+
+export {changeFilter, requestLogReload};
+
+export {loadVersion};
\ No newline at end of file
diff --git a/borgbutler-webapp/src/actions/log.js b/borgbutler-webapp/src/actions/log.js
new file mode 100644
index 0000000..26a4610
--- /dev/null
+++ b/borgbutler-webapp/src/actions/log.js
@@ -0,0 +1,54 @@
+import {getRestServiceUrl} from '../utilities/global';
+import {LOG_VIEW_CHANGE_FILTER, LOG_VIEW_FAILED_RELOAD, LOG_VIEW_RELOADED, LOG_VIEW_REQUEST_RELOAD} from './types';
+
+const requestedLogReload = () => ({
+    type: LOG_VIEW_REQUEST_RELOAD
+});
+
+const reloadedLog = (data) => ({
+    type: LOG_VIEW_RELOADED,
+    payload: data
+});
+
+const failedLogReload = () => ({
+    type: LOG_VIEW_FAILED_RELOAD
+});
+
+const changedFilter = (name, value) => ({
+    type: LOG_VIEW_CHANGE_FILTER,
+    payload: {name, value}
+});
+
+export const changeFilter = (name, value) => (dispatch) => dispatch(changedFilter(name, value));
+
+const loadLog = (dispatch, getState) => {
+    dispatch(requestedLogReload());
+
+    const {filters} = getState().log;
+
+    fetch(getRestServiceUrl('logging/query', {
+        search: filters.search,
+        treshold: filters.threshold,
+        maxSize: filters.maxSize,
+        showStackTrace: filters.showStackTrace,
+        ascendingOrder: filters.ascendingOrder,
+        // TODO ADD lastReceivedOrderNumber
+    }), {
+        method: 'GET',
+        headers: {
+            'Content-Type': 'application/json'
+        }
+    })
+        .then(response => response.json())
+        .then(json => dispatch(reloadedLog(json.map(entry => ({
+            ...entry,
+            level: entry.level.toLowerCase()
+        })))))
+        .catch(() => dispatch(failedLogReload()));
+};
+
+export const requestLogReload = () => (dispatch, getState) => {
+    if (!getState().log.loading) {
+        loadLog(dispatch, getState);
+    }
+};
diff --git a/borgbutler-webapp/src/actions/types.js b/borgbutler-webapp/src/actions/types.js
new file mode 100644
index 0000000..b337f4c
--- /dev/null
+++ b/borgbutler-webapp/src/actions/types.js
@@ -0,0 +1,8 @@
+export const LOG_VIEW_RELOADED = 'LOG_VIEW_RELOADED';
+export const LOG_VIEW_REQUEST_RELOAD = 'LOG_VIEW_REQUEST_RELOAD';
+export const LOG_VIEW_FAILED_RELOAD = 'LOG_VIEW_FAILED_RELOAD';
+export const LOG_VIEW_CHANGE_FILTER = 'LOG_VIEW_CHANGE_FILTER';
+
+export const VERSION_REQUEST_RELOAD = 'VERSION_REQUEST_RELOAD';
+export const VERSION_RELOADED = 'VERSION_RELOADED';
+export const VERSION_RELOAD_FAILED = 'VERSION_RELOAD_FAILED';
diff --git a/borgbutler-webapp/src/actions/version.js b/borgbutler-webapp/src/actions/version.js
new file mode 100644
index 0000000..6e95abd
--- /dev/null
+++ b/borgbutler-webapp/src/actions/version.js
@@ -0,0 +1,38 @@
+import {VERSION_RELOAD_FAILED, VERSION_RELOADED, VERSION_REQUEST_RELOAD} from './types';
+import {getRestServiceUrl} from '../utilities/global';
+
+const requestedVersionReload = () => ({
+    type: VERSION_REQUEST_RELOAD
+});
+
+const reloadedVersion = (json) => ({
+    type: VERSION_RELOADED,
+    payload: {
+        version: json.version,
+        language: json.language,
+        buildDate: json.buildDate,
+        updateVersion: json.updateVersion
+    }
+});
+
+const failedReload = () => ({
+    type: VERSION_RELOAD_FAILED
+});
+
+export const loadVersion = () => (dispatch, getState) => {
+    if (getState().version.loading) {
+        return;
+    }
+
+    dispatch(requestedVersionReload());
+
+    fetch(getRestServiceUrl('version'), {
+        method: 'GET',
+        headers: {
+            'Content-Type': 'application/json'
+        }
+    })
+        .then(response => response.json())
+        .then(json => dispatch(reloadedVersion(json)))
+        .catch(() => dispatch(failedReload()));
+};
diff --git a/borgbutler-webapp/src/components/general/BootstrapComponents.jsx b/borgbutler-webapp/src/components/general/BootstrapComponents.jsx
new file mode 100644
index 0000000..2064d9a
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/BootstrapComponents.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function PageHeader({children}) {
+    return (
+        <div className={'pb-2 mt-4 mb-2 border-bottom pageheader'}>
+            {children}
+        </div>
+    );
+}
+
+PageHeader.propTypes = {
+    children: PropTypes.node
+};
+
+PageHeader.defaultProps = {
+    validationState: 'no-validation',
+    children: null
+};
+
+export {
+    PageHeader
+};
diff --git a/borgbutler-webapp/src/components/general/ErrorAlert.jsx b/borgbutler-webapp/src/components/general/ErrorAlert.jsx
new file mode 100644
index 0000000..b027843
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/ErrorAlert.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import {Alert} from 'reactstrap';
+import {FormButton} from "./forms/FormComponents";
+import I18n from "./translation/I18n";
+
+const ErrorAlert = (props) => <Alert
+    color={'danger'}
+>
+    <h4>{props.titleKey ? <I18n name={props.titleKey}/> : props.title}</h4>
+    <p>{props.descriptionKey ? <I18n name={props.descriptionKey}/> :props.description}</p>
+    {props.action ? <p>
+        <FormButton bsStyle={props.action.style}
+            onClick={props.action.handleClick}
+        >
+            {props.action.titleKey ? <I18n name={props.action.titleKey}/> : props.action.title}
+        </FormButton>
+    </p> : undefined}
+</Alert>;
+
+export default ErrorAlert;
diff --git a/borgbutler-webapp/src/components/general/ErrorAlertGenericRestFailure.jsx b/borgbutler-webapp/src/components/general/ErrorAlertGenericRestFailure.jsx
new file mode 100644
index 0000000..65065ef
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/ErrorAlertGenericRestFailure.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ErrorAlert from './ErrorAlert';
+
+const ErrorAlertGenericRestFailure = (props) => <ErrorAlert
+        titleKey={'common.alert.cantLoadData'}
+        descriptionKey={'common.alert.genericRestAPIFailure'}
+        action={{
+            handleClick: props.handleClick,
+            titleKey: 'common.alert.tryAgain'
+        }}
+    />
+
+export default ErrorAlertGenericRestFailure;
diff --git a/borgbutler-webapp/src/components/general/IconComponents.jsx b/borgbutler-webapp/src/components/general/IconComponents.jsx
new file mode 100644
index 0000000..1bb6474
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/IconComponents.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import {
+    faCaretDown,
+    faCaretUp,
+    faCheck,
+    faCircleNotch,
+    faDownload,
+    faExclamationTriangle,
+    faInfoCircle,
+    faPlus,
+    faSortDown,
+    faSortUp,
+    faSync,
+    faTrash,
+    faTimes,
+    faSkullCrossbones,
+    faUpload
+} from '@fortawesome/free-solid-svg-icons'
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+
+function IconAdd() {
+    return (
+        <FontAwesomeIcon icon={faPlus}/>
+    );
+}
+
+function IconCancel() {
+    return (
+        <FontAwesomeIcon icon={faTimes}/>
+    );
+}
+
+function IconCheck() {
+    return (
+        <FontAwesomeIcon icon={faCheck}/>
+    );
+}
+
+function IconCollapseClose() {
+    return (
+        <FontAwesomeIcon icon={faCaretUp}/>
+    );
+}
+
+function IconCollapseOpen() {
+    return (
+        <FontAwesomeIcon icon={faCaretDown}/>
+    );
+}
+
+function IconDanger() {
+    return (
+        <FontAwesomeIcon icon={faSkullCrossbones}/>
+    );
+}
+
+function IconDownload() {
+    return (
+        <FontAwesomeIcon icon={faDownload}/>
+    );
+}
+
+function IconInfo() {
+    return (
+        <FontAwesomeIcon icon={faInfoCircle}/>
+    );
+}
+
+function IconRefresh() {
+    return (
+        <FontAwesomeIcon icon={faSync}/>
+    );
+}
+
+function IconRemove() {
+    return (
+        <FontAwesomeIcon icon={faTrash}/>
+    );
+}
+
+function IconSpinner() {
+    return (
+        <FontAwesomeIcon icon={faCircleNotch} spin={true} size={'3x'} color={'#aaaaaa'} />
+    );
+}
+
+function IconSortDown() {
+    return (
+        <FontAwesomeIcon icon={faSortDown}/>
+    );
+}
+
+function IconSortUp() {
+    return (
+        <FontAwesomeIcon icon={faSortUp}/>
+    );
+}
+
+function IconUpload() {
+    return (
+        <FontAwesomeIcon icon={faUpload}/>
+    );
+}
+
+function IconWarning() {
+    return (
+        <FontAwesomeIcon icon={faExclamationTriangle}/>
+    );
+}
+
+export {
+    IconAdd,
+    IconCancel,
+    IconCheck,
+    IconCollapseClose,
+    IconCollapseOpen,
+    IconDanger,
+    IconDownload,
+    IconInfo,
+    IconRefresh,
+    IconRemove,
+    IconSortDown,
+    IconSortUp,
+    IconSpinner,
+    IconUpload,
+    IconWarning
+};
diff --git a/borgbutler-webapp/src/components/general/LinkFile.jsx b/borgbutler-webapp/src/components/general/LinkFile.jsx
new file mode 100644
index 0000000..5863c99
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/LinkFile.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import {Button} from 'reactstrap';
+import {getRestServiceUrl} from "../../utilities/global";
+
+/*
+* Link to a file. If Merlin is running both local, the server and the client, the file will be opened (e. g. in Excel).
+* For remote clients the desired file will be downloaded.
+ */
+class LinkFile extends React.Component {
+    openFile() {
+        fetch(getRestServiceUrl('files/open-local-file', {primaryKey: this.props.primaryKey}), {
+            method: "GET",
+            dataType: "text",
+            headers: {
+                "Content-Type": "text/plain; charset=utf-8"
+            }
+        })
+            .then(response => response.text())
+            .then((text) => {
+                if (text !== 'OK') {
+                    alert(text);
+                }
+            })
+            .catch((error) => {
+                alert("Can't open file on local file system :-(")
+            })
+    }
+
+    render() {
+        return (
+            <Button color="link" onClick={() => this.openFile()}>{this.props.filepath}</Button>
+        )
+    }
+
+    constructor(props) {
+        super(props);
+        this.openFile = this.openFile.bind(this);
+    }
+}
+
+export default LinkFile;
\ No newline at end of file
diff --git a/borgbutler-webapp/src/components/general/Loading.jsx b/borgbutler-webapp/src/components/general/Loading.jsx
new file mode 100644
index 0000000..a967bde
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/Loading.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import I18n from "./translation/I18n";
+
+const Loading = (props) => <div>
+        <i><I18n name={'common.loading'}/></i>
+    </div>
+
+export default Loading;
diff --git a/borgbutler-webapp/src/components/general/Menu.jsx b/borgbutler-webapp/src/components/general/Menu.jsx
new file mode 100644
index 0000000..4980612
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/Menu.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import {NavLink as ReactRouterNavLink} from 'react-router-dom';
+import {Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink, UncontrolledTooltip} from 'reactstrap';
+import {getResponseHeaderFilename, getRestServiceUrl} from '../../utilities/global';
+import fileDownload from 'js-file-download';
+import LoadingOverlay from './loading/LoadingOverlay';
+import FailedOverlay from './loading/failed/Overlay';
+import I18n from "./translation/I18n";
+
+class Menu extends React.Component {
+    getNavElement = (route, index) => {
+        if (index === 0) {
+            return '';
+        }
+        let addition = '';
+        let className = '';
+        // Additional Route Settings
+        if (route.length >= 4) {
+            if (route[3].badge) {
+                addition = route[3].badge;
+            }
+
+            if (route[3].className) {
+                className = route[3].className;
+            }
+        }
+        return (
+            <NavItem key={index}>
+                <NavLink
+                    to={route[1]}
+                    tag={ReactRouterNavLink}
+                    className={className}
+                >
+                    {route[0]} {addition}
+                </NavLink>
+            </NavItem>
+        );
+    };
+
+    constructor(props) {
+        super(props);
+
+        this.toggle = this.toggle.bind(this);
+        this.state = {
+            loading: false,
+            failed: false,
+            isOpen: false
+        };
+        this.uploadFile = this.uploadFile.bind(this);
+    }
+
+     toggle() {
+        this.setState({
+            isOpen: !this.state.isOpen
+        });
+    }
+
+    render() {
+        return (
+            <Navbar className={'fixed-top'} color="light" light expand="lg">
+                <NavbarBrand to="/" tag={ReactRouterNavLink}><img alt={'BorgButler logo'}
+                                                                  src={'../../../images/merlin-icon.png'}
+                                                                  width={'50px'}/>BorgButler</NavbarBrand>
+                <NavbarToggler onClick={this.toggle}/>
+                <Collapse isOpen={this.state.isOpen} navbar>
+                    <Nav className="ml-auto" navbar>
+                        {
+                            this.props.routes.map((route, index) => (
+                                this.getNavElement(route, index)
+                            ))
+                        }
+                    </Nav>
+                    <DropArea id={'menuDropZone'} className={'menu'}
+                              upload={this.uploadFile}
+                    />
+                </Collapse>
+                <LoadingOverlay active={this.state.loading} />
+                <FailedOverlay
+                    title={'File Upload failed'}
+                    text={this.state.failed}
+                    closeModal={() => this.setState({failed: false})}
+                    active={this.state.failed}
+                />
+            </Navbar>
+        );
+    }
+}
+
+export default Menu;
diff --git a/borgbutler-webapp/src/components/general/OpenLocalFile.jsx b/borgbutler-webapp/src/components/general/OpenLocalFile.jsx
new file mode 100644
index 0000000..a188c2f
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/OpenLocalFile.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import {Button} from 'reactstrap';
+import {getRestServiceUrl} from "../../utilities/global";
+
+class OpenLocalFile extends React.Component {
+    openFile() {
+        fetch(getRestServiceUrl('files/open-local-file', {filepath: this.props.filepath}), {
+            method: "GET",
+            dataType: "text",
+            headers: {
+                "Content-Type": "text/plain; charset=utf-8"
+            }
+        })
+            .then(response => response.text())
+            .then((text) => {
+                if (text !== 'OK') {
+                    alert(text);
+                }
+            })
+            .catch((error) => {
+                alert("Can't open file on local file system :-(")
+            })
+    }
+    render() {
+        const service = this.props.service;
+        const params = this.props.params;
+        var url;
+        if (params) {
+            if (service === 'files/browse-local-filesystem') {
+                url = getRestServiceUrl(service) + '?' + params;
+            } else {
+                url = getRestServiceUrl(service) + '?prettyPrinter=true&' + params;
+            }
+        } else {
+            url = getRestServiceUrl(service) + '?prettyPrinter=true';
+        }
+        return (
+            <Button color="link" onClick={() => this.openFile()}>{this.props.filepath}</Button>
+        )
+    }
+
+    constructor(props) {
+        super(props);
+        this.openFile = this.openFile.bind(this);
+    }
+}
+export default OpenLocalFile;
\ No newline at end of file
diff --git a/borgbutler-webapp/src/components/general/forms/EditableTextField.css b/borgbutler-webapp/src/components/general/forms/EditableTextField.css
new file mode 100644
index 0000000..058984e
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/EditableTextField.css
@@ -0,0 +1,33 @@
+div.editable-text-field {
+    width: 100%;
+    display: block;
+    font-size: 16px;
+    margin: 5px 0;
+}
+
+div.editable-text-field div.editable-text-div {
+    display: block;
+    position: relative;
+}
+
+div.editable-text-field div.editable-text-field-label {
+    font-weight: 700;
+}
+
+.editable-text-field-value:hover {
+    cursor: pointer;
+}
+
+div.editable-text-field div.editable-text-field-value-container {
+    width: 100%;
+}
+
+div.editable-text-field div.editable-text-field-value-container input {
+    font-size: 16px;
+}
+
+div.editable-text-field div.editable-text-field-value-container span.editable-text-field-value {
+    padding: 6px 12px;
+    display: block;
+    width: 100%;
+}
diff --git a/borgbutler-webapp/src/components/general/forms/EditableTextField.jsx b/borgbutler-webapp/src/components/general/forms/EditableTextField.jsx
new file mode 100644
index 0000000..5e59d44
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/EditableTextField.jsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import './EditableTextField.css';
+import {Button, Input, InputGroup, InputGroupAddon} from 'reactstrap';
+import {IconCheck, IconCancel} from '../IconComponents'
+
+class EditableTextField extends React.Component {
+
+    state = {
+        editing: false
+    };
+
+    constructor(props) {
+        super(props);
+
+        this.startEditing = this.startEditing.bind(this);
+        this.stopEditing = this.stopEditing.bind(this);
+    }
+
+    startEditing = () => {
+        this.setState({
+            editing: true
+        });
+    };
+
+    stopEditing = value => {
+        this.setState({
+            editing: false
+        });
+
+        if (value && this.props.onChange) {
+            this.props.onChange(value, this.props.name, this.props.index);
+        }
+    };
+
+    render = () => <div className={'editable-text-field'}>
+        <div className={'editable-text-div editable-text-field-label'}>{this.props.title}</div>
+        <div className={'editable-text-div editable-text-field-value-container'}>
+            {this.state.editing ?
+                <EditableTextFieldInput
+                    type={this.props.type}
+                    value={this.props.value}
+                    name={this.props.name}
+                    stopEditing={this.stopEditing}
+                /> :
+                <div className={'text-truncate editable-text-field-value'} onClick={this.startEditing}
+                     title={this.props.value}>
+                    {this.props.value}
+                </div>
+            }
+        </div>
+    </div>;
+}
+
+class EditableTextFieldInput extends React.Component {
+
+    state = {
+        value: this.props.value
+    };
+
+    constructor(props) {
+        super(props);
+
+        this.setReference = this.setReference.bind(this);
+        this.handleInputChange = this.handleInputChange.bind(this);
+        this.stopEditing = this.stopEditing.bind(this);
+        this.handleMouseDown = this.handleMouseDown.bind(this);
+        this.handleKeyPress = this.handleKeyPress.bind(this);
+    }
+
+    componentDidMount = () => {
+        document.addEventListener('mousedown', this.handleMouseDown);
+        document.addEventListener('keypress', this.handleKeyPress);
+    };
+
+    componentWillUnmount = () => {
+        document.removeEventListener('mousedown', this.handleMouseDown);
+        document.removeEventListener('keypress', this.handleKeyPress);
+    };
+
+    setReference = reference => this.reference = reference;
+
+    handleInputChange = event => this.setState({
+        value: event.target.value
+    });
+
+    handleMouseDown = event =>
+        this.reference && !this.reference.contains(event.target) ?
+            this.stopEditing(false)() : undefined;
+
+    handleKeyPress = event => {
+        switch (event.key) {
+            case 'Escape':
+                this.stopEditing(false)();
+                return;
+            case 'Enter':
+                if (this.props.type !== 'textarea') {
+                    this.stopEditing(true)();
+                    return;
+                }
+                break;
+            default:
+        }
+    };
+
+    stopEditing = (save = false) => () =>
+        this.props.stopEditing(save ? this.state.value : undefined);
+
+    render = () => <div ref={this.setReference}>
+        <InputGroup>
+            <Input
+                autoFocus
+                type={this.props.type}
+                value={this.state.value}
+                onChange={this.handleInputChange}/>
+            <InputGroupAddon addonType={'append'}>
+                <Button
+                className={'btn-outline-primary'}
+                    onClick={this.stopEditing(true)}
+                    color={'success'}
+                >
+                    <IconCheck/>
+                </Button>
+                <Button
+                className={'btn-outline-primary'}
+                    onClick={this.stopEditing(false)}
+                    color={'danger'}
+                >
+                    <IconCancel/>
+                </Button>
+            </InputGroupAddon>
+        </InputGroup>
+    </div>;
+}
+
+
+export default EditableTextField;
diff --git a/borgbutler-webapp/src/components/general/forms/FormButton.jsx b/borgbutler-webapp/src/components/general/forms/FormButton.jsx
new file mode 100644
index 0000000..190a8a4
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/FormButton.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {UncontrolledTooltip} from 'reactstrap';
+import classNames from 'classnames';
+import {revisedRandId} from "../../../utilities/global";
+import I18n from "../translation/I18n";
+
+class FormButton extends React.Component {
+
+    _id = this.props.id || this.props.name || revisedRandId();
+
+    render() {
+        let tooltip = null;
+        const {className, hint, hintKey, hintPlacement, id, bsStyle, ...other} = this.props;
+        if (hint || hintKey) {
+            tooltip = <UncontrolledTooltip placement={hintPlacement} target={this._id}>
+                {hintKey ? <I18n name={hintKey}/> : hint}
+            </UncontrolledTooltip>;
+        }
+        return (
+            <React.Fragment>
+                <button
+                    {...other}
+                    id={this._id}
+                    className={classNames(`btn btn-${bsStyle}`, className)}
+                >
+                    {this.props.children}
+                </button>
+                {tooltip}
+            </React.Fragment>
+        );
+    }
+};
+
+FormButton.propTypes = {
+    id: PropTypes.string,
+    bsStyle: PropTypes.oneOf(['primary', 'outline-primary', null]),
+    type: PropTypes.string,
+    onClick: PropTypes.func,
+    hint: PropTypes.string,
+    hintKey: PropTypes.string,
+    hintPlacement: PropTypes.oneOf(['right', 'top']),
+    disabled: PropTypes.bool,
+    children: PropTypes.node
+};
+FormButton.defaultProps = {
+    bsStyle: 'outline-primary',
+    type: 'button',
+    hintPlacement: 'top',
+    disabled: false
+};
+
+export {
+    FormButton
+};
diff --git a/borgbutler-webapp/src/components/general/forms/FormCheckbox.jsx b/borgbutler-webapp/src/components/general/forms/FormCheckbox.jsx
new file mode 100644
index 0000000..4ba1b9d
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/FormCheckbox.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {UncontrolledTooltip} from 'reactstrap';
+import {revisedRandId} from "../../../utilities/global";
+import classNames from 'classnames';
+import I18n from "../translation/I18n";
+
+class FormCheckbox extends React.Component {
+
+    _id = this.props.id || revisedRandId();
+
+    render() {
+        const {id, className, labelKey, label, hint, hintKey, ...other} = this.props;
+        let tooltip = null;
+        let hintId = null;
+        if (hint || hintKey) {
+            hintId = `hint-${this._id}`;
+            tooltip =
+                <UncontrolledTooltip placement="right" target={hintId}>
+                    {hintKey ? <I18n name={hintKey}/> : hint}
+                </UncontrolledTooltip>;
+        }
+        let labelNode = <label
+            className={'custom-control-label'}
+            htmlFor={this._id}>
+            {labelKey ? <I18n name={labelKey}/> : this.props.label}
+        </label>;
+        return (
+            <React.Fragment>
+                <div className="custom-control custom-checkbox" id={hintId}>
+                    <input type="checkbox"
+                           id={this._id}
+                           className={classNames('custom-control-input', className)}
+                           {...other}
+                    />
+                    {labelNode}
+                </div>
+                {tooltip}
+            </React.Fragment>
+        );
+    }
+}
+
+FormCheckbox.propTypes = {
+    id: PropTypes.string,
+    name: PropTypes.string,
+    checked: PropTypes.bool,
+    onChange: PropTypes.func,
+    hint: PropTypes.string,
+    label: PropTypes.node,
+    labelKey: PropTypes.string
+};
+
+FormCheckbox.defaultProps = {
+    checked: false,
+    onChange: null
+};
+
+
+export {
+    FormCheckbox
+};
diff --git a/borgbutler-webapp/src/components/general/forms/FormComponents.jsx b/borgbutler-webapp/src/components/general/forms/FormComponents.jsx
new file mode 100644
index 0000000..4d8f4d0
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/FormComponents.jsx
@@ -0,0 +1,298 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import {FormFeedback, Input, UncontrolledTooltip} from 'reactstrap';
+import {FormCheckbox} from "./FormCheckbox";
+import {FormButton} from "./FormButton";
+import {FormSelect, FormOption} from "./FormSelect";
+import {revisedRandId} from "../../../utilities/global";
+import I18n from "../translation/I18n";
+
+const FormGroup = (props) => {
+    const {className, ...other} = props;
+    return (
+        <div className={classNames('form-group row', className)}
+             {...other}
+        >
+            {props.children}
+        </div>
+    );
+}
+
+FormGroup.propTypes = {
+    children: PropTypes.node,
+    className: PropTypes.string
+};
+
+FormGroup.defaultProps = {
+    children: null
+};
+
+const FormRow = (props) => {
+    const {className, ...other} = props;
+    return (
+        <div className={classNames('form-row', className)}
+             {...other}
+        >
+            {props.children}
+        </div>
+    );
+}
+
+FormGroup.propTypes = {
+    children: PropTypes.node,
+    className: PropTypes.string
+};
+
+FormGroup.defaultProps = {
+    children: null
+};
+
+const FormLabel = (props) => {
+    const {className, i18nKey, length, ...other} = props;
+    return (
+        <label className={classNames(`col-sm-${props.length} col-form-label`, className)}
+               {...other}
+        >
+            {i18nKey ? <I18n name={i18nKey}/> : props.children}
+        </label>
+    );
+}
+
+FormLabel.propTypes = {
+    length: PropTypes.number,
+    i18nKey: PropTypes.string,
+    htmlFor: PropTypes.string
+};
+
+FormLabel.defaultProps = {
+    length: 2,
+    htmlFor: null
+};
+
+
+const FormInput = (props) => {
+    let tooltip = null;
+    let targetId = props.id || props.name;
+    if (props.hint) {
+        tooltip = <UncontrolledTooltip placement={props.hintPlacement} target={targetId}>
+            {props.hint}
+        </UncontrolledTooltip>;
+    }
+    const {fieldLength, className, hint, hintPlacement, id, ...other} = props;
+    return (
+        <React.Fragment>
+            <Input id={targetId}
+                   className={classNames(`col-sm-${props.fieldLength}`, className)}
+                   {...other}
+            />
+            {tooltip}
+        </React.Fragment>
+    );
+}
+
+FormInput.propTypes = {
+    id: PropTypes.string,
+    name: PropTypes.string,
+    hint: PropTypes.string,
+    hintPlacement: PropTypes.oneOf(['right', 'top']),
+    fieldLength: PropTypes.number,
+    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    min: PropTypes.number,
+    max: PropTypes.number,
+    step: PropTypes.number,
+    type: PropTypes.string,
+    placeholder: PropTypes.string,
+    valid: PropTypes.bool,
+    invalid: PropTypes.bool,
+    className: PropTypes.string
+};
+
+FormInput.defaultProps = {
+    id: null,
+    name: '',
+    hint: null,
+    hintPlacement: 'top',
+    fieldLength: 10,
+    value: '',
+    min: null,
+    max: null,
+    step: 1,
+    type: 'text',
+    placeholder: '',
+    valid: null,
+    invalid: null
+};
+
+
+const FormField = (props) => {
+    const {className, id, validationMessage, ...other} = props;
+    return (
+        <div id={props.id}
+             className={classNames(`col-sm-${props.length}`, className)}
+             {...other}
+        >
+            {props.children}
+            {validationMessage ? <FormFeedback>{validationMessage}</FormFeedback> : ''}
+            {props.hint ? <small className={'text-muted'}>{props.hint}</small> : ''}
+        </div>
+    );
+}
+
+FormField.propTypes = {
+    id: PropTypes.string,
+    hint: PropTypes.node,
+    length: PropTypes.number,
+    validationMessage: PropTypes.string,
+    children: PropTypes.node,
+    className: PropTypes.string
+};
+
+FormField.defaultProps = {
+    id: null,
+    hint: null,
+    length: 10,
+    validationMessage: null,
+    children: null
+};
+
+
+const FormLabelField = (props) => {
+    const forId = props.children.props.id || props.htmlFor || revisedRandId();
+    return (
+        <FormGroup>
+            <FormLabel length={props.labelLength} htmlFor={forId}>
+                {props.label}
+            </FormLabel>
+            <FormField length={props.fieldLength} hint={props.hint} validationMessage={props.validationMessage}>
+                {React.cloneElement(props.children, {id: forId})}
+            </FormField>
+        </FormGroup>
+    );
+}
+
+FormLabelField.propTypes = {
+    id: PropTypes.string,
+    htmlFor: PropTypes.string,
+    validationMessage: PropTypes.string,
+    labelLength: PropTypes.number,
+    fieldLength: PropTypes.number,
+    label: PropTypes.node,
+    hint: PropTypes.string,
+    children: PropTypes.node
+};
+
+FormLabelField.defaultProps = {
+    id: null,
+    htmlFor: null,
+    validationMessage: null,
+    labelLength: 2,
+    fieldLength: 10,
+    label: '',
+    hint: '',
+    children: null
+};
+
+
+const FormLabelInputField = (props) => {
+    return (
+        <FormLabelField
+            htmlFor={props.id}
+            labelLength={props.labelLength}
+            label={props.label}
+            hint={props.hint}
+            validationState={props.validationState}
+        >
+            <FormInput
+                id={props.id}
+                name={props.name}
+                type={props.type}
+                min={props.min}
+                hint={props.hint}
+                hintPlacement={props.hintPlacement}
+                max={props.max}
+                step={props.step}
+                value={props.value}
+                onChange={props.onChange}
+                fieldLength={props.fieldLength}
+                placeholder={props.placeholder}
+            />
+        </FormLabelField>
+    );
+}
+
+FormLabelInputField.propTypes = {
+    id: PropTypes.string,
+    label: PropTypes.node,
+    labelLength: PropTypes.number,
+    fieldLength: PropTypes.number,
+    hint: PropTypes.string,
+    hintPlacement: PropTypes.oneOf(['right', 'top']),
+    validationState: PropTypes.oneOf(['success', 'warning', 'error', null]),
+    type: PropTypes.string,
+    name: PropTypes.string,
+    min: PropTypes.number,
+    max: PropTypes.number,
+    step: PropTypes.number,
+    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    onChange: PropTypes.func,
+    placeholder: PropTypes.string
+};
+
+FormLabelInputField.defaultProps = {
+    id: null,
+    label: '',
+    labelLength: 2,
+    fieldLength: 10,
+    hint: null,
+    hintPlacement: 'top',
+    validationState: null,
+    type: 'text',
+    name: '',
+    min: null,
+    max: null,
+    step: 1,
+    value: '',
+    onChange: null,
+    placeholder: ''
+};
+
+
+const FormFieldset = (props) => {
+    return (
+        <fieldset id={props.id}
+                  className={classNames('form-group', props.className)}
+        >
+            <legend>{props.text}</legend>
+            {props.children}
+        </fieldset>
+    );
+}
+
+FormFieldset.propTypes = {
+    id: PropTypes.string,
+    text: PropTypes.node,
+    children: PropTypes.node,
+    className: PropTypes.string
+};
+
+FormFieldset.defaultProps = {
+    id: null,
+    text: '',
+    children: null
+};
+
+export {
+    FormGroup,
+    FormLabel,
+    FormField,
+    FormLabelField,
+    FormInput,
+    FormSelect,
+    FormOption,
+    FormCheckbox,
+    FormLabelInputField,
+    FormFieldset,
+    FormButton,
+    FormRow
+};
diff --git a/borgbutler-webapp/src/components/general/forms/FormSelect.jsx b/borgbutler-webapp/src/components/general/forms/FormSelect.jsx
new file mode 100644
index 0000000..f4030c5
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/forms/FormSelect.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import {UncontrolledTooltip} from 'reactstrap';
+import {getTranslation} from '../../../utilities/i18n';
+
+const FormSelect = (props) => {
+    let tooltip = null;
+    let targetId = props.id || props.name;
+    if (props.hint) {
+        tooltip = <UncontrolledTooltip placement={props.hintPlacement} target={targetId}>
+            {props.hint}
+        </UncontrolledTooltip>;
+    }
+    const {className, hint, hintPlacement, id, ...other} = props;
+    return (
+        <React.Fragment>
+            <select id={targetId}
+                    className={classNames('custom-select form-control form-control-sm mr-1', className)}
+                    {...other}
+            >
+                {props.children}
+            </select>
+            {tooltip}
+        </React.Fragment>
+    );
+}
+
+FormSelect.propTypes = {
+    id: PropTypes.string,
+    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
+    name: PropTypes.string,
+    onChange: PropTypes.func,
+    hint: PropTypes.string,
+    hintPlacement: PropTypes.oneOf(['right', 'top']),
+    children: PropTypes.node,
+    className: PropTypes.string
+};
+
+FormSelect.defaultProps = {
+    hint: null,
+    hintPlacement: 'top',
+};
+
+
+const FormOption = (props) => {
+    let label;
+    if (props.i18nKey) {
+        label = getTranslation(props.i18nKey) || props.value;
+    } else {
+        label = props.label || props.value;
+    }
+    return (
+        <React.Fragment>
+            <option value={props.value}
+            >
+                {label}
+            </option>
+        </React.Fragment>
+    );
+}
+
+FormSelect.propTypes = {
+    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
+    i18nKey: PropTypes.string,
+    label: PropTypes.string
+};
+
+export {
+    FormSelect, FormOption
+};
diff --git a/borgbutler-webapp/src/components/general/loading/LoadingOverlay.jsx b/borgbutler-webapp/src/components/general/loading/LoadingOverlay.jsx
new file mode 100644
index 0000000..d14b72a
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/loading/LoadingOverlay.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './LoadingOverlay.module.css'
+import {IconSpinner} from '../IconComponents';
+
+function LoadingOverlay({active}) {
+    if (!active) {
+        return <React.Fragment />;
+    }
+
+    return (
+        <div className={styles.loadingOverlay}>
+            <div className={styles.content}>
+                <IconSpinner />
+            </div>
+        </div>
+    );
+}
+
+LoadingOverlay.propTypes = {
+    active: PropTypes.bool
+};
+
+LoadingOverlay.defaultProps = {
+    active: false
+};
+
+export default LoadingOverlay;
diff --git a/borgbutler-webapp/src/components/general/loading/LoadingOverlay.module.css b/borgbutler-webapp/src/components/general/loading/LoadingOverlay.module.css
new file mode 100644
index 0000000..a77c051
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/loading/LoadingOverlay.module.css
@@ -0,0 +1,18 @@
+div.loadingOverlay {
+    z-index: 99999;
+    display: block;
+    width: 100vw;
+    height: 100vh;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background-color: rgba(11, 11, 11, 0.3);
+    -webkit-backdrop-filter: blur(10px);
+}
+
+div.loadingOverlay div.content {
+    z-index: 999999;
+    color: #ff0f03;
+    margin-top: 150px;
+    text-align: center;
+}
diff --git a/borgbutler-webapp/src/components/general/loading/failed/Overlay.jsx b/borgbutler-webapp/src/components/general/loading/failed/Overlay.jsx
new file mode 100644
index 0000000..7ab7c43
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/loading/failed/Overlay.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import {Modal, ModalBody, ModalHeader} from 'reactstrap';
+
+function FailedOverlay({title, text, closeModal, active}) {
+    return (
+        <Modal isOpen={active} toggle={closeModal}>
+            <ModalHeader toggle={closeModal}>{title || 'Fetch failed'}</ModalHeader>
+            <ModalBody>
+                {text}
+            </ModalBody>
+        </Modal>
+    );
+}
+
+export default FailedOverlay;
diff --git a/borgbutler-webapp/src/components/general/translation/I18n.jsx b/borgbutler-webapp/src/components/general/translation/I18n.jsx
new file mode 100644
index 0000000..7977f37
--- /dev/null
+++ b/borgbutler-webapp/src/components/general/translation/I18n.jsx
@@ -0,0 +1,20 @@
+import PropTypes from 'prop-types';
+import {getTranslation} from '../../../utilities/i18n';
+import {isDevelopmentMode} from '../../../utilities/global';
+
+function I18n({name, children, params}) {
+    return getTranslation(name, params) || (isDevelopmentMode() ? `??? ${children ? children : name} ???` : children);
+}
+
+I18n.propTypes = {
+    name: PropTypes.string.isRequired,
+    children: PropTypes.node,
+    params: PropTypes.arrayOf(PropTypes.node)
+};
+
+I18n.defaultProps = {
+    children: '',
+    params: []
+};
+
+export default I18n;
diff --git a/borgbutler-webapp/src/components/views/Start.jsx b/borgbutler-webapp/src/components/views/Start.jsx
new file mode 100644
index 0000000..0e6a277
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/Start.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import {PageHeader} from '../general/BootstrapComponents';
+import I18n from "../general/translation/I18n";
+
+class Start extends React.Component {
+    render() {
+        return (
+            <React.Fragment>
+                <PageHeader>
+                    <I18n name={'startscreen.welcome.title'}/>
+                </PageHeader>
+                <div className="welcome-intro"><I18n name={'startscreen.welcome.text'}/></div>
+                <div className="welcome-enjoy"><I18n name={'startscreen.welcome.enjoy'}/></div>
+                <div className="welcome-documentation-link"><a className={'btn btn-link btn-outline-primary'} href={'/docs/index.html'} target="_blank" rel="noopener noreferrer"><I18n name={'startscreen.welcome.documentation'}/></a></div>
+            </React.Fragment>
+        );
+    }
+}
+
+export default Start;
diff --git a/borgbutler-webapp/src/components/views/config/ConfigurationAccountTab.jsx b/borgbutler-webapp/src/components/views/config/ConfigurationAccountTab.jsx
new file mode 100644
index 0000000..ac45ebe
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/config/ConfigurationAccountTab.jsx
@@ -0,0 +1,135 @@
+import React from 'react';
+import {UncontrolledTooltip} from 'reactstrap';
+import {
+    FormLabelField,
+    FormSelect, FormOption, FormGroup, FormLabel, FormField
+} from "../../general/forms/FormComponents";
+import {getRestServiceUrl} from "../../../utilities/global";
+import {clearDictionary} from '../../../utilities/i18n';
+import I18n from "../../general/translation/I18n";
+import ErrorAlertGenericRestFailure from "../../general/ErrorAlertGenericRestFailure";
+import Loading from "../../general/Loading";
+import {IconRefresh} from "../../general/IconComponents";
+
+class ConfigAccountTab extends React.Component {
+    loadConfig = () => {
+        this.setState({
+            loading: true,
+            failed: false
+        });
+        fetch(getRestServiceUrl('configuration/user'), {
+            method: 'GET',
+            dataType: 'JSON',
+            headers: {
+                'Content-Type': 'text/plain; charset=utf-8'
+            }
+        })
+            .then((resp) => {
+                return resp.json()
+            })
+            .then((data) => {
+                const {locale, dateFormat, ...user} = data;
+                this.setState({
+                    loading: false,
+                    locale: locale ? locale : '',
+                    dateFormat: dateFormat ? dateFormat : '',
+                    ...user
+                })
+            })
+            .catch((error) => {
+                console.log("error", error);
+                this.setState({
+                    loading: false,
+                    failed: true
+                });
+            })
+    };
+
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            loading: true,
+            failed: false,
+            locale: null,
+            dateFormat: null
+        };
+
+        this.handleTextChange = this.handleTextChange.bind(this);
+        this.loadConfig = this.loadConfig.bind(this);
+    }
+
+    componentDidMount() {
+        this.loadConfig();
+    }
+
+    handleTextChange = event => {
+        event.preventDefault();
+        this.setState({[event.target.name]: event.target.value});
+    }
+
+    save() {
+        var user = {
+            locale: this.state.locale,
+            dateFormat: this.state.dateFormat
+        };
+        return fetch(getRestServiceUrl("configuration/user"), {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(user)
+        })
+    }
+
+    render() {
+        if (this.state.loading) {
+            return <Loading/>;
+        }
+
+        if (this.state.failed) {
+            return <ErrorAlertGenericRestFailure handleClick={this.loadConfig}/>;
+        }
+
+        return (
+            <form>
+                <div id={'clearDictionary'}
+                     className={'btn btn-outline-primary refresh-button-right'}
+                     onClick={clearDictionary}
+                >
+                    <IconRefresh/>
+                    <UncontrolledTooltip placement={'left'} target={'clearDictionary'}>
+                        <I18n name={'configuration.reloadDictionary.hint'}/>
+                    </UncontrolledTooltip>
+                </div>
+                <FormLabelField label={<I18n name={'configuration.application.language'}/>} fieldLength={2}>
+                    <FormSelect value={this.state.locale} name={'locale'} onChange={this.handleTextChange}>
+                        <FormOption value={''} i18nKey={'configuration.application.language.option.default'}/>
+                        <FormOption value={'en'} i18nKey={'language.english'}/>
+                        <FormOption value={'de'} i18nKey={'language.german'}/>
+                    </FormSelect>
+                </FormLabelField>
+                <FormGroup>
+                    <FormLabel length={2} htmlFor={'dateFormat'}>
+                        <I18n name={'configuration.application.dateFormat'}/>
+                    </FormLabel>
+                    <FormField length={2}>
+                        <FormSelect value={this.state.dateFormat} name={'dateFormat'} onChange={this.handleTextChange}>
+                            <FormOption value={''} i18nKey={'configuration.application.dateFormat.option.auto'}/>
+                            <FormOption value={'dd.MM.yyyy'} label={'16.01.2018'}/>
+                            <FormOption value={'d.M.yy'} label={'16.1.18'}/>
+                            <FormOption value={'yyyy-MM-dd'} label={'2018-01-16'}/>
+                            <FormOption value={'dd/MM/yyyy'} label={'16/01/2018'}/>
+                            <FormOption value={'d/M/yy'} label={'16/1/18'}/>
+                            <FormOption value={'MM/dd/yyyy'} label={'01/16/2018'}/>
+                            <FormOption value={'M/d/yy'} label={'1/16/18'}/>
+                        </FormSelect>
+                    </FormField>
+                </FormGroup>
+            </form>
+        );
+    }
+}
+
+export default ConfigAccountTab;
+
diff --git a/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx b/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx
new file mode 100644
index 0000000..096eeb9
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/config/ConfigurationPage.jsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import {FormGroup, Nav, NavLink, TabContent, TabPane} from 'reactstrap';
+import {PageHeader} from '../../general/BootstrapComponents';
+import {FormButton, FormField} from '../../general/forms/FormComponents';
+import {isDevelopmentMode} from "../../../utilities/global";
+import {clearDictionary} from '../../../utilities/i18n';
+import I18n from "../../general/translation/I18n";
+import classNames from "classnames";
+import ConfigAccountTab from "./ConfigurationAccountTab";
+import ConfigServerTab from "./ConfigurationServerTab";
+import LoadingOverlay from '../../general/loading/LoadingOverlay';
+
+class ConfigurationPage
+    extends React
+        .Component {
+
+    constructor(props) {
+        super(props);
+        this.serverTabRef = React.createRef();
+        this.accountTabRef = React.createRef();
+        this.state = {
+            activeTab: '1',
+            loading: false,
+            reload: false
+        };
+
+        this.onSave = this.onSave.bind(this);
+        this.onCancel = this.onCancel.bind(this);
+    }
+
+    toggleTab = tab => () => {
+        this.setState({
+            activeTab: tab
+        })
+    };
+
+    setReload = () => {
+        this.setState({
+            reload: true
+        })
+    }
+
+    async onSave(event) {
+        this.setState({
+            loading: true
+        })
+        const cb1 = this.serverTabRef.current ? this.serverTabRef.current.save() : null;
+        const cb2 = this.accountTabRef.current ? this.accountTabRef.current.save() : null;
+        if (cb1) await cb1;
+        if (cb2) await cb2;
+        clearDictionary();
+        this.setState({
+            loading: false
+        })
+        this.setReload();
+    }
+
+    onCancel() {
+        this.setReload();
+    }
+
+    render() {
+        // https://codepen.io/_arpy/pen/xYoyPW
+        if (this.state.reload) {
+            window.location.reload();
+        }
+        let todo = '';
+        if (isDevelopmentMode()) {
+            todo = <code><h3>ToDo</h3>
+                <ul>
+                    <li>Do the form validation (server and/or client side) with error fields.</li>
+                </ul>
+            </code>
+        }
+        return (
+            <React.Fragment>
+                <PageHeader><I18n name={'configuration'}/></PageHeader>
+                <Nav tabs>
+                    <NavLink
+                        className={classNames({active: this.state.activeTab === '1'})}
+                        onClick={this.toggleTab('1')}
+                    >
+                        <I18n name={'configuration.account'}/>
+                    </NavLink>
+                    <NavLink
+                        className={classNames({active: this.state.activeTab === '2'})}
+                        onClick={this.toggleTab('2')}
+                    >
+                        <I18n name={'configuration.server'}/>
+                    </NavLink>
+                </Nav>
+                <TabContent activeTab={this.state.activeTab}>
+                    <TabPane tabId={'1'}>
+                        <ConfigAccountTab ref={this.accountTabRef}/>
+                    </TabPane>
+                </TabContent>
+                <TabContent activeTab={this.state.activeTab}>
+                    <TabPane tabId={'2'}>
+                        <ConfigServerTab ref={this.serverTabRef}/>
+                    </TabPane>
+                </TabContent>
+                <FormGroup>
+                    <FormField length={12}>
+                        <FormButton onClick={this.onCancel}
+                                    hintKey="configuration.cancel.hint"><I18n name={'common.cancel'}/>
+                        </FormButton>
+                        <FormButton onClick={this.onSave} bsStyle="primary"
+                                    hintKey="configuration.save.hint"><I18n name={'common.save'}/>
+                        </FormButton>
+                    </FormField>
+                </FormGroup>
+                {todo}
+                <LoadingOverlay active={this.state.loading} />
+            </React.Fragment>
+        );
+    }
+}
+
+export default ConfigurationPage;
+
diff --git a/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx b/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
new file mode 100644
index 0000000..a5df501
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/config/ConfigurationServerTab.jsx
@@ -0,0 +1,215 @@
+import React from 'react';
+import {Button, Collapse} from 'reactstrap';
+import DirectoryItemsFieldset from './DirectoryItemsFieldset';
+import {
+    FormButton,
+    FormCheckbox,
+    FormLabelField,
+    FormLabelInputField,
+    FormFieldset
+} from "../../general/forms/FormComponents";
+import {getRestServiceUrl} from "../../../utilities/global";
+import {IconDanger, IconWarning} from '../../general/IconComponents';
+import {getTranslation} from "../../../utilities/i18n";
+import I18n from "../../general/translation/I18n";
+import ErrorAlertGenericRestFailure from '../../general/ErrorAlertGenericRestFailure';
+import Loading from "../../general/Loading";
+
+var directoryItems = [];
+
+class ConfigServerTab extends React.Component {
+    loadConfig = () => {
+        this.setState({
+            loading: true,
+            failed: false
+        });
+        fetch(getRestServiceUrl('configuration/config'), {
+            method: 'GET',
+            dataType: 'JSON',
+            headers: {
+                'Content-Type': 'text/plain; charset=utf-8'
+            }
+        })
+            .then((resp) => {
+                return resp.json()
+            })
+            .then((data) => {
+                const {templatesDirs, ...config} = data;
+
+                directoryItems.splice(0, directoryItems.length);
+                let idx = 0;
+                if (templatesDirs) {
+                    templatesDirs.forEach(function (item) {
+                        directoryItems.push({index: idx++, directory: item.directory, recursive: item.recursive});
+                    });
+                }
+                config.directoryItems = directoryItems;
+
+                this.setState({
+                    loading: false,
+                    ...config
+                })
+            })
+            .catch((error) => {
+                console.log("error", error);
+                this.setState({
+                    loading: false,
+                    failed: true
+                });
+            })
+    };
+
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            loading: true,
+            failed: false,
+            port: 8042,
+            showTestData: true,
+            webDevelopmentMode: false,
+            directoryItems: [],
+            redirect: false,
+            expertSettingsOpen: false
+        };
+
+        this.handleTextChange = this.handleTextChange.bind(this);
+        this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
+        this.addDirectoryItem = this.addDirectoryItem.bind(this);
+        this.removeDirectoryItem = this.removeDirectoryItem.bind(this);
+        this.onResetConfiguration = this.onResetConfiguration.bind(this);
+        this.loadConfig = this.loadConfig.bind(this);
+    }
+
+    componentDidMount() {
+        this.loadConfig();
+    }
+
+    handleTextChange = event => {
+        event.preventDefault();
+        this.setState({[event.target.name]: event.target.value});
+    }
+
+    handleCheckboxChange = event => {
+        this.setState({[event.target.name]: event.target.checked});
+    }
+
+    handleDirectoryChange = (index, newDirectory) => {
+        const items = this.state.directoryItems;
+        items[index].directory = newDirectory;
+        // update state
+        this.setState({
+            directoryItems,
+        });
+    }
+
+    handleRecursiveFlagChange = (index, newRecursiveState) => {
+        const items = this.state.directoryItems;
+        items[index].recursive = newRecursiveState;
+        // update state
+        this.setState({
+            directoryItems,
+        });
+    }
+
+    save() {
+        var config = {
+            port: this.state.port,
+            showTestData: this.state.showTestData,
+            webDevelopmentMode: this.state.webDevelopmentMode,
+            templatesDirs: []
+        };
+        if (this.state.directoryItems) {
+            this.state.directoryItems.forEach(function (item) {
+                config.templatesDirs.push({directory: item.directory, recursive: item.recursive});
+            });
+        }
+        return fetch(getRestServiceUrl("configuration/config"), {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(config)
+        })
+    }
+
+    onResetConfiguration() {
+        if (window.confirm(getTranslation('configuration.resetAllSettings.question'))) {
+            fetch(getRestServiceUrl("configuration/reset?IKnowWhatImDoing=true"), {
+                method: "GET",
+                dataType: "JSON",
+                headers: {
+                    "Content-Type": "text/plain; charset=utf-8"
+                }
+            })
+        }
+    }
+
+    addDirectoryItem() {
+        directoryItems.push({
+            index: directoryItems.length + 1,
+            directory: "",
+            recursive: false
+        });
+        this.setState({directoryItems: directoryItems});
+    }
+
+    removeDirectoryItem(itemIndex) {
+        directoryItems.splice(itemIndex, 1);
+        this.setState({directoryItems: directoryItems});
+    }
+
+    render() {
+        if (this.state.loading) {
+            return <Loading/>;
+        }
+
+        if (this.state.failed) {
+            return <ErrorAlertGenericRestFailure handleClick={this.loadConfig} />;
+        }
+
+        return (
+            <form>
+                <FormLabelField label={<I18n name={'configuration.showTestData'}/>} fieldLength={2}>
+                    <FormCheckbox checked={this.state.showTestData}
+                                  name="showTestData"
+                                  onChange={this.handleCheckboxChange}/>
+                </FormLabelField>
+                <DirectoryItemsFieldset items={this.state.directoryItems} addItem={this.addDirectoryItem}
+                                        removeItem={this.removeDirectoryItem}
+                                        onDirectoryChange={this.handleDirectoryChange}
+                                        onRecursiveFlagChange={this.handleRecursiveFlagChange}/>
+                <FormLabelField>
+                    <Button className={'btn-outline-primary'}
+                            onClick={() => this.setState({expertSettingsOpen: !this.state.expertSettingsOpen})}>
+                        <IconWarning/> <I18n name={'configuration.forExpertsOnly'}/>
+                    </Button>
+                </FormLabelField>
+                <Collapse isOpen={this.state.expertSettingsOpen}>
+                    <FormFieldset text={<I18n name={'configuration.expertSettings'}/>}>
+                        <FormLabelInputField label={'Port'} fieldLength={2} type="number" min={0} max={65535}
+                                             step={1}
+                                             name={'port'} value={this.state.port}
+                                             onChange={this.handleTextChange}
+                                             placeholder="Enter port"/>
+                        <FormLabelField label={<I18n name={'configuration.webDevelopmentMode'}/>} fieldLength={2}>
+                            <FormCheckbox checked={this.state.webDevelopmentMode}
+                                          hintKey={'configuration.webDevelopmentMode.hint'}
+                                          name="webDevelopmentMode"
+                                          onChange={this.handleCheckboxChange}/>
+                        </FormLabelField>
+                        <FormLabelField>
+                            <FormButton id={'resetFactorySettings'} onClick={this.onResetConfiguration}
+                                        hintKey={'configuration.resetAllSettings.hint'}> <IconDanger/> <I18n
+                                name={'configuration.resetAllSettings'}/>
+                            </FormButton>
+                        </FormLabelField>
+                    </FormFieldset>
+                </Collapse>
+            </form>
+        );
+    }
+}
+
+export default ConfigServerTab;
+
diff --git a/borgbutler-webapp/src/components/views/config/UpdatePage.jsx b/borgbutler-webapp/src/components/views/config/UpdatePage.jsx
new file mode 100644
index 0000000..43e53cf
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/config/UpdatePage.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import {PageHeader} from '../../general/BootstrapComponents';
+import {getRestServiceUrl} from '../../../utilities/global';
+import {FormButton} from '../../general/forms/FormComponents';
+import {Table} from 'reactstrap';
+import I18n from "../../general/translation/I18n";
+
+
+class UpdatePage extends React.Component {
+
+    onUpdate = () => {
+        fetch(getRestServiceUrl('updates/install'), {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        });
+    }
+
+    componentDidMount() {
+        fetch(getRestServiceUrl("updates/info"), {
+            method: "GET",
+            dataType: "JSON",
+            headers: {
+                "Content-Type": "text/plain; charset=utf-8"
+            }
+        })
+            .then((resp) => {
+                return resp.json();
+            })
+            .then((data) => {
+                this.setState({
+                    version: data.version,
+                    installerUrl: data.installerUrl,
+                    fileSize: data.fileSize,
+                    filename: data.filename,
+                    baseUrl: data.baseUrl,
+                    comment: data.comment
+                });
+            })
+            .catch((error) => {
+                console.log(error, "Oups, what's happened?")
+            })
+    }
+
+    render() {
+        let content = <I18n name={'update.noUpdateAvailable'}/>;
+        if (this.props.updateVersion) {
+            let comment = null;
+            if (this.state.comment) {
+                comment = <tr>
+                    <th><I18n name={'common.comment'}/></th>
+                    <td>{this.state.comment}</td>
+                </tr>;
+            }
+            content = <React.Fragment>
+                <h2><I18n name={'update.newVersion'}/></h2>
+                <I18n name={'update.newVersionAvailable'} params={[this.props.updateVersion]}/>
+                <br/>
+                <I18n name={'update.newVersion.simplyClickButton'} />
+                <br/>
+                <br/>
+                <form>
+                    <FormButton
+                        onClick={this.onUpdate}
+                        hintKey={'update.update.button.hint'}
+                    >
+                        <I18n name={'common.update'}/>
+                    </FormButton>
+                </form>
+                <br/>
+                <I18n name={'update.description.line1'}/>
+                <br/>
+                <I18n name={'update.description.line2'}/>
+                <br/>
+                <br/>
+                <Table striped bordered hover size={'sm'}>
+                    <tbody>
+                    <tr>
+                        <th><I18n name={'version'}/></th>
+                        <td>{this.state.version}</td>
+                    </tr>
+                    <tr>
+                        <th><I18n name={'update.installerUrl'}/></th>
+                        <td><a href={this.state.installerUrl} target={'_new'}>{this.state.installerUrl}</a></td>
+                    </tr>
+                    <tr>
+                        <th><I18n name={'common.fileSize'}/></th>
+                        <td>{this.state.fileSize}</td>
+                    </tr>
+                    <tr>
+                        <th><I18n name={'common.filename'}/></th>
+                        <td>{this.state.filename}</td>
+                    </tr>
+                    {comment}
+                    </tbody>
+                </Table>
+
+            </React.Fragment>
+        }
+        return <React.Fragment>
+            <PageHeader><I18n name={'update'}/></PageHeader>
+            {content}
+        </React.Fragment>;
+    }
+
+    constructor(props) {
+        super(props);
+        this.state = {
+            version: null,
+            installerUrl: null,
+            baseUrl: null,
+            fileSize: null,
+            filename: null,
+            comment: null
+        }
+    }
+}
+
+const mapStateToProps = state => ({
+    updateVersion: state.version.updateVersion
+});
+
+export default connect(mapStateToProps, {})(UpdatePage);
+
diff --git a/borgbutler-webapp/src/components/views/develop/RestServices.jsx b/borgbutler-webapp/src/components/views/develop/RestServices.jsx
new file mode 100644
index 0000000..f3fad54
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/develop/RestServices.jsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import {PageHeader} from '../../general/BootstrapComponents';
+import {getRestServiceUrl, getResponseHeaderFilename} from "../../../utilities/global";
+import fileDownload from 'js-file-download';
+
+class RestUrlLink extends React.Component {
+    render() {
+        const service = this.props.service;
+        const params = this.props.params;
+        var url;
+        if (params) {
+            if (service === 'files/browse-local-filesystem') {
+                url = getRestServiceUrl(service) + '?' + params;
+            } else {
+                url = getRestServiceUrl(service) + '?prettyPrinter=true&' + params;
+            }
+        } else {
+            url = getRestServiceUrl(service) + '?prettyPrinter=true';
+        }
+        return (
+            <a href={url}>rest/{service}{params ? '?' + params : ''}</a>
+        )
+    }
+}
+
+class RestServices extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            templateDefinitionId: '',
+            templatePrimaryKey: '',
+        }
+        this.onRun = this.onRun.bind(this);
+    }
+
+    componentDidMount() {
+        fetch(getRestServiceUrl("templates/example-definitions"), {
+            method: "GET",
+            dataType: "JSON",
+            headers: {
+                "Content-Type": "text/plain; charset=utf-8",
+            }
+        })
+            .then((resp) => {
+                return resp.json()
+            })
+            .then((data) => {
+                    this.setState({
+                        templateDefinitionId: data.templateDefinitionId,
+                        templatePrimaryKey: data.templatePrimaryKey
+                    });
+                }
+            )
+            .catch((error) => {
+                    console.log(error, "Oups, what's happened?")
+                }
+            )
+    }
+
+    onRun() {
+        let filename;
+        fetch(getRestServiceUrl('templates/example-run-data'), {
+            method: "GET",
+            dataType: "JSON",
+            headers: {
+                "Content-Type": "text/plain; charset=utf-8"
+            }
+        })
+            .then((resp) => {
+                return resp.json()
+            })
+            .then((data) => {
+                fetch(getRestServiceUrl("templates/run"), {
+                    method: 'POST',
+                    body: JSON.stringify(data)
+                })
+                    .then(response => {
+                        filename = getResponseHeaderFilename(response.headers.get("Content-Disposition"));
+                        //this.setState({downloadFilename: filename});
+                        return response.blob();
+                    })
+                    .then(blob => fileDownload(blob, filename));
+            })
+            .catch((error) => {
+                console.log(error, "Oups, what's happened?")
+            })
+    }
+
+    render() {
+        return (
+            <React.Fragment>
+                <PageHeader>
+                    Rest Services
+                </PageHeader>
+                <h3>Templates</h3>
+                <ul>
+                    <li><RestUrlLink service='templates/list'/></li>
+                    <li><RestUrlLink service='templates/definition-list'/></li>
+                    <li><RestUrlLink service={'templates/definition'} params={`id=${this.state.templateDefinitionId}`}/> (by id)</li>
+                </ul>
+                <h4>How to get and run a template:</h4>
+                <ol>
+                    <li>Get a list of all templates:<br/>
+                        <RestUrlLink service='templates/list'/></li>
+                    <li>Get a single template from list or get one by its primary key via rest
+                        (primaryKey={this.state.templatePrimaryKey}):<br/>
+                        <RestUrlLink service='templates/template'
+                                     params={'primaryKey=' + encodeURIComponent(this.state.templatePrimaryKey)}/>
+                    </li>
+                    <li>You will receive a template including its template definition if assigned.</li>
+                    <li>Run template with <a
+                        href={getRestServiceUrl('templates/example-run-data') + '?prettyPrinter=true'}>json post parameter</a> for service<br/>
+                        <button tabIndex={1} onClick={this.onRun} type="button" className="btn btn-link">rest/templates/run</button>
+                    </li>
+                </ol>
+                <h3>
+                    Config
+                </h3>
+                <ul>
+                    <li><RestUrlLink service='configuration/user'/></li>
+                    <li><RestUrlLink service='configuration/config'/></li>
+                    <li><RestUrlLink service='configuration/config-ui'/> (as a trial for dynamic forms)</li>
+                    <li><RestUrlLink service='version'/> Gets the version and build date of the server.</li>
+                    <li><RestUrlLink service='updates/info'/> Gets the update version.</li>
+                    <li><RestUrlLink service='i18n/list'/> Gets all translations. And keys only:{' '}
+                        <RestUrlLink service='i18n/list' params={'keysOnly=true'}/> </li>
+                </ul>
+                <h3>Browse local filesystem</h3>
+                <ul>
+                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=dir'/></li>
+                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=excel'/></li>
+                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=word'/></li>
+                    <li><RestUrlLink service='files/browse-local-filesystem' params='type=file'/></li>
+                </ul>
+                <h3>Logging</h3>
+                <ul>
+                    <li><RestUrlLink service='logging/query'/> (all, default is info log level as treshold)</li>
+                    <li><RestUrlLink service='logging/query' params={'treshold=warn'}/> (only warnings)</li>
+                    <li><RestUrlLink service='logging/query' params={'treshold=info&search=server'}/> (search for server)</li>
+                </ul>
+            </React.Fragment>
+        );
+    }
+}
+
+export default RestServices;
diff --git a/borgbutler-webapp/src/components/views/footer/Footer.jsx b/borgbutler-webapp/src/components/views/footer/Footer.jsx
new file mode 100644
index 0000000..e7e0b6e
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/footer/Footer.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import {Link} from 'react-router-dom';
+import './style.css'
+import {formatDateTime} from '../../../utilities/global';
+import I18n from "../../general/translation/I18n";
+
+function Footer({versionInfo}) {
+
+    if (versionInfo.failed) {
+        return (
+            <div className={'footer'}>
+                <p className={'version'}>Cannot fetch version information.</p>
+            </div>
+        );
+    }
+
+    return <div className={'footer'}>
+        <p className={'version'}>
+            <I18n name={'version'}/> {versionInfo.version} * <I18n name={'version.buildDate'}/> {formatDateTime(versionInfo.buildDate)}
+            {versionInfo.updateVersion ? <span> * <Link to={'/update'}><I18n name={'version.updateAvailable'}/></Link></span> : ''}
+        </p>
+    </div>;
+}
+
+export default Footer;
diff --git a/borgbutler-webapp/src/components/views/footer/style.css b/borgbutler-webapp/src/components/views/footer/style.css
new file mode 100644
index 0000000..670b024
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/footer/style.css
@@ -0,0 +1,12 @@
+div.footer {
+    border-top: 1px solid #ccc;
+    height: 60px;
+}
+
+div.footer p.version {
+    margin-top: 15px;
+    text-align: center;
+    color: #aaa;
+    font-weight: 300;
+    font-size: 12px;
+}
\ No newline at end of file
diff --git a/borgbutler-webapp/src/components/views/logging/LogEmbeddedPanel.jsx b/borgbutler-webapp/src/components/views/logging/LogEmbeddedPanel.jsx
new file mode 100644
index 0000000..595e2e2
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/logging/LogEmbeddedPanel.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import {getRestServiceUrl} from '../../../utilities/global';
+import LogTable from './LogTable';
+import './LogViewer.css';
+import PropTypes from 'prop-types';
+
+class LogEmbeddedPanel extends React.Component {
+
+    componentDidMount = () => {
+        this.reload();
+    };
+
+    handleToggleSortOrder = () => {
+        this.setState({
+            ascendingOrder: (this.state.ascendingOrder === 'true') ? 'false' : 'true'
+        }, () => {
+            this.reload()
+        });
+    };
+
+    handleInputChange = (event) => {
+        this.props.changeFilter(event.target.name, event.target.value);
+    };
+
+    reload = () => {
+        this.setState({
+            isFetching: true,
+            failed: false,
+            logEntries: undefined
+        });
+        fetch(getRestServiceUrl("logging/query", {
+            search: this.state.search,
+            treshold: this.props.treshold,
+            maxSize: this.props.maxSize,
+            ascendingOrder: this.state.ascendingOrder,
+            mdcTemplatePk: this.props.mdcTemplatePk,
+            mdcTemplateDefinitionPk: this.props.mdcTemplateDefinitionPk
+        }), {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        })
+            .then(response => response.json())
+            .then(json => {
+                const logEntries = json.map(logEntry => {
+                    return {
+                        level: logEntry.level.toLowerCase(),
+                        message: logEntry.message,
+                        logDate: logEntry.logDate,
+                        javaClass: logEntry.javaClass,
+                        javaClassSimpleName: logEntry.javaClassSimpleName,
+                        lineNumber: logEntry.lineNumber,
+                        methodName: logEntry.methodName
+                    };
+                });
+                this.setState({
+                    isFetching: false,
+                    logEntries
+                })
+            })
+            .catch(() => this.setState({isFetching: false, failed: true}));
+    };
+
+
+    render = () => {
+        return <LogTable
+            search={''}
+            locationFormat={this.props.locationFormat}
+            entries={this.state.logEntries}
+            ascendingOrder={this.state.ascendingOrder}
+            toggleOrder={this.handleToggleSortOrder}
+            showStackTrace={this.props.showStackTrace}
+        />
+    };
+
+    constructor(props) {
+        super(props);
+        this.state = {
+            search: '',
+            ascendingOrder: 'false'
+        }
+    }
+}
+
+LogEmbeddedPanel.propTypes = {
+    locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
+    showStackTrace: PropTypes.oneOf(['true', 'false']),
+    threshold: PropTypes.oneOf(['error', 'warn', 'info', 'debug', 'trace']),
+    maxSize: PropTypes.oneOf(['50', '100', '500', '1000', '10000']),
+    mdcTemplatePk: PropTypes.string,
+    mdcTemplateDefinitionPk: PropTypes.string,
+    search: PropTypes.string
+};
+
+LogEmbeddedPanel.defaultProps = {
+    locationFormat: 'none',
+    showStackTrace: 'false',
+    treshold: 'info',
+    maxSize: '50',
+    mdcTemplatePk: null,
+    mdcTemplateDefinitionPk: null,
+    search: ''
+};
+
+export default LogEmbeddedPanel;
diff --git a/borgbutler-webapp/src/components/views/logging/LogEntry.jsx b/borgbutler-webapp/src/components/views/logging/LogEntry.jsx
new file mode 100644
index 0000000..e49b7f7
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/logging/LogEntry.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Highlight from 'react-highlighter';
+
+function LogEntry({entry, search, locationString, showStackTrace}) {
+    let message = (showStackTrace === 'true' && entry.stackTrace) ? entry.message + <br/> + entry.stackTrace : entry.message;
+    return (
+        <tr>
+            <td className={'tt'}>{entry.logDate}</td>
+            <td className={`tt log-${entry.level}`}><Highlight search={search}>{entry.level}</Highlight></td>
+            <td className={'tt'}><Highlight search={search}>{message}</Highlight></td>
+            {locationString ? <td className={'tt log-location'}><Highlight search={search}>{locationString}</Highlight></td> : undefined}
+        </tr>
+    );
+}
+
+LogEntry.propTypes = {
+    entry: PropTypes.shape({}).isRequired,
+    search: PropTypes.string,
+    locationString: PropTypes.string,
+    showStackTrace: PropTypes.oneOf(['true', 'false'])
+};
+
+export default LogEntry;
\ No newline at end of file
diff --git a/borgbutler-webapp/src/components/views/logging/LogFilters.jsx b/borgbutler-webapp/src/components/views/logging/LogFilters.jsx
new file mode 100644
index 0000000..2358488
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/logging/LogFilters.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {FormButton, FormInput, FormLabel, FormSelect, FormOption} from '../../general/forms/FormComponents';
+import {IconRefresh} from '../../general/IconComponents';
+import I18n from '../../general/translation/I18n';
+
+function LogFilters({loadLog, changeFilter, filters}) {
+
+    return (
+        <form
+            onSubmit={loadLog}
+            className={'form-inline'}
+        >
+            <FormLabel length={1}>
+                Filter:
+            </FormLabel>
+
+            <FormSelect
+                value={filters.threshold}
+                name={'threshold'}
+                onChange={changeFilter}
+                hint={<I18n name={'logviewer.filter.level.hint'}/>}
+            >
+                <FormOption value={'error'}/>
+                <FormOption value={'warn'}/>
+                <FormOption value={'info'}/>
+                <FormOption value={'debug'}/>
+                <FormOption value={'trace'}/>
+            </FormSelect>
+
+            <FormInput
+                value={filters.search}
+                name={'search'}
+                onChange={changeFilter}
+                fieldLength={5}
+            />
+
+            <FormSelect
+                value={filters.locationFormat}
+                name={'locationFormat'}
+                onChange={changeFilter}
+                hint={<I18n name={'logviewer.filter.location.hint'}/>}
+            >
+                <FormOption value={'none'} i18nKey={'common.none'}/>
+                <FormOption value={'short'} i18nKey={'logviewer.filter.location.option.short'}/>
+                <FormOption value={'normal'} i18nKey={'logviewer.filter.location.option.normal'}/>
+            </FormSelect>
+
+            <FormSelect
+                value={filters.showStackTrace}
+                name={'showStackTrace'}
+                onChange={changeFilter}
+                hint={<I18n name={'logviewer.filter.stacktraces.showHide.hint'}/>}
+            >
+                <FormOption value={'false'} i18nKey={'common.none'}/>
+                <FormOption value={'true'} i18nKey={'logviewer.filter.stacktraces'}/>
+            </FormSelect>
+
+            <FormSelect
+                value={filters.maxSize}
+                name={'maxSize'}
+                onChange={changeFilter}
+                hint={<I18n name={'common.limitsResultSize'} />}
+            >
+                <FormOption value={'50'} />
+                <FormOption value={'100'} />
+                <FormOption value={'500'} />
+                <FormOption value={'1000'} />
+                <FormOption value={'10000'} />
+            </FormSelect>
+            <FormButton type={'submit'} bsStyle={'primary'}>
+                <IconRefresh/>
+            </FormButton>
+        </form>
+    );
+}
+
+LogFilters.propTypes = {
+    changeFilter: PropTypes.func.isRequired,
+    filters: PropTypes.shape({
+        threshold: PropTypes.oneOf(['error', 'warn', 'info', 'debug', 'trace']),
+        search: PropTypes.string,
+        locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
+        showStackTrace: PropTypes.oneOf(['true', 'false']),
+        maxSize: PropTypes.oneOf(['50', '100', '500', '1000', '10000']),
+        ascendingOrder: PropTypes.oneOf(['true', 'false'])
+    }).isRequired,
+    loadLog: PropTypes.func.isRequired
+};
+
+export default LogFilters;
diff --git a/borgbutler-webapp/src/components/views/logging/LogPage.jsx b/borgbutler-webapp/src/components/views/logging/LogPage.jsx
new file mode 100644
index 0000000..10a6a16
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/logging/LogPage.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import PropTypes from 'prop-types';
+import {PageHeader} from '../../general/BootstrapComponents';
+import LogFilters from './LogFilters';
+import {changeFilter, requestLogReload} from '../../../actions';
+import LogTable from './LogTable';
+import I18n from '../../general/translation/I18n';
+import './LogViewer.css';
+import ErrorAlert from '../../general/ErrorAlert';
+
+class LogPage extends React.Component {
+
+    componentDidMount = () => this.props.loadLog();
+
+    handleToggleSortOrder = () => {
+        let filters = this.props.filters;
+        this.props.changeFilter('ascendingOrder', filters.ascendingOrder === 'true' ? 'false'  : 'true');
+        this.props.loadLog();
+    };
+
+    handleInputChange = (event) => {
+        this.props.changeFilter(event.target.name, event.target.value);
+    };
+
+    render = () => {
+
+        let content;
+
+        if (this.props.loading) {
+            content = <i>Loading...</i>
+        } else if (this.props.failed) {
+            content = <ErrorAlert
+                title={'Cannot load Log'}
+                description={'Something went wrong during contacting the rest api.'}
+                action={{
+                    handleClick: this.props.loadLog,
+                    title: 'Try again'
+                }}
+            />;
+        } else {
+            content = <LogTable
+                search={this.props.filters.search}
+                locationFormat={this.props.filters.locationFormat}
+                entries={this.props.entries}
+                ascendingOrder={this.props.filters.ascendingOrder}
+                toggleOrder={this.handleToggleSortOrder}
+                showStackTrace={this.props.filters.showStackTrace}
+            />;
+        }
+
+        return (
+            <React.Fragment>
+                <PageHeader><I18n name={'logviewer'} /></PageHeader>
+                <LogFilters
+                    filters={this.props.filters}
+                    changeFilter={this.handleInputChange}
+                    loadLog={(event) => {
+                        event.preventDefault();
+                        this.props.loadLog();
+                    }}
+                />
+                {content}
+            </React.Fragment>
+        );
+    };
+}
+
+LogPage.propTypes = {
+    changeFilter: PropTypes.func.isRequired,
+    filters: PropTypes.shape({
+        threshold: PropTypes.oneOf(['error', 'warn', 'info', 'debug', 'trace']),
+        search: PropTypes.string,
+        locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
+        showStackTrace: PropTypes.oneOf(['true', 'false']),
+        maxSize: PropTypes.oneOf(['50', '100', '500', '1000', '10000']),
+        ascendingOrder: PropTypes.oneOf(['true', 'false'])
+    }).isRequired,
+    loadLog: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => state.log;
+
+const actions = {
+    changeFilter,
+    loadLog: requestLogReload
+};
+
+export default connect(mapStateToProps, actions)(LogPage);
diff --git a/borgbutler-webapp/src/components/views/logging/LogTable.jsx b/borgbutler-webapp/src/components/views/logging/LogTable.jsx
new file mode 100644
index 0000000..df0c48d
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/logging/LogTable.jsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Table} from 'reactstrap';
+import LogEntry from './LogEntry';
+import {IconSortDown, IconSortUp} from '../../general/IconComponents';
+import I18n from '../../general/translation/I18n';
+
+const getLocationString = (locationFormat, entry) => {
+    switch (locationFormat) {
+        case 'short':
+            return `${entry.javaClassSimpleName}:${entry.methodName}:${entry.lineNumber}`;
+        case 'normal':
+            return `${entry.javaClass}:${entry.methodName}:${entry.lineNumber}`;
+        default:
+            return null;
+    }
+};
+
+function LogTable({locationFormat, showStackTrace, entries, search, ascendingOrder, toggleOrder}) {
+    const lowercaseSearch = search.toLowerCase();
+    let sort = ascendingOrder === 'true' ? <IconSortUp/> : <IconSortDown/>;
+    return (
+        <Table striped bordered hover size={'sm'} responsive>
+            <thead>
+            <tr>
+                <th style={{whiteSpace: 'nowrap'}}>
+                    <I18n name={'logviewer.timestamp'}>Timestamp</I18n>{' '}
+                    <button
+                        onClick={toggleOrder}
+                        type="button"
+                        className="btn btn-outline-primary btn-sm"
+                    >
+                        {sort}
+                    </button>
+                </th>
+                <th><I18n name={'logviewer.level'}>Level</I18n></th>
+                <th><I18n name={'logviewer.message'}>Message</I18n></th>
+                {locationFormat !== 'none' ? <th><I18n name={'logviewer.location'}>Location</I18n></th> : null}
+            </tr>
+            </thead>
+            <tbody>
+            {entries
+                .filter(entry => [entry.message, (showStackTrace === 'true') ? entry.stackTrace : '', getLocationString(locationFormat, entry), entry.level, entry.logDate]
+                    .join('|#|').toLowerCase()
+                    .indexOf(lowercaseSearch) !== -1)
+                .map((entry, index) => <LogEntry
+                    entry={entry}
+                    search={lowercaseSearch}
+                    locationString={getLocationString(locationFormat, entry)}
+                    showStackTrace={showStackTrace}
+                    key={index}
+                />)}
+            </tbody>
+        </Table>
+    );
+}
+
+LogTable.propTypes = {
+    locationFormat: PropTypes.oneOf(['none', 'short', 'normal']),
+    entries: PropTypes.array,
+    ascendingOrder: PropTypes.oneOf(['true', 'false']),
+    search: PropTypes.string
+};
+
+LogTable.defaultProps = {
+    locationFormat: 'none',
+    ascendingOrder: 'false',
+    entries: [],
+    search: ''
+};
+
+export default LogTable;
diff --git a/borgbutler-webapp/src/components/views/logging/LogViewer.css b/borgbutler-webapp/src/components/views/logging/LogViewer.css
new file mode 100644
index 0000000..823ae35
--- /dev/null
+++ b/borgbutler-webapp/src/components/views/logging/LogViewer.css
@@ -0,0 +1,12 @@
+td.tt {
+    white-space: pre-wrap;
+    font-family: monospace;
+}
+td.log-warn, td.log-error {
+    color: red;
+    font-weight: bold;
+}
+mark.highlight {
+    padding: 0px;
+    background-color: yellow;
+}
diff --git a/borgbutler-webapp/src/containers/WebApp.jsx b/borgbutler-webapp/src/containers/WebApp.jsx
new file mode 100644
index 0000000..9c9da92
--- /dev/null
+++ b/borgbutler-webapp/src/containers/WebApp.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import createBrowserHistory from 'history/createBrowserHistory';
+import {Route, Router, Switch} from 'react-router';
+import {connect} from 'react-redux';
+import {Badge} from 'reactstrap';
+
+import Menu from '../components/general/Menu';
+import Start from '../components/views/Start';
+import ConfigurationPage from '../components/views/config/ConfigurationPage';
+import RestServices from '../components/views/develop/RestServices';
+import {isDevelopmentMode} from '../utilities/global';
+import LogPage from '../components/views/logging/LogPage';
+import Footer from '../components/views/footer/Footer';
+import {loadVersion} from '../actions';
+import {getTranslation} from '../utilities/i18n';
+import I18n from "../components/general/translation/I18n";
+
+class WebApp extends React.Component {
+
+    history = createBrowserHistory();
+
+    componentDidMount = () => {
+        this.props.loadVersion();
+    };
+
+    render() {
+        let routes = [
+            ['Start', '/', Start],
+            [getTranslation('logviewer'), '/logging', LogPage],
+            [getTranslation('configuration'), '/config', ConfigurationPage]
+        ];
+
+        if (this.props.version.updateVersion) {
+            routes.push([getTranslation('update'), '/update', UpdatePage, {
+                badge: <Badge color={'danger'}><I18n name={'common.new'}/></Badge>,
+                className: 'danger'
+            }]);
+        }
+
+        if (isDevelopmentMode()) {
+            routes.push(['Rest services', '/restServices', RestServices]);
+        }
+
+        return (
+            <Router history={this.history}>
+                <div>
+                    <Menu routes={routes}/>
+                    <div className={'container main-view'}>
+                        <Switch>
+                            {
+                                routes.map((route, index) => (
+                                    <Route
+                                        key={index}
+                                        path={route[1]}
+                                        component={route[2]}
+                                        exact
+                                    />
+                                ))
+                            }
+                        </Switch>
+                    </div>
+                    <Footer versionInfo={this.props.version}/>
+                </div>
+            </Router>
+        );
+    }
+}
+
+const mapStateToProps = state => ({
+    version: state.version
+});
+
+const actions = {
+    loadVersion
+};
+
+export default connect(mapStateToProps, actions)(WebApp);
diff --git a/borgbutler-webapp/src/containers/WebApp.test.js b/borgbutler-webapp/src/containers/WebApp.test.js
new file mode 100644
index 0000000..dbddfe0
--- /dev/null
+++ b/borgbutler-webapp/src/containers/WebApp.test.js
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import WebApp from './WebApp';
+
+it('renders without crashing', () => {
+    const div = document.createElement('div');
+    ReactDOM.render(<WebApp/>, div);
+    ReactDOM.unmountComponentAtNode(div);
+});
diff --git a/borgbutler-webapp/src/css/my-style.css b/borgbutler-webapp/src/css/my-style.css
new file mode 100644
index 0000000..80feb7f
--- /dev/null
+++ b/borgbutler-webapp/src/css/my-style.css
@@ -0,0 +1,406 @@
+body {
+	color: #50555f;
+}
+
+a:hover {
+	color: #47a7eb;
+}
+
+a.card-link:hover {
+	color: #50555f !important;
+}
+
+
+/* Scaffolding */
+
+.container {
+	max-width: 1260px;
+	margin-top: 100px;
+}
+
+div.main-view {
+	min-height: calc(100vh - 140px);
+}
+
+/* Typo */
+
+h2 {
+	font-weight: bold;
+	margin-bottom: 20px;
+}
+
+h3 {
+	font-weight: bold;
+}
+
+h4 {
+	font-size: 18px;
+	font-weight: bold;
+	margin-top: 20px;
+	margin-bottom: 20px;
+}
+
+h5 {
+	margin-top: 30px;
+	margin-bottom: 10px;
+}
+
+/* Navbar */
+
+.navbar.bg-light {
+	background: white !important;
+	//box-shadow: 0 0 10px rgba(0,0,0,.1);
+	border-bottom: 1px solid rgba(0,0,0,.1);
+	height: 60px;
+}
+
+.navbar-brand {
+	font-weight: bold;
+}
+
+.navbar.navbar-light a.nav-link {
+	color: #7c8495;
+	font-size: 1rem;
+	font-weight: bold;
+}
+
+.navbar a.nav-link.danger {
+	color: #dc3545;
+}
+
+.navbar.navbar-light a.nav-link.active {
+	color: #47a7eb;
+}
+
+div.pageheader {
+	font-weight: bold;
+	font-size: 1.5rem;
+	color: #414650;
+	margin-bottom: 40px !important;
+}
+
+
+/* Forms */
+
+.form-inline {
+	margin-bottom: 30px;
+}
+
+input:focus,
+select:focus,
+textarea:focus,
+button:focus {
+	outline: none !important;
+	outline-width: 0 !important;
+}
+
+[contenteditable="true"]:focus {
+	outline: none;
+}
+
+.text-truncate.editable-text-field-value:hover {
+	color: #47a7eb;
+}
+
+.input-group > textarea {
+	min-height: 40px;
+}
+
+.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {
+	background: #7C8495;
+}
+
+input[name="search"]{
+	margin-right: 5px;
+}
+
+.text-truncate.editable-text-field-value {
+	padding: 5px 10px;
+	background: #f5f6f8;
+	border-radius: 3px;
+	min-height: 34px;
+}
+
+label.custom-control-label {
+	min-height: 10px;
+}
+
+/* Buttons */
+
+.btn.btn-outline-primary {
+	color: #47a7eb;
+	background-color: #fff;
+	border-color: #309ce8;
+}
+
+.btn.btn-outline-primary:hover {
+	color: #fff;
+	background-color: #47a7eb;
+	border-color: #47a7eb;
+}
+
+.input-group-append .btn {
+	max-height: 40px;
+	height: 40px
+}
+
+.form-group label {
+    margin-bottom: 0px;
+}
+
+.btn + .btn {
+	margin-left: 10px;
+}
+
+/* Tabs */
+
+.nav.nav-tabs {
+	margin-bottom: 25px;
+}
+
+.nav-tabs .nav-link,
+.nav-tabs .nav-link:hover,
+.nav-tabs .nav-link.active {
+	position: relative;
+	top: 1px;
+	border-bottom: none;
+}
+
+ul.nav-tabs .nav-link:hover {
+	cursor: pointer;
+	color: #47a7eb;
+}
+
+.tab-pane.active {
+	padding: 20px;
+}
+
+/* Tables */
+
+.table-responsive > .table-bordered {
+	font-size: 13px;
+	margin-bottom: 50px;
+}
+
+.table .btn-outline-primary.btn-sm {
+	margin: 0 10px;
+}
+
+.table .btn.btn-link {
+	padding-left: 0;
+}
+
+.table thead th {
+	vertical-align: middle;
+}
+
+
+/* Cards */
+
+
+.template.card {
+	color: #50555F;
+	max-width: 390px;
+	min-width: 390px;
+	margin-top: 10px;
+	margin-bottom: 20px;
+}
+
+.template.card:hover {
+	text-decoration: none;
+	transform: scale(1.01);
+	transition: 0.1s ease-in-out;
+}
+
+.template.card .card-header {
+	font-weight: bold;
+	color: #50555F;
+}
+
+.template.card .card-body {
+	padding: 0;
+}
+
+.template.card .card-footer {
+}
+
+.card.border-secondary {
+	border-color: #DEE2E6 !important;
+}
+
+code {
+	padding: 10px;
+	display: inline-block;
+	border-radius: 3px;
+	margin-top: 50px;
+	background: #ef94941a;
+}
+
+code h3 {
+	font-size: 16px;
+	font-weight: bold;
+}
+
+div.refresh-button-right {
+	cursor: pointer;
+	color: #555;
+	text-align: right;
+	float: right;
+}
+
+div.refresh-button-right + * {
+	clear: both;
+}
+
+div.refresh-button-right:hover {
+	color: #337ab7;
+}
+
+/* Droparea */
+
+div.drop-area {
+    overflow: hidden;
+    border: 2px dashed #888;
+    border-radius: 5px;
+    display: table;
+    cursor: pointer;
+}
+
+div.drop-area div.background {
+    background-color: #F2F3F5;
+    width: 100%;
+    height: 100%;
+    display: table;
+}
+
+div.drop-area.onDrag div.background {
+    background-color: #eee;
+}
+
+div.drop-area div.background:hover {
+    background: #DEE2E6;
+    cursor: grab;
+}
+
+
+div.drop-area input.file {
+    border: 0;
+    clip: rect(0, 0, 0, 0);
+    height: 1px;
+    margin: -1px;
+    overflow: hidden;
+    padding: 0;
+    position: absolute;
+    width: 1px;
+}
+
+div.drop-area span.info {
+    padding: 15px 30px;
+    text-align: center;
+    font-size: 16px;
+    color: #747474;
+    display: table-cell;
+    vertical-align: middle;
+}
+
+
+#menuDropZone {
+	border: 1px dashed #888;
+	width: 34px;
+	border-radius: 5px;
+	margin-left: 10px;
+}
+
+#menuDropZone.drop-area svg.fa-upload {
+	width: 0.65em;
+}
+
+div.drop-area span.info {
+	padding: 5px;
+}
+
+.main-view div.drop-area span.info {
+	padding: 15px 25px;
+}
+
+/* MenuĀ */
+
+.collapse.show.navbar-collapse {
+	background: #FFF;
+	padding-bottom: 10px;
+	width: 100%;
+	z-index: 9000;
+}
+
+.navbar-toggler {
+	margin-top: -17px;
+}
+
+.welcome-intro {
+}
+
+.welcome-enjoy {
+	padding-top: 20px;
+	font-weight: bold;
+	font-style: italic;
+}
+
+.welcome-documentation-link {
+	padding-top: 20px;
+}
+/* @media only screen and (max-width: 1050px) {
+	.navbar.navbar-light a.nav-link {
+		font-size: 0.8rem;
+	}
+
+	.collapse.show.navbar-collapse a {
+		font-size: 18px;
+	}
+} */
+
+/* setup tooltips */
+/*
+Doesn't work for now.
+.tooltip {
+	position: relative;
+}
+.tooltip:before,
+.tooltip:after {
+	display: block;
+	opacity: 0;
+	pointer-events: none;
+	position: absolute;
+}
+.tooltip:after {
+	border-right: 6px solid transparent;
+	border-bottom: 6px solid rgba(0,0,0,.75);
+	border-left: 6px solid transparent;
+	content: '';
+	height: 0;
+	top: 20px;
+	left: 20px;
+	width: 0;
+    transform: translate3d(0,6px,0);
+    transition: all .1s ease-in-out;
+}
+.tooltip:before {
+	background: rgba(0,0,0,.75);
+	border-radius: 2px;
+	color: #fff;
+	content: attr(data-title);
+	font-size: 14px;
+	padding: 6px 10px;
+	top: 26px;
+	white-space: nowrap;
+    transform: scale3d(.2,.2,1);
+    transition: all .2s ease-in-out;
+}
+
+.tooltip:hover:after {
+    opacity: 1;
+    transform: scale3d(1,1,1);
+}
+.tooltip:hover:after {
+    transition: all .2s .1s ease-in-out;
+}
+*/
\ No newline at end of file
diff --git a/borgbutler-webapp/src/index.js b/borgbutler-webapp/src/index.js
new file mode 100644
index 0000000..0d5a741
--- /dev/null
+++ b/borgbutler-webapp/src/index.js
@@ -0,0 +1,47 @@
+import '../node_modules/bootstrap/dist/css/bootstrap.css';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {applyMiddleware, createStore} from 'redux';
+import thunk from 'redux-thunk';
+import {Provider} from 'react-redux';
+
+import WebApp from './containers/WebApp';
+
+import reducers from './reducers';
+
+import './css/my-style.css';
+import {loadDictionary} from './utilities/i18n';
+
+
+let storedState = window.localStorage.getItem('state');
+
+if (storedState) {
+    storedState = JSON.parse(storedState);
+
+    if (storedState.version) {
+        storedState.version.loading = false;
+    }
+
+    if (storedState.log) {
+        storedState.log.loading = false;
+    }
+}
+
+const store = createStore(
+    reducers,
+    storedState || undefined,
+    applyMiddleware(thunk)
+);
+
+loadDictionary(store.getState().version.version, store.getState().version.language);
+
+store.subscribe(() => {
+    window.localStorage.setItem('state', JSON.stringify(store.getState()));
+});
+
+ReactDOM.render(
+    <Provider store={store}>
+        <WebApp/>
+    </Provider>,
+    document.getElementById('root')
+);
diff --git a/borgbutler-webapp/src/reducers/index.js b/borgbutler-webapp/src/reducers/index.js
new file mode 100644
index 0000000..f7ee479
--- /dev/null
+++ b/borgbutler-webapp/src/reducers/index.js
@@ -0,0 +1,12 @@
+import {combineReducers} from 'redux';
+import log from './log';
+import template from './template';
+import version from './version';
+
+const reducers = combineReducers({
+    log,
+    template,
+    version
+});
+
+export default reducers;
\ No newline at end of file
diff --git a/borgbutler-webapp/src/reducers/log.js b/borgbutler-webapp/src/reducers/log.js
new file mode 100644
index 0000000..fa4a087
--- /dev/null
+++ b/borgbutler-webapp/src/reducers/log.js
@@ -0,0 +1,52 @@
+import {
+    LOG_VIEW_CHANGE_FILTER,
+    LOG_VIEW_FAILED_RELOAD,
+    LOG_VIEW_RELOADED,
+    LOG_VIEW_REQUEST_RELOAD
+} from '../actions/types';
+
+const initialState = {
+    filters: {
+        threshold: 'info',
+        search: '',
+        locationFormat: 'none',
+        showStackTrace: 'false',
+        maxSize: '100',
+        ascendingOrder: 'false'
+    },
+    loading: false,
+    failed: false,
+    entries: []
+};
+
+const reducer = (state = initialState, action) => {
+    switch (action.type) {
+        case LOG_VIEW_REQUEST_RELOAD:
+            return Object.assign({}, state, {
+                loading: true,
+                failed: false
+            });
+        case LOG_VIEW_RELOADED:
+            return Object.assign({}, state, {
+                loading: false,
+                entries: action.payload
+            });
+        case LOG_VIEW_CHANGE_FILTER:
+            return Object.assign({}, state, {
+                filters: {
+                    ...state.filters,
+                    [action.payload.name]: action.payload.value
+                }
+            });
+        case LOG_VIEW_FAILED_RELOAD: {
+            return Object.assign({}, state, {
+                loading: false,
+                failed: true
+            });
+        }
+        default:
+            return state;
+    }
+};
+
+export default reducer;
diff --git a/borgbutler-webapp/src/reducers/version.js b/borgbutler-webapp/src/reducers/version.js
new file mode 100644
index 0000000..bc74803
--- /dev/null
+++ b/borgbutler-webapp/src/reducers/version.js
@@ -0,0 +1,44 @@
+import {VERSION_RELOAD_FAILED, VERSION_RELOADED, VERSION_REQUEST_RELOAD} from '../actions/types';
+import {fetchNewDictionary} from '../utilities/i18n';
+
+const initialState = {
+    version: '0.0.0',
+    buildDate: 'never',
+    language: null,
+    loading: false
+};
+
+const reducer = (state = initialState, action) => {
+    switch (action.type) {
+        case VERSION_REQUEST_RELOAD:
+            return Object.assign({}, state, {
+                loading: true,
+                failed: false
+            });
+        case VERSION_RELOADED:
+
+            if (state.version !== action.payload.version ||
+                state.language !== action.payload.language) {
+                //console.log("reducers/version.js: state.version=" + state.version + ", payload.version=" + action.payload.version + ", state.lang=" + state.language + ", payload.lang=" + action.payload.language);
+                fetchNewDictionary(action.payload.version, action.payload.language);
+            }
+
+            return Object.assign({}, state, {
+                loading: false,
+                failed: false,
+                version: action.payload.version,
+                language: action.payload.language,
+                buildDate: action.payload.buildDate,
+                updateVersion: action.payload.updateVersion
+            });
+        case VERSION_RELOAD_FAILED:
+            return Object.assign({}, state, {
+                loading: false,
+                failed: true
+            });
+        default:
+            return state;
+    }
+};
+
+export default reducer;
diff --git a/borgbutler-webapp/src/utilities/global.js b/borgbutler-webapp/src/utilities/global.js
new file mode 100644
index 0000000..0c50294
--- /dev/null
+++ b/borgbutler-webapp/src/utilities/global.js
@@ -0,0 +1,49 @@
+// Later Webpack, Context etc. should be used instead.
+
+export const isDevelopmentMode = () => {
+    return process.env.NODE_ENV === 'development';
+}
+
+global.testserver = 'http://localhost:8042';
+global.restBaseUrl = (isDevelopmentMode() ? global.testserver : '') + '/rest';
+
+// Remove registered server workers from Merlin version <= V0.5 and get rid of caching hell:
+if (window.navigator && navigator.serviceWorker) {
+    navigator.serviceWorker.getRegistrations()
+        .then(function (registrations) {
+            for (let registration of registrations) {
+                console.log('Found serviceWorker registration.');
+                registration.unregister();
+            }
+        });
+}
+
+
+const createQueryParams = params =>
+    Object.keys(params)
+        .map(k => `${k}=${encodeURI(params[k])}`)
+        .join('&');
+
+export const getRestServiceUrl = (restService, params) => {
+    if (params) return `${global.restBaseUrl}/${restService}?${createQueryParams(params)}`;
+    return `${global.restBaseUrl}/${restService}`;
+}
+
+export const getResponseHeaderFilename = contentDisposition => {
+    const matches = /filename[^;=\n]*=(UTF-8(['"]*))?(.*)/.exec(contentDisposition);
+    return matches && matches.length >= 3 && matches[3] ? decodeURI(matches[3].replace(/['"]/g, '')) : 'download';
+};
+
+export const formatDateTime = (millis) => {
+    const options = {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'};
+    const date = new Date(millis);
+    return date.toLocaleDateString(options) + ' ' + date.toLocaleTimeString(options);
+    //return date.toLocaleDateString("de-DE", options);
+}
+
+export const revisedRandId = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
+
+/* Checks if a given array is definied and is not empty. */
+export const arrayNotEmpty = (array) => {
+    return array && array.length;
+}
diff --git a/borgbutler-webapp/src/utilities/i18n.js b/borgbutler-webapp/src/utilities/i18n.js
new file mode 100644
index 0000000..9628af0
--- /dev/null
+++ b/borgbutler-webapp/src/utilities/i18n.js
@@ -0,0 +1,73 @@
+import {getRestServiceUrl} from './global';
+
+let dictionary;
+let version;
+let language;
+
+const clearDictionary = () => {
+    window.localStorage.removeItem('dictionary');
+}
+
+const loadDictionary = (version, language) => {
+
+    const localDictionary = window.localStorage.getItem('dictionary');
+
+    if (localDictionary) {
+        const json = JSON.parse(localDictionary);
+
+        if (json.version === version && json.language === language) {
+            dictionary = json.dictionary;
+            return;
+        }
+        //console.log("Version=" + version + ", lang="+ language + ", json.version=" + json.version + ", json.language=" + json.language);
+    } else {
+        //console.log("Version=" + version + ", lang="+ language + ", json=undefined");
+    }
+    fetchNewDictionary(version, language);
+};
+
+const fetchNewDictionary = (currentVersion, currentLanguage) => {
+    //e.log(new Date().toISOString() + ": version=" + currentVersion + ", lang=" + currentLanguage);
+    fetch(getRestServiceUrl('i18n/list'), {
+        method: 'GET',
+        headers: {
+            'Accept': 'application/json'
+        }
+    })
+        .then(response => {
+            if (!response.ok) {
+                throw new Error(response.statusText);
+            }
+
+            return response.json();
+        })
+        .then(json => {
+            dictionary = json;
+            version = currentVersion;
+            language = currentLanguage;
+            saveDictionary();
+        });
+};
+
+const saveDictionary = () => window.localStorage.setItem('dictionary', JSON.stringify({
+    version, language, dictionary
+}));
+
+const getTranslation = (key, params) => {
+
+    if (!dictionary) {
+        return '';
+    }
+
+    let message = dictionary[key];
+
+    if (message && params) {
+        params.forEach((param, index) => {
+            message = message.replace(`{${index}}`, param);
+        });
+    }
+
+    return message;
+};
+
+export {clearDictionary, getTranslation, loadDictionary, fetchNewDictionary};
diff --git a/settings.gradle b/settings.gradle
index 296ec85..738b884 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,4 +10,11 @@
 rootProject.name = 'borgbackup-butler'
 
 include 'borgbutler-core'
+include 'borgbutler-server'
+include 'borgbutler-webapp'
 
+//project(':borgbutler-docs').projectDir = "$rootDir/docs" as File
+project(':borgbutler-webapp').projectDir = "$rootDir/borgbutler-webapp" as File
+
+//startParameter.excludedTaskNames << ':borgbutler-desktop:distTar'
+startParameter.excludedTaskNames << ':borgbutler-main:distTar'

--
Gitblit v1.10.0