Просмотр исходного кода

Add feature switch for new design via config file, URL parameters, and Docker (#742)

* Initial plan

* Add feature switch for new design with config and URL parameter support

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Improve error handling and prevent infinite redirect loops

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Update Dockerfiles and entrypoint to support design feature switch

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Update design-switch.js

Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com>

* fix: copy actions in entrypoint

* Restructure design switch to place both designs at root level

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Flatten frontend assets in Docker to eliminate frontend directory

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* fix: entrypoint settings & server-list

disable entrypoint bash debug

* add link to modern design

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>
Co-authored-by: sstidl <sstidl@users.noreply.github.com>
Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com>
Copilot 7 месяцев назад
Родитель
Сommit
235fa6311b
10 измененных файлов с 891 добавлено и 513 удалено
  1. 96 0
      DESIGN_SWITCH.md
  2. 5 0
      Dockerfile
  3. 5 0
      Dockerfile.alpine
  4. 3 0
      config.json
  5. 72 0
      design-switch.js
  6. 1 0
      doc_docker.md
  7. 37 6
      docker/entrypoint.sh
  8. 518 0
      index-classic.html
  9. 151 0
      index-modern.html
  10. 3 507
      index.html

+ 96 - 0
DESIGN_SWITCH.md

@@ -0,0 +1,96 @@
+# Design Feature Switch
+
+LibreSpeed now supports switching between the classic design and the new modern design.
+
+## Default Behavior
+
+By default, LibreSpeed uses the **classic design** (located in `index-classic.html`).
+
+## Architecture
+
+### File Structure (Non-Docker)
+- **`index.html`** - Entry point (lightweight switcher)
+- **`index-classic.html`** - Classic design at root
+- **`index-modern.html`** - Modern design at root (references assets in subdirectories)
+- **`frontend/`** - Directory containing modern design assets (CSS, JS, images, fonts) - kept for non-Docker deployments
+
+### File Structure (Docker)
+In Docker deployments, the frontend assets are flattened to root-level subdirectories:
+- **`index.html`** - Entry point (lightweight switcher)
+- **`index-classic.html`** - Classic design
+- **`index-modern.html`** - Modern design  
+- **`styling/`** - CSS files for modern design
+- **`javascript/`** - JS files for modern design
+- **`images/`** - Images for modern design
+- **`fonts/`** - Fonts for modern design
+- **No `frontend/` directory** - Assets are copied directly to root subdirectories
+
+### Benefits of Root-Level Design Files
+✅ Both designs at same level - no path confusion
+✅ `results/` accessible from both designs with same relative path
+✅ `backend/` accessible from both designs with same relative path  
+✅ No subdirectory nesting issues
+✅ Clean separation of concerns
+✅ Docker containers have no `frontend/` parent directory
+
+## Browser Compatibility
+
+The feature switch uses modern JavaScript features (URLSearchParams, fetch API). It is compatible with all modern browsers. The new design itself requires modern browser features and has no backwards compatibility with older browsers (see `frontend/README.md`).
+
+## Enabling the New Design
+
+There are two ways to enable the new design:
+
+### Method 1: Configuration File (Persistent)
+
+Edit the `config.json` file in the root directory and set `useNewDesign` to `true`:
+
+```json
+{
+  "useNewDesign": true
+}
+```
+
+This will make the new design the default for all users visiting your site.
+
+### Method 2: URL Parameter (Temporary Override)
+
+You can override the configuration by adding a URL parameter:
+
+- To use the new design: `http://yoursite.com/?design=new`
+- To use the old design: `http://yoursite.com/?design=old`
+
+URL parameters take precedence over the configuration file, making them useful for testing or allowing users to choose their preferred design.
+
+## Design Locations
+
+### Non-Docker Deployments
+- **Entry Point**: Root `index.html` file (lightweight redirect page)
+- **Old Design**: `index-classic.html` at root
+- **New Design**: `index-modern.html` at root (references assets in `frontend/` subdirectory)
+- **Assets**: Frontend assets (CSS, JS, images, fonts) in `frontend/` subdirectory
+
+### Docker Deployments
+- **Entry Point**: Root `index.html` file (lightweight redirect page)
+- **Old Design**: `index-classic.html` at root
+- **New Design**: `index-modern.html` at root (references assets in root subdirectories)
+- **Assets**: Frontend assets copied directly to root subdirectories (`styling/`, `javascript/`, `images/`, `fonts/`)
+- **No `frontend/` directory** - Assets are flattened to root level
+
+Both designs are at the same directory level, ensuring that relative paths to shared resources like `backend/` and `results/` work correctly for both.
+
+## Technical Details
+
+The feature switch is implemented in `design-switch.js`, which is loaded by the root `index.html`. It checks:
+
+1. First, URL parameters (`?design=new` or `?design=old`)
+2. Then, the `config.json` configuration file
+3. Redirects to either `index-classic.html` or `index-modern.html`
+
+Both design HTML files are at the root level, eliminating path issues.
+
+### Non-Docker
+The modern design references assets from the `frontend/` subdirectory (e.g., `frontend/styling/index.css`), while both designs can access shared resources like `backend/` and `results/` using the same relative paths.
+
+### Docker
+In Docker deployments, the `frontend/` directory is flattened during container startup. Assets are copied directly to root-level subdirectories (`styling/`, `javascript/`, `images/`, `fonts/`), and `index-modern.html` references these root-level paths. This eliminates the `frontend/` parent directory in the container.

+ 5 - 0
Dockerfile

@@ -21,6 +21,10 @@ COPY results/*.php /speedtest/results/
 COPY results/*.ttf /speedtest/results/
 
 COPY *.js /speedtest/
+COPY index.html /speedtest/
+COPY index-classic.html /speedtest/
+COPY index-modern.html /speedtest/
+COPY config.json /speedtest/
 COPY favicon.ico /speedtest/
 
 COPY docker/servers.json /servers.json
@@ -36,6 +40,7 @@ ENV TELEMETRY=false
 ENV ENABLE_ID_OBFUSCATION=false
 ENV REDACT_IP_ADDRESSES=false
 ENV WEBPORT=8080
+ENV USE_NEW_DESIGN=false
 
 # https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
 STOPSIGNAL SIGWINCH

+ 5 - 0
Dockerfile.alpine

@@ -35,6 +35,10 @@ COPY results/*.php /speedtest/results/
 COPY results/*.ttf /speedtest/results/
 
 COPY *.js /speedtest/
+COPY index.html /speedtest/
+COPY index-classic.html /speedtest/
+COPY index-modern.html /speedtest/
+COPY config.json /speedtest/
 COPY favicon.ico /speedtest/
 
 COPY docker/servers.json /servers.json
@@ -50,6 +54,7 @@ ENV TELEMETRY=false
 ENV ENABLE_ID_OBFUSCATION=false
 ENV REDACT_IP_ADDRESSES=false
 ENV WEBPORT=8080
+ENV USE_NEW_DESIGN=false
 
 # https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
 STOPSIGNAL SIGWINCH

+ 3 - 0
config.json

@@ -0,0 +1,3 @@
+{
+  "useNewDesign": false
+}

+ 72 - 0
design-switch.js

@@ -0,0 +1,72 @@
+/**
+ * Feature switch for enabling the new LibreSpeed design
+ * 
+ * This script checks for:
+ * 1. URL parameter: ?design=new or ?design=old
+ * 2. Configuration file: config.json with useNewDesign flag
+ * 
+ * Default behavior: Shows the old design
+ * 
+ * Note: This script is only loaded on the root index.html
+ */
+(function() {
+    'use strict';
+    
+    // Don't run this script if we're already on a specific design page
+    // This prevents infinite redirect loops
+    const currentPath = window.location.pathname;
+    if (currentPath.includes('index-classic.html') || currentPath.includes('index-modern.html')) {
+        return;
+    }
+    
+    // Check URL parameters first (they override config)
+    const urlParams = new URLSearchParams(window.location.search);
+    const designParam = urlParams.get('design');
+    
+    if (designParam === 'new') {
+        redirectToNewDesign();
+        return;
+    }
+    
+    if (designParam === 'old' || designParam === 'classic') {
+        redirectToOldDesign();
+        return;
+    }
+    
+    // Check config.json for design preference
+    try {
+        const xhr = new XMLHttpRequest();
+        // Use a synchronous request to prevent a flash of the old design before redirecting
+        xhr.open('GET', 'config.json', false);
+        xhr.send(null);
+
+        // Check for a successful response, but not 304 Not Modified, which can have an empty response body
+        if (xhr.status >= 200 && xhr.status < 300) {
+            const config = JSON.parse(xhr.responseText);
+            if (config.useNewDesign === true) {
+                redirectToNewDesign();
+            } else {
+                redirectToOldDesign();
+            }
+        } else {
+            // Config not found or error - default to old design
+            redirectToOldDesign();
+        }
+    } catch (error) {
+        // If there's any error (e.g., network, JSON parse), default to old design
+        console.log('Using default (old) design:', error.message || 'config error');
+        redirectToOldDesign();
+    }
+    
+    function redirectToNewDesign() {
+        // Preserve any URL parameters when redirecting
+        const currentParams = window.location.search;
+        window.location.href = 'index-modern.html' + currentParams;
+    }
+    
+    function redirectToOldDesign() {
+        // Preserve any URL parameters when redirecting
+        const currentParams = window.location.search;
+        window.location.href = 'index-classic.html' + currentParams;
+    }
+})();

+ 1 - 0
doc_docker.md

@@ -57,6 +57,7 @@ The test can be accessed on port 80.
 Here's a list of additional environment variables available in this mode:
 
 * __`TITLE`__: Title of your speed test. Default value: `LibreSpeed`
+* __`USE_NEW_DESIGN`__: When set to `true`, enables the new modern frontend design. When set to `false` (default), uses the classic design. The design can also be switched using URL parameters (`?design=new` or `?design=old`). Default value: `false`
 * __`TELEMETRY`__: Whether to enable telemetry or not. If enabled, you maybe want your data to be persisted. See below. Default value: `false`
 * __`ENABLE_ID_OBFUSCATION`__: When set to true with telemetry enabled, test IDs are obfuscated, to avoid exposing the database internal sequential IDs. Default value: `false`
 * __`OBFUSCATION_SALT`__: The salt string that is used to obfuscate the test IDs. The format shoud be a 2 byte hex string (e.g. `0x1234abcd`). If not specified, a random one will be generated.

+ 37 - 6
docker/entrypoint.sh

@@ -1,7 +1,15 @@
 #!/bin/bash
 
+echo "Setting up docker env..."
+echo "MODE: $MODE"
+echo "USE_NEW_DESIGN: $USE_NEW_DESIGN"
+echo "WEBPORT: $WEBPORT"
+echo "REDACT_IP_ADDRESSES: $REDACT_IP_ADDRESSES"
+echo "DB_TYPE: $DB_TYPE"
+echo "ENABLE_ID_OBFUSCATION: $ENABLE_ID_OBFUSCATION"
+
 set -e
-set -x
+#set -x
 
 is_alpine() {
   [ -f /etc/alpine-release ]
@@ -13,6 +21,10 @@ rm -rf /var/www/html/*
 # Copy frontend files
 cp /speedtest/*.js /var/www/html/
 
+# Copy design switch files
+cp /speedtest/config.json /var/www/html/
+cp /speedtest/design-switch.js /var/www/html/
+
 # Copy favicon
 cp /speedtest/favicon.ico /var/www/html/
 
@@ -41,11 +53,30 @@ if [ "$MODE" == "backend" ]; then
 fi
 
 # Set up index.php for frontend-only or standalone modes
-if [[ "$MODE" == "frontend" || "$MODE" == "dual" ]]; then
-  cp -av /speedtest/frontend/* /var/www/html/
-elif [ "$MODE" == "standalone" ]; then
-  cp -av /speedtest/frontend/* /var/www/html/
-  echo '[{"name":"local","server":"/backend",  "dlURL": "garbage.php", "ulURL": "empty.php", "pingURL": "empty.php", "getIpURL": "getIP.php", "sponsorName": "", "sponsorURL": "", "id":1 }]' > /var/www/html/server-list.json
+if [[ "$MODE" == "frontend" || "$MODE" == "dual" ||  "$MODE" == "standalone" ]]; then
+  # Copy design files (switcher + both designs)
+  cp /speedtest/index.html /var/www/html/
+  cp /speedtest/index-classic.html /var/www/html/
+  cp /speedtest/index-modern.html /var/www/html/
+  
+  # Copy frontend assets directly to root-level subdirectories (no frontend/ parent dir)
+  mkdir -p /var/www/html/styling /var/www/html/javascript /var/www/html/images /var/www/html/fonts
+  cp -a /speedtest/frontend/styling/* /var/www/html/styling/
+  cp -a /speedtest/frontend/javascript/* /var/www/html/javascript/
+  cp -a /speedtest/frontend/images/* /var/www/html/images/
+  cp -a /speedtest/frontend/fonts/* /var/www/html/fonts/ 2>/dev/null || true
+  
+  # Copy frontend config files
+  cp /speedtest/frontend/settings.json /var/www/html/settings.json 2>/dev/null || true
+  if [ ! -f /var/www/html/server-list.json ]; then
+    echo "no server-list.json found, create one for local host"
+    # generate config for just the local server
+    echo '[{"name":"local","server":"/backend",  "dlURL": "garbage.php", "ulURL": "empty.php", "pingURL": "empty.php", "getIpURL": "getIP.php", "sponsorName": "", "sponsorURL": "", "id":1 }]' > /var/www/html/server-list.json
+  fi
+fi
+# Configure design preference via config.json
+if [ "$USE_NEW_DESIGN" == "true" ]; then
+  sed -i 's/"useNewDesign": false/"useNewDesign": true/' /var/www/html/config.json
 fi
 
 # Apply Telemetry settings when running in standalone or frontend mode and telemetry is enabled

+ 518 - 0
index-classic.html

@@ -0,0 +1,518 @@
+<!DOCTYPE html>
+<html>
+<head>
+<link rel="shortcut icon" href="favicon.ico">
+<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
+<meta charset="UTF-8" />
+<script type="text/javascript" src="speedtest.js"></script>
+<script type="text/javascript">
+function I(i){return document.getElementById(i);}
+
+//LIST OF TEST SERVERS. Leave empty if you're doing a standalone installation. See documentation for details
+var SPEEDTEST_SERVERS=[
+	/*{	//this server doesn't actually exist, remove it
+		name:"Example Server 1", //user friendly name for the server
+		server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
+		dlURL:"backend/garbage.php",  //path to download test on this server (garbage.php or replacement)
+		ulURL:"backend/empty.php",  //path to upload test on this server (empty.php or replacement)
+		pingURL:"backend/empty.php",  //path to ping/jitter test on this server (empty.php or replacement)
+		getIpURL:"backend/getIP.php"  //path to getIP on this server (getIP.php or replacement)
+	},
+	{	//this server doesn't actually exist, remove it
+		name:"Example Server 2", //user friendly name for the server
+		server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
+		dlURL:"garbage.php",  //path to download test on this server (garbage.php or replacement)
+		ulURL:"empty.php",  //path to upload test on this server (empty.php or replacement)
+		pingURL:"empty.php",  //path to ping/jitter test on this server (empty.php or replacement)
+		getIpURL:"getIP.php"  //path to getIP on this server (getIP.php or replacement)
+	}*/
+	//add other servers here, comma separated
+];
+
+//INITIALIZE SPEEDTEST
+var s=new Speedtest(); //create speed test object
+s.setParameter("telemetry_level","basic"); //enable basic telemetry (for results sharing)
+
+//SERVER AUTO SELECTION
+function initServers(){
+	if(SPEEDTEST_SERVERS.length==0){ //standalone installation
+		//just make the UI visible
+		I("loading").className="hidden";
+		I("serverArea").style.display="none";
+		I("testWrapper").className="visible";
+		initUI();
+	}else{ //multiple servers
+		var noServersAvailable=function(){
+			I("message").innerHTML="No servers available";
+		}
+		var runServerSelect=function(){
+			s.selectServer(function(server){
+				if(server!=null){ //at least 1 server is available
+					I("loading").className="hidden"; //hide loading message
+					//populate server list for manual selection
+					for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
+						if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
+						var option=document.createElement("option");
+						option.value=i;
+						option.textContent=SPEEDTEST_SERVERS[i].name;
+						if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
+						I("server").appendChild(option);
+					}
+					//show test UI
+					I("testWrapper").className="visible";
+					initUI();
+				}else{ //no servers are available, the test cannot proceed
+					noServersAvailable();
+				}
+			});
+		}
+		if(typeof SPEEDTEST_SERVERS === "string"){
+			//need to fetch list of servers from specified URL
+			s.loadServerList(SPEEDTEST_SERVERS,function(servers){
+				if(servers==null){ //failed to load server list
+					noServersAvailable();
+				}else{ //server list loaded
+					SPEEDTEST_SERVERS=servers;
+					runServerSelect();
+				}
+			});
+		}else{
+			//hardcoded server list
+			s.addTestPoints(SPEEDTEST_SERVERS);
+			runServerSelect();
+		}
+	}
+}
+
+var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
+var dlColor="#6060AA",
+	ulColor="#616161";
+var progColor=meterBk;
+
+//CODE FOR GAUGES
+function drawMeter(c,amount,bk,fg,progress,prog){
+	var ctx=c.getContext("2d");
+	var dp=window.devicePixelRatio||1;
+	var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
+	var sizScale=ch*0.0055;
+	if(c.width==cw&&c.height==ch){
+		ctx.clearRect(0,0,cw,ch);
+	}else{
+		c.width=cw;
+		c.height=ch;
+	}
+	ctx.beginPath();
+	ctx.strokeStyle=bk;
+	ctx.lineWidth=12*sizScale;
+	ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
+	ctx.stroke();
+	ctx.beginPath();
+	ctx.strokeStyle=fg;
+	ctx.lineWidth=12*sizScale;
+	ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
+	ctx.stroke();
+	if(typeof progress !== "undefined"){
+		ctx.fillStyle=prog;
+		ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
+	}
+}
+function mbpsToAmount(s){
+	return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
+}
+function format(d){
+    d=Number(d);
+    if(d<10) return d.toFixed(2);
+    if(d<100) return d.toFixed(1);
+    return d.toFixed(0);
+}
+
+//UI CODE
+var uiData=null;
+function startStop(){
+    if(s.getState()==3){
+		//speed test is running, abort
+		s.abort();
+		data=null;
+		I("startStopBtn").className="";
+		I("server").disabled=false;
+		initUI();
+	}else{
+		//test is not running, begin
+		I("startStopBtn").className="running";
+		I("shareArea").style.display="none";
+		I("server").disabled=true;
+		s.onupdate=function(data){
+            uiData=data;
+		};
+		s.onend=function(aborted){
+            I("startStopBtn").className="";
+            I("server").disabled=false;
+            updateUI(true);
+            if(!aborted){
+                //if testId is present, show sharing panel, otherwise do nothing
+                try{
+                    var testId=uiData.testId;
+                    if(testId!=null){
+                        var shareURL=window.location.href.substring(0,window.location.href.lastIndexOf("/"))+"/results/?id="+testId;
+                        I("resultsImg").src=shareURL;
+                        I("resultsURL").value=shareURL;
+                        I("testId").innerHTML=testId;
+                        I("shareArea").style.display="";
+                    }
+                }catch(e){}
+            }
+		};
+		s.start();
+	}
+}
+//this function reads the data sent back by the test and updates the UI
+function updateUI(forced){
+	if(!forced&&s.getState()!=3) return;
+	if(uiData==null) return;
+	var status=uiData.testState;
+	I("ip").textContent=uiData.clientIp;
+	I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"...":format(uiData.dlStatus);
+	drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
+	I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"...":format(uiData.ulStatus);
+	drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
+	I("pingText").textContent=format(uiData.pingStatus);
+	I("jitText").textContent=format(uiData.jitterStatus);
+}
+function oscillate(){
+	return 1+0.02*Math.sin(Date.now()/100);
+}
+//update the UI every frame
+window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
+function frame(){
+	requestAnimationFrame(frame);
+	updateUI();
+}
+frame(); //start frame loop
+//function to (re)initialize UI
+function initUI(){
+	drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
+	drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
+	I("dlText").textContent="";
+	I("ulText").textContent="";
+	I("pingText").textContent="";
+	I("jitText").textContent="";
+	I("ip").textContent="";
+}
+</script>
+<style type="text/css">
+	html,body{
+		border:none; padding:0; margin:0;
+		background:#FFFFFF;
+		color:#202020;
+	}
+	body{
+		text-align:center;
+		font-family:"Roboto",sans-serif;
+	}
+	h1{
+		color:#404040;
+	}
+	#loading{
+		background-color:#FFFFFF;
+		color:#404040;
+		text-align:center;
+	}
+	span.loadCircle{
+		display:inline-block;
+		width:2em;
+		height:2em;
+		vertical-align:middle;
+		background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAP1BMVEUAAAB2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZyFzwnAAAAFHRSTlMAEvRFvX406baecwbf0casimhSHyiwmqgAAADpSURBVHja7dbJbQMxAENRahnN5lkc//5rDRAkDeRgHszXgACJoKiIiIiIiIiIiIiIiIiIiIj4HHspsrpAVhdVVguzrA4OWc10WcEqpwKbnBo0OU1Q5NSpsoJFTgOecrrdEag85DRgktNqfoEdTjnd7hrEHMEJvmRUYJbTYk5Agy6nau6Abp5Cm7mDBtRdPi9gyKdU7w4p1fsLvyqs8hl4z9/w3n/Hmr9WoQ65lAU4d7lMYOz//QboRR5jBZibLMZdAR6O/Vfa1PlxNr3XdS3HzK/HVPRu/KnLs8iAOh993VpRRERERMT/fAN60wwWaVyWwAAAAABJRU5ErkJggg==');
+		background-size:2em 2em;
+		margin-right:0.5em;
+		animation: spin 0.6s linear infinite;
+	}
+	@keyframes spin{
+		0%{transform:rotate(0deg);}
+		100%{transform:rotate(359deg);}
+	}
+	#startStopBtn{
+		display:inline-block;
+		margin:0 auto;
+		color:#6060AA;
+		background-color:rgba(0,0,0,0);
+		border:0.15em solid #6060FF;
+		border-radius:0.3em;
+		transition:all 0.3s;
+		box-sizing:border-box;
+		width:8em; height:3em;
+		line-height:2.7em;
+		cursor:pointer;
+		box-shadow: 0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);
+	}
+	#startStopBtn:hover{
+		box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);
+	}
+	#startStopBtn.running{
+		background-color:#FF3030;
+		border-color:#FF6060;
+		color:#FFFFFF;
+	}
+	#startStopBtn:before{
+		content:"Start";
+	}
+	#startStopBtn.running:before{
+		content:"Abort";
+	}
+	#serverArea{
+		margin-top:1em;
+	}
+	#server{
+		font-size:1em;
+		padding:0.2em;
+	}
+	#test{
+		margin-top:2em;
+		margin-bottom:12em;
+	}
+	div.testArea{
+		display:inline-block;
+		width:16em;
+		height:12.5em;
+		position:relative;
+		box-sizing:border-box;
+	}
+	div.testArea2{
+		display:inline-block;
+		width:14em;
+		height:7em;
+		position:relative;
+		box-sizing:border-box;
+		text-align:center;
+	}
+	div.testArea div.testName{
+		position:absolute;
+		top:0.1em; left:0;
+		width:100%;
+		font-size:1.4em;
+		z-index:9;
+	}
+	div.testArea2 div.testName{
+        display:block;
+        text-align:center;
+        font-size:1.4em;
+	}
+	div.testArea div.meterText{
+		position:absolute;
+		bottom:1.55em; left:0;
+		width:100%;
+		font-size:2.5em;
+		z-index:9;
+	}
+	div.testArea2 div.meterText{
+        display:inline-block;
+        font-size:2.5em;
+	}
+	div.meterText:empty:before{
+		content:"0.00";
+	}
+	div.testArea div.unit{
+		position:absolute;
+		bottom:2em; left:0;
+		width:100%;
+		z-index:9;
+	}
+	div.testArea2 div.unit{
+		display:inline-block;
+	}
+	div.testArea canvas{
+		position:absolute;
+		top:0; left:0; width:100%; height:100%;
+		z-index:1;
+	}
+	div.testGroup{
+		display:block;
+        margin: 0 auto;
+	}
+	#shareArea{
+		width:95%;
+		max-width:40em;
+		margin:0 auto;
+		margin-top:2em;
+	}
+	#shareArea > *{
+		display:block;
+		width:100%;
+		height:auto;
+		margin: 0.25em 0;
+	}
+	#privacyPolicy{
+        position:fixed;
+        top:2em;
+        bottom:2em;
+        left:2em;
+        right:2em;
+        overflow-y:auto;
+        width:auto;
+        height:auto;
+        box-shadow:0 0 3em 1em #000000;
+        z-index:999999;
+        text-align:left;
+        background-color:#FFFFFF;
+        padding:1em;
+	}
+	a.privacy{
+        text-align:center;
+        font-size:0.8em;
+        color:#808080;
+        padding: 0 3em;
+	}
+    div.closePrivacyPolicy {
+        width: 100%;
+        text-align: center;
+    }
+    div.closePrivacyPolicy a.privacy {
+        padding: 1em 3em;
+    }
+	@media all and (max-width:40em){
+		body{
+			font-size:0.8em;
+		}
+	}
+	div.visible{
+		animation: fadeIn 0.4s;
+		display:block;
+	}
+	div.hidden{
+		animation: fadeOut 0.4s;
+		display:none;
+	}
+	@keyframes fadeIn{
+		0%{
+			opacity:0;
+		}
+		100%{
+			opacity:1;
+		}
+	}
+	@keyframes fadeOut{
+		0%{
+			display:block;
+			opacity:1;
+		}
+		100%{
+			display:block;
+			opacity:0;
+		}
+	}
+	@media all and (prefers-color-scheme: dark){
+		html,body,#loading{
+			background:#202020;
+			color:#F4F4F4;
+			color-scheme:dark;
+		}
+		h1{
+			color:#E0E0E0;
+		}
+		a{
+			color:#9090FF;
+		}
+		#privacyPolicy{
+			background:#000000;
+		}
+		#resultsImg{
+			filter: invert(1);
+		}
+	}
+</style>
+<title>LibreSpeed</title>
+</head>
+<body onload="initServers()">
+<h1>LibreSpeed</h1>
+<div id="loading" class="visible">
+	<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
+</div>
+<div id="testWrapper" class="hidden">
+	<div id="startStopBtn" onclick="startStop()"></div><br/>
+	<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
+	<div id="serverArea">
+		Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
+	</div>
+	<div id="test">
+		<div class="testGroup">
+            <div class="testArea2">
+				<div class="testName">Ping</div>
+				<div id="pingText" class="meterText" style="color:#AA6060"></div>
+				<div class="unit">ms</div>
+			</div>
+			<div class="testArea2">
+				<div class="testName">Jitter</div>
+				<div id="jitText" class="meterText" style="color:#AA6060"></div>
+				<div class="unit">ms</div>
+			</div>
+		</div>
+		<div class="testGroup">
+			<div class="testArea">
+				<div class="testName">Download</div>
+				<canvas id="dlMeter" class="meter"></canvas>
+				<div id="dlText" class="meterText"></div>
+				<div class="unit">Mbit/s</div>
+			</div>
+			<div class="testArea">
+				<div class="testName">Upload</div>
+				<canvas id="ulMeter" class="meter"></canvas>
+				<div id="ulText" class="meterText"></div>
+				<div class="unit">Mbit/s</div>
+			</div>
+		</div>
+		<div id="ipArea">
+			<span id="ip"></span>
+		</div>
+		<div id="shareArea" style="display:none">
+			<h3>Share results</h3>
+			<p>Test ID: <span id="testId"></span></p>
+			<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
+			<img src="" id="resultsImg" />
+		</div>
+	</div>
+	<a href="index.html?design=new">Try the modern design</a><br>
+	<a href="https://github.com/librespeed/speedtest">Source code</a>
+</div>
+<div id="privacyPolicy" style="display:none">
+    <h2>Privacy Policy</h2>
+    <p>This HTML5 speed test server is configured with telemetry enabled.</p>
+    <h4>What data we collect</h4>
+    <p>
+        At the end of the test, the following data is collected and stored:
+        <ul>
+            <li>Test ID</li>
+            <li>Time of testing</li>
+            <li>Test results (download and upload speed, ping and jitter)</li>
+            <li>IP address</li>
+            <li>ISP information</li>
+            <li>Approximate location (inferred from IP address, not GPS)</li>
+            <li>User agent and browser locale</li>
+            <li>Test log (contains no personal information)</li>
+        </ul>
+    </p>
+    <h4>How we use the data</h4>
+    <p>
+        Data collected through this service is used to:
+        <ul>
+            <li>Allow sharing of test results (sharable image for forums, etc.)</li>
+            <li>To improve the service offered to you (for instance, to detect problems on our side)</li>
+        </ul>
+        No personal information is disclosed to third parties.
+    </p>
+    <h4>Your consent</h4>
+    <p>
+        By starting the test, you consent to the terms of this privacy policy.
+    </p>
+    <h4>Data removal</h4>
+    <p>
+        If you want to have your information deleted, you need to provide either the ID of the test or your IP address. This is the only way to identify your data, without this information we won't be able to comply with your request.<br/><br/>
+        Contact this email address for all deletion requests: <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
+    </p>
+    <br/><br/>
+    <div class="closePrivacyPolicy">
+        <a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
+    </div>
+    <br/>
+</div>
+</body>
+</html>

+ 151 - 0
index-modern.html

@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1, shrink-to-fit=no"
+    />
+    <meta
+      name="description"
+      content="Free and Open Source Speedtest. Run it right now in your browser, or self-host on a PHP, Golang, Rust or Node server. License: LGPL."
+    />
+    <link rel="shortcut icon" href="images/favicon.svg" />
+    <script type="text/javascript" src="speedtest.js"></script>
+    <script type="text/javascript" src="javascript/index.js"></script>
+    <link rel="stylesheet" type="text/css" href="styling/index.css" />
+    <title>LibreSpeed - Free and Open Source Speedtest</title>
+  </head>
+
+  <body>
+    <header>
+      <img src="images/logo.svg" alt="LibreSpeed" />
+    </header>
+    <main>
+      <h1>Free and Open Source Speedtest.</h1>
+      <p class="tagline">No Flash, No Java, No Websockets, No Bullsh*t</p>
+
+      <div class="server-selector">
+        <div class="chosen">
+          <div class="chevron">
+            <img src="images/chevron.svg" alt="select..." />
+          </div>
+          <p>current server</p>
+          <h2 id="selected-server">searching nearest server...</h2>
+        </div>
+        <ul class="servers"></ul>
+        <p class="sponsor" id="sponsor">&nbsp;</p>
+      </div>
+
+      <p id="privacy-warning" class="hidden">
+        by clicking the start button you agree to our privacy policy<br />
+        <a href="#" id="choose-privacy">or choose your privacy options</a>
+      </p>
+      <button class="disabled" id="start-button"></button>
+
+      <div class="gauge-layout">
+        <div class="ping hidden">
+          <span class="label">Ping</span>:&nbsp;
+          <span class="value" id="ping">00</span>ms
+        </div>
+
+        <div class="gauge download" id="download-gauge">
+          <div class="progress"></div>
+          <div class="speed"></div>
+          <h1><span id="download-speed">00</span> Mbps</h1>
+          <h2>Download</h2>
+        </div>
+
+        <div class="gauge upload" id="upload-gauge">
+          <div class="progress"></div>
+          <div class="speed"></div>
+          <h1><span id="upload-speed">00</span> Mbps</h1>
+          <h2>Upload</h2>
+        </div>
+
+        <div class="jitter hidden">
+          <span class="label">Jitter</span>:&nbsp;
+          <span class="value" id="jitter">00</span>ms
+        </div>
+      </div>
+
+      <button class="small inverted hidden" id="share-results">
+        Share results
+      </button>
+    </main>
+    <footer>
+      <p class="source">
+        <a href="https://github.com/librespeed/speedtest">source code</a>
+      </p>
+    </footer>
+
+    <dialog id="share">
+      <div class="close-dialog">
+        <img src="images/close-button.svg" alt="Close" />
+      </div>
+      <img id="results" src="" alt="Test results in graphical form" />
+      <button id="copy-link">Copy link</button>
+    </dialog>
+
+    <dialog id="privacy">
+      <div class="close-dialog">
+        <img src="images/close-button.svg" alt="Close" />
+      </div>
+      <section>
+        <h1>Privacy Policy</h1>
+        <p>
+          This HTML5 speed test server is configured with telemetry enabled.
+        </p>
+
+        <h2>What data we collect</h2>
+        <p>
+          At the end of the test, the following data is collected and stored:
+        </p>
+
+        <ul>
+          <li>Test ID</li>
+          <li>Time of testing</li>
+          <li>Test results (download and upload speed, ping and jitter)</li>
+          <li>IP address</li>
+          <li>ISP information</li>
+          <li>Approximate location (inferred from IP address, not GPS)</li>
+          <li>User agent and browser locale</li>
+          <li>Test log (contains no personal information)</li>
+        </ul>
+
+        <h2>How we use the data</h2>
+        <p>Data collected through this service is used to:</p>
+
+        <ul>
+          <li>
+            Allow sharing of test results (sharable image for forums, etc.)
+          </li>
+          <li>
+            To improve the service offered to you (for instance, to detect
+            problems on our side)
+          </li>
+        </ul>
+
+        <p>No personal information is disclosed to third parties.</p>
+
+        <h2>Your consent</h2>
+        <p>
+          By starting the test, you consent to the terms of this privacy policy.
+        </p>
+
+        <h2>Data removal</h2>
+        <p>
+          If you want to have your information deleted, you need to provide
+          either the ID of the test or your IP address. This is the only way to
+          identify your data, without this information we won't be able to
+          comply with your request.
+        </p>
+        <p>
+          Contact this email address for all deletion requests:
+          <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
+        </p>
+      </section>
+      <button id="close-privacy">Close</button>
+    </dialog>
+  </body>
+</html>

+ 3 - 507
index.html

@@ -4,514 +4,10 @@
 <link rel="shortcut icon" href="favicon.ico">
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
 <meta charset="UTF-8" />
-<script type="text/javascript" src="speedtest.js"></script>
-<script type="text/javascript">
-function I(i){return document.getElementById(i);}
-
-//LIST OF TEST SERVERS. Leave empty if you're doing a standalone installation. See documentation for details
-var SPEEDTEST_SERVERS=[
-	/*{	//this server doesn't actually exist, remove it
-		name:"Example Server 1", //user friendly name for the server
-		server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
-		dlURL:"backend/garbage.php",  //path to download test on this server (garbage.php or replacement)
-		ulURL:"backend/empty.php",  //path to upload test on this server (empty.php or replacement)
-		pingURL:"backend/empty.php",  //path to ping/jitter test on this server (empty.php or replacement)
-		getIpURL:"backend/getIP.php"  //path to getIP on this server (getIP.php or replacement)
-	},
-	{	//this server doesn't actually exist, remove it
-		name:"Example Server 2", //user friendly name for the server
-		server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
-		dlURL:"garbage.php",  //path to download test on this server (garbage.php or replacement)
-		ulURL:"empty.php",  //path to upload test on this server (empty.php or replacement)
-		pingURL:"empty.php",  //path to ping/jitter test on this server (empty.php or replacement)
-		getIpURL:"getIP.php"  //path to getIP on this server (getIP.php or replacement)
-	}*/
-	//add other servers here, comma separated
-];
-
-//INITIALIZE SPEEDTEST
-var s=new Speedtest(); //create speed test object
-s.setParameter("telemetry_level","basic"); //enable basic telemetry (for results sharing)
-
-//SERVER AUTO SELECTION
-function initServers(){
-	if(SPEEDTEST_SERVERS.length==0){ //standalone installation
-		//just make the UI visible
-		I("loading").className="hidden";
-		I("serverArea").style.display="none";
-		I("testWrapper").className="visible";
-		initUI();
-	}else{ //multiple servers
-		var noServersAvailable=function(){
-			I("message").innerHTML="No servers available";
-		}
-		var runServerSelect=function(){
-			s.selectServer(function(server){
-				if(server!=null){ //at least 1 server is available
-					I("loading").className="hidden"; //hide loading message
-					//populate server list for manual selection
-					for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
-						if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
-						var option=document.createElement("option");
-						option.value=i;
-						option.textContent=SPEEDTEST_SERVERS[i].name;
-						if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
-						I("server").appendChild(option);
-					}
-					//show test UI
-					I("testWrapper").className="visible";
-					initUI();
-				}else{ //no servers are available, the test cannot proceed
-					noServersAvailable();
-				}
-			});
-		}
-		if(typeof SPEEDTEST_SERVERS === "string"){
-			//need to fetch list of servers from specified URL
-			s.loadServerList(SPEEDTEST_SERVERS,function(servers){
-				if(servers==null){ //failed to load server list
-					noServersAvailable();
-				}else{ //server list loaded
-					SPEEDTEST_SERVERS=servers;
-					runServerSelect();
-				}
-			});
-		}else{
-			//hardcoded server list
-			s.addTestPoints(SPEEDTEST_SERVERS);
-			runServerSelect();
-		}
-	}
-}
-
-var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
-var dlColor="#6060AA",
-	ulColor="#616161";
-var progColor=meterBk;
-
-//CODE FOR GAUGES
-function drawMeter(c,amount,bk,fg,progress,prog){
-	var ctx=c.getContext("2d");
-	var dp=window.devicePixelRatio||1;
-	var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
-	var sizScale=ch*0.0055;
-	if(c.width==cw&&c.height==ch){
-		ctx.clearRect(0,0,cw,ch);
-	}else{
-		c.width=cw;
-		c.height=ch;
-	}
-	ctx.beginPath();
-	ctx.strokeStyle=bk;
-	ctx.lineWidth=12*sizScale;
-	ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
-	ctx.stroke();
-	ctx.beginPath();
-	ctx.strokeStyle=fg;
-	ctx.lineWidth=12*sizScale;
-	ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
-	ctx.stroke();
-	if(typeof progress !== "undefined"){
-		ctx.fillStyle=prog;
-		ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
-	}
-}
-function mbpsToAmount(s){
-	return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
-}
-function format(d){
-    d=Number(d);
-    if(d<10) return d.toFixed(2);
-    if(d<100) return d.toFixed(1);
-    return d.toFixed(0);
-}
-
-//UI CODE
-var uiData=null;
-function startStop(){
-    if(s.getState()==3){
-		//speed test is running, abort
-		s.abort();
-		data=null;
-		I("startStopBtn").className="";
-		I("server").disabled=false;
-		initUI();
-	}else{
-		//test is not running, begin
-		I("startStopBtn").className="running";
-		I("shareArea").style.display="none";
-		I("server").disabled=true;
-		s.onupdate=function(data){
-            uiData=data;
-		};
-		s.onend=function(aborted){
-            I("startStopBtn").className="";
-            I("server").disabled=false;
-            updateUI(true);
-            if(!aborted){
-                //if testId is present, show sharing panel, otherwise do nothing
-                try{
-                    var testId=uiData.testId;
-                    if(testId!=null){
-                        var shareURL=window.location.href.substring(0,window.location.href.lastIndexOf("/"))+"/results/?id="+testId;
-                        I("resultsImg").src=shareURL;
-                        I("resultsURL").value=shareURL;
-                        I("testId").innerHTML=testId;
-                        I("shareArea").style.display="";
-                    }
-                }catch(e){}
-            }
-		};
-		s.start();
-	}
-}
-//this function reads the data sent back by the test and updates the UI
-function updateUI(forced){
-	if(!forced&&s.getState()!=3) return;
-	if(uiData==null) return;
-	var status=uiData.testState;
-	I("ip").textContent=uiData.clientIp;
-	I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"...":format(uiData.dlStatus);
-	drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
-	I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"...":format(uiData.ulStatus);
-	drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
-	I("pingText").textContent=format(uiData.pingStatus);
-	I("jitText").textContent=format(uiData.jitterStatus);
-}
-function oscillate(){
-	return 1+0.02*Math.sin(Date.now()/100);
-}
-//update the UI every frame
-window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
-function frame(){
-	requestAnimationFrame(frame);
-	updateUI();
-}
-frame(); //start frame loop
-//function to (re)initialize UI
-function initUI(){
-	drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
-	drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
-	I("dlText").textContent="";
-	I("ulText").textContent="";
-	I("pingText").textContent="";
-	I("jitText").textContent="";
-	I("ip").textContent="";
-}
-</script>
-<style type="text/css">
-	html,body{
-		border:none; padding:0; margin:0;
-		background:#FFFFFF;
-		color:#202020;
-	}
-	body{
-		text-align:center;
-		font-family:"Roboto",sans-serif;
-	}
-	h1{
-		color:#404040;
-	}
-	#loading{
-		background-color:#FFFFFF;
-		color:#404040;
-		text-align:center;
-	}
-	span.loadCircle{
-		display:inline-block;
-		width:2em;
-		height:2em;
-		vertical-align:middle;
-		background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAP1BMVEUAAAB2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZyFzwnAAAAFHRSTlMAEvRFvX406baecwbf0casimhSHyiwmqgAAADpSURBVHja7dbJbQMxAENRahnN5lkc//5rDRAkDeRgHszXgACJoKiIiIiIiIiIiIiIiIiIiIj4HHspsrpAVhdVVguzrA4OWc10WcEqpwKbnBo0OU1Q5NSpsoJFTgOecrrdEag85DRgktNqfoEdTjnd7hrEHMEJvmRUYJbTYk5Agy6nau6Abp5Cm7mDBtRdPi9gyKdU7w4p1fsLvyqs8hl4z9/w3n/Hmr9WoQ65lAU4d7lMYOz//QboRR5jBZibLMZdAR6O/Vfa1PlxNr3XdS3HzK/HVPRu/KnLs8iAOh993VpRRERERMT/fAN60wwWaVyWwAAAAABJRU5ErkJggg==');
-		background-size:2em 2em;
-		margin-right:0.5em;
-		animation: spin 0.6s linear infinite;
-	}
-	@keyframes spin{
-		0%{transform:rotate(0deg);}
-		100%{transform:rotate(359deg);}
-	}
-	#startStopBtn{
-		display:inline-block;
-		margin:0 auto;
-		color:#6060AA;
-		background-color:rgba(0,0,0,0);
-		border:0.15em solid #6060FF;
-		border-radius:0.3em;
-		transition:all 0.3s;
-		box-sizing:border-box;
-		width:8em; height:3em;
-		line-height:2.7em;
-		cursor:pointer;
-		box-shadow: 0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);
-	}
-	#startStopBtn:hover{
-		box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);
-	}
-	#startStopBtn.running{
-		background-color:#FF3030;
-		border-color:#FF6060;
-		color:#FFFFFF;
-	}
-	#startStopBtn:before{
-		content:"Start";
-	}
-	#startStopBtn.running:before{
-		content:"Abort";
-	}
-	#serverArea{
-		margin-top:1em;
-	}
-	#server{
-		font-size:1em;
-		padding:0.2em;
-	}
-	#test{
-		margin-top:2em;
-		margin-bottom:12em;
-	}
-	div.testArea{
-		display:inline-block;
-		width:16em;
-		height:12.5em;
-		position:relative;
-		box-sizing:border-box;
-	}
-	div.testArea2{
-		display:inline-block;
-		width:14em;
-		height:7em;
-		position:relative;
-		box-sizing:border-box;
-		text-align:center;
-	}
-	div.testArea div.testName{
-		position:absolute;
-		top:0.1em; left:0;
-		width:100%;
-		font-size:1.4em;
-		z-index:9;
-	}
-	div.testArea2 div.testName{
-        display:block;
-        text-align:center;
-        font-size:1.4em;
-	}
-	div.testArea div.meterText{
-		position:absolute;
-		bottom:1.55em; left:0;
-		width:100%;
-		font-size:2.5em;
-		z-index:9;
-	}
-	div.testArea2 div.meterText{
-        display:inline-block;
-        font-size:2.5em;
-	}
-	div.meterText:empty:before{
-		content:"0.00";
-	}
-	div.testArea div.unit{
-		position:absolute;
-		bottom:2em; left:0;
-		width:100%;
-		z-index:9;
-	}
-	div.testArea2 div.unit{
-		display:inline-block;
-	}
-	div.testArea canvas{
-		position:absolute;
-		top:0; left:0; width:100%; height:100%;
-		z-index:1;
-	}
-	div.testGroup{
-		display:block;
-        margin: 0 auto;
-	}
-	#shareArea{
-		width:95%;
-		max-width:40em;
-		margin:0 auto;
-		margin-top:2em;
-	}
-	#shareArea > *{
-		display:block;
-		width:100%;
-		height:auto;
-		margin: 0.25em 0;
-	}
-	#privacyPolicy{
-        position:fixed;
-        top:2em;
-        bottom:2em;
-        left:2em;
-        right:2em;
-        overflow-y:auto;
-        width:auto;
-        height:auto;
-        box-shadow:0 0 3em 1em #000000;
-        z-index:999999;
-        text-align:left;
-        background-color:#FFFFFF;
-        padding:1em;
-	}
-	a.privacy{
-        text-align:center;
-        font-size:0.8em;
-        color:#808080;
-        padding: 0 3em;
-	}
-    div.closePrivacyPolicy {
-        width: 100%;
-        text-align: center;
-    }
-    div.closePrivacyPolicy a.privacy {
-        padding: 1em 3em;
-    }
-	@media all and (max-width:40em){
-		body{
-			font-size:0.8em;
-		}
-	}
-	div.visible{
-		animation: fadeIn 0.4s;
-		display:block;
-	}
-	div.hidden{
-		animation: fadeOut 0.4s;
-		display:none;
-	}
-	@keyframes fadeIn{
-		0%{
-			opacity:0;
-		}
-		100%{
-			opacity:1;
-		}
-	}
-	@keyframes fadeOut{
-		0%{
-			display:block;
-			opacity:1;
-		}
-		100%{
-			display:block;
-			opacity:0;
-		}
-	}
-	@media all and (prefers-color-scheme: dark){
-		html,body,#loading{
-			background:#202020;
-			color:#F4F4F4;
-			color-scheme:dark;
-		}
-		h1{
-			color:#E0E0E0;
-		}
-		a{
-			color:#9090FF;
-		}
-		#privacyPolicy{
-			background:#000000;
-		}
-		#resultsImg{
-			filter: invert(1);
-		}
-	}
-</style>
+<script type="text/javascript" src="design-switch.js"></script>
 <title>LibreSpeed</title>
 </head>
-<body onload="initServers()">
-<h1>LibreSpeed</h1>
-<div id="loading" class="visible">
-	<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
-</div>
-<div id="testWrapper" class="hidden">
-	<div id="startStopBtn" onclick="startStop()"></div><br/>
-	<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
-	<div id="serverArea">
-		Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
-	</div>
-	<div id="test">
-		<div class="testGroup">
-            <div class="testArea2">
-				<div class="testName">Ping</div>
-				<div id="pingText" class="meterText" style="color:#AA6060"></div>
-				<div class="unit">ms</div>
-			</div>
-			<div class="testArea2">
-				<div class="testName">Jitter</div>
-				<div id="jitText" class="meterText" style="color:#AA6060"></div>
-				<div class="unit">ms</div>
-			</div>
-		</div>
-		<div class="testGroup">
-			<div class="testArea">
-				<div class="testName">Download</div>
-				<canvas id="dlMeter" class="meter"></canvas>
-				<div id="dlText" class="meterText"></div>
-				<div class="unit">Mbit/s</div>
-			</div>
-			<div class="testArea">
-				<div class="testName">Upload</div>
-				<canvas id="ulMeter" class="meter"></canvas>
-				<div id="ulText" class="meterText"></div>
-				<div class="unit">Mbit/s</div>
-			</div>
-		</div>
-		<div id="ipArea">
-			<span id="ip"></span>
-		</div>
-		<div id="shareArea" style="display:none">
-			<h3>Share results</h3>
-			<p>Test ID: <span id="testId"></span></p>
-			<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
-			<img src="" id="resultsImg" />
-		</div>
-	</div>
-	<a href="https://github.com/librespeed/speedtest">Source code</a>
-</div>
-<div id="privacyPolicy" style="display:none">
-    <h2>Privacy Policy</h2>
-    <p>This HTML5 speed test server is configured with telemetry enabled.</p>
-    <h4>What data we collect</h4>
-    <p>
-        At the end of the test, the following data is collected and stored:
-        <ul>
-            <li>Test ID</li>
-            <li>Time of testing</li>
-            <li>Test results (download and upload speed, ping and jitter)</li>
-            <li>IP address</li>
-            <li>ISP information</li>
-            <li>Approximate location (inferred from IP address, not GPS)</li>
-            <li>User agent and browser locale</li>
-            <li>Test log (contains no personal information)</li>
-        </ul>
-    </p>
-    <h4>How we use the data</h4>
-    <p>
-        Data collected through this service is used to:
-        <ul>
-            <li>Allow sharing of test results (sharable image for forums, etc.)</li>
-            <li>To improve the service offered to you (for instance, to detect problems on our side)</li>
-        </ul>
-        No personal information is disclosed to third parties.
-    </p>
-    <h4>Your consent</h4>
-    <p>
-        By starting the test, you consent to the terms of this privacy policy.
-    </p>
-    <h4>Data removal</h4>
-    <p>
-        If you want to have your information deleted, you need to provide either the ID of the test or your IP address. This is the only way to identify your data, without this information we won't be able to comply with your request.<br/><br/>
-        Contact this email address for all deletion requests: <a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
-    </p>
-    <br/><br/>
-    <div class="closePrivacyPolicy">
-        <a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
-    </div>
-    <br/>
-</div>
+<body>
+<p>Loading...</p>
 </body>
 </html>