Reorganized files

This commit is contained in:
2025-12-20 19:20:48 -08:00
parent ea0a34258b
commit edd3693fb6
5 changed files with 8 additions and 8 deletions

474
src/index.html Normal file
View 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 = "&bull;";
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>

84
src/run_speed_test.sh Executable file
View File

@@ -0,0 +1,84 @@
#!/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}
echo "[$(date)] Updated speed 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
src/start.sh Normal file
View File

@@ -0,0 +1,42 @@
#!/bin/sh
set -e
CRON_MINUTE=${CRON_MINUTE:-10}
PORT=${PORT:-8080}
# 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
# Ensure the cron job exists in Cron
CRON_FILE=/etc/cron.d/speedtest-cron
if [ -f ${CRON_FILE} ] && grep run_speed_test ${CRON_FILE}; then
echo "cron entry already present"
else
echo ${CRON_MINUTE} '* * * * root /usr/local/bin/run_speed_test.sh >> /var/log/cron.log 2>&1' > ${CRON_FILE}
fi
# Start Debian cron in background
service cron start || cron || 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 >> /var/log/cron.log 2>&1 || true &
# 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