Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c5f5c30
Add internet stability test feature
tomrhudson Mar 15, 2026
6f1d8c5
Fix code review issues: server loading, subdir nav, chart perf
tomrhudson Mar 15, 2026
475cae5
EXPERIMENT: drop FROM php:8-alpine in favor of FROM alpine:3.21
akirayamamoto Apr 26, 2026
8c22140
Make ca-certificates dependency explicit
akirayamamoto Apr 27, 2026
903a702
Merge origin/master into internet-stability
May 16, 2026
8db7cdc
fix: clarify stability failed requests metric
May 16, 2026
a919730
Fix stability test server list and timeouts
May 16, 2026
94104e9
Update README for stability test
sstidl May 16, 2026
7b5c334
Merge branch 'master' into experiment/alpine-base-no-php
sstidl May 16, 2026
c11ee9f
Merge pull request #800 from akirayamamoto/experiment/alpine-base-no-php
sstidl May 16, 2026
4ec3a75
feat: sort server list by country name instead of city name
valer23 Apr 17, 2026
42d700c
fix: avoid mutating original server array and add null guard
valer23 Apr 17, 2026
69fa7a4
fix: handle parenthetical qualifiers and multi-comma server names
valer23 Apr 17, 2026
5cd7ce2
perf: avoid O(n²) indexOf lookup in classic UI server loop
valer23 Apr 17, 2026
7839a43
Add internet stability test feature
tomrhudson Mar 15, 2026
5278912
Fix code review issues: server loading, subdir nav, chart perf
tomrhudson Mar 15, 2026
fd883ba
fix: clarify stability failed requests metric
May 16, 2026
7a99528
Fix stability test server list and timeouts
May 16, 2026
89e14e9
Update README for stability test
sstidl May 16, 2026
8ba87a9
Merge branch 'internet-stability' of https://github.com/librespeed/sp…
sstidl May 16, 2026
48263c4
remove duplicated lines from rebase
sstidl May 16, 2026
320b908
Add stability feature Playwright coverage, fix Server selection start…
sstidl May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@
COPY index-classic.html /speedtest/
COPY index-modern.html /speedtest/
COPY config.json /speedtest/
COPY stability.html /speedtest/
COPY favicon.ico /speedtest/

COPY docker/*.php /speedtest/
COPY docker/entrypoint.sh /

# Prepare default environment variables
ENV TITLE=LibreSpeed
ENV TAGLINE="No Flash, No Java, No Websockets, No Bullsh*t"
ENV MODE=standalone
ENV PASSWORD=password

Check warning on line 37 in Dockerfile

View workflow job for this annotation

GitHub Actions / build (./Dockerfile, ghcr.io/librespeed/speedtest)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV TELEMETRY=false
ENV ENABLE_ID_OBFUSCATION=false
ENV REDACT_IP_ADDRESSES=false
Expand Down
12 changes: 4 additions & 8 deletions Dockerfile.alpine
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
FROM php:8-alpine
FROM alpine:3.23
RUN apk add --quiet --no-cache \
bash \
apache2 \
ca-certificates \
php \
php-apache2 \
php-ctype \
php-phar \
Expand All @@ -15,12 +17,6 @@
php-session \
php-sqlite3

# # use docker-php-extension-installer for automatically get the right packages installed
# ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

# # Install extensions
# RUN install-php-extensions iconv gd pdo pdo_mysql pdo_pgsql pgsql

RUN ln -sf /dev/stdout /var/log/apache2/access.log && \
ln -sf /dev/stderr /var/log/apache2/error.log

Expand All @@ -39,16 +35,16 @@
COPY index-classic.html /speedtest/
COPY index-modern.html /speedtest/
COPY config.json /speedtest/
COPY stability.html /speedtest/
COPY favicon.ico /speedtest/

COPY docker/*.php /speedtest/
COPY docker/entrypoint.sh /

# Prepare default environment variables
ENV TITLE=LibreSpeed
ENV TAGLINE="No Flash, No Java, No Websockets, No Bullsh*t"
ENV MODE=standalone
ENV PASSWORD=password

Check warning on line 47 in Dockerfile.alpine

View workflow job for this annotation

GitHub Actions / build (./Dockerfile.alpine, ghcr.io/librespeed/speedtest, -alpine)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV TELEMETRY=false
ENV ENABLE_ID_OBFUSCATION=false
ENV REDACT_IP_ADDRESSES=false
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Works with mobile versions too.
* Telemetry (optional)
* Results sharing (optional)
* Multiple Points of Test (optional)
* Connection stability test with latency charting, loss tracking, threshold alerts, and CSV export

![Screenrecording of a running Speedtest](https://speedtest.fdossena.com/mpot_v7.gif)

Expand All @@ -40,7 +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`, `design-switch.js`, `config.json`, `speedtest.js`, `speedtest_worker.js`, `favicon.ico`, and the `backend` folder.
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. 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.
Expand Down Expand Up @@ -72,6 +73,12 @@ If you want to contribute or develop with LibreSpeed, see [DEVELOPMENT.md](DEVEL

LibreSpeed supports both the classic and modern UI. The root `index.html` acts as a lightweight switcher and redirects to `index-classic.html` or `index-modern.html` based on `config.json` (`useNewDesign`) or URL overrides (`?design=new` / `?design=old`). For architecture and deployment details (including Docker behavior), see [DESIGN_SWITCH.md](DESIGN_SWITCH.md).

## Stability test

LibreSpeed includes a standalone connection stability test at `stability.html`, linked from both the classic and modern interfaces. It repeatedly measures ping over a selected duration and reports current, average, minimum, maximum, jitter, and failed request percentage values with a live chart.

The stability test can target the local LibreSpeed backend, one of the configured multiple points of test, or built-in external targets such as Google, Cloudflare, and Apple. It also supports optional latency threshold alerts and CSV export of the collected samples. Docker deployments copy `stability.html` and `stability_worker.js` into the web root and reuse the same server list configuration as the main UI.

## Docker

A docker image is available on [GitHub](https://github.com/librespeed/speedtest/pkgs/container/speedtest), check our [docker documentation](doc_docker.md) for more info about it.
Expand Down
20 changes: 16 additions & 4 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ rm -rf /var/www/html/*

# Copy frontend files
cp /speedtest/*.js /var/www/html/
cp /speedtest/stability.html /var/www/html/

# Copy design switch files
cp /speedtest/config.json /var/www/html/
Expand All @@ -52,6 +53,11 @@ else
fi


# Copy servers.json for stability page (frontend/dual modes)
if [[ "$MODE" == "frontend" || "$MODE" == "dual" ]]; then
cp /servers.json /var/www/html/servers.json
fi

# Set up backend side for standlone modes
if [[ "$MODE" == "standalone" || "$MODE" == "dual" ]]; then
cp -r /speedtest/backend/ /var/www/html/backend
Expand All @@ -73,14 +79,14 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]];
cp /speedtest/index.html /var/www/html/
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

# Copy frontend config files
cp /speedtest/frontend/settings.json /var/www/html/settings.json 2>/dev/null || true
if [ -f /servers.json ]; then
Expand All @@ -96,6 +102,12 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]];
SERVER_LIST_URL_ESCAPED=$(printf '%s\n' "$SERVER_LIST_URL" | sed 's/[&/\\]/\\&/g; s/\$/\\$/g')
sed -i "s/var SPEEDTEST_SERVERS = \"server-list.json\";/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";/" /var/www/html/index-modern.html
sed -i "s/var SPEEDTEST_SERVERS = \\[/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";\\n\\t\\t\\/\\*/" /var/www/html/index-classic.html
sed -i "s/var SPEEDTEST_SERVERS = \"server-list.json\";/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";/" /var/www/html/stability.html
fi

# The stability page reads the same local server list as the main UI when present.
if [ -f /var/www/html/server-list.json ]; then
cp /var/www/html/server-list.json /var/www/html/servers.json
fi

# Replace title placeholders if TITLE is set
Expand All @@ -117,7 +129,7 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]];
TAGLINE_ESCAPED=$(sed_escape "$TAGLINE_HTML_ESCAPED")
sed -i "s/<p class=\"tagline\">No Flash, No Java, No Websockets, No Bullsh\\*t<\\/p>/<p class=\"tagline\">$TAGLINE_ESCAPED<\\/p>/g" /var/www/html/index-modern.html
fi

# Support legacy EMAIL env var as fallback for GDPR_EMAIL
if [ -z "$GDPR_EMAIL" ] && [ ! -z "$EMAIL" ]; then
echo "WARNING: EMAIL env var is deprecated, please use GDPR_EMAIL instead" >&2
Expand All @@ -129,7 +141,7 @@ if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]];
if [ ! -z "$GDPR_EMAIL" ]; then
# Escape special sed characters: & (replacement), / (delimiter), \ (escape), $ (variable)
GDPR_EMAIL_ESCAPED=$(printf '%s\n' "$GDPR_EMAIL" | sed 's/[&/\\]/\\&/g; s/\$/\\$/g')

for html_file in /var/www/html/index-modern.html /var/www/html/index-classic.html; do
if [ -f "$html_file" ]; then
sed -i "s/TO BE FILLED BY DEVELOPER/$GDPR_EMAIL_ESCAPED/g; s/PUT@YOUR_EMAIL.HERE/$GDPR_EMAIL_ESCAPED/g" "$html_file"
Expand Down
1 change: 1 addition & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ <h2>Upload</h2>
</main>
<footer>
<p class="source">
<a href="stability.html">stability test</a> |
<a href="https://github.com/librespeed/speedtest">source code</a>
</p>
</footer>
Expand Down
28 changes: 27 additions & 1 deletion frontend/javascript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,34 @@ function populateDropdown(servers) {
});
}

// Sort servers by country, then by city within the same country.
// Name formats: "City, Country", "City, Country (qualifier)", "City, Country, Provider", "Country"
const parseServerName = (name) => {
const parts = (name || "").split(",").map((s) => s.trim());
let country, city;
if (parts.length >= 3) {
// "City, Country, Provider" — use second part as country
country = parts[1];
city = parts[0];
} else if (parts.length === 2) {
country = parts[1];
city = parts[0];
} else {
country = parts[0];
city = "";
}
// Strip parenthetical qualifiers for sorting: "Germany (1) (Hetzner)" → "Germany"
country = country.replace(/\s*\([^)]*\)\s*/g, "").trim();
return { country, city };
};
const sorted = [...servers].sort((a, b) => {
const pa = parseServerName(a.name);
const pb = parseServerName(b.name);
return pa.country.localeCompare(pb.country) || pa.city.localeCompare(pb.city);
});

// Populate the list to choose from
servers.forEach((server) => {
sorted.forEach((server) => {
const item = document.createElement("li");
const link = document.createElement("a");
link.href = "#";
Expand Down
40 changes: 34 additions & 6 deletions index-classic.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,41 @@
s.selectServer(function (server) {
if (server != null) { //at least 1 server is available
I("loading").className = "hidden"; //hide loading message
//sort servers by country, then by city
//name formats: "City, Country", "City, Country (qualifier)", "City, Country, Provider", "Country"
function parseServerName(name) {
var parts = (name || "").split(",");
for (var p = 0; p < parts.length; p++) parts[p] = parts[p].trim();
var country, city;
if (parts.length >= 3) {
country = parts[1];
city = parts[0];
} else if (parts.length === 2) {
country = parts[1];
city = parts[0];
} else {
country = parts[0];
city = "";
}
country = country.replace(/\s*\([^)]*\)\s*/g, "").trim();
return { country: country, city: city };
}
var indexed = [];
for (var j = 0; j < SPEEDTEST_SERVERS.length; j++) {
indexed.push({ idx: j, server: SPEEDTEST_SERVERS[j] });
}
indexed.sort(function (a, b) {
var pa = parseServerName(a.server.name);
var pb = parseServerName(b.server.name);
return pa.country.localeCompare(pb.country) || pa.city.localeCompare(pb.city);
});
//populate server list for manual selection
for (var i = 0; i < SPEEDTEST_SERVERS.length; i++) {
if (SPEEDTEST_SERVERS[i].pingT == -1) continue;
for (var i = 0; i < indexed.length; i++) {
if (indexed[i].server.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;
option.value = indexed[i].idx;
option.textContent = indexed[i].server.name;
if (indexed[i].server === server) option.selected = true;
I("server").appendChild(option);
}
//show test UI
Expand Down Expand Up @@ -539,7 +567,7 @@ <h3>Share results</h3>
</div>
</div>
<a href="index.html?design=new">Try the modern design</a><br>
<a href="https://github.com/librespeed/speedtest">Source code</a>
<a href="stability.html">Stability Test</a> | <a href="https://github.com/librespeed/speedtest">Source code</a>
</div>
<div id="privacyPolicy" style="display:none">
<h2>Privacy Policy</h2>
Expand Down
2 changes: 1 addition & 1 deletion index-modern.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ <h2>Upload</h2>
</main>
<footer>
<p class="source">
<a href="https://github.com/librespeed/speedtest">source code</a>
<a href="stability.html">stability test</a> | <a href="https://github.com/librespeed/speedtest">source code</a>
</p>
</footer>

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"test": "echo \"No automated tests configured yet\" && exit 0",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"lint": "eslint speedtest.js speedtest_worker.js",
"lint:fix": "eslint --fix speedtest.js speedtest_worker.js",
"lint": "eslint speedtest.js speedtest_worker.js stability_worker.js",
"lint:fix": "eslint --fix speedtest.js speedtest_worker.js stability_worker.js",
"format": "prettier --write \"*.js\"",
"format:check": "prettier --check \"*.js\"",
"validate": "npm run format:check && npm run lint",
Expand Down Expand Up @@ -46,6 +46,8 @@
"files": [
"speedtest.js",
"speedtest_worker.js",
"stability_worker.js",
"stability.html",
"index.html",
"favicon.ico",
"backend/",
Expand Down
Loading
Loading