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
|
||||
|
||||
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