Initial commit
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
.git
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
*.log
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install required packages: curl, cron, busybox (httpd), ca-certificates
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl cron busybox ca-certificates gnupg dirmngr \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Ookla Speedtest CLI via packagecloud repository
|
||||||
|
RUN curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends speedtest \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create web root and data directories
|
||||||
|
RUN mkdir -p /var/www && mkdir -p /var/www/data/speed
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
# Copy web assets and scripts
|
||||||
|
COPY index.html /var/www/index.html
|
||||||
|
COPY run_speed_test.sh /usr/local/bin/run_speed_test.sh
|
||||||
|
COPY start.sh /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
# Ensure scripts are executable
|
||||||
|
RUN chmod +x /usr/local/bin/run_speed_test.sh /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
# Add cron job to run every hour at minute 0
|
||||||
|
RUN printf "0 * * * * root /usr/local/bin/run_speed_test.sh >> /var/log/cron.log 2>&1\n" > /etc/cron.d/speedtest-cron \
|
||||||
|
&& chmod 0644 /etc/cron.d/speedtest-cron
|
||||||
|
|
||||||
|
# Expose default HTTP port and declare data volume
|
||||||
|
VOLUME ["/var/www/data/speed"]
|
||||||
|
# Default port for http server inside container. You can override at runtime with -e PORT=xxxx
|
||||||
|
ENV PORT=8080
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start script will launch cron and the HTTP server
|
||||||
|
CMD ["/usr/local/bin/start.sh"]
|
||||||
48
README.md
48
README.md
@@ -1,3 +1,49 @@
|
|||||||
# speed-data-docker
|
# speed-data-docker
|
||||||
|
|
||||||
Docker container for Speed test app
|
Debian-based Docker image that runs hourly network speed tests (using speedtest-cli) and serves a simple web UI.
|
||||||
|
|
||||||
|
What the container provides
|
||||||
|
- Base: debian
|
||||||
|
- Installs: speedtest-cli
|
||||||
|
- Runs: a cron job to execute `run_speed_test.sh` hourly
|
||||||
|
- Serves: `index.html` and generated JS files on port 8080 via busybox httpd
|
||||||
|
|
||||||
|
Files added/edited
|
||||||
|
- `Dockerfile` - builds the image
|
||||||
|
- `start.sh` - entrypoint: starts cron and httpd
|
||||||
|
- `run_speed_test.sh` - existing script (copied into image)
|
||||||
|
|
||||||
|
Build locally
|
||||||
|
Make sure Docker is installed locally. From the project root run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t speed-data-app .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with a named Docker volume (recommended). The container serves on $PORT inside the container
|
||||||
|
# (default 8080). Map a host port to the container port with -p <host_port>:<container_port>.
|
||||||
|
docker volume create speed-data-volume
|
||||||
|
docker run -d --name speed-data -e PORT=8080 -p 8080:8080 --restart unless-stopped \
|
||||||
|
-v speed-data-volume:/var/www/data/speed \
|
||||||
|
speed-data-app
|
||||||
|
|
||||||
|
# Or run with a host bind mount (for direct access to files on the host):
|
||||||
|
docker run -d --name speed-data -e PORT=8080 -p 8080:8080 --restart unless-stopped \
|
||||||
|
-v /path/on/host/speed-data:/var/www/data/speed \
|
||||||
|
speed-data-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify
|
||||||
|
- Visit http://localhost:8080 to view `index.html` (which reads `speedtest.js` and `speedtest.short.js`).
|
||||||
|
- Cron runs at minute 0 every hour. Logs go to `/var/log/cron.log` inside the container.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Data files (`speedtest.js`, `speedtest.short.js`, and the raw JSON file) are written to `/var/www/data/speed` inside the container. That path is declared as a Docker VOLUME in the `Dockerfile` so you can mount a named volume or a host directory.
|
||||||
|
- If you mount a host directory, ensure the directory is writeable by the container process. You can either:
|
||||||
|
- run the container as root (not recommended), or
|
||||||
|
- chown/chmod the host directory appropriately (e.g., `chown 1000:1000 /path/on/host` or `chmod a+rw /path/on/host`) so the container can write files.
|
||||||
|
|
||||||
|
Reminder: I built the image successfully after you started Docker locally. If you'd like, I can remove/recreate the container on your machine; tell me to proceed and I'll run the commands here.
|
||||||
|
|||||||
474
index.html
Normal file
474
index.html
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Speedtest data</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@700&family=Inter:wght@400;900&display=swap" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style type="text/css">
|
||||||
|
/* highcharts */
|
||||||
|
.highcharts-figure,
|
||||||
|
.highcharts-data-table table {
|
||||||
|
/* min-width: 560px;
|
||||||
|
max-width: 1000px;*/
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-data-table table {
|
||||||
|
font-family: Verdana, sans-serif;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
margin: 10px auto;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
/*max-width: 500px;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-data-table caption {
|
||||||
|
padding: 1em 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-data-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-data-table td,
|
||||||
|
.highcharts-data-table th,
|
||||||
|
.highcharts-data-table caption {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-data-table thead tr,
|
||||||
|
.highcharts-data-table tr:nth-child(even) {
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-data-table tr:hover {
|
||||||
|
background: #f1f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* General */
|
||||||
|
body {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
h1, h2, h5 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recent boxes */
|
||||||
|
#recent_samples {
|
||||||
|
width: max-content;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
#recent .recent_sample {
|
||||||
|
display: block;
|
||||||
|
background-color: #eee;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
#recent .recent_sample .recent_dn,
|
||||||
|
#recent .recent_sample .recent_up {
|
||||||
|
font-family: "Fira Mono", sans-serif;
|
||||||
|
color: #777;
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
#recent .recent_sample .recent_dn {
|
||||||
|
color: #2caffe;
|
||||||
|
}
|
||||||
|
#recent .recent_sample .recent_up {
|
||||||
|
color: #544fc5;
|
||||||
|
padding-left: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent .recent_spd {
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent_box .recent_fld {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
.recent_box {
|
||||||
|
margin: 5px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.recent_box .recent_fld .recent_fld_name {
|
||||||
|
color: #777;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.recent_box .recent_fld .recent_fld_val {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
.recent_box .recent_fld .recent_fld_name,
|
||||||
|
.recent_box .recent_fld .recent_fld_val {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
#spinner {
|
||||||
|
border: 16px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 16px solid #544fc5;
|
||||||
|
border-bottom: 16px solid #2caffe;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
-webkit-animation: spin 2s linear infinite; /* Safari */
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
|
||||||
|
margin: 5em auto;
|
||||||
|
}
|
||||||
|
#spinner-desc {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 300px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safari */
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% { -webkit-transform: rotate(0deg); }
|
||||||
|
100% { -webkit-transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.result_url {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media only screen and (max-width: 1100px) {
|
||||||
|
#recent #recent_1 .recent_dn, #recent #recent_1 .recent_up {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
#recent #recent_2 .recent_dn, #recent #recent_2 .recent_up {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
#recent #recent_3 .recent_dn, #recent #recent_3 .recent_up,
|
||||||
|
#recent #recent_4 .recent_dn, #recent #recent_4 .recent_up,
|
||||||
|
#recent #recent_5 .recent_dn, #recent #recent_5 .recent_up {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent #recent_1 .recent_fld {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
#recent #recent_2 .recent_fld {
|
||||||
|
font-size: 0.5em;
|
||||||
|
}
|
||||||
|
#recent #recent_3 .recent_fld,
|
||||||
|
#recent #recent_4 .recent_fld,
|
||||||
|
#recent #recent_5 .recent_fld {
|
||||||
|
font-size: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent_box {
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent .recent_sample {
|
||||||
|
flex-basis: content;
|
||||||
|
margin: 5px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highcharts-figure {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 1100px) {
|
||||||
|
#recent #recent_1 .recent_dn, #recent #recent_1 .recent_up {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
#recent #recent_2 .recent_dn, #recent #recent_2 .recent_up {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
#recent #recent_3 .recent_dn, #recent #recent_3 .recent_up,
|
||||||
|
#recent #recent_4 .recent_dn, #recent #recent_4 .recent_up,
|
||||||
|
#recent #recent_5 .recent_dn, #recent #recent_5 .recent_up {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent #recent_1 .recent_fld {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
#recent #recent_2 .recent_fld {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
#recent #recent_3 .recent_fld,
|
||||||
|
#recent #recent_4 .recent_fld,
|
||||||
|
#recent #recent_5 .recent_fld {
|
||||||
|
font-size: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent {
|
||||||
|
display: inline-block;
|
||||||
|
width: max-content;
|
||||||
|
height: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recent .recent_sample {
|
||||||
|
float: left;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="spinner"></div>
|
||||||
|
<div id="spinner-desc">Preparing data ...</div>
|
||||||
|
<div id="recent_samples">
|
||||||
|
<div id="recent">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="clear:both"></div>
|
||||||
|
<figure class="highcharts-figure">
|
||||||
|
<div id="container"></div>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<script src="https://code.highcharts.com/highcharts.js"></script>
|
||||||
|
<script src="https://code.highcharts.com/modules/boost.js"></script>
|
||||||
|
<script src="https://code.highcharts.com/modules/exporting.js"></script>
|
||||||
|
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var shortContent = ($(window).width() < 1000);
|
||||||
|
var refDate = new Date;
|
||||||
|
var isp = "";
|
||||||
|
|
||||||
|
function getDate(d) {
|
||||||
|
var dt = Date.parse(d);
|
||||||
|
dt = dt - refDate.getTimezoneOffset() * 60 * 1000;
|
||||||
|
return dt;
|
||||||
|
}
|
||||||
|
function getFmtDate(d) {
|
||||||
|
const dt = new Date(d);
|
||||||
|
return dt.toDateString()
|
||||||
|
+ ", " + dt.getHours() + ":" + dt.getMinutes() + ":" + dt.getSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadData(d) {
|
||||||
|
var arr = [];
|
||||||
|
d.forEach((sd) => {
|
||||||
|
if (sd.error == undefined) {
|
||||||
|
var dt = getDate(sd.timestamp);
|
||||||
|
arr.push([
|
||||||
|
dt,
|
||||||
|
sd.download.bandwidth*8/1000000
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
function getUploadData(d) {
|
||||||
|
var arr = [];
|
||||||
|
d.forEach((sd) => {
|
||||||
|
if (sd.error == undefined) {
|
||||||
|
var dt = getDate(sd.timestamp);
|
||||||
|
arr.push([
|
||||||
|
dt,
|
||||||
|
sd.upload.bandwidth*8/1000000
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare_chart_data() {
|
||||||
|
dndata = getDownloadData(speeddata);
|
||||||
|
updata = getUploadData(speeddata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_chart() {
|
||||||
|
prepare_chart_data();
|
||||||
|
|
||||||
|
console.time('line');
|
||||||
|
|
||||||
|
Highcharts.chart('container', {
|
||||||
|
|
||||||
|
chart: {
|
||||||
|
type: 'spline',
|
||||||
|
zoomType: 'x',
|
||||||
|
height: '40%'
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
text: 'Trend in last 500 samples'
|
||||||
|
},
|
||||||
|
|
||||||
|
subtitle: {
|
||||||
|
text: 'Measured hourly by Speedtest CLI'
|
||||||
|
},
|
||||||
|
|
||||||
|
accessibility: {
|
||||||
|
screenReaderSection: {
|
||||||
|
beforeChartFormat: '<{headingTagName}>{chartTitle}</{headingTagName}><div>{chartSubtitle}</div><div>{chartLongdesc}</div><div>{xAxisDescription}</div><div>{yAxisDescription}</div>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
valueDecimals: 2
|
||||||
|
},
|
||||||
|
|
||||||
|
xAxis: {
|
||||||
|
type: 'datetime'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
title: {
|
||||||
|
text: 'Speed (MB/s)'
|
||||||
|
},
|
||||||
|
plotBands: [{ // Fast
|
||||||
|
from: 900,
|
||||||
|
to: 1000,
|
||||||
|
color: 'rgba(68, 170, 68, 0.1)',
|
||||||
|
label: {
|
||||||
|
text: 'Fast',
|
||||||
|
style: {
|
||||||
|
color: '#606060'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { // Slow
|
||||||
|
from: 300,
|
||||||
|
to: 499,
|
||||||
|
color: 'rgba(170, 50, 50, 0.1)',
|
||||||
|
label: {
|
||||||
|
text: 'Slow',
|
||||||
|
style: {
|
||||||
|
color: '#606060'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { // Crawling
|
||||||
|
from: 0,
|
||||||
|
to: 299,
|
||||||
|
color: 'rgba(200, 50, 50, 0.3)',
|
||||||
|
label: {
|
||||||
|
text: 'Crawling',
|
||||||
|
style: {
|
||||||
|
color: '#606060'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: dndata,
|
||||||
|
lineWidth: 2,
|
||||||
|
name: 'Download (MB/s)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: updata,
|
||||||
|
lineWidth: 2,
|
||||||
|
name: 'Upload (MB/s)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
});
|
||||||
|
console.timeEnd('line');
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_speed(num) {
|
||||||
|
return (num*8/1000000).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_up_dn_icon(dt, dtprev, isdn) {
|
||||||
|
var icon = "•";
|
||||||
|
var colr = "#0a0";
|
||||||
|
var titl = "Upward trend";
|
||||||
|
|
||||||
|
if (isdn) {
|
||||||
|
if (dt.download.bandwidth < dtprev.download.bandwidth) {
|
||||||
|
colr = "#a00";
|
||||||
|
titl = "Downward trend";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dt.upload.bandwidth < dtprev.upload.bandwidth) {
|
||||||
|
colr = "#a00";
|
||||||
|
titl = "Downward trend";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<span style='color:" + colr + "' title='" + titl + "'>" + icon + "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_one_sample(dt, dtprev, idx, dest) {
|
||||||
|
dest.append($("<div class='recent_sample' id='recent_" + idx + "'></div>")
|
||||||
|
.append($("<div class='recent_spd_tbl'></div>")
|
||||||
|
.append($("<table></table>")
|
||||||
|
.append($("<tr class='recent_spd'></tr>")
|
||||||
|
.append($("<td class='recent_dn' title='Download speed in MB/s'>↓"
|
||||||
|
+ get_speed(dt.download.bandwidth) + get_up_dn_icon(dt, dtprev, 1) + "</td>"),
|
||||||
|
$("<td class='recent_up' title='Upload speed in MB/s'>↑"
|
||||||
|
+ get_speed(dt.upload.bandwidth) + get_up_dn_icon(dt, dtprev, 0) + "</td>")))))
|
||||||
|
.append($("<div class='recent_box'></div>")
|
||||||
|
.append($("<div class='recent_fld'></div>")
|
||||||
|
.append($("<div class='recent_fld_val'>" + getFmtDate(dt.timestamp) + "<a class='result_url' target='_blank' href='"
|
||||||
|
+ dt.result.url + "'> ➔</a></div>")),
|
||||||
|
$("<div class='recent_fld'></div>")
|
||||||
|
.append($("<div class='recent_fld_val'>" + dt.server.name + " ("
|
||||||
|
+ dt.server.location + ", "
|
||||||
|
+ dt.server.country + ")</div>")))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_data() {
|
||||||
|
var data_js = (shortContent) ? "speedtest.short.js" : "speedtest.js";
|
||||||
|
var data_path = "/data/speed/" + data_js;
|
||||||
|
|
||||||
|
$.getScript(data_path, function() { show_content() });
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_content() {
|
||||||
|
var snaps = 5;
|
||||||
|
|
||||||
|
isp = speeddata[speeddata.length-1].isp;
|
||||||
|
console.log(isp);
|
||||||
|
|
||||||
|
for (let itr = speeddata.length-1, idx = 1; idx <= snaps; itr --, idx ++ ) {
|
||||||
|
show_one_sample(speeddata[itr], speeddata[itr-1], idx, $('#recent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shortContent) {
|
||||||
|
show_chart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On done
|
||||||
|
$("#spinner-desc").hide();
|
||||||
|
$("#spinner")
|
||||||
|
.after("<h2>Most recent samples</h2>")
|
||||||
|
.after("<h5>(ISP: " + isp + ")</h5>")
|
||||||
|
.after("<h1>Network Speed Data</h1>")
|
||||||
|
.hide();
|
||||||
|
console.timeEnd("speedtest");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
console.time("speedtest");
|
||||||
|
load_data();
|
||||||
|
});
|
||||||
|
// vim: ts=4 sts=4 sw=4 et
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
82
run_speed_test.sh
Executable file
82
run_speed_test.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
RAW_DATA=/var/www/data/speed/raw.speedtest.json
|
||||||
|
TMP_DATA=/tmp/speedtest.js
|
||||||
|
WEB_DATA=/var/www/data/speed/speedtest.js
|
||||||
|
WEB_SHORT_DATA=/var/www/data/speed/speedtest.short.js
|
||||||
|
|
||||||
|
/usr/bin/speedtest --format=json --accept-license >> ${RAW_DATA} 2>/dev/null || speedtest --format=json --accept-license >> ${RAW_DATA} 2>/dev/null
|
||||||
|
|
||||||
|
# Prepare information for web display
|
||||||
|
tail -500 ${RAW_DATA} > ${TMP_DATA} && \
|
||||||
|
sed -i -e 's/$/,/' ${TMP_DATA} && \
|
||||||
|
echo "var speeddata = [" > ${WEB_DATA} && \
|
||||||
|
cat ${TMP_DATA} >> ${WEB_DATA} && \
|
||||||
|
echo "];" >> ${WEB_DATA}
|
||||||
|
# Prepare short information for quick web display
|
||||||
|
tail -6 ${RAW_DATA} > ${TMP_DATA} && \
|
||||||
|
sed -i -e 's/$/,/' ${TMP_DATA} && \
|
||||||
|
echo "var speeddata = [" > ${WEB_SHORT_DATA} && \
|
||||||
|
cat ${TMP_DATA} >> ${WEB_SHORT_DATA} && \
|
||||||
|
echo "];" >> ${WEB_SHORT_DATA}
|
||||||
|
|
||||||
|
# Trim raw data to keep it lean
|
||||||
|
tail -1000 ${RAW_DATA} > ${TMP_DATA} && \
|
||||||
|
cp ${TMP_DATA} ${RAW_DATA}
|
||||||
|
|
||||||
|
# Above arguments produce the following output in single line. Expanded for readability below:
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# "type":"result",
|
||||||
|
# "timestamp":"2023-04-24T03:57:04Z",
|
||||||
|
# "ping":{
|
||||||
|
# "jitter":0.193,
|
||||||
|
# "latency":1.751,
|
||||||
|
# "low":1.530,
|
||||||
|
# "high":1.982
|
||||||
|
# },
|
||||||
|
# "download":{
|
||||||
|
# "bandwidth":85545427,
|
||||||
|
# "bytes":1017170768,
|
||||||
|
# "elapsed":12616,
|
||||||
|
# "latency":{
|
||||||
|
# "iqm":1.894,
|
||||||
|
# "low":1.374,
|
||||||
|
# "high":2.916,
|
||||||
|
# "jitter":0.300
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "upload":{
|
||||||
|
# "bandwidth":91982628,
|
||||||
|
# "bytes":331336109,
|
||||||
|
# "elapsed":3601,
|
||||||
|
# "latency":{
|
||||||
|
# "iqm":4.135,
|
||||||
|
# "low":2.281,
|
||||||
|
# "high":5.322,
|
||||||
|
# "jitter":0.498
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "packetLoss":0,
|
||||||
|
# "isp":"GigaMonster",
|
||||||
|
# "interface":{
|
||||||
|
# "internalIp":"192.168.1.200",
|
||||||
|
# "name":"eno1",
|
||||||
|
# "macAddr":"B8:AE:ED:7D:8A:36",
|
||||||
|
# "isVpn":false,
|
||||||
|
# "externalIp":"24.72.150.52"
|
||||||
|
# },
|
||||||
|
# "server":{
|
||||||
|
# "id":36565,
|
||||||
|
# "host":"speedpdx2.ortelco.net",
|
||||||
|
# "port":8080,
|
||||||
|
# "name":"OTC Connections",
|
||||||
|
# "location":"Portland, OR",
|
||||||
|
# "country":"United States",
|
||||||
|
# "ip":"38.87.96.27"},
|
||||||
|
# "result":{
|
||||||
|
# "id":"9b547022-8e1b-42c5-871c-2b6810b641bb",
|
||||||
|
# "url":"https://www.speedtest.net/result/c/9b547022-8e1b-42c5-871c-2b6810b641bb",
|
||||||
|
# "persisted":true
|
||||||
|
# }
|
||||||
|
# }
|
||||||
42
start.sh
Normal file
42
start.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Ensure data directory exists
|
||||||
|
mkdir -p /var/www/data/speed
|
||||||
|
|
||||||
|
# Ensure raw data file exists
|
||||||
|
touch /var/www/data/speed/raw.speedtest.json
|
||||||
|
|
||||||
|
# If the data directory is a mounted volume it may be owned by root; allow writes
|
||||||
|
# by ensuring the directory is writable.
|
||||||
|
chown -R root:root /var/www/data || true
|
||||||
|
chmod -R a+rwX /var/www/data || true
|
||||||
|
|
||||||
|
# Run the initial speed test once (in background) to populate files if possible
|
||||||
|
# We run it in background so server starts promptly. The cron will run hourly.
|
||||||
|
/usr/local/bin/run_speed_test.sh || true &
|
||||||
|
|
||||||
|
# Ensure the cron job exists in root's crontab (so `crontab -l` shows it)
|
||||||
|
CRON_ENTRY="* * * * * /usr/local/bin/run_speed_test.sh >> /var/log/cron.log 2>&1"
|
||||||
|
if crontab -l 2>/dev/null | grep -F "$CRON_ENTRY" >/dev/null 2>&1; then
|
||||||
|
echo "cron entry already present"
|
||||||
|
else
|
||||||
|
(crontab -l 2>/dev/null; echo "$CRON_ENTRY") | crontab -
|
||||||
|
echo "installed cron entry into root crontab"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Debian cron in background
|
||||||
|
service cron start || cron || true
|
||||||
|
|
||||||
|
PORT=${PORT:-8080}
|
||||||
|
|
||||||
|
# Start busybox httpd serving /var/www on configured port in foreground
|
||||||
|
if command -v busybox >/dev/null 2>&1; then
|
||||||
|
echo "starting busybox httpd on port ${PORT} serving /var/www"
|
||||||
|
busybox httpd -f -p ${PORT} -h /var/www
|
||||||
|
else
|
||||||
|
echo "warning: busybox httpd not found; container will keep running with cron only" >&2
|
||||||
|
# keep the script running so container doesn't exit
|
||||||
|
tail -f /var/log/cron.log
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user