Commit 88d692e3 authored by Vitaly Lipatov's avatar Vitaly Lipatov

check_system/check-blocked: add --json output and web dashboard

Add --json FILE option to check-blocked.sh for machine-readable output. Add check-blocked-web.py: minimal web server displaying results as a color-coded HTML table with auto-refresh every 5 min. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 645168f2
#!/usr/bin/python3
"""
Web dashboard for check-blocked.sh results.
Reads JSON output from check-blocked.sh and serves an HTML page
with color-coded site availability table.
GET / — HTML dashboard
GET /api/results — raw JSON results
"""
import http.server
import json
import os
import sys
import time
import urllib.parse
LISTEN_HOST = "0.0.0.0"
LISTEN_PORT = 80
RESULTS_FILE = os.environ.get(
"CHECK_BLOCKED_JSON",
"/root/check-blocked-results.json",
)
HTML_PAGE = """\
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Site Block Check — %(hostname)s</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5;
color: #333; max-width: 1100px; margin: 0 auto; padding: 20px; }
h1 { margin-bottom: 4px; font-size: 1.4em; }
.subtitle { color: #666; margin-bottom: 16px; font-size: 0.9em; }
.meta { color: #666; font-size: 0.85em; margin-bottom: 16px; }
.meta span { margin-right: 16px; }
table { width: 100%%; border-collapse: collapse; background: #fff;
border-radius: 6px; overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
th { background: #f8f9fa; padding: 10px 12px; text-align: center;
font-size: 0.85em; font-weight: 600; border-bottom: 2px solid #dee2e6; }
th:first-child { text-align: left; }
td { padding: 8px 12px; text-align: center; border-bottom: 1px solid #eee;
font-size: 0.9em; }
td:first-child { text-align: left; font-family: monospace; font-weight: 500; }
td:first-child a { color: #333; text-decoration: none; }
td:first-child a:hover { color: #4a90d9; text-decoration: underline; }
tr:last-child td { border-bottom: none; }
tr:hover { background: #f8f9fa; }
.ok { background: #d4edda; color: #155724; }
.slow { background: #fff3cd; color: #856404; }
.http { background: #ffe0cc; color: #8a4500; }
.block { background: #f8d7da; color: #721c24; }
.speed { display: block; font-size: 0.75em; color: #666; margin-top: 2px; }
.legend { margin-top: 16px; color: #888; font-size: 0.8em; }
.legend span { margin-right: 16px; }
.legend-dot { display: inline-block; width: 10px; height: 10px;
border-radius: 2px; margin-right: 3px; vertical-align: middle; }
.footer { margin-top: 12px; color: #aaa; font-size: 0.75em; text-align: center; }
.no-data { text-align: center; padding: 40px; color: #999; }
</style>
</head>
<body>
<h1>Site Block Check</h1>
<p class="subtitle">%(hostname)s</p>
<div id="content"><div class="no-data">Loading...</div></div>
<div class="legend">
<span><span class="legend-dot" style="background:#d4edda"></span> OK</span>
<span><span class="legend-dot" style="background:#fff3cd"></span> Slow (&lt;0.10 MB/s)</span>
<span><span class="legend-dot" style="background:#ffe0cc"></span> HTTP error</span>
<span><span class="legend-dot" style="background:#f8d7da"></span> Blocked</span>
</div>
<div class="footer">Auto-refresh every 5 min &middot; <a href="/api/results">JSON API</a></div>
<script>
function el(tag, attrs, children) {
var e = document.createElement(tag);
if (attrs) Object.keys(attrs).forEach(function(k) {
if (k === 'cls') e.className = attrs[k];
else if (k === 'text') e.textContent = attrs[k];
else e.setAttribute(k, attrs[k]);
});
if (children) children.forEach(function(c) { e.appendChild(c); });
return e;
}
function render(data) {
var c = document.getElementById('content');
c.textContent = '';
if (!data || !data.results || !data.results.length) {
c.appendChild(el('div', {cls: 'no-data', text: 'No results yet. Waiting for first check...'}));
return;
}
var ts = new Date(data.timestamp * 1000);
var ago = Math.round((Date.now()/1000 - data.timestamp) / 60);
var meta = el('div', {cls: 'meta'}, [
el('span', {text: 'Checked: ' + ts.toLocaleString('ru')}),
el('span', {text: '(' + ago + ' min ago)'})
]);
c.appendChild(meta);
// Build header row
var headerCells = [el('th', {text: 'Site'})];
data.gateways.forEach(function(gw) {
headerCells.push(el('th', {text: gw}));
});
var headerRow = el('tr', null, headerCells);
var thead = el('thead', null, [headerRow]);
// Build body rows
var rows = [];
data.results.forEach(function(r) {
var link = el('a', {href: 'https://' + r.site + '/', text: r.site, target: '_blank'});
var cells = [el('td', null, [link])];
data.gateways.forEach(function(gw) {
var ch = r.checks[gw];
if (!ch) { cells.push(el('td', {text: '-'})); return; }
var cls, label;
if (ch.status === 'OK') { cls = 'ok'; label = 'OK'; }
else if (ch.status === 'SLOW') { cls = 'slow'; label = 'SLOW'; }
else if (ch.status === 'BLOCK') { cls = 'block'; label = 'BLOCK'; }
else { cls = 'http'; label = ch.status; }
var children = [document.createTextNode(label)];
if (ch.speed !== null) {
children.push(el('span', {cls: 'speed', text: ch.speed + ' MB/s'}));
}
cells.push(el('td', {cls: cls}, children));
});
rows.push(el('tr', null, cells));
});
var tbody = el('tbody', null, rows);
c.appendChild(el('table', null, [thead, tbody]));
}
function refresh() {
fetch('/api/results')
.then(function(r) { return r.json(); })
.then(render)
.catch(function() {
var c = document.getElementById('content');
c.textContent = '';
c.appendChild(el('div', {cls: 'no-data', text: 'Failed to load results'}));
});
}
refresh();
setInterval(refresh, 5 * 60 * 1000);
</script>
</body>
</html>
"""
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), fmt % args))
def send_html(self, html, code=200):
body = html.encode("utf-8")
self.send_response(code)
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 send_json(self, data, code=200):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def read_results(self):
try:
with open(RESULTS_FILE, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return None
def do_GET(self):
path = urllib.parse.urlparse(self.path).path
if path == "/":
hostname = "check-blocked"
data = self.read_results()
if data:
hostname = data.get("hostname", hostname)
self.send_html(HTML_PAGE % {"hostname": hostname})
elif path == "/api/results":
data = self.read_results()
if data:
self.send_json(data)
else:
self.send_json({"error": "no results yet"}, 404)
else:
self.send_json({"error": "not found"}, 404)
def main():
print("Results file: %s" % RESULTS_FILE, file=sys.stderr)
server = http.server.ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), Handler)
print("Listening on %s:%d" % (LISTEN_HOST, LISTEN_PORT), file=sys.stderr)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
if __name__ == "__main__":
main()
#!/bin/sh #!/bin/sh
# Check if sites are blocked/throttled through different gateways # Check if sites are blocked/throttled through different gateways
# Usage: check-blocked.sh [site...] # Usage: check-blocked.sh [--json FILE] [site...]
# Without arguments checks the default site list. # Without arguments checks the default site list.
# #
# Tests each site through configured gateways via SOCKS5 proxies # Tests each site through configured gateways via SOCKS5 proxies
...@@ -63,6 +63,20 @@ done <<EOF ...@@ -63,6 +63,20 @@ done <<EOF
$(echo "$GATEWAYS" | sed '/^$/d') $(echo "$GATEWAYS" | sed '/^$/d')
EOF EOF
# Parse arguments
JSON_FILE=""
while [ $# -gt 0 ] ; do
case "$1" in
--json)
JSON_FILE="$2"
shift 2
;;
*)
break
;;
esac
done
# Determine sites # Determine sites
sites="${*:-$DEFAULT_SITES}" sites="${*:-$DEFAULT_SITES}"
...@@ -91,14 +105,20 @@ done ...@@ -91,14 +105,20 @@ done
printf "\n" printf "\n"
# Check each site # Check each site
json_results=""
for site in $sites ; do for site in $sites ; do
url="https://$site/" url="https://$site/"
printf "%-20s" "$site" printf "%-20s" "$site"
json_checks=""
i=0 i=0
while [ "$i" -lt "$gw_count" ] ; do while [ "$i" -lt "$gw_count" ] ; do
eval "gw=\$gw_name_$i"
eval "proxy=\$gw_proxy_$i" eval "proxy=\$gw_proxy_$i"
speed_val="null"
http_code_val="null"
# Step 1: availability # Step 1: availability
if run_eget "$proxy" --check-url "$url" >/dev/null 2>&1 ; then if run_eget "$proxy" --check-url "$url" >/dev/null 2>&1 ; then
# Step 2: speed # Step 2: speed
...@@ -109,23 +129,39 @@ for site in $sites ; do ...@@ -109,23 +129,39 @@ for site in $sites ; do
if [ "$is_slow" = "1" ] ; then if [ "$is_slow" = "1" ] ; then
cell="SLOW($speed)" cell="SLOW($speed)"
status="SLOW"
else else
cell="OK($speed)" cell="OK($speed)"
status="OK"
fi fi
speed_val="$speed"
http_code_val="200"
else else
# --check-url failed: distinguish BLOCK (no connection) from HTTP error # --check-url failed: distinguish BLOCK (no connection) from HTTP error
http_code=$(run_eget "$proxy" --get-response "$url" 2>/dev/null | head -1 | awk '{print $2}') http_code=$(run_eget "$proxy" --get-response "$url" 2>/dev/null | head -1 | awk '{print $2}')
if [ -n "$http_code" ] ; then if [ -n "$http_code" ] ; then
cell="HTTP$http_code" cell="HTTP$http_code"
status="HTTP$http_code"
http_code_val="$http_code"
else else
cell="BLOCK" cell="BLOCK"
status="BLOCK"
fi fi
fi fi
printf " %-14s" "$cell" printf " %-14s" "$cell"
# Accumulate JSON for this gateway
[ -n "$json_checks" ] && json_checks="$json_checks,"
json_checks="$json_checks\"$gw\":{\"status\":\"$status\",\"speed\":$speed_val,\"http_code\":$http_code_val}"
i=$((i + 1)) i=$((i + 1))
done done
printf "\n" printf "\n"
# Accumulate JSON for this site
[ -n "$json_results" ] && json_results="$json_results,"
json_results="$json_results{\"site\":\"$site\",\"checks\":{$json_checks}}"
done done
echo echo
...@@ -133,3 +169,21 @@ echo "Legend: OK(N) = available, N MB/s download speed" ...@@ -133,3 +169,21 @@ echo "Legend: OK(N) = available, N MB/s download speed"
echo " SLOW(N) = available but speed < ${SLOW_THRESHOLD} MB/s (throttled)" echo " SLOW(N) = available but speed < ${SLOW_THRESHOLD} MB/s (throttled)"
echo " HTTP403 = server responded with error (e.g. Cloudflare challenge)" echo " HTTP403 = server responded with error (e.g. Cloudflare challenge)"
echo " BLOCK = no connection (blocked or down)" echo " BLOCK = no connection (blocked or down)"
# Write JSON output if requested
if [ -n "$JSON_FILE" ] ; then
# Build gateways array
json_gateways=""
i=0
while [ "$i" -lt "$gw_count" ] ; do
eval "gw=\$gw_name_$i"
[ -n "$json_gateways" ] && json_gateways="$json_gateways,"
json_gateways="$json_gateways\"$gw\""
i=$((i + 1))
done
cat > "$JSON_FILE" <<JSONEOF
{"timestamp":$(date +%s),"hostname":"$hostname","gateways":[$json_gateways],"results":[$json_results]}
JSONEOF
echo "JSON results written to $JSON_FILE"
fi
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment