Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 30 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,12 @@ def create_test(

@app.get("/ping")
def run_ping(host: str, count: int = 4):
"""Run a ping test against a host and return the raw output."""
"""Run a ping test against a host and return the raw output.

The response also includes the parsed round trip time in milliseconds when
available. This value can be used by the frontend to record a test result
via the ``/tests`` API.
"""
try:
result = subprocess.run(
["ping", "-c", str(count), host],
Expand All @@ -188,7 +193,11 @@ def run_ping(host: str, count: int = 4):
timeout=10,
)
if result.returncode == 0:
return {"output": result.stdout}
data = {"output": result.stdout}
match = re.search(r"time[=<]([0-9.]+) ms", result.stdout)
if match:
data["ping_ms"] = float(match.group(1))
return data
return {"error": result.stderr or "Ping failed"}
except FileNotFoundError:
return {"error": "ping command not found"}
Expand Down Expand Up @@ -222,3 +231,22 @@ def run_traceroute(host: str, download: bool = False):
return {"error": "traceroute command not found"}
except Exception as exc:
return {"error": str(exc)}


@app.get("/speedtest")
def run_speedtest():
"""Run a basic network speed test.

The test returns the download and upload speeds in bits per second. Any
errors from the underlying ``speedtest`` module are captured and returned to
the caller so the frontend can display a helpful message.
"""
try:
import speedtest # type: ignore

st = speedtest.Speedtest()
download = st.download()
upload = st.upload()
return {"download": download, "upload": upload}
except Exception as exc:
return {"error": str(exc)}
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pydantic
requests
jinja2
httpx
speedtest-cli
70 changes: 67 additions & 3 deletions backend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
padding: 0.5rem;
overflow-x: auto;
}
table, th, td {
border: 1px solid #0f0;
border-collapse: collapse;
padding: 0.3rem;
}
</style>
</head>
<body>
Expand All @@ -36,23 +41,82 @@ <h1>Your Connection Info</h1>
</ul>

<h2>Recent Tests</h2>
<ul>
<table>
<tr>
<th>IP</th>
<th>Location</th>
<th>ASN</th>
<th>ISP</th>
<th>Ping</th>
<th>Recorded</th>
</tr>
{% for r in records %}
<li>{{ r.client_ip|mask_ip }}{% if r.location %} - {{ r.location }}{% endif %}{% if r.ping_ms %} - {{ '%.2f'|format(r.ping_ms) }} ms{% endif %} - {{ r.timestamp|short_ts }}</li>
<tr>
<td>{{ r.client_ip|mask_ip }}</td>
<td>{{ r.location or '' }}</td>
<td>{{ r.asn or '' }}</td>
<td>{{ r.isp or '' }}</td>
<td>{% if r.ping_ms %}{{ '%.2f'|format(r.ping_ms) }} ms{% endif %}</td>
<td>{{ r.timestamp|short_ts }}</td>
</tr>
{% endfor %}
</ul>
</table>

<h2>Manual Ping Test</h2>
<input id="host" value="8.8.8.8" />
<button onclick="runPing()">Ping</button>
<pre id="ping-output"></pre>

<h2>Traceroute</h2>
<input id="trace-host" value="8.8.8.8" />
<button onclick="runTraceroute()">Traceroute</button>
<button onclick="downloadTraceroute()">Download</button>
<pre id="trace-output"></pre>

<h2>Speed Test</h2>
<button onclick="runSpeedtest()">Run Speed Test</button>
<pre id="speed-output"></pre>

<script>
async function runPing() {
const host = document.getElementById('host').value;
const res = await fetch('/ping?host=' + encodeURIComponent(host));
const data = await res.json();
document.getElementById('ping-output').textContent = data.output || data.error || 'No output';
if (data.ping_ms) {
await fetch('/tests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test_target: host, ping_ms: data.ping_ms })
});
}
}

async function runTraceroute(download = false) {
const host = document.getElementById('trace-host').value;
if (download) {
window.location = '/traceroute?host=' + encodeURIComponent(host) + '&download=true';
return;
}
const res = await fetch('/traceroute?host=' + encodeURIComponent(host));
const data = await res.json();
document.getElementById('trace-output').textContent = data.output || data.error || 'No output';
}

function downloadTraceroute() {
runTraceroute(true);
}

async function runSpeedtest() {
const res = await fetch('/speedtest');
const data = await res.json();
if (data.download) {
const down = (data.download / 1e6).toFixed(2);
const up = (data.upload / 1e6).toFixed(2);
document.getElementById('speed-output').textContent = `Download: ${down} Mbps\nUpload: ${up} Mbps`;
} else {
document.getElementById('speed-output').textContent = data.error || 'No output';
}
}
</script>
</body>
Expand Down
1 change: 1 addition & 0 deletions backend/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def test_ping_endpoint_localhost():
data = res.json()
assert "output" in data
assert "ttl" in data["output"]
assert "ping_ms" in data


def test_traceroute_endpoint_download():
Expand Down