diff --git a/public/assets/components/sidebar.js b/public/assets/components/sidebar.js
index e9f27215f..b879cb7d6 100644
--- a/public/assets/components/sidebar.js
+++ b/public/assets/components/sidebar.js
@@ -134,9 +134,9 @@ async function ctrlNavigationPane(render, { $sidebar, nRestart }) {
const $active = qs($sidebar, `[data-path="${chunk.toString()}"] a`);
$active.classList.add("active");
if (checkVisible($active) === false) {
- $active.offsetTop < window.innerHeight ?
- $sidebar.firstChild.scrollTo({top: 0, behavior: "smooth"}) :
- $active.scrollIntoView({ behavior: "smooth" });
+ $active.offsetTop < window.innerHeight
+ ? $sidebar.firstChild.scrollTo({ top: 0, behavior: "smooth" })
+ : $active.scrollIntoView({ behavior: "smooth" });
}
} catch (err) {}
diff --git a/public/assets/css/designsystem.css b/public/assets/css/designsystem.css
index d2cb802fb..7c37ef1e8 100644
--- a/public/assets/css/designsystem.css
+++ b/public/assets/css/designsystem.css
@@ -13,7 +13,7 @@
@import url("./designsystem_alert.css");
:root {
- --bg-color: #f9f9fa; /*#fafafa;*/
+ --bg-color: #f9f9fa;
--color: #57595A;
--emphasis: #466372;
--primary: #9AD1ED;
diff --git a/public/assets/sw.js b/public/assets/sw.js
new file mode 100644
index 000000000..ff203ede5
--- /dev/null
+++ b/public/assets/sw.js
@@ -0,0 +1,111 @@
+const VERSION = "v1";
+const CACHENAME = "assets";
+
+/*
+ * This Service Worker is an optional optimisation to load the app faster.
+ * Whenever using raw es module without any build, we had a large number
+ * of assets getting through the network. When we looked through the
+ * developer console -> network, and look at the timing, 98% of the time
+ * was spent "waiting for the server response".
+ * HTTP2/3 should solve that issue but we don't control the proxy side of
+ * things of how people install Filestash, hence the idea to bulk download
+ * as much as we can through SSE, store it onto a cache and get our
+ * service worker to inject the response.
+ * This approach alone make the app a lot faster to load but relies on
+ * the server being able to bundle our assets via SSE.
+ *
+ * TODO:
+ * - wait until browser support DecompressionStream("brotli") natively
+ * and use that. As of 2025, downloading a brotli decompress library
+ * make the gain br / gz negative for our app
+ * - wait until Firefox support SSE within service worker. As of 2025,
+ * someone was implementing it in Firefox but it's not everywhere yet
+ * Once that's done, we want to be 100% sure everything is working great
+ */
+
+self.addEventListener("install", (event) => {
+ if (!self.EventSource) throw new Error("turboload not supported on this platform");
+
+ event.waitUntil((async() => {
+ await self.skipWaiting();
+ })());
+});
+
+self.addEventListener("activate", (event) => {
+ event.waitUntil((async() => {
+ for (const name of await caches.keys()) await caches.delete(name);
+ await self.clients.claim();
+ })());
+});
+
+self.addEventListener("fetch", (event) => {
+ if (!event.request.url.startsWith(location.origin + "/assets/")) return;
+
+ event.respondWith((async() => {
+ const cachedResponse = await caches.match(event.request);
+ if (cachedResponse) return cachedResponse;
+ return fetch(event.request);
+ })());
+});
+
+self.addEventListener("message", (event) => {
+ if (event.data.type === "preload") handlePreloadMessage(
+ event.data.payload,
+ () => event.source.postMessage({ type: "preload", status: "ok" }),
+ (err) => event.source.postMessage({ type: "preload", status: "error", msg: err.message }),
+ );
+});
+
+const handlePreloadMessage = (() => {
+ const cleanup = [];
+ return async(chunks, resolve, reject) => {
+ cleanup.forEach((fn) => fn());
+ try {
+ caches.delete(CACHENAME);
+ const cache = await caches.open(CACHENAME);
+ await Promise.all(chunks.map((urls) => {
+ return preload({ urls, cache, cleanup });
+ }));
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ };
+})();
+
+async function preload({ urls, cache, cleanup }) {
+ const evtsrc = new self.EventSource("/assets/bundle?" + urls.map((url) => `url=${url}`).join("&"));
+ cleanup.push(() => evtsrc.close());
+
+ let i = 0;
+ const messageHandler = (resolve, event) => {
+ const url = event.lastEventId;
+ let mime = "application/octet-stream";
+ if (url.endsWith(".css")) mime = "text/css";
+ else if (url.endsWith(".js")) mime = "application/javascript";
+
+ i += 1;
+ cache.put(
+ location.origin + event.lastEventId,
+ new Response(
+ new Blob([Uint8Array.from(atob(event.data), (c) => c.charCodeAt(0))])
+ .stream()
+ .pipeThrough(new DecompressionStream("gzip")),
+ { headers: { "Content-Type": mime } },
+ ),
+ );
+ if (i === urls.length) {
+ evtsrc.close();
+ resolve();
+ }
+ };
+ const errorHandler = (reject, err) => {
+ evtsrc.close();
+ reject(err);
+ };
+
+ await new Promise((resolve, reject) => {
+ evtsrc.onmessage = async(event) => messageHandler(resolve, event);
+ evtsrc.onerror = (err) => errorHandler(reject, err);
+ });
+}
diff --git a/public/index.frontoffice.html b/public/index.frontoffice.html
index 45fe7b76b..cb5aa7380 100644
--- a/public/index.frontoffice.html
+++ b/public/index.frontoffice.html
@@ -4,219 +4,386 @@