Refactor basically everything

This commit is contained in:
Helix K
2026-04-29 13:35:53 -05:00
parent e1e4a60b6d
commit 2791ed59e3
11 changed files with 615 additions and 220 deletions
View File
+120
View File
@@ -0,0 +1,120 @@
import socket
import threading
import time
import httpx
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from jinja2 import Environment, PackageLoader
from velping.config import Config, ServiceConfig, load_cfg
http = httpx.Client(
headers={"User-Agent": "velping"},
follow_redirects=True,
)
app = FastAPI()
app.mount("/static", StaticFiles(packages=["velping"]), name="static")
uptime: dict[str, list[str]] = {}
templates = Environment(loader=PackageLoader("velping", "templates"))
cfg = load_cfg()
def handle_service_status(
service_name: str,
down: bool = False,
error: str | None = None,
) -> None:
"""Update service status and send notifications if configured."""
last_status = uptime[service_name][-1]
new_status = "O" if down else "I"
if cfg.ntfy.enabled and last_status != new_status:
message = f"Service {service_name} is {'offline' if down else 'online'}"
http.post(
url=f"https://{cfg.ntfy.server}/{cfg.ntfy.topic}",
data=message,
)
uptime[service_name].pop(0)
uptime[service_name].append(new_status)
if down:
print(f"[E] Error pinging {service_name}: {error}")
else:
print(f"[I] Pinging {service_name} succeeded!")
def tcp_ping(service: ServiceConfig) -> None:
"""Continuously ping a service via TCP."""
socket_type = socket.AF_INET if service.ipv == 4 else socket.AF_INET6
while True:
print(f"[I] Pinging {service.name}")
try:
with socket.socket(family=socket_type, type=socket.SOCK_STREAM) as sock:
sock.settimeout(service.timeout)
sock.connect((service.hostname, service.port))
handle_service_status(service_name=service.name, down=False)
except socket.timeout:
handle_service_status(
service_name=service.name,
down=True,
error=f"Connection to {service.name} timed out",
)
except Exception as err:
handle_service_status(
service_name=service.name,
down=True,
error=str(err),
)
time.sleep(cfg.pinging.rate)
def http_ping(service: ServiceConfig) -> None:
"""Continuously ping a service via HTTP."""
while True:
print(f"[I] Pinging {service.name}")
try:
resp = http.get(url=service.url, timeout=service.timeout)
resp.raise_for_status()
handle_service_status(service_name=service.name, down=False)
except Exception as e:
handle_service_status(
service_name=service.name,
down=True,
error=str(e),
)
time.sleep(cfg.pinging.rate)
@app.get("/")
async def root(cfg: Config) -> str:
template = templates.get_template("index.xht")
return template.render(
cfg=cfg,
services=cfg.services,
uptime=uptime,
)
for service in cfg.services:
uptime[service.name] = ["?"] * cfg.web.pings
ping_func = http_ping if service.type == "http" else tcp_ping
threading.Thread(
target=ping_func,
args=(service,),
daemon=True,
).start()
uvicorn.run(app, host=cfg.web.addr, port=cfg.web.port)
+64
View File
@@ -0,0 +1,64 @@
import os
import tomllib
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
class ServiceConfig(BaseModel):
name: str
type: Literal["http", "tcp"]
timeout: int = 5
class HTTPServiceConfig(ServiceConfig):
url: str = Field(alias="address")
class TCPServiceConfig(ServiceConfig):
port: int
hostname: str
ipv: Literal[4, 6] = Field(alias="ip_version", default=4)
class WebConfig(BaseModel):
addr: str = Field(alias="address", default="::")
port: int = 4200
name: str = "velping"
pings: int = 25
class PingingConfig(BaseModel):
rate: int = Field(alias="seconds_between_ping", default=60)
class NtfyConfig(BaseModel):
enabled: bool = False
server: str = "ntfy.sh"
topic: str
Service = TCPServiceConfig | HTTPServiceConfig
class Config(BaseModel):
web: WebConfig = Field(alias="frontend", default={})
ntfy: NtfyConfig = NtfyConfig(topic="")
pinging: PingingConfig
# NOTE: dict is legacy and likely to be removed in a future release.
services: list[Service] | dict[str, Service] = []
def load_cfg() -> Config:
try:
with Path(os.environ.get("VELPING_CONFIG", "config.toml")).open("rb") as f:
cfg = Config(**tomllib.load(f))
if isinstance(cfg.services, dict):
print("Your services config is using a dict, which is deprecated.")
cfg.services = list(cfg.services.values())
except ValidationError as e:
raise SystemExit(str(e))
return cfg
+53
View File
@@ -0,0 +1,53 @@
::selection {
background-color: #eeadb7;
color: #1e1e2e;
}
::-moz-selection {
background-color: #eeadb7;
color: #1e1e2e;
}
body {
font-family: 'Adwaita Mono', monospace;
padding-left: 20px;
padding-top: 10px;
margin: 0;
max-width: 1000px;
background-color: #1e1e2e;
color: #eeadb7;
}
.green, .green::selection, .green::-moz-selection {
color: #5cdd8b;
}
.red, .red::selection, .red::-moz-selection {
color: #dc3545;
}
.grey, .grey::selection, .grey::-moz-selection {
color: #dadada;
}
.service {
width: 400px;
height: 20px;
padding: 10px;
display: flex;
align-items: center;
border-radius: 10px;
}
.service p {
width: 33%
}
.status {
display: flex;
width: 66%
}
.status p {
width: 9px;
}
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en-gb" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ cfg.web.name }}</title>
<link href="/static/style.css" rel="stylesheet" />
</head>
<body>
<h2>{{ cfg.web.name }}</h2>
{% for service in services %}
<div class="service">
<p><b>{{ service.name }}</b></p>
<div class="status">
{% for ping in uptime[service.name] %}
{% if ping == "I" %}
<p class="green">█</p>
{% elif ping == "O" %}
<p class="red">█</p>
{% else %}
<p class="grey">█</p>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</body>
</html>