fix: Update parsing of history to fix issues
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
This commit is contained in:
parent
fa78390fc7
commit
d0d3de0a1e
143
server.py
143
server.py
@ -58,7 +58,7 @@ if not os.path.exists(log_dir):
|
|||||||
|
|
||||||
if not os.path.exists(f"{log_dir}/node_status.json"):
|
if not os.path.exists(f"{log_dir}/node_status.json"):
|
||||||
with open(f"{log_dir}/node_status.json", "w") as file:
|
with open(f"{log_dir}/node_status.json", "w") as file:
|
||||||
json.dump([], file)
|
json.dump([], file)
|
||||||
|
|
||||||
if not os.path.exists(f"{log_dir}/sent_notifications.json"):
|
if not os.path.exists(f"{log_dir}/sent_notifications.json"):
|
||||||
with open(f"{log_dir}/sent_notifications.json", "w") as file:
|
with open(f"{log_dir}/sent_notifications.json", "w") as file:
|
||||||
@ -68,7 +68,6 @@ else:
|
|||||||
sent_notifications = json.load(file)
|
sent_notifications = json.load(file)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
print(f"Log directory: {log_dir}", flush=True)
|
print(f"Log directory: {log_dir}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
@ -174,7 +173,7 @@ def check_doh(ip: str) -> bool:
|
|||||||
sock = socket.create_connection((ip, 443))
|
sock = socket.create_connection((ip, 443))
|
||||||
context = ssl.create_default_context()
|
context = ssl.create_default_context()
|
||||||
ssock = context.wrap_socket(sock, server_hostname="hnsdoh.com")
|
ssock = context.wrap_socket(sock, server_hostname="hnsdoh.com")
|
||||||
|
|
||||||
ssock.sendall(wireframe_request)
|
ssock.sendall(wireframe_request)
|
||||||
response_data = b""
|
response_data = b""
|
||||||
while True:
|
while True:
|
||||||
@ -262,6 +261,7 @@ def format_relative_time(expiry_date: datetime) -> str:
|
|||||||
else:
|
else:
|
||||||
return f"in {delta.seconds} seconds" if delta.seconds > 1 else "in 1 second"
|
return f"in {delta.seconds} seconds" if delta.seconds > 1 else "in 1 second"
|
||||||
|
|
||||||
|
|
||||||
def format_last_check(last_log: datetime) -> str:
|
def format_last_check(last_log: datetime) -> str:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
delta = now - last_log
|
delta = now - last_log
|
||||||
@ -279,6 +279,7 @@ def format_last_check(last_log: datetime) -> str:
|
|||||||
else:
|
else:
|
||||||
return f"{delta.seconds} seconds ago" if delta.seconds > 1 else "1 second ago"
|
return f"{delta.seconds} seconds ago" if delta.seconds > 1 else "1 second ago"
|
||||||
|
|
||||||
|
|
||||||
def check_nodes() -> list:
|
def check_nodes() -> list:
|
||||||
global nodes
|
global nodes
|
||||||
if last_log > datetime.now() - relativedelta.relativedelta(minutes=1):
|
if last_log > datetime.now() - relativedelta.relativedelta(minutes=1):
|
||||||
@ -336,7 +337,13 @@ def check_nodes() -> list:
|
|||||||
|
|
||||||
# Send notifications if any nodes are down
|
# Send notifications if any nodes are down
|
||||||
for node in node_status:
|
for node in node_status:
|
||||||
if not node["plain_dns"] or not node["doh"] or not node["dot"] or not node["cert"]["valid"] or not node["cert_853"]["valid"]:
|
if (
|
||||||
|
not node["plain_dns"]
|
||||||
|
or not node["doh"]
|
||||||
|
or not node["dot"]
|
||||||
|
or not node["cert"]["valid"]
|
||||||
|
or not node["cert_853"]["valid"]
|
||||||
|
):
|
||||||
send_down_notification(node)
|
send_down_notification(node)
|
||||||
continue
|
continue
|
||||||
# Check if cert is expiring in 7 days
|
# Check if cert is expiring in 7 days
|
||||||
@ -350,9 +357,10 @@ def check_nodes() -> list:
|
|||||||
node["cert_853"]["expiry_date"], "%b %d %H:%M:%S %Y GMT"
|
node["cert_853"]["expiry_date"], "%b %d %H:%M:%S %Y GMT"
|
||||||
)
|
)
|
||||||
if cert_853_expiry < datetime.now() + relativedelta.relativedelta(days=7):
|
if cert_853_expiry < datetime.now() + relativedelta.relativedelta(days=7):
|
||||||
send_down_notification(node)
|
send_down_notification(node)
|
||||||
return node_status
|
return node_status
|
||||||
|
|
||||||
|
|
||||||
def check_nodes_from_log() -> list:
|
def check_nodes_from_log() -> list:
|
||||||
global last_log
|
global last_log
|
||||||
# Load the last log
|
# Load the last log
|
||||||
@ -374,7 +382,8 @@ def check_nodes_from_log() -> list:
|
|||||||
last_log = newest["date"]
|
last_log = newest["date"]
|
||||||
return node_status
|
return node_status
|
||||||
|
|
||||||
def send_notification(title, description,author):
|
|
||||||
|
def send_notification(title, description, author):
|
||||||
discord_hook = os.getenv("DISCORD_HOOK")
|
discord_hook = os.getenv("DISCORD_HOOK")
|
||||||
if discord_hook:
|
if discord_hook:
|
||||||
data = {
|
data = {
|
||||||
@ -406,22 +415,34 @@ def send_down_notification(node):
|
|||||||
|
|
||||||
# Check if a notification has already been sent
|
# Check if a notification has already been sent
|
||||||
if node["ip"] not in sent_notifications:
|
if node["ip"] not in sent_notifications:
|
||||||
sent_notifications[node["ip"]] = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
|
sent_notifications[node["ip"]] = datetime.strftime(
|
||||||
|
datetime.now(), "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
last_send = datetime.strptime(sent_notifications[node["ip"]], "%Y-%m-%d %H:%M:%S")
|
last_send = datetime.strptime(
|
||||||
|
sent_notifications[node["ip"]], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
if last_send > datetime.now() - relativedelta.relativedelta(hours=1):
|
if last_send > datetime.now() - relativedelta.relativedelta(hours=1):
|
||||||
print(f"Notification already sent for {node['name']} in the last hr", flush=True)
|
print(
|
||||||
|
f"Notification already sent for {node['name']} in the last hr",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only send certain notifications once per day
|
# Only send certain notifications once per day
|
||||||
if node["plain_dns"] and node["doh"] and node["dot"]:
|
if node["plain_dns"] and node["doh"] and node["dot"]:
|
||||||
if last_send > datetime.now() - relativedelta.relativedelta(days=1):
|
if last_send > datetime.now() - relativedelta.relativedelta(days=1):
|
||||||
print(f"Notification already sent for {node['name']} in the last day", flush=True)
|
print(
|
||||||
|
f"Notification already sent for {node['name']} in the last day",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save the notification to the file
|
# Save the notification to the file
|
||||||
sent_notifications[node["ip"]] = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
|
sent_notifications[node["ip"]] = datetime.strftime(
|
||||||
|
datetime.now(), "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
with open(f"{log_dir}/sent_notifications.json", "w") as file:
|
with open(f"{log_dir}/sent_notifications.json", "w") as file:
|
||||||
json.dump(sent_notifications, file, indent=4)
|
json.dump(sent_notifications, file, indent=4)
|
||||||
|
|
||||||
@ -438,12 +459,12 @@ def send_down_notification(node):
|
|||||||
description += "- Certificate on port 443 is invalid\n"
|
description += "- Certificate on port 443 is invalid\n"
|
||||||
if not node["cert_853"]["valid"]:
|
if not node["cert_853"]["valid"]:
|
||||||
description += "- Certificate on port 853 is invalid\n"
|
description += "- Certificate on port 853 is invalid\n"
|
||||||
|
|
||||||
# Also add the expiry date of the certificates
|
# Also add the expiry date of the certificates
|
||||||
description += "\nCertificate expiry dates:\n"
|
description += "\nCertificate expiry dates:\n"
|
||||||
description += f"- Certificate on port 443 expires {node['cert']['expires']}\n"
|
description += f"- Certificate on port 443 expires {node['cert']['expires']}\n"
|
||||||
description += f"- Certificate on port 853 expires {node['cert_853']['expires']}\n"
|
description += f"- Certificate on port 853 expires {node['cert_853']['expires']}\n"
|
||||||
send_notification(title, description, node['name'])
|
send_notification(title, description, node["name"])
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
@ -514,6 +535,7 @@ def summarize_history(history: list) -> dict:
|
|||||||
lambda: {
|
lambda: {
|
||||||
"name": "",
|
"name": "",
|
||||||
"location": "",
|
"location": "",
|
||||||
|
"ip": "",
|
||||||
"plain_dns": {"last_down": "Never", "percentage": 0},
|
"plain_dns": {"last_down": "Never", "percentage": 0},
|
||||||
"doh": {"last_down": "Never", "percentage": 0},
|
"doh": {"last_down": "Never", "percentage": 0},
|
||||||
"dot": {"last_down": "Never", "percentage": 0},
|
"dot": {"last_down": "Never", "percentage": 0},
|
||||||
@ -542,17 +564,18 @@ def summarize_history(history: list) -> dict:
|
|||||||
if nodes_status[ip]["name"] == "":
|
if nodes_status[ip]["name"] == "":
|
||||||
nodes_status[ip]["name"] = node.get("name", "")
|
nodes_status[ip]["name"] = node.get("name", "")
|
||||||
nodes_status[ip]["location"] = node.get("location", "")
|
nodes_status[ip]["location"] = node.get("location", "")
|
||||||
|
nodes_status[ip]["ip"] = ip
|
||||||
|
|
||||||
# Update counts and last downtime
|
# Update counts and last downtime
|
||||||
for key in ["plain_dns", "doh", "dot"]:
|
for key in ["plain_dns", "doh", "dot"]:
|
||||||
status = node.get(key, "up")
|
status = node.get(key, "up")
|
||||||
if status == "down":
|
if status == False:
|
||||||
total_counts[ip][key]["down"] += 1
|
total_counts[ip][key]["down"] += 1
|
||||||
total_counts[ip][key]["total"] += 1
|
total_counts[ip][key]["total"] += 1
|
||||||
|
|
||||||
# Update last downtime for each key
|
# Update last downtime for each key
|
||||||
for key in ["plain_dns", "doh", "dot"]:
|
for key in ["plain_dns", "doh", "dot"]:
|
||||||
if node.get(key) == "down":
|
if node.get(key) == False:
|
||||||
nodes_status[ip][key]["last_down"] = date.strftime(
|
nodes_status[ip][key]["last_down"] = date.strftime(
|
||||||
"%Y-%m-%d %H:%M:%S"
|
"%Y-%m-%d %H:%M:%S"
|
||||||
)
|
)
|
||||||
@ -566,6 +589,8 @@ def summarize_history(history: list) -> dict:
|
|||||||
down = total_counts[ip][key]["down"]
|
down = total_counts[ip][key]["down"]
|
||||||
if total > 0:
|
if total > 0:
|
||||||
node_data[key]["percentage"] = ((total - down) / total) * 100
|
node_data[key]["percentage"] = ((total - down) / total) * 100
|
||||||
|
# Round to 2 decimal places
|
||||||
|
node_data[key]["percentage"] = round(node_data[key]["percentage"], 2)
|
||||||
else:
|
else:
|
||||||
node_data[key]["percentage"] = 100
|
node_data[key]["percentage"] = 100
|
||||||
node_list.append(node_data)
|
node_list.append(node_data)
|
||||||
@ -586,6 +611,8 @@ def summarize_history(history: list) -> dict:
|
|||||||
down = overall_counts[key]["down"]
|
down = overall_counts[key]["down"]
|
||||||
if total > 0:
|
if total > 0:
|
||||||
overall_status[key]["percentage"] = ((total - down) / total) * 100
|
overall_status[key]["percentage"] = ((total - down) / total) * 100
|
||||||
|
# Round to 2 decimal places
|
||||||
|
overall_status[key]["percentage"] = round(overall_status[key]["percentage"], 2)
|
||||||
last_downs = [
|
last_downs = [
|
||||||
nodes_status[ip][key]["last_down"]
|
nodes_status[ip][key]["last_down"]
|
||||||
for ip in nodes_status
|
for ip in nodes_status
|
||||||
@ -602,9 +629,9 @@ def summarize_history(history: list) -> dict:
|
|||||||
def convert_nodes_to_dict(nodes):
|
def convert_nodes_to_dict(nodes):
|
||||||
nodes_dict = {}
|
nodes_dict = {}
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
name = node.get("name")
|
ip = node.get("ip")
|
||||||
if name:
|
if ip:
|
||||||
nodes_dict[name] = node
|
nodes_dict[ip] = node
|
||||||
return nodes_dict
|
return nodes_dict
|
||||||
|
|
||||||
|
|
||||||
@ -612,6 +639,52 @@ def convert_nodes_to_dict(nodes):
|
|||||||
|
|
||||||
|
|
||||||
# region API routes
|
# region API routes
|
||||||
|
@app.route("/api")
|
||||||
|
def api_index():
|
||||||
|
agent = request.headers.get("User-Agent")
|
||||||
|
# Check if the request is not from a browser
|
||||||
|
nonBrowser = ["curl", "Postman", "Insomnia", "httpie", "wget", "python-requests"]
|
||||||
|
|
||||||
|
endpoints = [
|
||||||
|
{"route": "/api/nodes", "description": "Get the current status of all nodes"},
|
||||||
|
{
|
||||||
|
"route": "/api/history",
|
||||||
|
"description": "Get a summary of the last x days of node status",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "days",
|
||||||
|
"type": "int",
|
||||||
|
"description": "Number of days to get the history for",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/api/history/<int:days>",
|
||||||
|
"description": "Get a summary of the last x days of node status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"route": "/api/full",
|
||||||
|
"description": "Get the full history of node status for the last x days",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "days",
|
||||||
|
"type": "int",
|
||||||
|
"description": "Number of days to get the history for",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"route": "/api/refresh", "description": "Force a status check of all nodes"},
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(agent.lower().find(x) != -1 for x in nonBrowser):
|
||||||
|
print("API request", flush=True)
|
||||||
|
return jsonify({"status": "ok", "endpoints": endpoints})
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Redirect to the main page
|
||||||
|
return render_template("api.html",endpoints=endpoints)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/nodes")
|
@app.route("/api/nodes")
|
||||||
def api_nodes():
|
def api_nodes():
|
||||||
node_status = check_nodes_from_log()
|
node_status = check_nodes_from_log()
|
||||||
@ -654,11 +727,13 @@ def api_all():
|
|||||||
history = get_history(history_days)
|
history = get_history(history_days)
|
||||||
return jsonify(history)
|
return jsonify(history)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/refresh")
|
@app.route("/api/refresh")
|
||||||
def api_refresh():
|
def api_refresh():
|
||||||
node_status = check_nodes()
|
node_status = check_nodes()
|
||||||
return jsonify(node_status)
|
return jsonify(node_status)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@ -689,9 +764,7 @@ def index():
|
|||||||
node["cert"]["expiry_date"], "%b %d %H:%M:%S %Y GMT"
|
node["cert"]["expiry_date"], "%b %d %H:%M:%S %Y GMT"
|
||||||
)
|
)
|
||||||
if cert_expiry < datetime.now():
|
if cert_expiry < datetime.now():
|
||||||
alerts.append(
|
alerts.append(f"The {node['name']} node's certificate has expired")
|
||||||
f"The {node['name']} node's certificate has expired"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
elif cert_expiry < datetime.now() + relativedelta.relativedelta(days=7):
|
elif cert_expiry < datetime.now() + relativedelta.relativedelta(days=7):
|
||||||
warnings.append(
|
warnings.append(
|
||||||
@ -710,7 +783,6 @@ def index():
|
|||||||
warnings.append(
|
warnings.append(
|
||||||
f"The {node['name']} node's certificate is expiring {format_relative_time(cert_853_expiry)} for DNS over TLS (port 853)"
|
f"The {node['name']} node's certificate is expiring {format_relative_time(cert_853_expiry)} for DNS over TLS (port 853)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
history_days = 7
|
history_days = 7
|
||||||
if "history" in request.args:
|
if "history" in request.args:
|
||||||
@ -720,9 +792,27 @@ def index():
|
|||||||
pass
|
pass
|
||||||
history = get_history(history_days)
|
history = get_history(history_days)
|
||||||
history_summary = summarize_history(history)
|
history_summary = summarize_history(history)
|
||||||
history_summary["nodes"] = convert_nodes_to_dict(history_summary["nodes"])
|
|
||||||
last_check = format_last_check(last_log)
|
# Convert time to relative time
|
||||||
|
for node in history_summary["nodes"]:
|
||||||
|
for key in ["plain_dns", "doh", "dot"]:
|
||||||
|
if node[key]["last_down"] == "Never":
|
||||||
|
continue
|
||||||
|
node[key]["last_down"] = format_last_check(
|
||||||
|
datetime.strptime(node[key]["last_down"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in ["plain_dns", "doh", "dot"]:
|
||||||
|
if history_summary["overall"][key]["last_down"] == "Never":
|
||||||
|
continue
|
||||||
|
history_summary["overall"][key]["last_down"] = format_last_check(
|
||||||
|
datetime.strptime(history_summary["overall"][key]["last_down"], "%Y-%m-%d %H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
|
history_summary["nodes"] = convert_nodes_to_dict(history_summary["nodes"])
|
||||||
|
|
||||||
|
last_check = format_last_check(last_log)
|
||||||
|
|
||||||
# Replace true/false with up/down
|
# Replace true/false with up/down
|
||||||
for node in node_status:
|
for node in node_status:
|
||||||
for key in ["plain_dns", "doh", "dot"]:
|
for key in ["plain_dns", "doh", "dot"]:
|
||||||
@ -731,7 +821,6 @@ def index():
|
|||||||
else:
|
else:
|
||||||
node[key] = "Down"
|
node[key] = "Down"
|
||||||
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
nodes=node_status,
|
nodes=node_status,
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>HNSDoH Status</title>
|
<title>Status | HNS DoH</title>
|
||||||
<link rel="icon" href="/favicon.png" type="image/png">
|
<link rel="icon" href="/assets/img/HNS.png" type="image/png">
|
||||||
<link rel="stylesheet" href="/assets/css/404.css">
|
<link rel="stylesheet" href="/assets/css/404.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
45
templates/api.html
Normal file
45
templates/api.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>API | HNS DoH</title>
|
||||||
|
<link rel="icon" href="/assets/img/HNS.png" type="image/png">
|
||||||
|
<link rel="stylesheet" href="/assets/css/api.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<div class="centre">
|
||||||
|
{% if endpoints %}
|
||||||
|
<h1 style="font-size: xx-large;">API Info</h1>
|
||||||
|
<p>Available endpoints:</p>
|
||||||
|
<ul style="width: fit-content;margin: auto;">
|
||||||
|
{% for endpoint in endpoints %}
|
||||||
|
<li class="top">
|
||||||
|
<strong>{{ endpoint.route }}</strong> - {{ endpoint.description }}
|
||||||
|
{% if endpoint.parameters %}
|
||||||
|
<ul>
|
||||||
|
<li><em>Parameters:</em></li>
|
||||||
|
{% for param in endpoint.parameters %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ param.name }}:</strong>
|
||||||
|
({{ param.type }}) - {{ param.description }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<h1>404 | Page not found</h1>
|
||||||
|
<p>Sorry, the page you are looking for does not exist.</p>
|
||||||
|
<p><a href="/">Go back to the homepage</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
66
templates/assets/css/api.css
Normal file
66
templates/assets/css/api.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
body {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.centre {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 100px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none; /* Remove bullet points */
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the route names */
|
||||||
|
li strong {
|
||||||
|
color: #9ccdff; /* Darker shade for route names */
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for descriptions */
|
||||||
|
li em {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ffa44f; /* Lighter shade for parameter labels */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested parameter list */
|
||||||
|
li ul {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li ul li {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #cfcfcf; /* Parameter details */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling parameter names */
|
||||||
|
li ul li strong {
|
||||||
|
color: #3498db; /* Light blue for parameter names */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a subtle hover effect */
|
||||||
|
li.top:hover {
|
||||||
|
/* Invert colours */
|
||||||
|
filter: invert(1);
|
||||||
|
background-color: #000000;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
padding-left: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
@ -111,12 +111,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="node-info">
|
<div class="node-info">
|
||||||
<h5>Stats</h5>
|
<h5>Stats</h5>
|
||||||
<p>Plain DNS: {{history.nodes[node.name].plain_dns.percentage}}% uptime (last down
|
<p>Plain DNS: {{history.nodes[node.ip].plain_dns.percentage}}% uptime (last down
|
||||||
{{history.nodes[node.name].plain_dns.last_down}})</p>
|
{{history.nodes[node.ip].plain_dns.last_down}})</p>
|
||||||
<p>DNS over HTTPS: {{history.nodes[node.name].doh.percentage}}% uptime (last down
|
<p>DNS over HTTPS: {{history.nodes[node.ip].doh.percentage}}% uptime (last down
|
||||||
{{history.nodes[node.name].doh.last_down}})</p>
|
{{history.nodes[node.ip].doh.last_down}})</p>
|
||||||
<p>DNS over TLS: {{history.nodes[node.name].dot.percentage}}% uptime (last down
|
<p>DNS over TLS: {{history.nodes[node.ip].dot.percentage}}% uptime (last down
|
||||||
{{history.nodes[node.name].dot.last_down}})</p>
|
{{history.nodes[node.ip].dot.last_down}})</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="node-info">
|
<div class="node-info">
|
||||||
<p style="font-weight: bold;">{{node.name}}: {{node.ip}}</p>
|
<p style="font-weight: bold;">{{node.name}}: {{node.ip}}</p>
|
||||||
|
Loading…
Reference in New Issue
Block a user