Browse Source

Merge pull request #834 from BKPepe/fix/modern-frontend-asset-paths

fix: preserve the modern frontend layout in deployments (#834)

Serve the modern UI assets from frontend/ in both repository and Docker
deployments, matching the paths used by index-modern.html.

Move settings.json and server-list.json to the web root, where the modern
page resolves its configuration, and keep Docker, installation guidance, and
design-switch documentation consistent with that layout.

Add E2E coverage for:
- serving the repository unchanged with Python's static HTTP server;
- required modern UI assets and a real 404 for missing paths;
- Alpine standalone settings and modern frontend rendering.

Fixes #804
sstidl 1 ngày trước cách đây
mục cha
commit
f3df5db6ab

+ 16 - 20
DESIGN_SWITCH.md

@@ -15,23 +15,19 @@ By default, LibreSpeed uses the **classic design** (located in `index-classic.ht
 - **`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:
+Docker deployments preserve the same layout as non-Docker deployments:
 - **`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
+- **`index-classic.html`** - Classic design at root
+- **`index-modern.html`** - Modern design at root
+- **`frontend/`** - Modern design assets, copied into the web root unchanged
+- **`settings.json` and `server-list.json`** - Configuration files at root, next to `index-modern.html`
+
+### Benefits of the Shared Layout
+✅ Docker and non-Docker deployments use the same paths
+✅ Both designs are at the same level
+✅ `results/` and `backend/` use the same relative paths from both designs
+✅ The modern design loads assets consistently from `frontend/`
+✅ Configuration files stay at the web root, where the modern page expects them
 
 ## Browser Compatibility
 
@@ -73,9 +69,9 @@ URL parameters take precedence over the configuration file, making them useful f
 ### 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
+- **New Design**: `index-modern.html` at root (references assets in `frontend/` subdirectory)
+- **Assets**: Frontend assets in `frontend/`, copied into the web root unchanged
+- Same layout as a non-Docker deployment, so the two cannot drift apart
 
 Both designs are at the same directory level, ensuring that relative paths to shared resources like `backend/` and `results/` work correctly for both.
 
@@ -93,4 +89,4 @@ Both design HTML files are at the root level, eliminating path issues.
 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.
+In Docker deployments, `frontend/` is copied into the web root as it stands during container startup, so the container serves the same layout the repository has and the same paths `index-modern.html` asks for.

+ 2 - 0
Dockerfile

@@ -27,6 +27,8 @@ COPY index.html /speedtest/
 COPY index-classic.html /speedtest/
 COPY index-modern.html /speedtest/
 COPY config.json /speedtest/
+COPY settings.json /speedtest/
+COPY server-list.json /speedtest/
 COPY stability.html /speedtest/
 COPY favicon.ico /speedtest/
 COPY manifest.webmanifest /speedtest/

+ 2 - 0
Dockerfile.alpine

@@ -46,6 +46,8 @@ COPY index.html /speedtest/
 COPY index-classic.html /speedtest/
 COPY index-modern.html /speedtest/
 COPY config.json /speedtest/
+COPY settings.json /speedtest/
+COPY server-list.json /speedtest/
 COPY stability.html /speedtest/
 COPY favicon.ico /speedtest/
 COPY manifest.webmanifest /speedtest/

+ 1 - 2
README.md

@@ -41,8 +41,7 @@ Works with mobile versions too.
 Assuming you have PHP and a web server installed, the installation steps are quite simple.
 
 1. Download the source code and extract it
-1. Copy the project files to your web server's shared folder (ie. `/var/www/html/speedtest` for Apache). For the current layout, the web root should contain `index.html`, `index-classic.html`, `index-modern.html`, `stability.html`, `design-switch.js`, `config.json`, `speedtest.js`, `speedtest_worker.js`, `stability_worker.js`, `favicon.ico`, and the `backend` folder.
-1. Also copy the contents of `frontend/` into the same web root so the modern UI assets end up in `styling/`, `javascript/`, `images/`, and `fonts/` next to the HTML files.
+1. Copy the project files to your web server's shared folder (ie. `/var/www/html/speedtest` for Apache), keeping the layout as it is in the repository. The modern UI loads its assets from `frontend/`, so that directory is copied as a whole rather than unpacked.
 1. Optionally, copy the results folder too, and set up the database using the config file in it.
 1. Be sure your permissions allow read and execute access where needed.
 1. Visit YOURSITE/speedtest/index.html and voila!

+ 4 - 2
doc.md

@@ -197,18 +197,20 @@ Requirements:
   * FreeType 2 and its PHP module
   * The PHP gd library
 
-To install the speed test frontend, copy the project files to your web server and keep the modern UI assets next to the HTML files:
+To install the speed test frontend, copy the project files to your web server, keeping the layout they have in the repository:
 
 * `index.html`
 * `index-classic.html`
 * `index-modern.html`
 * `design-switch.js`
 * `config.json`
+* `settings.json`
+* `server-list.json`
 * `speedtest.js`
 * `speedtest_worker.js`
 * `favicon.ico`
 * the `backend` folder
-* the contents of `frontend/`, copied so `styling/`, `javascript/`, `images/`, and `fonts/` sit next to the HTML files
+* the `frontend` folder, copied as a whole: the modern UI loads its assets from `frontend/`
 * Optionally, the `results` folder
 
 __Important:__ The speed test needs read and execute permissions in the installation folder where applicable!

+ 4 - 7
docker/entrypoint.sh

@@ -82,15 +82,12 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" ||  "$MODE" == "standalone" ]];
   cp /speedtest/index-classic.html /var/www/html/
   cp /speedtest/index-modern.html /var/www/html/
   cp /speedtest/stability.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
+  # Keep the frontend assets under frontend/, which is where the HTML looks for
+  # them and where they sit in the repository
+  cp -a /speedtest/frontend /var/www/html/
 
   # Copy frontend config files
-  cp /speedtest/frontend/settings.json /var/www/html/settings.json 2>/dev/null || true
+  cp /speedtest/settings.json /var/www/html/settings.json 2>/dev/null || true
   if [ -f /servers.json ]; then
     echo "using mounted /servers.json for server-list.json"
     cp /servers.json /var/www/html/server-list.json

+ 9 - 6
frontend/README.md

@@ -5,15 +5,18 @@ This directory contains the modern LibreSpeed UI assets.
 ## Deployment
 
 For installation and deployment, follow the top-level [README.md](../README.md)
-and [DESIGN_SWITCH.md](../DESIGN_SWITCH.md). In non-Docker deployments, the
-contents of this directory must be copied so `styling/`, `javascript/`,
-`images/`, and `fonts/` sit next to the root HTML files.
+and [DESIGN_SWITCH.md](../DESIGN_SWITCH.md). This directory is copied as a
+whole: `index-modern.html` loads its assets from `frontend/`, so the layout
+here is the layout that gets served.
 
 ## Configuration
 
-- `server-list.json` contains the default server list used by the modern UI.
-- `settings.json` overrides selected `speedtest_worker.js` settings.
-- `index.html` and `index-modern.html` show how the frontend is wired up.
+The two configuration files live at the top level rather than here, because the
+page fetches them relative to itself:
+
+- `../server-list.json` contains the default server list used by the modern UI.
+- `../settings.json` overrides selected `speedtest_worker.js` settings.
+- `index.html` and `../index-modern.html` show how the frontend is wired up.
 
 ## Notes
 

+ 7 - 7
index-modern.html

@@ -6,7 +6,7 @@
   <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" />
+  <link rel="shortcut icon" href="frontend/images/favicon.svg" />
   <link rel="manifest" href="manifest.webmanifest" />
   <link rel="apple-touch-icon" href="images/icon-192.png" />
   <meta name="theme-color" content="#000000" />
@@ -15,14 +15,14 @@
     // Set this to a different URL to load the server list from another location.
     var SPEEDTEST_SERVERS = "server-list.json";
   </script>
-  <script type="text/javascript" src="javascript/index.js"></script>
-  <link rel="stylesheet" type="text/css" href="styling/index.css" />
+  <script type="text/javascript" src="frontend/javascript/index.js"></script>
+  <link rel="stylesheet" type="text/css" href="frontend/styling/index.css" />
   <title>LibreSpeed - Free and Open Source Speedtest</title>
 </head>
 
 <body>
   <header>
-    <img src="images/logo.svg" alt="LibreSpeed" />
+    <img src="frontend/images/logo.svg" alt="LibreSpeed" />
   </header>
   <main>
     <h1>Free and Open Source Speedtest.</h1>
@@ -31,7 +31,7 @@
     <div class="server-selector">
       <div class="chosen">
         <div class="chevron">
-          <img src="images/chevron.svg" alt="select..." />
+          <img src="frontend/images/chevron.svg" alt="select..." />
         </div>
         <p>current server</p>
         <h2 id="selected-server">searching nearest server...</h2>
@@ -84,7 +84,7 @@
 
   <dialog id="share">
     <div class="close-dialog">
-      <img src="images/close-button.svg" alt="Close" />
+      <img src="frontend/images/close-button.svg" alt="Close" />
     </div>
     <img id="results" src="" alt="Test results in graphical form" />
     <button id="copy-link">Copy link</button>
@@ -92,7 +92,7 @@
 
   <dialog id="privacy">
     <div class="close-dialog">
-      <img src="images/close-button.svg" alt="Close" />
+      <img src="frontend/images/close-button.svg" alt="Close" />
     </div>
     <section>
       <h1>Privacy Policy</h1>

+ 5 - 0
playwright.config.js

@@ -18,6 +18,11 @@ module.exports = defineConfig({
   },
   globalSetup: require.resolve('./tests/e2e/global-setup.js'),
   globalTeardown: require.resolve('./tests/e2e/global-teardown.js'),
+  webServer: {
+    command: 'python3 -m http.server 18184 --bind 127.0.0.1 --directory .',
+    url: 'http://127.0.0.1:18184',
+    reuseExistingServer: !process.env.CI,
+  },
   projects: [
     {
       name: 'chromium',

+ 0 - 0
frontend/server-list.json → server-list.json


+ 0 - 0
frontend/settings.json → settings.json


+ 11 - 0
tests/docker-compose-playwright.yml

@@ -31,6 +31,17 @@ services:
     ports:
       - "18185:8080"
 
+  standalone-alpine:
+    build:
+      context: ..
+      dockerfile: Dockerfile.alpine
+    environment:
+      - MODE=standalone
+      - WEBPORT=8080
+      - USE_NEW_DESIGN=true
+    ports:
+      - "18187:8080"
+
   standalone-apostrophe:
     build:
       context: ..

+ 1 - 0
tests/e2e/global-setup.js

@@ -68,6 +68,7 @@ module.exports = async () => {
   const timeoutMs = 180_000;
   await waitForReady('standalone', 'http://127.0.0.1:18180/index.html', timeoutMs);
   await waitForReady('standalone-new', 'http://127.0.0.1:18185/index.html', timeoutMs);
+  await waitForReady('standalone-alpine', 'http://127.0.0.1:18187/index.html', timeoutMs);
   await waitForReady('backend', 'http://127.0.0.1:18181/empty.php', timeoutMs);
   await waitForReady('frontend', 'http://127.0.0.1:18182/index-modern.html', timeoutMs);
   await waitForReady('dual', 'http://127.0.0.1:18183/index-modern.html', timeoutMs);

+ 1 - 0
tests/e2e/helpers/env.js

@@ -5,6 +5,7 @@ const baseUrls = {
   dual: 'http://127.0.0.1:18183',
   standaloneNew: 'http://127.0.0.1:18185',
   standaloneApostrophe: 'http://127.0.0.1:18186',
+  standaloneAlpine: 'http://127.0.0.1:18187',
 };
 
 module.exports = {

+ 9 - 0
tests/e2e/modes.spec.js

@@ -23,6 +23,15 @@ test.describe('Runtime mode smoke coverage', () => {
     await expect(page.locator('main > p.tagline')).toHaveText(defaultTagline);
   });
 
+  test('Alpine standalone serves modern frontend settings', async ({ page, request }) => {
+    const settings = await request.get(`${baseUrls.standaloneAlpine}/settings.json`);
+    expect(settings.ok()).toBeTruthy();
+    await expect(settings.json()).resolves.toMatchObject({ telemetry_level: 'off', time_dl_max: 12 });
+
+    await page.goto(`${baseUrls.standaloneAlpine}/index-modern.html`);
+    await expect(modernStartButton(page)).toBeVisible();
+  });
+
   test('backend exposes only local backend contract endpoints', async ({ request }) => {
     for (const endpoint of ['/empty.php', '/garbage.php', '/getIP.php']) {
       const response = await request.get(`${baseUrls.backend}${endpoint}`);

+ 35 - 0
tests/e2e/static-repository-assets.spec.js

@@ -0,0 +1,35 @@
+const { test, expect } = require("@playwright/test");
+
+const staticRepositoryUrl = "http://127.0.0.1:18184";
+
+const expectedAssets = [
+  "/index-modern.html",
+  "/settings.json",
+  "/server-list.json",
+  "/frontend/styling/index.css",
+  "/frontend/javascript/index.js",
+  "/frontend/images/logo.svg",
+  "/frontend/images/favicon.svg",
+  "/frontend/images/close-button.svg",
+  "/frontend/images/chevron.svg",
+  "/frontend/fonts/Inter-latin.woff2",
+  "/frontend/fonts/Inter-latin-ext.woff2",
+  "/frontend/images/background.jpeg",
+  "/speedtest.js",
+  "/speedtest_worker.js",
+  "/design-switch.js",
+  "/config.json",
+  "/images/icon-192.png"
+];
+
+test.describe("Unmodified repository static assets", () => {
+  test("serves every modern frontend dependency and returns 404 for a missing path", async ({ request }) => {
+    for (const path of expectedAssets) {
+      const response = await request.get(`${staticRepositoryUrl}${path}`);
+      expect(response.ok(), `${path} should return 2xx`).toBeTruthy();
+    }
+
+    const missing = await request.get(`${staticRepositoryUrl}/does-not-exist`);
+    expect(missing.status()).toBe(404);
+  });
+});