MAJOR REFACTOR P1

This commit is contained in:
2025-09-05 20:39:42 +02:00
parent 3b87da9292
commit 4cfd5b2dbe
28 changed files with 293 additions and 301 deletions

21
app/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate, upgrade, stamp
from .flask_app import app
from pathlib import Path
__all__ = ["app"]
# Create db
db = SQLAlchemy(app=app)
# Set up migration
migration = Migrate(app=app, db=db)
# Init and upgrade
with app.app_context():
# Check if DB file is missing
if not (Path("./instance/app.db").is_file()):
db.create_all()
stamp()
# Upgrade db if any new migrations exist
upgrade()

View File

@@ -0,0 +1,3 @@
from .worker import start_worker, stop_event
__all__ = ["start_worker", "stop_event"]

47
app/aio_client/client.py Normal file
View File

@@ -0,0 +1,47 @@
import aiohttp
from app.models import service, log
from types import SimpleNamespace
from typing import Optional
async def ping(
client: aiohttp.ClientSession,
s: service,
ctx: Optional[SimpleNamespace] = None,
) -> int:
ctx = ctx or SimpleNamespace()
match s.ping_method:
case 0:
r = await client.head(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
trace_request_ctx=ctx, # type: ignore
)
case 1:
r = await client.get(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
trace_request_ctx=ctx, # type: ignore
)
case _:
raise Exception("UNKNOWN PING METHOD")
return r.status
async def ping_service(client: aiohttp.ClientSession, s: service) -> log:
try:
ctx = SimpleNamespace()
status = await ping(client=client, s=s, ctx=ctx)
if status == 200:
return log(service_id=s.id, ping=int(ctx.duration_ms))
else:
return log(service_id=s.id, ping=None)
except aiohttp.ConnectionTimeoutError:
return log(service_id=s.id, ping=None, timeout=True)
except Exception as e:
print(e)
return log(service_id=s.id, ping=None)

89
app/aio_client/worker.py Normal file
View File

@@ -0,0 +1,89 @@
from sqlalchemy.orm import sessionmaker
from app.config import timeout as timeout_
import aiohttp
import asyncio
import time
from types import SimpleNamespace
from app import db, app
from app.models import service
from .client import ping_service
import threading
stop_event = threading.Event()
def start_worker():
try:
# Creates new event loop in new thread
loop = asyncio.new_event_loop()
# Creates new task on new loop
loop.create_task(update_services())
# Schedule loop to run forever
loop.run_forever()
except Exception as e:
print("Worker thread exception:", e)
stop_event.set()
def setup_client() -> aiohttp.ClientSession:
timeout = aiohttp.client.ClientTimeout(total=timeout_ / 1000)
# Each request will get its own context
trace_config = aiohttp.TraceConfig()
async def on_start(
session: aiohttp.ClientSession,
context: SimpleNamespace,
params: aiohttp.TraceRequestStartParams,
):
ctx = context.trace_request_ctx
ctx.start = time.perf_counter() # store per-request
async def on_end(
session: aiohttp.ClientSession,
context: SimpleNamespace,
params: aiohttp.TraceRequestEndParams,
):
ctx = context.trace_request_ctx
ctx.end = time.perf_counter()
ctx.duration_ms = int((ctx.end - ctx.start) * 1000)
trace_config.on_request_start.append(on_start)
trace_config.on_request_end.append(on_end)
client = aiohttp.ClientSession(
timeout=timeout, auto_decompress=False, trace_configs=[trace_config]
)
return client
async def update_services():
try:
print("Starting service updates...")
# Create new session
with app.app_context():
WorkerSession = sessionmaker(bind=db.engine)
client = setup_client()
# Actual update loop
while True:
session = WorkerSession()
sleeptask = asyncio.create_task(asyncio.sleep(timeout_ / 1000 + 1))
tasks = [
ping_service(client=client, s=s)
for s in session.query(service).all()
]
logs = await asyncio.gather(*tasks)
await sleeptask
try:
session.add_all(logs)
session.commit()
except Exception as e:
session.rollback()
raise e
finally:
session.close()
except Exception as e:
print("Worker thread exception:", e)
stop_event.set()

1
app/config.py Normal file
View File

@@ -0,0 +1 @@
timeout: int = 4000

20
app/flask_app.py Normal file
View File

@@ -0,0 +1,20 @@
import threading
from flask import Flask
stop_event = threading.Event()
# Flask app to serve status
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
def start_flask() -> None:
try:
# Run flask app
from .routes import bp
app.register_blueprint(bp)
app.run(host="0.0.0.0", port=80, debug=True, use_reloader=False)
except Exception as e:
print("Worker thread exception:", e)
stop_event.set()

70
app/models.py Normal file
View File

@@ -0,0 +1,70 @@
from app import db
from datetime import datetime, timezone
from validators import url as is_url
from typing import Any, Optional
class log(db.Model):
id: int = db.Column(db.Integer, primary_key=True) # TODO: Switch to UUID
dateCreated: datetime = db.Column(db.DateTime, nullable=False, index=True)
service_id: int = db.Column(
db.Integer,
db.ForeignKey("service.id"),
nullable=False,
)
ping: Optional[int] = db.Column(db.Integer, nullable=True)
timeout: bool = db.Column(db.Boolean, nullable=False)
def __init__(
self, service_id: int, ping: Optional[int], timeout: bool = False
):
super().__init__()
self.service_id = service_id
self.ping = ping
self.dateCreated = datetime.now(timezone.utc)
self.timeout = timeout
def to_dict(self) -> dict[str, Any]:
return {
"log_id": self.id,
"service_id": self.service_id,
"ping": self.ping,
"dateCreated": self.dateCreatedUTC(),
"timeout": self.timeout,
}
def dateCreatedUTC(self) -> datetime:
return self.dateCreated.replace(tzinfo=timezone.utc)
class service(db.Model):
id: int = db.Column(db.Integer, primary_key=True) # TODO: Switch to UUID
url: str = db.Column(db.String, nullable=False)
label: str = db.Column(db.String(15), nullable=False)
public_access: bool = db.Column(db.Boolean, nullable=False)
ping_method: int = db.Column(db.Integer, nullable=False)
logs = db.relationship("log", lazy="dynamic")
def __init__(
self, url: str, label: str, public_access: bool, ping_method: int
):
if not is_url(url):
raise Exception("URL IS NOT A VALID URL")
if len(label) > 15:
raise Exception("LABEL EXCEEDS MAXIMUM LENGTH (15)")
super().__init__()
self.url = url
self.label = label
self.public_access = public_access
self.ping_method = ping_method
def to_dict(self) -> dict[str, Any]:
return {
"url": self.url,
"public_access": self.public_access,
"label": self.label,
"service_id": self.id,
"ping_method": self.ping_method,
}

95
app/routes.py Normal file
View File

@@ -0,0 +1,95 @@
from flask import Blueprint, render_template, abort, jsonify, send_file, json
from typing import cast, Optional, Any
from datetime import datetime, timedelta, timezone
from app.config import timeout
from .models import service, log
from app import app, db
bp = Blueprint(
"api",
"__name__",
url_prefix="/api",
static_folder="static",
)
# Prepares log data for chart.js chart
def prepare_chart_data(
logs: list[log],
) -> tuple[list[str], list[Optional[int]]]:
if len(logs) <= 0: # Return empty if there are no logs
return ([], [])
x = [logs[0].dateCreatedUTC().isoformat()]
y = [logs[0].ping]
for i in range(1, len(logs)):
log1 = logs[i]
log2 = logs[i - 1]
# Check if the gap in points exceeds a threshold
if (abs(log1.dateCreatedUTC() - log2.dateCreatedUTC())) > timedelta(
milliseconds=1.5 * (timeout + 1000)
):
x.append(log2.dateCreatedUTC().isoformat())
y.append(None)
x.append(log1.dateCreatedUTC().isoformat())
y.append(log1.ping)
return (x, y)
@bp.route("/")
def homepage():
return render_template("home.html")
@bp.route("/chart/<int:id>")
def chart(id: int):
with app.app_context():
logs = []
s = db.session.query(service).filter_by(id=id).first()
if s:
logs = cast(
list[log],
s.logs.order_by(log.dateCreated.desc()) # type: ignore
.limit(300)
.all(),
)
else:
return abort(code=403)
x, y = prepare_chart_data(logs=logs)
now = datetime.now(timezone.utc)
max_ = now
min_ = now - timedelta(hours=1)
return render_template(
"chart.html",
dates=x,
values=json.dumps(y),
min=min_.isoformat(),
max=max_.isoformat(),
)
@bp.route("/status")
def status():
results: list[dict[str, Any]] = []
with app.app_context():
a = db.session.query(service).all()
for s in a:
b = cast(
Optional[log],
s.logs.order_by(
log.dateCreated.desc() # type: ignore
).first(),
)
if b:
results.append(s.to_dict() | b.to_dict())
return jsonify(results)
@bp.route("/favicon.svg")
def favicon():
return send_file("/static/favicon.svg")

23
app/static/favicon.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="250px" height="250px" viewBox="-77.997 -77.998 250 250" enable-background="new -77.997 -77.998 250 250"
xml:space="preserve">
<circle fill="#009090" cx="47.003" cy="47.002" r="121.834"/>
<g>
<path fill="#FFFFFF" d="M135.518,28.755c0,5.432-0.891,10.791-2.618,15.942c-8.112,36.712-78.279,81.124-81.264,83.017
c-0.934,0.581-1.978,0.884-3.046,0.884c-1.063,0-2.125-0.271-3.06-0.884c-0.8-0.521-20.569-12.996-40.611-30.144
c-2.4-2.071-2.653-5.639-0.639-8.026c2.062-2.37,5.627-2.661,8.031-0.645c14.94,12.766,30.039,23.081,36.308,27.238
c21.07-13.738,67.592-48.691,73.293-74.471c1.489-4.553,2.164-8.695,2.164-12.899c0-14.636-7.967-28.126-20.797-35.195
c-16.387-9.091-38.32-4.898-50.309,9.551c-2.151,2.61-6.583,2.61-8.778,0C32.171-11.335,10.254-15.495-6.107-6.441
c-12.871,7.083-20.839,20.567-20.839,35.187c0,4.177,0.707,8.335,2.025,12.354c0.845,3.76,2.462,7.437,4.52,11.25h33.237
L24.25,26.116c0.978-2.245,3.313-3.657,5.714-3.405c2.447,0.214,4.487,1.973,5.032,4.363L43.4,62.599l10.868-17.824
c1.024-1.693,2.869-2.727,4.86-2.727h20.629c3.137,0,5.689,2.541,5.689,5.696c0,3.137-2.553,5.693-5.689,5.693H62.323
L45.948,80.335c-1.044,1.714-2.885,2.739-4.842,2.739c-0.311,0-0.611-0.025-0.89-0.067c-2.293-0.369-4.101-2.062-4.652-4.306
l-7.678-32.358l-6.068,13.967c-0.906,2.098-2.96,3.423-5.226,3.423h-66.41c-3.117,0-5.694-2.55-5.694-5.693
c0-3.138,2.577-5.694,5.694-5.694h16.698c-1.185-2.826-2.142-5.57-2.735-8.224c-1.589-4.608-2.449-9.975-2.449-15.379
c0-18.779,10.213-36.071,26.716-45.163c19.114-10.573,44.406-7.249,60.202,7.594c15.754-14.845,41.027-18.167,60.175-7.594
C125.272-7.339,135.518,9.964,135.518,28.755z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

12
app/static/icons/0.svg Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<style>
.st1{fill:#fff}
</style>
<g id="Icon">
<circle cx="512" cy="512" r="512" style="fill:#609926"/>
<path class="st1" d="M762.2 350.3c-100.9 5.3-160.7 8-212 8.5v114.1l-16-7.9-.1-106.1c-58.9 0-110.7-3.1-209.1-8.6-12.3-.1-29.5-2.4-47.9-2.5-47.1-.1-110.2 33.5-106.7 118C175.8 597.6 296 609.9 344 610.9c5.3 24.7 61.8 110.1 103.6 114.6H631c109.9-8.2 192.3-373.8 131.2-375.2zm-546 117.3c-4.7-36.6 11.8-74.8 73.2-73.2C296.1 462 307 501.5 329 561.9c-56.2-7.4-104-25.7-112.8-94.3zm415.6 83.5-51.3 105.6c-6.5 13.4-22.7 19-36.2 12.5l-105.6-51.3c-13.4-6.5-19-22.7-12.5-36.2l51.3-105.6c6.5-13.4 22.7-19 36.2-12.5l105.6 51.3c13.4 6.6 19 22.8 12.5 36.2z"/>
<path class="st1" d="M555 609.9c.1-.2.2-.3.2-.5 17.2-35.2 24.3-49.8 19.8-62.4-3.9-11.1-15.5-16.6-36.7-26.6-.8-.4-1.7-.8-2.5-1.2.2-2.3-.1-4.7-1-7-.8-2.3-2.1-4.3-3.7-6l13.6-27.8-11.9-5.8-13.7 28.4c-2 0-4.1.3-6.2 1-8.9 3.2-13.5 13-10.3 21.9.7 1.9 1.7 3.5 2.8 5l-23.6 48.4c-1.9 0-3.8.3-5.7 1-8.9 3.2-13.5 13-10.3 21.9 3.2 8.9 13 13.5 21.9 10.3 8.9-3.2 13.5-13 10.3-21.9-.9-2.5-2.3-4.6-4-6.3l23-47.2c2.5.2 5 0 7.5-.9 2.1-.8 3.9-1.9 5.5-3.3.9.4 1.9.9 2.7 1.3 17.4 8.2 27.9 13.2 30 19.1 2.6 7.5-5.1 23.4-19.3 52.3-.1.2-.2.5-.4.7-2.2-.1-4.4.2-6.5 1-8.9 3.2-13.5 13-10.3 21.9 3.2 8.9 13 13.5 21.9 10.3 8.9-3.2 13.5-13 10.3-21.9-.6-2-1.9-4-3.4-5.7z"/>
</g>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
app/static/icons/1.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="15%" fill="#282a2d"/><path d="M256 70H148l108 186-108 186h108l108-186z" fill="#e5a00d"/></svg>

After

Width:  |  Height:  |  Size: 191 B

27
app/static/icons/10.svg Normal file
View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 1125.628 1125.628" xml:space="preserve">
<g>
<path d="M562.812,0.002C252.476,0.002,0,252.479,0,562.814s252.476,562.812,562.812,562.812
c310.34,0,562.817-252.476,562.817-562.812S873.152,0.002,562.812,0.002z M309.189,739.263l-68.974-101h-17.735v101h-70v-357h70
v203h15.889l57.901-93h77.963l-79.808,111.736l92.036,135.264H309.189z M468.184,672.88c7.299,13.589,20.325,20.382,38.317,20.382
c11.995,0,21.792-3.329,29.023-10.286c7.226-6.952,11.026-14.712,11.026-27.712h61.131l0.69,1.237
c0.612,25.224-8.88,46.258-28.489,63.246c-19.605,16.997-43.942,25.452-73.007,25.452c-37.218,0-65.962-11.781-86.11-35.309
c-20.144-23.529-30.283-53.763-30.283-90.671v-6.925c0-36.753,10.102-66.968,30.169-90.652
c20.071-23.68,48.745-35.524,85.958-35.524c30.76,0,55.57,8.766,74.412,26.297c18.833,17.531,27.954,41.73,27.342,70.334
l-0.453,2.516H546.55c0-14-3.54-24.775-10.611-33.312c-7.075-8.533-16.837-13.365-29.298-13.365
c-17.837,0-31.158,6.628-38.457,20.446c-7.308,13.818-11.703,31.349-11.703,53.151v6.911
C456.481,641.362,460.876,659.29,468.184,672.88z M793.142,739.263c-2.462-4-4.582-11.157-6.345-17.465
c-1.772-6.304-3.038-12.499-3.805-19.113c-6.925,12.15-16.033,22.354-27.338,30.348c-11.301,7.998-24.798,12.061-40.484,12.061
c-26.141,0-46.285-6.691-60.432-20.148c-14.151-13.457-21.222-31.78-21.222-54.998c0-24.456,9.414-43.221,28.256-56.683
c18.833-13.452,46.327-20.003,82.467-20.003h39.242v-20.18c0-11.995-3.974-21.3-10.282-27.914
c-6.303-6.609-16.019-9.917-28.32-9.917c-10.922,0-19.545,2.65-25.465,7.957c-5.92,5.303-8.982,12.648-8.982,22.026l-65.101-0.228
l-0.259-1.384c-1.073-21.066,8.063-39.251,27.44-54.553c19.377-15.302,44.822-22.953,76.349-22.953
c29.832,0,54.075,7.578,72.684,22.72c18.605,15.151,27.938,36.716,27.938,64.703v103.113c0,11.689,0.854,22.156,2.622,32.461
c1.768,10.3,4.55,21.149,8.396,30.149H793.142z M902.481,739.263v-357h70v357H902.481z"/>
<path d="M711.712,640.846c-7.382,7.153-11.072,16.229-11.072,26.379c0,8.304,2.768,15.211,8.304,20.285
c5.536,5.075,13.069,7.717,22.606,7.717c11.84,0,23.195-2.865,32.422-8.707c9.227-5.847,14.509-12.558,19.509-20.246v-37.012
h-39.242C729.933,629.263,719.093,633.698,711.712,640.846z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

7
app/static/icons/11.svg Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Ubiquiti" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#399cdb"/><path d="M112 94v18h18V94h-18zm288 0c-82 0-90 31-90 61v172a147 147 0 01-3 28c43-9 72-36 86-82l7-23V94zm-234 18v18h18v-18h-18zm-18 18v18h18v-18h-18zm36 9v18h18v-18h-18zm-72 4v147c0 73 53 128 144 128 0 0-54-30-54-91V197h-18v66h-18v-39h-18v17h-18v-98h-18zm54 18v18h18v-18h-18zm-18 27v18h18v-18h-18zm252 87c-19 64-65 92-131 89-24-1-43-7-57-16 10 42 46 63 48 64l10 6c82-5 130-59 130-128v-15z" fill="#ffffff"/></svg>

After

Width:  |  Height:  |  Size: 680 B

5
app/static/icons/2.svg Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#0095d5"/>
<path d="M780 468.41v114.21L535.48 723.86v-114.2zM488.69 609.66v114.21L244 582.62V468.41l110.57 63.79a1.81 1.81 0 0 0 .4.24zM611 512l-98.91 57.15-99-57.15 99-57.15zm145.7-84.09-99 57.15-122.22-70.64V300.13zm-268-127.81v114.32L366.3 485l-99-57.06z" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B

9
app/static/icons/3.svg Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="#ffffff" width="800px" height="800px" viewBox="-3.2 -3.2 38.40 38.40" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)">
<rect x="-3.2" y="-3.2" width="38.40" height="38.40" rx="19.2" fill="#00678e" strokewidth="0"/>

After

Width:  |  Height:  |  Size: 1.4 KiB

5
app/static/icons/4.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="57.54 57.53 396.92 396.93">
<path d="M0 0 C0.77839233 0.70552002 0.77839233 0.70552002 1.57250977 1.42529297 C20.51724698 18.95857955 35.3183542 42.25039645 45.25 65.9375 C45.75917969 67.13761719 46.26835937 68.33773437 46.79296875 69.57421875 C53.13904333 85.15591751 57.42335993 102.21306817 59.25 118.9375 C59.35957031 119.79472656 59.46914063 120.65195313 59.58203125 121.53515625 C65.30647708 174.58168758 49.01780615 227.1014046 15.875 268.5 C12.4098987 272.65198864 8.83161111 276.6927508 5.1875 280.6875 C4.48197998 281.46589233 4.48197998 281.46589233 3.76220703 282.26000977 C-13.77107955 301.20474698 -37.06289645 316.0058542 -60.75 325.9375 C-61.95011719 326.44667969 -63.15023438 326.95585937 -64.38671875 327.48046875 C-79.96841751 333.82654333 -97.02556817 338.11085993 -113.75 339.9375 C-114.60722656 340.04707031 -115.46445312 340.15664063 -116.34765625 340.26953125 C-169.39418758 345.99397708 -221.9139046 329.70530615 -263.3125 296.5625 C-267.46448864 293.0973987 -271.5052508 289.51911111 -275.5 285.875 C-276.27839233 285.16947998 -276.27839233 285.16947998 -277.07250977 284.44970703 C-296.01724698 266.91642045 -310.8183542 243.62460355 -320.75 219.9375 C-321.25917969 218.73738281 -321.76835937 217.53726562 -322.29296875 216.30078125 C-328.63904333 200.71908249 -332.92335993 183.66193183 -334.75 166.9375 C-334.85957031 166.08027344 -334.96914063 165.22304688 -335.08203125 164.33984375 C-340.80647708 111.29331242 -324.51780615 58.7735954 -291.375 17.375 C-287.9098987 13.22301136 -284.33161111 9.1822492 -280.6875 5.1875 C-280.21715332 4.66857178 -279.74680664 4.14964355 -279.26220703 3.61499023 C-261.72892045 -15.32974698 -238.43710355 -30.1308542 -214.75 -40.0625 C-213.54988281 -40.57167969 -212.34976563 -41.08085938 -211.11328125 -41.60546875 C-195.53158249 -47.95154333 -178.47443183 -52.23585993 -161.75 -54.0625 C-160.89277344 -54.17207031 -160.03554687 -54.28164063 -159.15234375 -54.39453125 C-99.82500101 -60.79676248 -43.54726444 -39.72491441 0 0 Z " fill="#A289F9" transform="translate(393.75,113.0625)"/>
<path d="M0 0 C2.63622485 2.22708275 5.19150738 4.52921782 7.7409668 6.85546875 C8.46670898 7.49871094 9.19245117 8.14195312 9.94018555 8.8046875 C28.36748008 26.03588989 41.75155651 53.35960955 42.86425781 78.65869141 C43.70797362 112.3608808 36.58885956 142.14006064 13.7409668 167.85546875 C12.54020508 169.20705078 12.54020508 169.20705078 11.31518555 170.5859375 C-8.53153214 191.53705519 -37.09680019 204.15705607 -65.8137207 205.140625 C-77.94100422 205.37709014 -89.5422865 205.25443065 -101.2590332 201.85546875 C-103.0224707 201.37980469 -103.0224707 201.37980469 -104.8215332 200.89453125 C-121.40302939 195.94116057 -136.3527812 187.31831389 -149.2590332 175.85546875 C-150.14848633 175.0665625 -151.03793945 174.27765625 -151.9543457 173.46484375 C-167.40909083 158.83370573 -177.61511815 140.25052542 -183.2590332 119.85546875 C-183.5787207 118.70820313 -183.8984082 117.5609375 -184.2277832 116.37890625 C-188.26683057 98.91376625 -188.98196274 77.02425735 -183.2590332 59.85546875 C-182.5990332 58.86546875 -181.9390332 57.87546875 -181.2590332 56.85546875 C-180.93032227 57.46132812 -180.60161133 58.0671875 -180.26293945 58.69140625 C-172.21889547 72.86113142 -160.79177658 83.491992 -144.8840332 88.23046875 C-127.13242009 91.2910917 -110.45150547 88.72225434 -95.2590332 78.85546875 C-83.60677615 70.49132656 -75.75104634 56.9235307 -73.2590332 42.85546875 C-71.45757952 24.03946126 -74.73970981 8.78864654 -86.8762207 -6.1328125 C-90.96217608 -10.5413433 -95.77705169 -13.57401274 -100.90356445 -16.6640625 C-103.2590332 -18.14453125 -103.2590332 -18.14453125 -105.2590332 -20.14453125 C-103.57312478 -20.62926803 -101.88520775 -21.10702051 -100.1965332 -21.58203125 C-98.78694336 -21.98228516 -98.78694336 -21.98228516 -97.34887695 -22.390625 C-64.24168178 -30.4686132 -26.24452842 -21.45755623 0 0 Z " fill="#131928" transform="translate(344.259033203125,182.14453125)"/>
<path d="M0 0 C27.51668329 -1.54459553 56.68622642 12.36261605 76.97143555 30.24731445 C100.05828616 51.22761351 116.14269665 79.83089922 118.22119141 111.35473633 C119.4842267 149.41509562 109.54536352 181.25369492 83.42504883 209.56640625 C61.20694161 232.76768274 30.18258643 245.68447023 -1.72436523 246.47045898 C-37.47694444 246.75019142 -67.91707948 234.63666274 -93.52539062 209.50195312 C-112.03605933 190.64804024 -127.24579555 162.86302866 -128.09765625 135.91015625 C-128.08605469 134.97042969 -128.07445312 134.03070313 -128.0625 133.0625 C-128.05347656 132.10472656 -128.04445312 131.14695312 -128.03515625 130.16015625 C-128.01775391 129.09087891 -128.01775391 129.09087891 -128 128 C-127.67 128 -127.34 128 -127 128 C-126.79246094 129.23492187 -126.58492187 130.46984375 -126.37109375 131.7421875 C-122.03641631 156.12747696 -112.65378734 175.70645513 -96 194 C-95.43023437 194.65226563 -94.86046875 195.30453125 -94.2734375 195.9765625 C-77.53338041 214.09016274 -49.82674725 227.04001852 -25.19677734 228.12329102 C8.55585455 228.96826963 38.21751895 221.8135893 64 199 C64.82757813 198.27296875 65.65515625 197.5459375 66.5078125 196.796875 C87.15165343 177.37583998 99.32367565 148.51732778 100.28515625 120.4453125 C100.52162139 108.31802898 100.3989619 96.7167467 97 85 C96.68289063 83.824375 96.36578125 82.64875 96.0390625 81.4375 C91.07002873 64.80357134 82.4108957 49.99996858 71 37 C70.28457031 36.18402344 69.56914062 35.36804688 68.83203125 34.52734375 C50.47303107 14.99722536 25.98974766 5.20070823 0 1 C0 0.67 0 0.34 0 0 Z " fill="#6865CD" transform="translate(286,158)"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

1
app/static/icons/5.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256"/><path d="M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 389 B

1
app/static/icons/6.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" style="fill:#292b2f"/><path d="M380.6 344.1c-14.8 0-27.5 8.6-33.5 21.1l-53.2-7.7c-.2-20.3-16.7-36.8-37.1-36.8-7.3 0-14 2.1-19.8 5.7l-75.6-85.6c4.4-6.1 6.9-13.5 6.9-21.6 0-20.5-16.6-37.1-37.1-37.1s-37.1 16.6-37.1 37.1 16.6 37.1 37.1 37.1c6.3 0 12.3-1.6 17.5-4.4l76.4 86.5c-3.4 5.6-5.4 12.2-5.4 19.3 0 20.5 16.6 37.1 37.1 37.1 14.7 0 27.4-8.5 33.4-20.9l53.3 7.7c.3 20.2 16.8 36.5 37.1 36.5 20.5 0 37.1-16.6 37.1-37.1.1-20.3-16.6-36.9-37.1-36.9" style="fill:#fff"/><path d="M380.6 93.7c-20.5 0-37.1 16.6-37.1 37.1 0 10.1 4 19.2 10.6 25.9L271.8 273c-4.6-2-9.6-3.1-15-3.1-20.5 0-37.1 16.6-37.1 37.1q0 5.55 1.5 10.5l-62.5 38.7c-6.8-7.4-16.6-12.1-27.4-12.1-20.5 0-37.1 16.6-37.1 37.1s16.6 37.1 37.1 37.1 37.1-16.6 37.1-37.1c0-3.5-.5-7-1.4-10.2l62.6-38.8c6.8 7.3 16.5 11.9 27.2 11.9 20.5 0 37.1-16.6 37.1-37.1 0-9-3.2-17.3-8.6-23.8l83-117.3c3.8 1.3 7.9 2.1 12.2 2.1 20.5 0 37.1-16.6 37.1-37.1s-16.5-37.2-37-37.2" style="fill:#e5a00d"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
app/static/icons/7.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

7
app/static/icons/8.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

1
app/static/icons/9.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

5
app/static/lock.svg Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="#878e96"/>
<path d="M15.7491 10.73V10C15.7491 9.07 15.7491 6.25 11.9991 6.25C8.24915 6.25 8.24915 9.07 8.24915 10V10.73C7.02915 11 6.61914 11.79 6.61914 13.5V14.5C6.61914 16.7 7.29915 17.38 9.49915 17.38H14.4991C16.6991 17.38 17.3792 16.7 17.3792 14.5V13.5C17.3792 11.79 16.9691 11 15.7491 10.73ZM11.9991 15.1C11.3891 15.1 10.8992 14.61 10.8992 14C10.8992 13.39 11.3891 12.9 11.9991 12.9C12.6091 12.9 13.0991 13.39 13.0991 14C13.0991 14.61 12.6091 15.1 11.9991 15.1ZM14.2491 10.62H9.74915V10C9.74915 8.54 10.1091 7.75 11.9991 7.75C13.8891 7.75 14.2491 8.54 14.2491 10V10.62Z" fill="#292D32"/>
</svg>

After

Width:  |  Height:  |  Size: 950 B

6
app/static/no_access.svg Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 36 36" version="1.1" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>no-access-solid</title>
<path d="M18,2A16,16,0,1,0,34,18,16,16,0,0,0,18,2ZM29.15,20H6.85A.85.85,0,0,1,6,19.15V16.85A.85.85,0,0,1,6.85,16H29.15a.85.85,0,0,1,.85.85v2.29A.85.85,0,0,1,29.15,20Z" class="clr-i-solid clr-i-solid-path-1"></path>
<rect x="0" y="0" width="36" height="36" fill-opacity="0"/>
</svg>

After

Width:  |  Height:  |  Size: 642 B

60
app/templates/chart.html Normal file
View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Line Chart</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<script src="https://www.chartjs.org/docs/latest/samples/utils.js"></script>
</head>
<body>
<canvas id="myChart"></canvas>
</body>
<script>
const chartDates = ({{ dates | safe }}).map(dt => new Date(dt));
const data = {
labels: chartDates,
datasets: [{
label: 'Ping',
data: {{ values }},
}]
};
const ctx = document.getElementById('myChart').getContext('2d');
// Current time in UTC
const nowUTC = new Date();
// One hour ago in UTC
const oneDayAgoUTC = new Date(nowUTC.getTime() - 24 * 60 * 60 * 1000);
const min = "{{ min }}"
const max = "{{ max }}"
new Chart(ctx, {
type: 'line',
data: data,
options: {
scales: {
x: {
type: 'time', // Important for datetime axis
time: {
unit: 'hour',
tooltipFormat: 'HH:mm:ss',
displayFormats: {
hour: 'HH:mm'
}
},
min: min,
max: max
}
}
}
});
</script>
</html>

104
app/templates/home.html Normal file
View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" charset=" UTF-8">
<title>Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<style>
/* Prevent Bootstrap's default 0.6s width tween that causes lag */
.progress-bar {
transition: none !important;
}
</style>
</head>
<body class="m-2 bg-light-subtle pt-3">
<div class="progress fixed-top mt-1" style="height: auto;">
<div id="progressBar" class="progress-bar rounded-pill" role="progressbar" style="width: 100%; margin: 0 auto;">
<h5 class="m-0">5s</h5>
</div>
</div>
<div id="main_body" class="d-flex flex-wrap justify-content-center"></div>
</body>
<script>
const main_body = document.getElementById("main_body");
const url = '/api/status';
const interval = 5000; // 5 seconds between requests
const progressBar = document.getElementById('progressBar');
function fetchData() {
fetch(url, { cache: 'no-store' })
.then(response => response.json())
.then(data => {
console.log(data)
updateWebpage(data)
})
.catch(error => console.error("Error fetching data", error))
.finally(() => {
// Animate progress bar over 'interval' before next fetch
animateProgress(interval, () => {
fetchData(); // next fetch after animation
});
});
}
function animateProgress(duration, callback) {
const start = performance.now();
function frame(now) {
const elapsed = now - start;
const ratio = Math.min(elapsed / duration, 1);
progressBar.style.width = 100 * (1 - ratio) + "%";
progressBar.innerHTML = "<h5 class='m-0'>" + (interval * (1 - ratio) / 1000).toFixed(1) + "s</h5>";
if (ratio < 1) {
requestAnimationFrame(frame);
} else {
callback();
}
}
// reset bar and start animation
progressBar.style.width = '100%';
progressBar.textContent = '100%';
requestAnimationFrame(frame);
}
function updateWebpage(services) {
main_body.innerHTML = ''; // Clear webpage
// Build all service divs as a single string
main_body.innerHTML = services.map(s => `
<a href="${s.url}" class="d-block text-body text-decoration-none m-2 border border-3 ${s.ping ? 'border-success' : 'border-danger'}" style="width: 175px">
<div class="bg-body-tertiary d-flex flex-column align-items-center">
<div class="bg-dark w-100">
<h4 class="text-center text-truncate m-0 p-1">${s.label}</h4>
</div>
<div class="position-relative ratio ratio-1x1">
<div class="d-flex justify-content-center align-items-center">
<img src="{{ url_for('static', filename="icons") }}/${s.service_id - 1}.svg" class="img-fluid w-75">
</div>
<div>
${s.public_access ? `` : `<img src='{{ url_for('static', filename='lock.svg') }}' class='img-fluid position-absolute bottom-0 end-0 w-25'>`}
<div class="position-absolute bottom-0 text-body bg-dark bg-opacity-75 px-1 rounded">${s.ping ? s.ping + "ms" : ""}</div>
</div>
</div>
</div>
</a>
`).join(''); // join into a single string
}
fetchData(); // start the loop
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"></script>
</html>