added control page
This commit is contained in:
@@ -2,4 +2,5 @@ import os
|
||||
|
||||
DB_PATH = os.getenv("DB_PATH") or "speedtest.db"
|
||||
PING_INTERVAL = int(os.getenv("PING_INTERVAL", "5"))
|
||||
SPEEDTEST_ENABLED = os.getenv("SPEEDTEST_ENABLED", "true").strip().lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
96
measurement/control.py
Normal file
96
measurement/control.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
from db import init_settings_db, get_setting, set_setting
|
||||
from config import SPEEDTEST_ENABLED
|
||||
|
||||
GRAFANA_PORT = os.getenv("GRAFANA_PORT", "3000")
|
||||
|
||||
PAGE = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Network Monitor</title>
|
||||
<style>
|
||||
body {{ font-family: sans-serif; max-width: 26rem; margin: 4rem auto; padding: 0 1rem; text-align: center; }}
|
||||
.state {{ font-size: 1.4rem; margin: 1.5rem 0; }}
|
||||
.on {{ color: #1a7f37; }}
|
||||
.off {{ color: #b35900; }}
|
||||
button {{ font-size: 1.3rem; padding: 1rem 2.5rem; border-radius: 0.5rem; border: none; color: white; cursor: pointer; }}
|
||||
.turn-off {{ background: #b35900; }}
|
||||
.turn-on {{ background: #1a7f37; }}
|
||||
.hint {{ color: #666; margin-top: 1.5rem; }}
|
||||
a {{ color: #3274d9; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Network Monitor</h1>
|
||||
<p class="state">Speedtests are <strong class="{cls}">{state}</strong></p>
|
||||
<form method="post" action="/toggle">
|
||||
<button class="{btn_cls}" type="submit">{btn_label}</button>
|
||||
</form>
|
||||
<p class="hint">A speedtest briefly uses the full connection about every 15 minutes.
|
||||
Turn them off while gaming or in calls, and back on afterwards.</p>
|
||||
<p><a href="{grafana_url}">Open the dashboard</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def speedtests_on() -> bool:
|
||||
return get_setting("speedtest_enabled") == "1"
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def grafana_url(self) -> str:
|
||||
host = self.headers.get("Host", "localhost")
|
||||
if host.startswith("["): # IPv6 literal like [::1]:80
|
||||
host = host.split("]")[0] + "]"
|
||||
else:
|
||||
host = host.split(":")[0]
|
||||
# kiosk mode shows just the dashboard without the Grafana UI around it;
|
||||
# Escape exits it for anyone who wants the full interface
|
||||
return f"http://{host}:{GRAFANA_PORT}/d/speed-tests-v2?kiosk"
|
||||
|
||||
def do_GET(self):
|
||||
if self.path != "/":
|
||||
self.send_error(404)
|
||||
return
|
||||
on = speedtests_on()
|
||||
body = PAGE.format(
|
||||
cls="on" if on else "off",
|
||||
state="ON" if on else "OFF",
|
||||
btn_cls="turn-off" if on else "turn-on",
|
||||
btn_label="Turn off" if on else "Turn on",
|
||||
grafana_url=self.grafana_url(),
|
||||
).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
if self.path != "/toggle":
|
||||
self.send_error(404)
|
||||
return
|
||||
set_setting("speedtest_enabled", "0" if speedtests_on() else "1")
|
||||
self.send_response(303)
|
||||
self.send_header("Location", "/")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def run(port: int = 80):
|
||||
init_settings_db()
|
||||
# Seed the toggle from the env var on first start; after that the DB row
|
||||
# written by the toggle button is the single source of truth.
|
||||
if get_setting("speedtest_enabled") is None:
|
||||
set_setting("speedtest_enabled", "1" if SPEEDTEST_ENABLED else "0")
|
||||
server = HTTPServer(("", port), Handler)
|
||||
print(f"Control page listening on :{port}", flush=True)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -44,6 +44,27 @@ def init_ping_db(db_path=DB_PATH):
|
||||
pass # column already exists
|
||||
conn.close()
|
||||
|
||||
def init_settings_db(db_path=DB_PATH):
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_setting(key: str, db_path=DB_PATH) -> str | None:
|
||||
conn = sqlite3.connect(db_path)
|
||||
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
|
||||
conn.close()
|
||||
return row[0] if row else None
|
||||
|
||||
def set_setting(key: str, value: str, db_path=DB_PATH):
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute(
|
||||
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
(key, value)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def insert_ping_result(target_ip: str, success: bool, latency_ms: float | None, is_gateway: bool = False, db_path=DB_PATH):
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from db import init_db, insert_result
|
||||
from db import init_db, insert_result, init_settings_db, get_setting
|
||||
from config import SPEEDTEST_ENABLED
|
||||
|
||||
def speedtests_enabled() -> bool:
|
||||
# The control page writes the live toggle to the settings table;
|
||||
# the env var only covers the case where the toggle was never used.
|
||||
init_settings_db()
|
||||
value = get_setting("speedtest_enabled")
|
||||
if value is None:
|
||||
return SPEEDTEST_ENABLED
|
||||
return value == "1"
|
||||
|
||||
def run_test_and_save():
|
||||
if not speedtests_enabled():
|
||||
print("Speedtests are disabled, skipping run")
|
||||
sys.exit(0)
|
||||
|
||||
print("==== Running Ookla Speedtest ====")
|
||||
init_db()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user