From e327583aa56a5129999d6295ff8a2be1ffa6d2a1 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Sun, 28 Nov 2021 13:02:27 +0100
Subject: [PATCH] Enabled trust-dns and some updates.

- Enabled trust-dns feature which seems to help a bit when DNS is
causing long timeouts. Though in the blocking version it is less visible
then on the async branch.
- Updated crates
- Removed some redundant code
- Updated javascript/css libraries

Resolves #2118
Resolves #2119
---
 Cargo.lock                                 | 307 +++++--
 Cargo.toml                                 |  15 +-
 src/api/admin.rs                           |   6 +-
 src/main.rs                                |  10 +
 src/static/scripts/bootstrap-native.js     | 391 +++++----
 src/static/scripts/bootstrap.css           | 937 +++++++++++++++------
 src/static/scripts/datatables.css          |   4 +-
 src/static/scripts/datatables.js           | 245 ++++--
 src/static/templates/admin/diagnostics.hbs |   2 +-
 9 files changed, 1295 insertions(+), 622 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 1e5e0363..df139857 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -55,6 +55,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "async-trait"
+version = "0.1.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.82",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -230,9 +241,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
 
 [[package]]
 name = "cc"
-version = "1.0.71"
+version = "1.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
 
 [[package]]
 name = "cfg-if"
@@ -322,9 +333,9 @@ dependencies = [
 
 [[package]]
 name = "cookie_store"
-version = "0.15.0"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55b4ac5559dd39f7bdc516f769cb412b151585d8886d216871a8435ed7f862cd"
+checksum = "b3f7034c0932dc36f5bd8ec37368d971346809435824f277cb3b8299fc56167c"
 dependencies = [
  "cookie 0.15.1",
  "idna 0.2.3",
@@ -363,9 +374,9 @@ dependencies = [
 
 [[package]]
 name = "crc32fast"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -409,8 +420,9 @@ checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
 
 [[package]]
 name = "data-url"
-version = "0.1.0"
-source = "git+https://github.com/servo/rust-url?rev=eb7330b5296c0d43816d1346211b74182bb4ae37#eb7330b5296c0d43816d1346211b74182bb4ae37"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
 dependencies = [
  "matches",
 ]
@@ -441,7 +453,7 @@ dependencies = [
  "bitflags",
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -469,7 +481,7 @@ checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -527,6 +539,18 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "enum-as-inner"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
+dependencies = [
+ "heck",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.82",
+]
+
 [[package]]
 name = "error-chain"
 version = "0.11.0"
@@ -638,9 +662,9 @@ dependencies = [
 
 [[package]]
 name = "futures"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
+checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -653,9 +677,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
+checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -663,15 +687,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -680,42 +704,39 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
+checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
+checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
 dependencies = [
- "autocfg",
- "proc-macro-hack",
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
+checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
 
 [[package]]
 name = "futures-task"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
 
 [[package]]
 name = "futures-util"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
 dependencies = [
- "autocfg",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -725,8 +746,6 @@ dependencies = [
  "memchr",
  "pin-project-lite",
  "pin-utils",
- "proc-macro-hack",
- "proc-macro-nested",
  "slab",
 ]
 
@@ -810,9 +829,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
 
 [[package]]
 name = "handlebars"
-version = "4.1.3"
+version = "4.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66b09e2322d20d14bc2572401ce7c1d60b4748580a76c230ed9c1f8938f0c833"
+checksum = "8ad84da8f63da982543fc85fcabaee2ad1fdd809d99d64a48887e2e942ddfe46"
 dependencies = [
  "log 0.4.14",
  "pest",
@@ -829,6 +848,15 @@ version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
 
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.19"
@@ -880,7 +908,7 @@ dependencies = [
  "markup5ever",
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -913,9 +941,9 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
 
 [[package]]
 name = "httpdate"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
 [[package]]
 name = "hyper"
@@ -938,9 +966,9 @@ dependencies = [
 
 [[package]]
 name = "hyper"
-version = "0.14.14"
+version = "0.14.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
+checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c"
 dependencies = [
  "bytes 1.1.0",
  "futures-channel",
@@ -953,7 +981,7 @@ dependencies = [
  "httpdate",
  "itoa",
  "pin-project-lite",
- "socket2",
+ "socket2 0.4.2",
  "tokio",
  "tower-service",
  "tracing",
@@ -979,7 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
 dependencies = [
  "bytes 1.1.0",
- "hyper 0.14.14",
+ "hyper 0.14.15",
  "native-tls",
  "tokio",
  "tokio-native-tls",
@@ -1035,6 +1063,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "ipconfig"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7"
+dependencies = [
+ "socket2 0.3.19",
+ "widestring",
+ "winapi 0.3.9",
+ "winreg 0.6.2",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.3.1"
@@ -1131,9 +1171,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.106"
+version = "0.2.108"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
+checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
 
 [[package]]
 name = "libsqlite3-sys"
@@ -1146,6 +1186,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
 [[package]]
 name = "lock_api"
 version = "0.4.5"
@@ -1173,6 +1219,15 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "lru-cache"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "mac"
 version = "0.1.1"
@@ -1253,7 +1308,7 @@ dependencies = [
  "migrations_internals",
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -1473,7 +1528,7 @@ checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -1563,9 +1618,9 @@ dependencies = [
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.70"
+version = "0.9.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf"
+checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
 dependencies = [
  "autocfg",
  "cc",
@@ -1660,9 +1715,9 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
+checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5"
 
 [[package]]
 name = "pear"
@@ -1738,7 +1793,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -1880,12 +1935,6 @@ version = "0.5.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
-[[package]]
-name = "proc-macro-nested"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
-
 [[package]]
 name = "proc-macro2"
 version = "0.4.30"
@@ -1906,9 +1955,9 @@ dependencies = [
 
 [[package]]
 name = "psl-types"
-version = "2.0.7"
+version = "2.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66b398073e7cdd6f05934389a8f5961e3aabfa66675b6f440df4e2c793d51a4f"
+checksum = "4af8f675df9e68626b5059f8909ae261b8f5c3e8ab14813ad7f6cc7a134dcafb"
 
 [[package]]
 name = "publicsuffix"
@@ -1954,9 +2003,9 @@ dependencies = [
 
 [[package]]
 name = "quoted_printable"
-version = "0.4.3"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5"
+checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
 
 [[package]]
 name = "r2d2"
@@ -2134,9 +2183,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.6"
+version = "0.11.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280"
+checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
 dependencies = [
  "async-compression",
  "base64 0.13.0",
@@ -2148,7 +2197,7 @@ dependencies = [
  "futures-util",
  "http",
  "http-body",
- "hyper 0.14.14",
+ "hyper 0.14.15",
  "hyper-tls",
  "ipnet",
  "js-sys",
@@ -2158,19 +2207,30 @@ dependencies = [
  "native-tls",
  "percent-encoding 2.1.0",
  "pin-project-lite",
+ "proc-macro-hack",
  "serde",
  "serde_json",
  "serde_urlencoded",
- "time 0.2.27",
  "tokio",
  "tokio-native-tls",
  "tokio-socks",
  "tokio-util",
+ "trust-dns-resolver",
  "url 2.2.2",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
- "winreg",
+ "winreg 0.7.0",
+]
+
+[[package]]
+name = "resolv-conf"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
+dependencies = [
+ "hostname",
+ "quick-error 1.2.3",
 ]
 
 [[package]]
@@ -2301,9 +2361,9 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568"
 
 [[package]]
 name = "safemem"
@@ -2429,14 +2489,14 @@ checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.68"
+version = "1.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
+checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
 dependencies = [
  "itoa",
  "ryu",
@@ -2537,6 +2597,17 @@ version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
 
+[[package]]
+name = "socket2"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "socket2"
 version = "0.4.2"
@@ -2598,7 +2669,7 @@ dependencies = [
  "quote 1.0.10",
  "serde",
  "serde_derive",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -2614,7 +2685,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "sha1",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -2668,9 +2739,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.81"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
@@ -2731,7 +2802,7 @@ checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -2789,14 +2860,14 @@ dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
  "standback",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
 name = "tinyvec"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2809,9 +2880,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.13.0"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
+checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
 dependencies = [
  "autocfg",
  "bytes 1.1.0",
@@ -2907,7 +2978,7 @@ checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
 ]
 
 [[package]]
@@ -2925,6 +2996,51 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
 
+[[package]]
+name = "trust-dns-proto"
+version = "0.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0d7f5db438199a6e2609debe3f69f808d074e0a2888ee0bccb45fe234d03f4"
+dependencies = [
+ "async-trait",
+ "cfg-if 1.0.0",
+ "data-encoding",
+ "enum-as-inner",
+ "futures-channel",
+ "futures-io",
+ "futures-util",
+ "idna 0.2.3",
+ "ipnet",
+ "lazy_static",
+ "log 0.4.14",
+ "rand 0.8.4",
+ "smallvec 1.7.0",
+ "thiserror",
+ "tinyvec",
+ "tokio",
+ "url 2.2.2",
+]
+
+[[package]]
+name = "trust-dns-resolver"
+version = "0.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ad17b608a64bd0735e67bde16b0636f8aa8591f831a25d18443ed00a699770"
+dependencies = [
+ "cfg-if 1.0.0",
+ "futures-util",
+ "ipconfig",
+ "lazy_static",
+ "log 0.4.14",
+ "lru-cache",
+ "parking_lot 0.11.2",
+ "resolv-conf",
+ "smallvec 1.7.0",
+ "thiserror",
+ "tokio",
+ "trust-dns-proto",
+]
+
 [[package]]
 name = "try-lock"
 version = "0.2.3"
@@ -3017,6 +3133,12 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
 [[package]]
 name = "unicode-xid"
 version = "0.1.0"
@@ -3201,7 +3323,7 @@ dependencies = [
  "log 0.4.14",
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
  "wasm-bindgen-shared",
 ]
 
@@ -3235,7 +3357,7 @@ checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
 dependencies = [
  "proc-macro2 1.0.32",
  "quote 1.0.10",
- "syn 1.0.81",
+ "syn 1.0.82",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -3295,6 +3417,12 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "widestring"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
+
 [[package]]
 name = "winapi"
 version = "0.2.8"
@@ -3338,6 +3466,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "winreg"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "winreg"
 version = "0.7.0"
diff --git a/Cargo.toml b/Cargo.toml
index 87b7f871..5d4617ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,11 +34,11 @@ rocket = { version = "=0.5.0-dev", features = ["tls"], default-features = false
 rocket_contrib = "=0.5.0-dev"
 
 # HTTP client
-reqwest = { version = "0.11.6", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies"] }
+reqwest = { version = "0.11.7", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies", "trust-dns"] }
 
 # Used for custom short lived cookie jar
 cookie = "0.15.1"
-cookie_store = "0.15.0"
+cookie_store = "0.15.1"
 bytes = "1.1.0"
 url = "2.2.2"
 
@@ -56,7 +56,7 @@ chashmap = "2.2.2"
 
 # A generic serialization/deserialization framework
 serde = { version = "1.0.130", features = ["derive"] }
-serde_json = "1.0.68"
+serde_json = "1.0.72"
 
 # Logging
 log = "0.4.14"
@@ -115,13 +115,13 @@ tracing = { version = "0.1.29", features = ["log"] } # Needed to have lettre tra
 lettre = { version = "0.10.0-rc.4", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
 
 # Template library
-handlebars = { version = "4.1.3", features = ["dir_source"] }
+handlebars = { version = "4.1.5", features = ["dir_source"] }
 
 # For favicon extraction from main website
 html5ever = "0.25.1"
 markup5ever_rcdom = "0.1.0"
 regex = { version = "1.5.4", features = ["std", "perf", "unicode-perl"], default-features = false }
-data-url = "0.1.0"
+data-url = "0.1.1"
 
 # Used by U2F, JWT and Postgres
 openssl = "0.10.38"
@@ -138,16 +138,13 @@ pico-args = "0.4.2"
 backtrace = "0.3.63"
 
 # Macro ident concatenation
-paste = "1.0.5"
+paste = "1.0.6"
 
 [patch.crates-io]
 # Use newest ring
 rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
 rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = '263e39b5b429de1913ce7e3036575a7b4d88b6d7' }
 
-# For favicon extraction from main website
-data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = 'eb7330b5296c0d43816d1346211b74182bb4ae37' }
-
 # The maintainer of the `job_scheduler` crate doesn't seem to have responded
 # to any issues or PRs for almost a year (as of April 2021). This hopefully
 # temporary fork updates Cargo.toml to use more up-to-date dependencies.
diff --git a/src/api/admin.rs b/src/api/admin.rs
index 37337aee..74fd6d8a 100644
--- a/src/api/admin.rs
+++ b/src/api/admin.rs
@@ -1,7 +1,7 @@
 use once_cell::sync::Lazy;
 use serde::de::DeserializeOwned;
 use serde_json::Value;
-use std::{env, time::Duration};
+use std::env;
 
 use rocket::{
     http::{Cookie, Cookies, SameSite, Status},
@@ -462,13 +462,13 @@ struct GitCommit {
 fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
     let github_api = get_reqwest_client();
 
-    Ok(github_api.get(url).timeout(Duration::from_secs(10)).send()?.error_for_status()?.json::<T>()?)
+    Ok(github_api.get(url).send()?.error_for_status()?.json::<T>()?)
 }
 
 fn has_http_access() -> bool {
     let http_access = get_reqwest_client();
 
-    match http_access.head("https://github.com/dani-garcia/vaultwarden").timeout(Duration::from_secs(10)).send() {
+    match http_access.head("https://github.com/dani-garcia/vaultwarden").send() {
         Ok(r) => r.status().is_success(),
         _ => false,
     }
diff --git a/src/main.rs b/src/main.rs
index 3942f9b3..e23b2e4c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -108,6 +108,14 @@ fn launch_info() {
 }
 
 fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
+    // Depending on the main log level we either want to disable or enable logging for trust-dns.
+    // Else if there are timeouts it will clutter the logs since trust-dns uses warn for this.
+    let trust_dns_level = if level >= log::LevelFilter::Debug {
+        level
+    } else {
+        log::LevelFilter::Off
+    };
+
     let mut logger = fern::Dispatch::new()
         .level(level)
         // Hide unknown certificate errors if using self-signed
@@ -126,6 +134,8 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
         .level_for("hyper::client", log::LevelFilter::Off)
         // Prevent cookie_store logs
         .level_for("cookie_store", log::LevelFilter::Off)
+        // Variable level for trust-dns used by reqwest
+        .level_for("trust_dns_proto", trust_dns_level)
         .chain(std::io::stdout());
 
     // Enable smtp debug logging only specifically for smtp when need.
diff --git a/src/static/scripts/bootstrap-native.js b/src/static/scripts/bootstrap-native.js
index 2159b3db..3827dfa6 100644
--- a/src/static/scripts/bootstrap-native.js
+++ b/src/static/scripts/bootstrap-native.js
@@ -1,5 +1,5 @@
 /*!
-  * Native JavaScript for Bootstrap v4.0.6 (https://thednp.github.io/bootstrap.native/)
+  * Native JavaScript for Bootstrap v4.0.8 (https://thednp.github.io/bootstrap.native/)
   * Copyright 2015-2021 © dnp_theme
   * Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
   */
@@ -7,7 +7,7 @@
   typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
   typeof define === 'function' && define.amd ? define(factory) :
   (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BSN = factory());
-}(this, (function () { 'use strict';
+})(this, (function () { 'use strict';
 
   const transitionEndEvent = 'webkitTransition' in document.head.style ? 'webkitTransitionEnd' : 'transitionend';
 
@@ -188,7 +188,7 @@
     element.dispatchEvent(closedAlertEvent);
 
     self.dispose();
-    element.parentNode.removeChild(element);
+    element.remove();
   }
 
   // ALERT PRIVATE METHOD
@@ -1022,9 +1022,9 @@
   function isEmptyAnchor(elem) {
     const parentAnchor = elem.closest('A');
     // anchor href starts with #
-    return elem && ((elem.href && elem.href.slice(-1) === '#')
+    return elem && ((elem.hasAttribute('href') && elem.href.slice(-1) === '#')
       // OR a child of an anchor with href starts with #
-      || (parentAnchor && parentAnchor.href && parentAnchor.href.slice(-1) === '#'));
+      || (parentAnchor && parentAnchor.hasAttribute('href') && parentAnchor.href.slice(-1) === '#'));
   }
 
   function setFocus(element) {
@@ -1487,7 +1487,7 @@
 
   function appendOverlay(hasFade, isModal) {
     toggleOverlayType(isModal);
-    document.body.appendChild(overlay);
+    document.body.append(overlay);
     if (hasFade) addClass(overlay, fadeClass);
   }
 
@@ -1501,12 +1501,11 @@
   }
 
   function removeOverlay() {
-    const bd = document.body;
     const currentOpen = getCurrentOpen();
 
     if (!currentOpen) {
       removeClass(overlay, fadeClass);
-      bd.removeChild(overlay);
+      overlay.remove();
       resetScrollbar();
     }
   }
@@ -1928,7 +1927,7 @@
 
     if ((!element.contains(target) && options.backdrop
       && (!trigger || (trigger && !triggers.includes(trigger))))
-      || offCanvasDismiss.contains(target)) {
+      || (offCanvasDismiss && offCanvasDismiss.contains(target))) {
       self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null;
       self.hide();
     }
@@ -2122,19 +2121,6 @@
       .some((mediaType) => element instanceof mediaType);
   }
 
-  function closestRelative(element) {
-    let retval = null;
-    let el = element;
-    while (el !== document.body) {
-      el = el.parentElement;
-      if (getComputedStyle(el).position === 'relative') {
-        retval = el;
-        break;
-      }
-    }
-    return retval;
-  }
-
   // both popovers and tooltips (this, event)
   function styleTip(self, e) {
     const tipClasses = /\b(top|bottom|start|end)+/;
@@ -2148,32 +2134,32 @@
     let tipDimensions = { w: tip.offsetWidth, h: tip.offsetHeight };
     const windowWidth = (document.documentElement.clientWidth || document.body.clientWidth);
     const windowHeight = (document.documentElement.clientHeight || document.body.clientHeight);
-    const { element, options, arrow } = self;
+    const {
+      element, options, arrow, positions,
+    } = self;
     let { container, placement } = options;
     let parentIsBody = container === document.body;
-    const targetPosition = getComputedStyle(element).position;
-    const parentPosition = getComputedStyle(container).position;
-    const staticParent = !parentIsBody && parentPosition === 'static';
-    let relativeParent = !parentIsBody && parentPosition === 'relative';
-    const relContainer = staticParent && closestRelative(container);
+
+    const { elementPosition, containerIsStatic, relContainer } = positions;
+    let { containerIsRelative } = positions;
     // static containers should refer to another relative container or the body
     container = relContainer || container;
-    relativeParent = staticParent && relContainer ? 1 : relativeParent;
+    containerIsRelative = containerIsStatic && relContainer ? 1 : containerIsRelative;
     parentIsBody = container === document.body;
     const parentRect = container.getBoundingClientRect();
-    const leftBoundry = relativeParent ? parentRect.left : 0;
-    const rightBoundry = relativeParent ? parentRect.right : windowWidth;
+    const leftBoundry = containerIsRelative ? parentRect.left : 0;
+    const rightBoundry = containerIsRelative ? parentRect.right : windowWidth;
     // this case should not be possible
-    // absoluteParent = !parentIsBody && parentPosition === 'absolute',
-    // this case requires a container with placement: relative
-    const absoluteTarget = targetPosition === 'absolute';
+    // containerIsAbsolute = !parentIsBody && containerPosition === 'absolute',
+    // this case requires a container with position: relative
+    const absoluteTarget = elementPosition === 'absolute';
     const targetRect = element.getBoundingClientRect();
     const scroll = parentIsBody
       ? { x: window.pageXOffset, y: window.pageYOffset }
       : { x: container.scrollLeft, y: container.scrollTop };
     const elemDimensions = { w: element.offsetWidth, h: element.offsetHeight };
-    const top = relativeParent ? element.offsetTop : targetRect.top;
-    const left = relativeParent ? element.offsetLeft : targetRect.left;
+    const top = containerIsRelative ? element.offsetTop : targetRect.top;
+    const left = containerIsRelative ? element.offsetLeft : targetRect.left;
     // reset arrow style
     arrow.style.top = '';
     arrow.style.left = '';
@@ -2245,8 +2231,12 @@
       }
     } else if (['top', 'bottom'].includes(placement)) {
       if (e && isMedia(element)) {
-        const eX = !relativeParent ? e.pageX : e.layerX + (absoluteTarget ? element.offsetLeft : 0);
-        const eY = !relativeParent ? e.pageY : e.layerY + (absoluteTarget ? element.offsetTop : 0);
+        const eX = !containerIsRelative
+          ? e.pageX
+          : e.layerX + (absoluteTarget ? element.offsetLeft : 0);
+        const eY = !containerIsRelative
+          ? e.pageY
+          : e.layerY + (absoluteTarget ? element.offsetTop : 0);
 
         if (placement === 'top') {
           topPosition = eY - tipDimensions.h - (isPopover ? arrowWidth : arrowHeight);
@@ -2323,6 +2313,36 @@
     return modal || navbarFixed || document.body;
   }
 
+  function closestRelative(element) {
+    let retval = null;
+    let el = element;
+    while (el !== document.body) {
+      el = el.parentElement;
+      if (getComputedStyle(el).position === 'relative') {
+        retval = el;
+        break;
+      }
+    }
+    return retval;
+  }
+
+  function setHtml(element, content, sanitizeFn) {
+    if (typeof content === 'string' && !content.length) return;
+
+    if (typeof content === 'object') {
+      element.append(content);
+    } else {
+      let dirty = content.trim(); // fixing #233
+
+      if (typeof sanitizeFn === 'function') dirty = sanitizeFn(dirty);
+
+      const domParser = new DOMParser();
+      const tempDocument = domParser.parseFromString(dirty, 'text/html');
+      const method = tempDocument.children.length ? 'innerHTML' : 'innerText';
+      element[method] = tempDocument.body[method];
+    }
+  }
+
   /* Native JavaScript for Bootstrap 5 | Popover
   ---------------------------------------------- */
 
@@ -2335,12 +2355,13 @@
     template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>', // string
     title: null, // string
     content: null, // string
-    sanitizeFn: null, // function
     customClass: null, // string
-    dismissible: false, // boolean
-    animation: true, // boolean
     trigger: 'hover', // string
     placement: 'top', // string
+    btnClose: '<button class="btn-close" aria-label="Close"></button>', // string
+    sanitizeFn: null, // function
+    dismissible: false, // boolean
+    animation: true, // boolean
     delay: 200, // number
   };
 
@@ -2350,11 +2371,8 @@
   const isIphone = navigator.userAgentData
     ? navigator.userAgentData.brands.some((x) => appleBrands.test(x.brand))
     : appleBrands.test(navigator.userAgent);
-  // popoverArrowClass = `${popoverString}-arrow`,
   const popoverHeaderClass = `${popoverString}-header`;
   const popoverBodyClass = `${popoverString}-body`;
-  // close btn for dissmissible popover
-  let popoverCloseButton = '<button type="button" class="btn-close"></button>';
 
   // POPOVER CUSTOM EVENTS
   // =====================
@@ -2387,51 +2405,59 @@
     const {
       animation, customClass, sanitizeFn, placement, dismissible,
     } = options;
-    let { title, content, template } = options;
+    let {
+      title, content,
+    } = options;
+    const {
+      template, btnClose,
+    } = options;
 
     // set initial popover class
     const placementClass = `bs-${popoverString}-${tipClassPositions[placement]}`;
 
-    // fixing #233
-    title = title ? title.trim() : null;
-    content = content ? content.trim() : null;
-
-    // sanitize title && content
-    if (sanitizeFn) {
-      title = title ? sanitizeFn(title) : null;
-      content = content ? sanitizeFn(content) : null;
-      template = template ? sanitizeFn(template) : null;
-      popoverCloseButton = sanitizeFn(popoverCloseButton);
+    // load template
+    let popoverTemplate;
+    if (typeof template === 'object') {
+      popoverTemplate = template;
+    } else {
+      const htmlMarkup = document.createElement('div');
+      setHtml(htmlMarkup, template, sanitizeFn);
+      popoverTemplate = htmlMarkup.firstChild;
     }
+    // set popover markup
+    self.popover = popoverTemplate.cloneNode(true);
 
-    self.popover = document.createElement('div');
     const { popover } = self;
 
-    // set id and aria-describedby
+    // set id and role attributes
     popover.setAttribute('id', id);
     popover.setAttribute('role', 'tooltip');
 
-    // load template
-    const popoverTemplate = document.createElement('div');
-    popoverTemplate.innerHTML = template.trim();
-    popover.className = popoverTemplate.firstChild.className;
-    popover.innerHTML = popoverTemplate.firstChild.innerHTML;
-
     const popoverHeader = queryElement(`.${popoverHeaderClass}`, popover);
     const popoverBody = queryElement(`.${popoverBodyClass}`, popover);
 
-    // set arrow
+    // set arrow and enable access for styleTip
     self.arrow = queryElement(`.${popoverString}-arrow`, popover);
 
     // set dismissible button
     if (dismissible) {
-      title = title ? title + popoverCloseButton : title;
-      content = title === null ? +popoverCloseButton : content;
+      if (title) {
+        if (title instanceof Element) setHtml(title, btnClose, sanitizeFn);
+        else title += btnClose;
+      } else {
+        if (popoverHeader) popoverHeader.remove();
+        if (content instanceof Element) setHtml(content, btnClose, sanitizeFn);
+        else content += btnClose;
+      }
     }
 
-    // fill the template with content from data attributes
-    if (title && popoverHeader) popoverHeader.innerHTML = title.trim();
-    if (content && popoverBody) popoverBody.innerHTML = content.trim();
+    // fill the template with content from options / data attributes
+    // also sanitize title && content
+    if (title && popoverHeader) setHtml(popoverHeader, title, sanitizeFn);
+    if (content && popoverBody) setHtml(popoverBody, content, sanitizeFn);
+
+    // set btn and enable access for styleTip
+    [self.btn] = popover.getElementsByClassName('btn-close');
 
     // set popover animation and placement
     if (!hasClass(popover, popoverString)) addClass(popover, popoverString);
@@ -2443,9 +2469,9 @@
   }
 
   function removePopover(self) {
-    const { element, popover, options } = self;
+    const { element, popover } = self;
     element.removeAttribute(ariaDescribedBy);
-    options.container.removeChild(popover);
+    popover.remove();
     self.timer = null;
   }
 
@@ -2470,12 +2496,11 @@
 
   function dismissHandlerToggle(self, add) {
     const action = add ? addEventListener : removeEventListener;
-    const { options, element, popover } = self;
+    const { options, element, btn } = self;
     const { trigger, dismissible } = options;
 
     if (dismissible) {
-      const [btnClose] = popover.getElementsByClassName('btn-close');
-      if (btnClose) btnClose[action]('click', self.hide);
+      if (btn) btn[action]('click', self.hide);
     } else {
       if (trigger === 'focus') element[action]('focusout', self.hide);
       if (trigger === 'hover') document[action]('touchstart', popoverTouchHandler, passiveHandler);
@@ -2488,12 +2513,10 @@
   }
 
   function popoverShowTrigger(self) {
-    dismissHandlerToggle(self, 1);
     self.element.dispatchEvent(shownPopoverEvent);
   }
 
   function popoverHideTrigger(self) {
-    dismissHandlerToggle(self);
     removePopover(self);
     self.element.dispatchEvent(hiddenPopoverEvent);
   }
@@ -2514,6 +2537,7 @@
       self.timer = null;
       self.popover = null;
       self.arrow = null;
+      self.btn = null;
       self.enabled = false;
       // set unique ID for aria-describedby
       self.id = `${popoverString}-${getUID(element)}`;
@@ -2535,6 +2559,21 @@
       // crate popover
       createPopover(self);
 
+      // set positions
+      const { container } = self.options;
+      const elementPosition = getComputedStyle(element).position;
+      const containerPosition = getComputedStyle(container).position;
+      const parentIsBody = container === document.body;
+      const containerIsStatic = !parentIsBody && containerPosition === 'static';
+      const containerIsRelative = !parentIsBody && containerPosition === 'relative';
+      const relContainer = containerIsStatic && closestRelative(container);
+      self.positions = {
+        elementPosition,
+        containerIsRelative,
+        containerIsStatic,
+        relContainer,
+      };
+
       // bind
       self.update = self.update.bind(self);
 
@@ -2563,23 +2602,21 @@
       const { container } = options;
 
       clearTimeout(self.timer);
+      if (!isVisibleTip(popover, container)) {
+        element.dispatchEvent(showPopoverEvent);
+        if (showPopoverEvent.defaultPrevented) return;
 
-      self.timer = setTimeout(() => {
-        if (!isVisibleTip(popover, container)) {
-          element.dispatchEvent(showPopoverEvent);
-          if (showPopoverEvent.defaultPrevented) return;
+        // append to the container
+        container.append(popover);
+        element.setAttribute(ariaDescribedBy, id);
 
-          // append to the container
-          container.appendChild(popover);
-          element.setAttribute(ariaDescribedBy, id);
+        self.update(e);
+        if (!hasClass(popover, showClass)) addClass(popover, showClass);
+        dismissHandlerToggle(self, 1);
 
-          self.update(e);
-          if (!hasClass(popover, showClass)) addClass(popover, showClass);
-
-          if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
-          else popoverShowTrigger(self);
-        }
-      }, 17);
+        if (options.animation) emulateTransitionEnd(popover, () => popoverShowTrigger(self));
+        else popoverShowTrigger(self);
+      }
     }
 
     hide(e) {
@@ -2596,13 +2633,13 @@
       const { element, popover, options } = self;
 
       clearTimeout(self.timer);
-
       self.timer = setTimeout(() => {
         if (isVisibleTip(popover, options.container)) {
           element.dispatchEvent(hidePopoverEvent);
           if (hidePopoverEvent.defaultPrevented) return;
 
           removeClass(popover, showClass);
+          dismissHandlerToggle(self);
 
           if (options.animation) emulateTransitionEnd(popover, () => popoverHideTrigger(self));
           else popoverHideTrigger(self);
@@ -2648,7 +2685,7 @@
       const { popover, options } = self;
       const { container, animation } = options;
       if (animation && isVisibleTip(popover, container)) {
-        options.delay = 0; // reset delay
+        self.options.delay = 0; // reset delay
         self.hide();
         emulateTransitionEnd(popover, () => togglePopoverHandlers(self));
       } else {
@@ -3067,7 +3104,7 @@
   const toastSelector = `.${toastString}`;
   const toastDismissSelector = `[${dataBsDismiss}="${toastString}"]`;
   const showingClass = 'showing';
-  const hideClass = 'hide';
+  const hideClass = 'hide'; // marked as deprecated
   const toastDefaultOptions = {
     animation: true,
     autohide: true,
@@ -3085,10 +3122,7 @@
   // =====================
   function showToastComplete(self) {
     const { element, options } = self;
-    if (!options.animation) {
-      removeClass(element, showingClass);
-      addClass(element, showClass);
-    }
+    removeClass(element, showingClass);
 
     element.dispatchEvent(shownToastEvent);
     if (options.autohide) self.hide();
@@ -3096,13 +3130,15 @@
 
   function hideToastComplete(self) {
     const { element } = self;
-    addClass(element, hideClass);
+    removeClass(element, showingClass);
+    removeClass(element, showClass);
+    addClass(element, hideClass); // B/C
     element.dispatchEvent(hiddenToastEvent);
   }
 
-  function closeToast(self) {
+  function hideToast(self) {
     const { element, options } = self;
-    removeClass(element, showClass);
+    addClass(element, showingClass);
 
     if (options.animation) {
       reflow(element);
@@ -3112,15 +3148,14 @@
     }
   }
 
-  function openToast(self) {
+  function showToast(self) {
     const { element, options } = self;
-    removeClass(element, hideClass);
+    removeClass(element, hideClass); // B/C
+    reflow(element);
+    addClass(element, showClass);
+    addClass(element, showingClass);
 
     if (options.animation) {
-      reflow(element);
-      addClass(element, showingClass);
-      addClass(element, showClass);
-
       emulateTransitionEnd(element, () => showToastComplete(self));
     } else {
       showToastComplete(self);
@@ -3148,9 +3183,13 @@
       super(toastComponent, target, toastDefaultOptions, config);
       // bind
       const self = this;
+      const { element, options } = self;
 
+      // set fadeClass, the options.animation will override the markup
+      if (options.animation && !hasClass(element, fadeClass)) addClass(element, fadeClass);
+      else if (!options.animation && hasClass(element, fadeClass)) removeClass(element, fadeClass);
       // dismiss button
-      self.dismiss = queryElement(toastDismissSelector, self.element);
+      self.dismiss = queryElement(toastDismissSelector, element);
 
       // bind
       self.show = self.show.bind(self);
@@ -3165,13 +3204,12 @@
     show() {
       const self = this;
       const { element } = self;
-      if (element && hasClass(element, hideClass)) {
+      if (element && !hasClass(element, showClass)) {
         element.dispatchEvent(showToastEvent);
         if (showToastEvent.defaultPrevented) return;
 
-        addClass(element, fadeClass);
         clearTimeout(self.timer);
-        self.timer = setTimeout(() => openToast(self), 10);
+        self.timer = setTimeout(() => showToast(self), 10);
       }
     }
 
@@ -3184,7 +3222,7 @@
         if (hideToastEvent.defaultPrevented) return;
 
         clearTimeout(self.timer);
-        self.timer = setTimeout(() => closeToast(self),
+        self.timer = setTimeout(() => hideToast(self),
           noTimer ? 10 : options.delay);
       }
     }
@@ -3192,7 +3230,7 @@
     dispose() {
       const self = this;
       const { element, options } = self;
-      self.hide();
+      self.hide(1);
 
       if (options.animation) emulateTransitionEnd(element, () => completeDisposeToast(self));
       else completeDisposeToast(self);
@@ -3221,13 +3259,14 @@
   const titleAttr = 'title';
   const tooltipInnerClass = `${tooltipString}-inner`;
   const tooltipDefaultOptions = {
-    title: null,
     template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
-    placement: 'top',
-    animation: true,
-    customClass: null,
-    delay: 200,
-    sanitizeFn: null,
+    title: null, // string
+    customClass: null, // string | null
+    placement: 'top', // string
+    sanitizeFn: null, // function
+    animation: true, // bool
+    html: false, // bool
+    delay: 200, // number
   };
 
   // TOOLTIP CUSTOM EVENTS
@@ -3241,51 +3280,48 @@
   // =======================
   function createTooltip(self) {
     const { options, id } = self;
-    const placementClass = `bs-${tooltipString}-${tipClassPositions[options.placement]}`;
-    let titleString = options.title.trim();
+    const {
+      title, template, customClass, animation, placement, sanitizeFn,
+    } = options;
+    const placementClass = `bs-${tooltipString}-${tipClassPositions[placement]}`;
 
-    // sanitize stuff
-    if (options.sanitizeFn) {
-      titleString = options.sanitizeFn(titleString);
-      options.template = options.sanitizeFn(options.template);
+    if (!title) return;
+
+    // load template
+    let tooltipTemplate;
+    if (typeof template === 'object') {
+      tooltipTemplate = template;
+    } else {
+      const htmlMarkup = document.createElement('div');
+      setHtml(htmlMarkup, template, sanitizeFn);
+      tooltipTemplate = htmlMarkup.firstChild;
     }
 
-    if (!titleString) return;
-
     // create tooltip
-    self.tooltip = document.createElement('div');
+    self.tooltip = tooltipTemplate.cloneNode(true);
     const { tooltip } = self;
-
-    // set aria
+    // set title
+    setHtml(queryElement(`.${tooltipInnerClass}`, tooltip), title, sanitizeFn);
+    // set id & role attribute
     tooltip.setAttribute('id', id);
-
-    // set markup
-    const tooltipMarkup = document.createElement('div');
-    tooltipMarkup.innerHTML = options.template.trim();
-
-    tooltip.className = tooltipMarkup.firstChild.className;
-    tooltip.innerHTML = tooltipMarkup.firstChild.innerHTML;
-
-    queryElement(`.${tooltipInnerClass}`, tooltip).innerHTML = titleString;
+    tooltip.setAttribute('role', tooltipString);
 
     // set arrow
     self.arrow = queryElement(`.${tooltipString}-arrow`, tooltip);
 
-    // set class and role attribute
-    tooltip.setAttribute('role', tooltipString);
     // set classes
     if (!hasClass(tooltip, tooltipString)) addClass(tooltip, tooltipString);
-    if (options.animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
-    if (options.customClass && !hasClass(tooltip, options.customClass)) {
-      addClass(tooltip, options.customClass);
+    if (animation && !hasClass(tooltip, fadeClass)) addClass(tooltip, fadeClass);
+    if (customClass && !hasClass(tooltip, customClass)) {
+      addClass(tooltip, customClass);
     }
     if (!hasClass(tooltip, placementClass)) addClass(tooltip, placementClass);
   }
 
   function removeTooltip(self) {
-    const { element, options, tooltip } = self;
+    const { element, tooltip } = self;
     element.removeAttribute(ariaDescribedBy);
-    options.container.removeChild(tooltip);
+    tooltip.remove();
     self.timer = null;
   }
 
@@ -3387,6 +3423,21 @@
       self.id = `${tooltipString}-${getUID(element)}`;
       createTooltip(self);
 
+      // set positions
+      const { container } = self.options;
+      const elementPosition = getComputedStyle(element).position;
+      const containerPosition = getComputedStyle(container).position;
+      const parentIsBody = container === document.body;
+      const containerIsStatic = !parentIsBody && containerPosition === 'static';
+      const containerIsRelative = !parentIsBody && containerPosition === 'relative';
+      const relContainer = containerIsStatic && closestRelative(container);
+      self.positions = {
+        elementPosition,
+        containerIsRelative,
+        containerIsStatic,
+        relContainer,
+      };
+
       // attach events
       toggleTooltipHandlers(self, 1);
     }
@@ -3398,22 +3449,23 @@
       const {
         options, tooltip, element, id,
       } = self;
+      const {
+        container, animation,
+      } = options;
       clearTimeout(self.timer);
-      self.timer = setTimeout(() => {
-        if (!isVisibleTip(tooltip, options.container)) {
-          element.dispatchEvent(showTooltipEvent);
-          if (showTooltipEvent.defaultPrevented) return;
+      if (!isVisibleTip(tooltip, container)) {
+        element.dispatchEvent(showTooltipEvent);
+        if (showTooltipEvent.defaultPrevented) return;
 
-          // append to container
-          options.container.appendChild(tooltip);
-          element.setAttribute(ariaDescribedBy, id);
+        // append to container
+        container.append(tooltip);
+        element.setAttribute(ariaDescribedBy, id);
 
-          self.update(e);
-          if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
-          if (options.animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
-          else tooltipShownAction(self);
-        }
-      }, 20);
+        self.update(e);
+        if (!hasClass(tooltip, showClass)) addClass(tooltip, showClass);
+        if (animation) emulateTransitionEnd(tooltip, () => tooltipShownAction(self));
+        else tooltipShownAction(self);
+      }
     }
 
     hide(e) {
@@ -3498,20 +3550,9 @@
     constructor: Tooltip,
   };
 
-  var version = "4.0.6";
+  var version = "4.0.8";
 
-  // import { alertInit } from '../components/alert-native.js';
-  // import { buttonInit } from '../components/button-native.js';
-  // import { carouselInit } from '../components/carousel-native.js';
-  // import { collapseInit } from '../components/collapse-native.js';
-  // import { dropdownInit } from '../components/dropdown-native.js';
-  // import { modalInit } from '../components/modal-native.js';
-  // import { offcanvasInit } from '../components/offcanvas-native.js';
-  // import { popoverInit } from '../components/popover-native.js';
-  // import { scrollSpyInit } from '../components/scrollspy-native.js';
-  // import { tabInit } from '../components/tab-native.js';
-  // import { toastInit } from '../components/toast-native.js';
-  // import { tooltipInit } from '../components/tooltip-native.js';
+  const Version = version;
 
   const componentsInit = {
     Alert: Alert.init,
@@ -3547,7 +3588,7 @@
     document.addEventListener('DOMContentLoaded', () => initCallback(), { once: true });
   }
 
-  var index = {
+  const BSN = {
     Alert,
     Button,
     Carousel,
@@ -3562,9 +3603,9 @@
     Tooltip,
 
     initCallback,
-    Version: version,
+    Version,
   };
 
-  return index;
+  return BSN;
 
-})));
+}));
\ No newline at end of file
diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css
index 892302a6..f16c5be8 100644
--- a/src/static/scripts/bootstrap.css
+++ b/src/static/scripts/bootstrap.css
@@ -1,6 +1,6 @@
 @charset "UTF-8";
 /*!
- * Bootstrap v5.0.2 (https://getbootstrap.com/)
+ * Bootstrap v5.1.3 (https://getbootstrap.com/)
  * Copyright 2011-2021 The Bootstrap Authors
  * Copyright 2011-2021 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@@ -19,6 +19,15 @@
   --bs-white: #fff;
   --bs-gray: #6c757d;
   --bs-gray-dark: #343a40;
+  --bs-gray-100: #f8f9fa;
+  --bs-gray-200: #e9ecef;
+  --bs-gray-300: #dee2e6;
+  --bs-gray-400: #ced4da;
+  --bs-gray-500: #adb5bd;
+  --bs-gray-600: #6c757d;
+  --bs-gray-700: #495057;
+  --bs-gray-800: #343a40;
+  --bs-gray-900: #212529;
   --bs-primary: #0d6efd;
   --bs-secondary: #6c757d;
   --bs-success: #198754;
@@ -27,9 +36,27 @@
   --bs-danger: #dc3545;
   --bs-light: #f8f9fa;
   --bs-dark: #212529;
+  --bs-primary-rgb: 13, 110, 253;
+  --bs-secondary-rgb: 108, 117, 125;
+  --bs-success-rgb: 25, 135, 84;
+  --bs-info-rgb: 13, 202, 240;
+  --bs-warning-rgb: 255, 193, 7;
+  --bs-danger-rgb: 220, 53, 69;
+  --bs-light-rgb: 248, 249, 250;
+  --bs-dark-rgb: 33, 37, 41;
+  --bs-white-rgb: 255, 255, 255;
+  --bs-black-rgb: 0, 0, 0;
+  --bs-body-color-rgb: 33, 37, 41;
+  --bs-body-bg-rgb: 255, 255, 255;
   --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
   --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
   --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
+  --bs-body-font-family: var(--bs-font-sans-serif);
+  --bs-body-font-size: 1rem;
+  --bs-body-font-weight: 400;
+  --bs-body-line-height: 1.5;
+  --bs-body-color: #212529;
+  --bs-body-bg: #fff;
 }
 
 *,
@@ -46,12 +73,13 @@
 
 body {
   margin: 0;
-  font-family: var(--bs-font-sans-serif);
-  font-size: 1rem;
-  font-weight: 400;
-  line-height: 1.5;
-  color: #212529;
-  background-color: #fff;
+  font-family: var(--bs-body-font-family);
+  font-size: var(--bs-body-font-size);
+  font-weight: var(--bs-body-font-weight);
+  line-height: var(--bs-body-line-height);
+  color: var(--bs-body-color);
+  text-align: var(--bs-body-text-align);
+  background-color: var(--bs-body-bg);
   -webkit-text-size-adjust: 100%;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 }
@@ -420,6 +448,10 @@ legend + * {
   padding: 0;
 }
 
+::-webkit-file-upload-button {
+  font: inherit;
+}
+
 ::file-selector-button {
   font: inherit;
 }
@@ -633,16 +665,16 @@ progress {
   --bs-gutter-y: 0;
   display: flex;
   flex-wrap: wrap;
-  margin-top: calc(var(--bs-gutter-y) * -1);
-  margin-right: calc(var(--bs-gutter-x) * -.5);
-  margin-left: calc(var(--bs-gutter-x) * -.5);
+  margin-top: calc(-1 * var(--bs-gutter-y));
+  margin-right: calc(-0.5 * var(--bs-gutter-x));
+  margin-left: calc(-0.5 * var(--bs-gutter-x));
 }
 .row > * {
   flex-shrink: 0;
   width: 100%;
   max-width: 100%;
-  padding-right: calc(var(--bs-gutter-x) * .5);
-  padding-left: calc(var(--bs-gutter-x) * .5);
+  padding-right: calc(var(--bs-gutter-x) * 0.5);
+  padding-left: calc(var(--bs-gutter-x) * 0.5);
   margin-top: var(--bs-gutter-y);
 }
 
@@ -685,206 +717,6 @@ progress {
   width: 16.6666666667%;
 }
 
-@media (min-width: 576px) {
-  .col-sm {
-    flex: 1 0 0%;
-  }
-
-  .row-cols-sm-auto > * {
-    flex: 0 0 auto;
-    width: auto;
-  }
-
-  .row-cols-sm-1 > * {
-    flex: 0 0 auto;
-    width: 100%;
-  }
-
-  .row-cols-sm-2 > * {
-    flex: 0 0 auto;
-    width: 50%;
-  }
-
-  .row-cols-sm-3 > * {
-    flex: 0 0 auto;
-    width: 33.3333333333%;
-  }
-
-  .row-cols-sm-4 > * {
-    flex: 0 0 auto;
-    width: 25%;
-  }
-
-  .row-cols-sm-5 > * {
-    flex: 0 0 auto;
-    width: 20%;
-  }
-
-  .row-cols-sm-6 > * {
-    flex: 0 0 auto;
-    width: 16.6666666667%;
-  }
-}
-@media (min-width: 768px) {
-  .col-md {
-    flex: 1 0 0%;
-  }
-
-  .row-cols-md-auto > * {
-    flex: 0 0 auto;
-    width: auto;
-  }
-
-  .row-cols-md-1 > * {
-    flex: 0 0 auto;
-    width: 100%;
-  }
-
-  .row-cols-md-2 > * {
-    flex: 0 0 auto;
-    width: 50%;
-  }
-
-  .row-cols-md-3 > * {
-    flex: 0 0 auto;
-    width: 33.3333333333%;
-  }
-
-  .row-cols-md-4 > * {
-    flex: 0 0 auto;
-    width: 25%;
-  }
-
-  .row-cols-md-5 > * {
-    flex: 0 0 auto;
-    width: 20%;
-  }
-
-  .row-cols-md-6 > * {
-    flex: 0 0 auto;
-    width: 16.6666666667%;
-  }
-}
-@media (min-width: 992px) {
-  .col-lg {
-    flex: 1 0 0%;
-  }
-
-  .row-cols-lg-auto > * {
-    flex: 0 0 auto;
-    width: auto;
-  }
-
-  .row-cols-lg-1 > * {
-    flex: 0 0 auto;
-    width: 100%;
-  }
-
-  .row-cols-lg-2 > * {
-    flex: 0 0 auto;
-    width: 50%;
-  }
-
-  .row-cols-lg-3 > * {
-    flex: 0 0 auto;
-    width: 33.3333333333%;
-  }
-
-  .row-cols-lg-4 > * {
-    flex: 0 0 auto;
-    width: 25%;
-  }
-
-  .row-cols-lg-5 > * {
-    flex: 0 0 auto;
-    width: 20%;
-  }
-
-  .row-cols-lg-6 > * {
-    flex: 0 0 auto;
-    width: 16.6666666667%;
-  }
-}
-@media (min-width: 1200px) {
-  .col-xl {
-    flex: 1 0 0%;
-  }
-
-  .row-cols-xl-auto > * {
-    flex: 0 0 auto;
-    width: auto;
-  }
-
-  .row-cols-xl-1 > * {
-    flex: 0 0 auto;
-    width: 100%;
-  }
-
-  .row-cols-xl-2 > * {
-    flex: 0 0 auto;
-    width: 50%;
-  }
-
-  .row-cols-xl-3 > * {
-    flex: 0 0 auto;
-    width: 33.3333333333%;
-  }
-
-  .row-cols-xl-4 > * {
-    flex: 0 0 auto;
-    width: 25%;
-  }
-
-  .row-cols-xl-5 > * {
-    flex: 0 0 auto;
-    width: 20%;
-  }
-
-  .row-cols-xl-6 > * {
-    flex: 0 0 auto;
-    width: 16.6666666667%;
-  }
-}
-@media (min-width: 1400px) {
-  .col-xxl {
-    flex: 1 0 0%;
-  }
-
-  .row-cols-xxl-auto > * {
-    flex: 0 0 auto;
-    width: auto;
-  }
-
-  .row-cols-xxl-1 > * {
-    flex: 0 0 auto;
-    width: 100%;
-  }
-
-  .row-cols-xxl-2 > * {
-    flex: 0 0 auto;
-    width: 50%;
-  }
-
-  .row-cols-xxl-3 > * {
-    flex: 0 0 auto;
-    width: 33.3333333333%;
-  }
-
-  .row-cols-xxl-4 > * {
-    flex: 0 0 auto;
-    width: 25%;
-  }
-
-  .row-cols-xxl-5 > * {
-    flex: 0 0 auto;
-    width: 20%;
-  }
-
-  .row-cols-xxl-6 > * {
-    flex: 0 0 auto;
-    width: 16.6666666667%;
-  }
-}
 .col-auto {
   flex: 0 0 auto;
   width: auto;
@@ -1055,6 +887,45 @@ progress {
 }
 
 @media (min-width: 576px) {
+  .col-sm {
+    flex: 1 0 0%;
+  }
+
+  .row-cols-sm-auto > * {
+    flex: 0 0 auto;
+    width: auto;
+  }
+
+  .row-cols-sm-1 > * {
+    flex: 0 0 auto;
+    width: 100%;
+  }
+
+  .row-cols-sm-2 > * {
+    flex: 0 0 auto;
+    width: 50%;
+  }
+
+  .row-cols-sm-3 > * {
+    flex: 0 0 auto;
+    width: 33.3333333333%;
+  }
+
+  .row-cols-sm-4 > * {
+    flex: 0 0 auto;
+    width: 25%;
+  }
+
+  .row-cols-sm-5 > * {
+    flex: 0 0 auto;
+    width: 20%;
+  }
+
+  .row-cols-sm-6 > * {
+    flex: 0 0 auto;
+    width: 16.6666666667%;
+  }
+
   .col-sm-auto {
     flex: 0 0 auto;
     width: auto;
@@ -1229,6 +1100,45 @@ progress {
   }
 }
 @media (min-width: 768px) {
+  .col-md {
+    flex: 1 0 0%;
+  }
+
+  .row-cols-md-auto > * {
+    flex: 0 0 auto;
+    width: auto;
+  }
+
+  .row-cols-md-1 > * {
+    flex: 0 0 auto;
+    width: 100%;
+  }
+
+  .row-cols-md-2 > * {
+    flex: 0 0 auto;
+    width: 50%;
+  }
+
+  .row-cols-md-3 > * {
+    flex: 0 0 auto;
+    width: 33.3333333333%;
+  }
+
+  .row-cols-md-4 > * {
+    flex: 0 0 auto;
+    width: 25%;
+  }
+
+  .row-cols-md-5 > * {
+    flex: 0 0 auto;
+    width: 20%;
+  }
+
+  .row-cols-md-6 > * {
+    flex: 0 0 auto;
+    width: 16.6666666667%;
+  }
+
   .col-md-auto {
     flex: 0 0 auto;
     width: auto;
@@ -1403,6 +1313,45 @@ progress {
   }
 }
 @media (min-width: 992px) {
+  .col-lg {
+    flex: 1 0 0%;
+  }
+
+  .row-cols-lg-auto > * {
+    flex: 0 0 auto;
+    width: auto;
+  }
+
+  .row-cols-lg-1 > * {
+    flex: 0 0 auto;
+    width: 100%;
+  }
+
+  .row-cols-lg-2 > * {
+    flex: 0 0 auto;
+    width: 50%;
+  }
+
+  .row-cols-lg-3 > * {
+    flex: 0 0 auto;
+    width: 33.3333333333%;
+  }
+
+  .row-cols-lg-4 > * {
+    flex: 0 0 auto;
+    width: 25%;
+  }
+
+  .row-cols-lg-5 > * {
+    flex: 0 0 auto;
+    width: 20%;
+  }
+
+  .row-cols-lg-6 > * {
+    flex: 0 0 auto;
+    width: 16.6666666667%;
+  }
+
   .col-lg-auto {
     flex: 0 0 auto;
     width: auto;
@@ -1577,6 +1526,45 @@ progress {
   }
 }
 @media (min-width: 1200px) {
+  .col-xl {
+    flex: 1 0 0%;
+  }
+
+  .row-cols-xl-auto > * {
+    flex: 0 0 auto;
+    width: auto;
+  }
+
+  .row-cols-xl-1 > * {
+    flex: 0 0 auto;
+    width: 100%;
+  }
+
+  .row-cols-xl-2 > * {
+    flex: 0 0 auto;
+    width: 50%;
+  }
+
+  .row-cols-xl-3 > * {
+    flex: 0 0 auto;
+    width: 33.3333333333%;
+  }
+
+  .row-cols-xl-4 > * {
+    flex: 0 0 auto;
+    width: 25%;
+  }
+
+  .row-cols-xl-5 > * {
+    flex: 0 0 auto;
+    width: 20%;
+  }
+
+  .row-cols-xl-6 > * {
+    flex: 0 0 auto;
+    width: 16.6666666667%;
+  }
+
   .col-xl-auto {
     flex: 0 0 auto;
     width: auto;
@@ -1751,6 +1739,45 @@ progress {
   }
 }
 @media (min-width: 1400px) {
+  .col-xxl {
+    flex: 1 0 0%;
+  }
+
+  .row-cols-xxl-auto > * {
+    flex: 0 0 auto;
+    width: auto;
+  }
+
+  .row-cols-xxl-1 > * {
+    flex: 0 0 auto;
+    width: 100%;
+  }
+
+  .row-cols-xxl-2 > * {
+    flex: 0 0 auto;
+    width: 50%;
+  }
+
+  .row-cols-xxl-3 > * {
+    flex: 0 0 auto;
+    width: 33.3333333333%;
+  }
+
+  .row-cols-xxl-4 > * {
+    flex: 0 0 auto;
+    width: 25%;
+  }
+
+  .row-cols-xxl-5 > * {
+    flex: 0 0 auto;
+    width: 20%;
+  }
+
+  .row-cols-xxl-6 > * {
+    flex: 0 0 auto;
+    width: 16.6666666667%;
+  }
+
   .col-xxl-auto {
     flex: 0 0 auto;
     width: auto;
@@ -1951,8 +1978,8 @@ progress {
 .table > thead {
   vertical-align: bottom;
 }
-.table > :not(:last-child) > :last-child > * {
-  border-bottom-color: currentColor;
+.table > :not(:first-child) {
+  border-top: 2px solid currentColor;
 }
 
 .caption-top {
@@ -1973,8 +2000,11 @@ progress {
 .table-borderless > :not(caption) > * > * {
   border-bottom-width: 0;
 }
+.table-borderless > :not(:first-child) {
+  border-top-width: 0;
+}
 
-.table-striped > tbody > tr:nth-of-type(odd) {
+.table-striped > tbody > tr:nth-of-type(odd) > * {
   --bs-table-accent-bg: var(--bs-table-striped-bg);
   color: var(--bs-table-striped-color);
 }
@@ -1984,7 +2014,7 @@ progress {
   color: var(--bs-table-active-color);
 }
 
-.table-hover > tbody > tr:hover {
+.table-hover > tbody > tr:hover > * {
   --bs-table-accent-bg: var(--bs-table-hover-bg);
   color: var(--bs-table-hover-color);
 }
@@ -2200,6 +2230,22 @@ progress {
   background-color: #e9ecef;
   opacity: 1;
 }
+.form-control::-webkit-file-upload-button {
+  padding: 0.375rem 0.75rem;
+  margin: -0.375rem -0.75rem;
+  -webkit-margin-end: 0.75rem;
+  margin-inline-end: 0.75rem;
+  color: #212529;
+  background-color: #e9ecef;
+  pointer-events: none;
+  border-color: inherit;
+  border-style: solid;
+  border-width: 0;
+  border-inline-end-width: 1px;
+  border-radius: 0;
+  -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
 .form-control::file-selector-button {
   padding: 0.375rem 0.75rem;
   margin: -0.375rem -0.75rem;
@@ -2216,10 +2262,17 @@ progress {
   transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
 }
 @media (prefers-reduced-motion: reduce) {
+  .form-control::-webkit-file-upload-button {
+    -webkit-transition: none;
+    transition: none;
+  }
   .form-control::file-selector-button {
     transition: none;
   }
 }
+.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {
+  background-color: #dde0e3;
+}
 .form-control:hover:not(:disabled):not([readonly])::file-selector-button {
   background-color: #dde0e3;
 }
@@ -2266,11 +2319,17 @@ progress {
 }
 
 .form-control-sm {
-  min-height: calc(1.5em + (0.5rem + 2px));
+  min-height: calc(1.5em + 0.5rem + 2px);
   padding: 0.25rem 0.5rem;
   font-size: 0.875rem;
   border-radius: 0.2rem;
 }
+.form-control-sm::-webkit-file-upload-button {
+  padding: 0.25rem 0.5rem;
+  margin: -0.25rem -0.5rem;
+  -webkit-margin-end: 0.5rem;
+  margin-inline-end: 0.5rem;
+}
 .form-control-sm::file-selector-button {
   padding: 0.25rem 0.5rem;
   margin: -0.25rem -0.5rem;
@@ -2285,11 +2344,17 @@ progress {
 }
 
 .form-control-lg {
-  min-height: calc(1.5em + (1rem + 2px));
+  min-height: calc(1.5em + 1rem + 2px);
   padding: 0.5rem 1rem;
   font-size: 1.25rem;
   border-radius: 0.3rem;
 }
+.form-control-lg::-webkit-file-upload-button {
+  padding: 0.5rem 1rem;
+  margin: -0.5rem -1rem;
+  -webkit-margin-end: 1rem;
+  margin-inline-end: 1rem;
+}
 .form-control-lg::file-selector-button {
   padding: 0.5rem 1rem;
   margin: -0.5rem -1rem;
@@ -2304,17 +2369,17 @@ progress {
 }
 
 textarea.form-control {
-  min-height: calc(1.5em + (0.75rem + 2px));
+  min-height: calc(1.5em + 0.75rem + 2px);
 }
 textarea.form-control-sm {
-  min-height: calc(1.5em + (0.5rem + 2px));
+  min-height: calc(1.5em + 0.5rem + 2px);
 }
 textarea.form-control-lg {
-  min-height: calc(1.5em + (1rem + 2px));
+  min-height: calc(1.5em + 1rem + 2px);
 }
 
 .form-control-color {
-  max-width: 3rem;
+  width: 3rem;
   height: auto;
   padding: 0.375rem;
 }
@@ -2378,6 +2443,7 @@ textarea.form-control-lg {
   padding-bottom: 0.25rem;
   padding-left: 0.5rem;
   font-size: 0.875rem;
+  border-radius: 0.2rem;
 }
 
 .form-select-lg {
@@ -2385,6 +2451,7 @@ textarea.form-control-lg {
   padding-bottom: 0.5rem;
   padding-left: 1rem;
   font-size: 1.25rem;
+  border-radius: 0.3rem;
 }
 
 .form-check {
@@ -3430,6 +3497,16 @@ textarea.form-control-lg {
     transition: none;
   }
 }
+.collapsing.collapse-horizontal {
+  width: 0;
+  height: auto;
+  transition: width 0.35s ease;
+}
+@media (prefers-reduced-motion: reduce) {
+  .collapsing.collapse-horizontal {
+    transition: none;
+  }
+}
 
 .dropup,
 .dropend,
@@ -4047,6 +4124,33 @@ textarea.form-control-lg {
   .navbar-expand-sm .navbar-toggler {
     display: none;
   }
+  .navbar-expand-sm .offcanvas-header {
+    display: none;
+  }
+  .navbar-expand-sm .offcanvas {
+    position: inherit;
+    bottom: 0;
+    z-index: 1000;
+    flex-grow: 1;
+    visibility: visible !important;
+    background-color: transparent;
+    border-right: 0;
+    border-left: 0;
+    transition: none;
+    transform: none;
+  }
+  .navbar-expand-sm .offcanvas-top,
+.navbar-expand-sm .offcanvas-bottom {
+    height: auto;
+    border-top: 0;
+    border-bottom: 0;
+  }
+  .navbar-expand-sm .offcanvas-body {
+    display: flex;
+    flex-grow: 0;
+    padding: 0;
+    overflow-y: visible;
+  }
 }
 @media (min-width: 768px) {
   .navbar-expand-md {
@@ -4073,6 +4177,33 @@ textarea.form-control-lg {
   .navbar-expand-md .navbar-toggler {
     display: none;
   }
+  .navbar-expand-md .offcanvas-header {
+    display: none;
+  }
+  .navbar-expand-md .offcanvas {
+    position: inherit;
+    bottom: 0;
+    z-index: 1000;
+    flex-grow: 1;
+    visibility: visible !important;
+    background-color: transparent;
+    border-right: 0;
+    border-left: 0;
+    transition: none;
+    transform: none;
+  }
+  .navbar-expand-md .offcanvas-top,
+.navbar-expand-md .offcanvas-bottom {
+    height: auto;
+    border-top: 0;
+    border-bottom: 0;
+  }
+  .navbar-expand-md .offcanvas-body {
+    display: flex;
+    flex-grow: 0;
+    padding: 0;
+    overflow-y: visible;
+  }
 }
 @media (min-width: 992px) {
   .navbar-expand-lg {
@@ -4099,6 +4230,33 @@ textarea.form-control-lg {
   .navbar-expand-lg .navbar-toggler {
     display: none;
   }
+  .navbar-expand-lg .offcanvas-header {
+    display: none;
+  }
+  .navbar-expand-lg .offcanvas {
+    position: inherit;
+    bottom: 0;
+    z-index: 1000;
+    flex-grow: 1;
+    visibility: visible !important;
+    background-color: transparent;
+    border-right: 0;
+    border-left: 0;
+    transition: none;
+    transform: none;
+  }
+  .navbar-expand-lg .offcanvas-top,
+.navbar-expand-lg .offcanvas-bottom {
+    height: auto;
+    border-top: 0;
+    border-bottom: 0;
+  }
+  .navbar-expand-lg .offcanvas-body {
+    display: flex;
+    flex-grow: 0;
+    padding: 0;
+    overflow-y: visible;
+  }
 }
 @media (min-width: 1200px) {
   .navbar-expand-xl {
@@ -4125,6 +4283,33 @@ textarea.form-control-lg {
   .navbar-expand-xl .navbar-toggler {
     display: none;
   }
+  .navbar-expand-xl .offcanvas-header {
+    display: none;
+  }
+  .navbar-expand-xl .offcanvas {
+    position: inherit;
+    bottom: 0;
+    z-index: 1000;
+    flex-grow: 1;
+    visibility: visible !important;
+    background-color: transparent;
+    border-right: 0;
+    border-left: 0;
+    transition: none;
+    transform: none;
+  }
+  .navbar-expand-xl .offcanvas-top,
+.navbar-expand-xl .offcanvas-bottom {
+    height: auto;
+    border-top: 0;
+    border-bottom: 0;
+  }
+  .navbar-expand-xl .offcanvas-body {
+    display: flex;
+    flex-grow: 0;
+    padding: 0;
+    overflow-y: visible;
+  }
 }
 @media (min-width: 1400px) {
   .navbar-expand-xxl {
@@ -4151,6 +4336,33 @@ textarea.form-control-lg {
   .navbar-expand-xxl .navbar-toggler {
     display: none;
   }
+  .navbar-expand-xxl .offcanvas-header {
+    display: none;
+  }
+  .navbar-expand-xxl .offcanvas {
+    position: inherit;
+    bottom: 0;
+    z-index: 1000;
+    flex-grow: 1;
+    visibility: visible !important;
+    background-color: transparent;
+    border-right: 0;
+    border-left: 0;
+    transition: none;
+    transform: none;
+  }
+  .navbar-expand-xxl .offcanvas-top,
+.navbar-expand-xxl .offcanvas-bottom {
+    height: auto;
+    border-top: 0;
+    border-bottom: 0;
+  }
+  .navbar-expand-xxl .offcanvas-body {
+    display: flex;
+    flex-grow: 0;
+    padding: 0;
+    overflow-y: visible;
+  }
 }
 .navbar-expand {
   flex-wrap: nowrap;
@@ -4176,6 +4388,33 @@ textarea.form-control-lg {
 .navbar-expand .navbar-toggler {
   display: none;
 }
+.navbar-expand .offcanvas-header {
+  display: none;
+}
+.navbar-expand .offcanvas {
+  position: inherit;
+  bottom: 0;
+  z-index: 1000;
+  flex-grow: 1;
+  visibility: visible !important;
+  background-color: transparent;
+  border-right: 0;
+  border-left: 0;
+  transition: none;
+  transform: none;
+}
+.navbar-expand .offcanvas-top,
+.navbar-expand .offcanvas-bottom {
+  height: auto;
+  border-top: 0;
+  border-bottom: 0;
+}
+.navbar-expand .offcanvas-body {
+  display: flex;
+  flex-grow: 0;
+  padding: 0;
+  overflow-y: visible;
+}
 
 .navbar-light .navbar-brand {
   color: rgba(0, 0, 0, 0.9);
@@ -4299,9 +4538,6 @@ textarea.form-control-lg {
   margin-bottom: 0;
 }
 
-.card-link:hover {
-  text-decoration: none;
-}
 .card-link + .card-link {
   margin-left: 1rem;
 }
@@ -5177,10 +5413,10 @@ textarea.form-control-lg {
   box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
   border-radius: 0.25rem;
 }
-.toast:not(.showing):not(.show) {
+.toast.showing {
   opacity: 0;
 }
-.toast.hide {
+.toast:not(.show) {
   display: none;
 }
 
@@ -5220,7 +5456,7 @@ textarea.form-control-lg {
   position: fixed;
   top: 0;
   left: 0;
-  z-index: 1060;
+  z-index: 1055;
   display: none;
   width: 100%;
   height: 100%;
@@ -5285,7 +5521,7 @@ textarea.form-control-lg {
   position: fixed;
   top: 0;
   left: 0;
-  z-index: 1040;
+  z-index: 1050;
   width: 100vw;
   height: 100vh;
   background-color: #000;
@@ -6010,7 +6246,7 @@ textarea.form-control-lg {
 .offcanvas {
   position: fixed;
   bottom: 0;
-  z-index: 1050;
+  z-index: 1045;
   display: flex;
   flex-direction: column;
   max-width: 100%;
@@ -6026,6 +6262,22 @@ textarea.form-control-lg {
   }
 }
 
+.offcanvas-backdrop {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 1040;
+  width: 100vw;
+  height: 100vh;
+  background-color: #000;
+}
+.offcanvas-backdrop.fade {
+  opacity: 0;
+}
+.offcanvas-backdrop.show {
+  opacity: 0.5;
+}
+
 .offcanvas-header {
   display: flex;
   align-items: center;
@@ -6089,6 +6341,69 @@ textarea.form-control-lg {
   transform: none;
 }
 
+.placeholder {
+  display: inline-block;
+  min-height: 1em;
+  vertical-align: middle;
+  cursor: wait;
+  background-color: currentColor;
+  opacity: 0.5;
+}
+.placeholder.btn::before {
+  display: inline-block;
+  content: "";
+}
+
+.placeholder-xs {
+  min-height: 0.6em;
+}
+
+.placeholder-sm {
+  min-height: 0.8em;
+}
+
+.placeholder-lg {
+  min-height: 1.2em;
+}
+
+.placeholder-glow .placeholder {
+  -webkit-animation: placeholder-glow 2s ease-in-out infinite;
+  animation: placeholder-glow 2s ease-in-out infinite;
+}
+
+@-webkit-keyframes placeholder-glow {
+  50% {
+    opacity: 0.2;
+  }
+}
+
+@keyframes placeholder-glow {
+  50% {
+    opacity: 0.2;
+  }
+}
+.placeholder-wave {
+  -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);
+  mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);
+  -webkit-mask-size: 200% 100%;
+  mask-size: 200% 100%;
+  -webkit-animation: placeholder-wave 2s linear infinite;
+  animation: placeholder-wave 2s linear infinite;
+}
+
+@-webkit-keyframes placeholder-wave {
+  100% {
+    -webkit-mask-position: -200% 0%;
+    mask-position: -200% 0%;
+  }
+}
+
+@keyframes placeholder-wave {
+  100% {
+    -webkit-mask-position: -200% 0%;
+    mask-position: -200% 0%;
+  }
+}
 .clearfix::after {
   display: block;
   clear: both;
@@ -6173,15 +6488,15 @@ textarea.form-control-lg {
 }
 
 .ratio-4x3 {
-  --bs-aspect-ratio: calc(3 / 4 * 100%);
+  --bs-aspect-ratio: 75%;
 }
 
 .ratio-16x9 {
-  --bs-aspect-ratio: calc(9 / 16 * 100%);
+  --bs-aspect-ratio: 56.25%;
 }
 
 .ratio-21x9 {
-  --bs-aspect-ratio: calc(9 / 21 * 100%);
+  --bs-aspect-ratio: 42.8571428571%;
 }
 
 .fixed-top {
@@ -6247,6 +6562,20 @@ textarea.form-control-lg {
     z-index: 1020;
   }
 }
+.hstack {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  align-self: stretch;
+}
+
+.vstack {
+  display: flex;
+  flex: 1 1 auto;
+  flex-direction: column;
+  align-self: stretch;
+}
+
 .visually-hidden,
 .visually-hidden-focusable:not(:focus):not(:focus-within) {
   position: absolute !important;
@@ -6276,6 +6605,15 @@ textarea.form-control-lg {
   white-space: nowrap;
 }
 
+.vr {
+  display: inline-block;
+  align-self: stretch;
+  width: 1px;
+  min-height: 1em;
+  background-color: currentColor;
+  opacity: 0.25;
+}
+
 .align-baseline {
   vertical-align: baseline !important;
 }
@@ -6312,6 +6650,26 @@ textarea.form-control-lg {
   float: none !important;
 }
 
+.opacity-0 {
+  opacity: 0 !important;
+}
+
+.opacity-25 {
+  opacity: 0.25 !important;
+}
+
+.opacity-50 {
+  opacity: 0.5 !important;
+}
+
+.opacity-75 {
+  opacity: 0.75 !important;
+}
+
+.opacity-100 {
+  opacity: 1 !important;
+}
+
 .overflow-auto {
   overflow: auto !important;
 }
@@ -7335,105 +7693,176 @@ textarea.form-control-lg {
 
 /* rtl:end:remove */
 .text-primary {
-  color: #0d6efd !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-secondary {
-  color: #6c757d !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-success {
-  color: #198754 !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-info {
-  color: #0dcaf0 !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-warning {
-  color: #ffc107 !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-danger {
-  color: #dc3545 !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-light {
-  color: #f8f9fa !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-dark {
-  color: #212529 !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-black {
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-white {
-  color: #fff !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-body {
-  color: #212529 !important;
+  --bs-text-opacity: 1;
+  color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;
 }
 
 .text-muted {
+  --bs-text-opacity: 1;
   color: #6c757d !important;
 }
 
 .text-black-50 {
+  --bs-text-opacity: 1;
   color: rgba(0, 0, 0, 0.5) !important;
 }
 
 .text-white-50 {
+  --bs-text-opacity: 1;
   color: rgba(255, 255, 255, 0.5) !important;
 }
 
 .text-reset {
+  --bs-text-opacity: 1;
   color: inherit !important;
 }
 
+.text-opacity-25 {
+  --bs-text-opacity: 0.25;
+}
+
+.text-opacity-50 {
+  --bs-text-opacity: 0.5;
+}
+
+.text-opacity-75 {
+  --bs-text-opacity: 0.75;
+}
+
+.text-opacity-100 {
+  --bs-text-opacity: 1;
+}
+
 .bg-primary {
-  background-color: #0d6efd !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-secondary {
-  background-color: #6c757d !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-success {
-  background-color: #198754 !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-info {
-  background-color: #0dcaf0 !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-warning {
-  background-color: #ffc107 !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-danger {
-  background-color: #dc3545 !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-light {
-  background-color: #f8f9fa !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-dark {
-  background-color: #212529 !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
 }
 
-.bg-body {
-  background-color: #fff !important;
+.bg-black {
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-white {
-  background-color: #fff !important;
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-body {
+  --bs-bg-opacity: 1;
+  background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;
 }
 
 .bg-transparent {
+  --bs-bg-opacity: 1;
   background-color: transparent !important;
 }
 
+.bg-opacity-10 {
+  --bs-bg-opacity: 0.1;
+}
+
+.bg-opacity-25 {
+  --bs-bg-opacity: 0.25;
+}
+
+.bg-opacity-50 {
+  --bs-bg-opacity: 0.5;
+}
+
+.bg-opacity-75 {
+  --bs-bg-opacity: 0.75;
+}
+
+.bg-opacity-100 {
+  --bs-bg-opacity: 1;
+}
+
 .bg-gradient {
   background-image: var(--bs-gradient) !important;
 }
diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css
index 585f9390..989e0960 100644
--- a/src/static/scripts/datatables.css
+++ b/src/static/scripts/datatables.css
@@ -4,10 +4,10 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs5/dt-1.11.2
+ *   https://datatables.net/download/#bs5/dt-1.11.3
  *
  * Included libraries:
- *   DataTables 1.11.2
+ *   DataTables 1.11.3
  */
 
 @charset "UTF-8";
diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js
index 93bbe80b..0d88756e 100644
--- a/src/static/scripts/datatables.js
+++ b/src/static/scripts/datatables.js
@@ -4,20 +4,20 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs5/dt-1.11.2
+ *   https://datatables.net/download/#bs5/dt-1.11.3
  *
  * Included libraries:
- *   DataTables 1.11.2
+ *   DataTables 1.11.3
  */
 
-/*! DataTables 1.11.2
+/*! DataTables 1.11.3
  * ©2008-2021 SpryMedia Ltd - datatables.net/license
  */
 
 /**
  * @summary     DataTables
  * @description Paginate, search and order HTML tables
- * @version     1.11.2
+ * @version     1.11.3
  * @file        jquery.dataTables.js
  * @author      SpryMedia Ltd
  * @contact     www.datatables.net
@@ -1626,6 +1626,14 @@
 		return out;
 	}
 	
+	var _includes = function (search, start) {
+		if (start === undefined) {
+			start = 0;
+		}
+	
+		return this.indexOf(search, start) !== -1;	
+	};
+	
 	// Array.isArray polyfill.
 	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
 	if (! Array.isArray) {
@@ -1634,6 +1642,10 @@
 	    };
 	}
 	
+	if (! Array.prototype.includes) {
+		Array.prototype.includes = _includes;
+	}
+	
 	// .trim() polyfill
 	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
 	if (!String.prototype.trim) {
@@ -1642,6 +1654,10 @@
 	  };
 	}
 	
+	if (! String.prototype.includes) {
+		String.prototype.includes = _includes;
+	}
+	
 	/**
 	 * DataTables utility methods
 	 * 
@@ -2808,9 +2824,18 @@
 			return cellData.call( rowData );
 		}
 	
-		if ( cellData === null && type == 'display' ) {
+		if ( cellData === null && type === 'display' ) {
 			return '';
 		}
+	
+		if ( type === 'filter' ) {
+			var fomatters = DataTable.ext.type.search;
+	
+			if ( fomatters[ col.sType ] ) {
+				cellData = fomatters[ col.sType ]( cellData );
+			}
+		}
+	
 		return cellData;
 	}
 	
@@ -4565,7 +4590,6 @@
 		var columns = settings.aoColumns;
 		var column;
 		var i, j, ien, jen, filterData, cellData, row;
-		var fomatters = DataTable.ext.type.search;
 		var wasInvalidated = false;
 	
 		for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
@@ -4580,10 +4604,6 @@
 					if ( column.bSearchable ) {
 						cellData = _fnGetCellData( settings, i, j, 'filter' );
 	
-						if ( fomatters[ column.sType ] ) {
-							cellData = fomatters[ column.sType ]( cellData );
-						}
-	
 						// Search in DataTables 1.10 is string based. In 1.11 this
 						// should be altered to also allow strict type checking.
 						if ( cellData === null ) {
@@ -6374,6 +6394,10 @@
 	 */
 	function _fnSaveState ( settings )
 	{
+		if (settings._bLoadingState) {
+			return;
+		}
+	
 		/* Store the interesting variables */
 		var state = {
 			time:    +new Date(),
@@ -6408,99 +6432,129 @@
 	 */
 	function _fnLoadState ( settings, oInit, callback )
 	{
-		var i, ien;
-		var columns = settings.aoColumns;
-		var loaded = function ( s ) {
-			if ( ! s || ! s.time ) {
-				callback();
-				return;
-			}
-	
-			// Allow custom and plug-in manipulation functions to alter the saved data set and
-			// cancelling of loading by returning false
-			var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
-			if ( $.inArray( false, abStateLoad ) !== -1 ) {
-				callback();
-				return;
-			}
-	
-			// Reject old data
-			var duration = settings.iStateDuration;
-			if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
-				callback();
-				return;
-			}
-	
-			// Number of columns have changed - all bets are off, no restore of settings
-			if ( s.columns && columns.length !== s.columns.length ) {
-				callback();
-				return;
-			}
-	
-			// Store the saved state so it might be accessed at any time
-			settings.oLoadedState = $.extend( true, {}, s );
-	
-			// Restore key features - todo - for 1.11 this needs to be done by
-			// subscribed events
-			if ( s.start !== undefined ) {
-				settings._iDisplayStart    = s.start;
-				settings.iInitDisplayStart = s.start;
-			}
-			if ( s.length !== undefined ) {
-				settings._iDisplayLength   = s.length;
-			}
-	
-			// Order
-			if ( s.order !== undefined ) {
-				settings.aaSorting = [];
-				$.each( s.order, function ( i, col ) {
-					settings.aaSorting.push( col[0] >= columns.length ?
-						[ 0, col[1] ] :
-						col
-					);
-				} );
-			}
-	
-			// Search
-			if ( s.search !== undefined ) {
-				$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
-			}
-	
-			// Columns
-			//
-			if ( s.columns ) {
-				for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
-					var col = s.columns[i];
-	
-					// Visibility
-					if ( col.visible !== undefined ) {
-						columns[i].bVisible = col.visible;
-					}
-	
-					// Search
-					if ( col.search !== undefined ) {
-						$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
-					}
-				}
-			}
-	
-			_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
-			callback();
-		};
-	
 		if ( ! settings.oFeatures.bStateSave ) {
 			callback();
 			return;
 		}
 	
+		var loaded = function(state) {
+			_fnImplementState(settings, state, callback);
+		}
+	
 		var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
 	
 		if ( state !== undefined ) {
-			loaded( state );
+			_fnImplementState( settings, state, callback );
 		}
 		// otherwise, wait for the loaded callback to be executed
+	
+		return true;
 	}
 	
+	function _fnImplementState ( settings, s, callback) {
+		var i, ien;
+		var columns = settings.aoColumns;
+		settings._bLoadingState = true;
+	
+		// When StateRestore was introduced the state could now be implemented at any time
+		// Not just initialisation. To do this an api instance is required in some places
+		var api = settings._bInitComplete ? new DataTable.Api(settings) : null;
+	
+		if ( ! s || ! s.time ) {
+			settings._bLoadingState = false;
+			callback();
+			return;
+		}
+	
+		// Allow custom and plug-in manipulation functions to alter the saved data set and
+		// cancelling of loading by returning false
+		var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
+		if ( $.inArray( false, abStateLoad ) !== -1 ) {
+			settings._bLoadingState = false;
+			callback();
+			return;
+		}
+	
+		// Reject old data
+		var duration = settings.iStateDuration;
+		if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
+			settings._bLoadingState = false;
+			callback();
+			return;
+		}
+	
+		// Number of columns have changed - all bets are off, no restore of settings
+		if ( s.columns && columns.length !== s.columns.length ) {
+			settings._bLoadingState = false;
+			callback();
+			return;
+		}
+	
+		// Store the saved state so it might be accessed at any time
+		settings.oLoadedState = $.extend( true, {}, s );
+	
+		// Restore key features - todo - for 1.11 this needs to be done by
+		// subscribed events
+		if ( s.start !== undefined ) {
+			settings._iDisplayStart    = s.start;
+			if(api === null) {
+				settings.iInitDisplayStart = s.start;
+			}
+		}
+		if ( s.length !== undefined ) {
+			settings._iDisplayLength   = s.length;
+		}
+	
+		// Order
+		if ( s.order !== undefined ) {
+			settings.aaSorting = [];
+			$.each( s.order, function ( i, col ) {
+				settings.aaSorting.push( col[0] >= columns.length ?
+					[ 0, col[1] ] :
+					col
+				);
+			} );
+		}
+	
+		// Search
+		if ( s.search !== undefined ) {
+			$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
+		}
+	
+		// Columns
+		if ( s.columns ) {
+			for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
+				var col = s.columns[i];
+	
+				// Visibility
+				if ( col.visible !== undefined ) {
+					// If the api is defined, the table has been initialised so we need to use it rather than internal settings
+					if (api) {
+						// Don't redraw the columns on every iteration of this loop, we will do this at the end instead
+						api.column(i).visible(col.visible, false);
+					}
+					else {
+						columns[i].bVisible = col.visible;
+					}
+				}
+	
+				// Search
+				if ( col.search !== undefined ) {
+					$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
+				}
+			}
+			
+			// If the api is defined then we need to adjust the columns once the visibility has been changed
+			if (api) {
+				api.columns.adjust();
+			}
+		}
+	
+		settings._bLoadingState = false;
+		_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
+		callback();
+	};
+	
 	
 	/**
 	 * Return the settings object for a particular table
@@ -9590,7 +9644,7 @@
 	 *  @type string
 	 *  @default Version number
 	 */
-	DataTable.version = "1.11.2";
+	DataTable.version = "1.11.3";
 
 	/**
 	 * Private data store, containing all of the settings objects that are
@@ -14015,7 +14069,7 @@
 		 *
 		 *  @type string
 		 */
-		build:"bs5/dt-1.11.2",
+		build:"bs5/dt-1.11.3",
 	
 	
 		/**
@@ -15048,6 +15102,10 @@
 	 */
 	
 	var __htmlEscapeEntities = function ( d ) {
+		if (Array.isArray(d)) {
+			d = d.join(',');
+		}
+	
 		return typeof d === 'string' ?
 			d
 				.replace(/&/g, '&amp;')
@@ -15242,6 +15300,7 @@
 		_fnSortData: _fnSortData,
 		_fnSaveState: _fnSaveState,
 		_fnLoadState: _fnLoadState,
+		_fnImplementState: _fnImplementState,
 		_fnSettingsFromNode: _fnSettingsFromNode,
 		_fnLog: _fnLog,
 		_fnMap: _fnMap,
diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs
index 315acc97..6fd08f4a 100644
--- a/src/static/templates/admin/diagnostics.hbs
+++ b/src/static/templates/admin/diagnostics.hbs
@@ -150,7 +150,7 @@
 
                     <dt class="col-sm-5">Domain configuration
                         <span class="badge bg-success d-none" id="domain-success" title="The domain variable matches the browser location and seems to be configured correctly.">Match</span>
-                        <span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not matches the browsers location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
+                        <span class="badge bg-danger d-none" id="domain-warning" title="The domain variable does not match the browser location.&#013;&#010;The domain variable does not seem to be configured correctly.&#013;&#010;Some features may not work as expected!">No Match</span>
                         <span class="badge bg-success d-none" id="https-success" title="Configurued to use HTTPS">HTTPS</span>
                         <span class="badge bg-danger d-none" id="https-warning" title="Not configured to use HTTPS.&#013;&#010;Some features may not work as expected!">No HTTPS</span>
                     </dt>