MAJOR REFACTOR P1
21
app/__init__.py
Normal 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()
|
||||
3
app/aio_client/__init__.py
Normal 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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
timeout: int = 4000
|
||||
20
app/flask_app.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 7.8 KiB |
7
app/static/icons/8.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
app/static/icons/9.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
5
app/static/lock.svg
Normal 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
@@ -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
@@ -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
@@ -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>
|
||||