roundabout,
created on Sunday, 7 April 2024, 04:40:24 (1712464824),
received on Sunday, 7 April 2024, 12:44:08 (1712493848)
Author identity: vlad <vlad.muntoiu@gmail.com>
96c9bc1fc10c421a0983dff39a702d0fc3b42ba4
.idea/workspace.xml
@@ -4,7 +4,7 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="411335b4-e813-41ad-9046-18b77b97ee46" name="Changes" comment="Endpoint management">
<list default="true" id="411335b4-e813-41ad-9046-18b77b97ee46" name="Changes" comment="Save ping results">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app.py" beforeDir="false" afterPath="$PROJECT_DIR$/app.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/static/style.css" beforeDir="false" afterPath="$PROJECT_DIR$/static/style.css" afterDir="false" />
@@ -91,7 +91,15 @@
<option name="project" value="LOCAL" />
<updated>1712411232132</updated>
</task>
<option name="localTasksCounter" value="4" />
<task id="LOCAL-00004" summary="Save ping results">
<option name="closed" value="true" />
<created>1712420788013</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1712420788013</updated>
</task>
<option name="localTasksCounter" value="5" />
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
@@ -109,6 +117,7 @@
<MESSAGE value="Initial commit" />
<MESSAGE value="More" />
<MESSAGE value="Endpoint management" />
<option name="LAST_COMMIT_MESSAGE" value="Endpoint management" />
<MESSAGE value="Save ping results" />
<option name="LAST_COMMIT_MESSAGE" value="Save ping results" />
</component>
</project>
app.py
@@ -2,6 +2,7 @@ import datetime
import celery
import flask
import sqlalchemy
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate
@@ -37,6 +38,11 @@ celery_app = celery_init_app(app)
app.config["SQLALCHEMY_DATABASE_URI"] = \
"postgresql://echo:1234@localhost:5432/echo"
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
"connect_args": {
"options": "-c timezone=utc"
}
}
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
migrate = Migrate(app, db)
@@ -76,8 +82,10 @@ with app.app_context():
name = db.Column(db.String(64), nullable=False)
comment = db.Column(db.String(2048), nullable=True)
ping_interval = db.Column(db.Integer, default=300, nullable=False)
buggy = db.Column(db.Boolean, default=False)
application = db.relationship("Application", back_populates="endpoints")
statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic")
def __init__(self, application, name, address, ping_interval, comment=""):
self.application_id = application.id
@@ -88,11 +96,13 @@ with app.app_context():
class Status(db.Model):
id = db.Column(db.Integer, unique=True, nullable=False, autoincrement=True, primary_key=True, default=0)
endpoint_id = db.Column(db.Integer, nullable=False)
id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True)
endpoint_id = db.Column(db.Integer, db.ForeignKey("endpoint.id"), nullable=False)
time = db.Column(db.DateTime, default=datetime.datetime.utcnow)
status = db.Column(db.SmallInteger, nullable=False)
buggy = db.Column(db.Boolean, default=False)
endpoint = db.relationship("Endpoint", back_populates="statuses")
def __init__(self, endpoint_id, status):
self.endpoint_id = endpoint_id
@@ -218,10 +228,37 @@ def signup_post():
return flask.redirect("/", code=303)
# UTC filter
@app.template_filter("utc")
def utc_filter(timestamp):
return datetime.datetime.utcfromtimestamp(timestamp)
@app.route("/app/<int:app_id>/")
def app_info(app_id):
app_ = db.session.get(Application, app_id)
return flask.render_template("app.html", app=app_)
time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(seconds=int(flask.request.args.get("bar_duration", 30)) * 60 * (i+1)),
datetime.datetime.utcnow() - datetime.timedelta(seconds=int(flask.request.args.get("bar_duration", 30)) * i))
for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)))]
slice_results = []
for slice_ in time_slices:
slice_results.append(db.session.query(Status).filter(
sqlalchemy.and_(Status.endpoint.has(application_id=app_id),
Status.time >= slice_[0],
Status.time < slice_[1])).all())
return flask.render_template("app.html", app=app_, sorted=sorted, list=list,
sorting=lambda x: x.time, reverse=True,
is_ok=lambda x: all(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
for status in x), and_=sqlalchemy.and_,
bar_duration=int(flask.request.args.get("bar_duration", 30)), int=int, Status=Status,
time_period=int(flask.request.args.get("time_period", 1440)),
now=round(datetime.datetime.utcnow().timestamp()), func=sqlalchemy.func,
reversed=reversed, fromtimestamp=datetime.datetime.utcfromtimestamp,
slices=slice_results)
@app.route("/app/<int:app_id>/edit/")
@@ -238,11 +275,15 @@ def endpoint_edit(app_id, endpoint_id):
flask.abort(403)
endpoint = db.session.get(Endpoint, endpoint_id)
if flask.request.form.get("delete") == "delete":
statuses = db.session.query(Status).filter_by(endpoint_id=endpoint_id).all()
for status in statuses:
db.session.delete(status)
db.session.delete(endpoint)
db.session.commit()
else:
endpoint.name = flask.request.form["name"]
endpoint.address = flask.request.form["url"]
endpoint.ping_interval = max(15, int(flask.request.form["ping_interval"]))
endpoint.comment = flask.request.form["comment"]
db.session.commit()
return flask.redirect(f"/app/{app_id}/edit", code=303)
@@ -256,9 +297,12 @@ def app_add_endpoint(app_id):
endpoint = Endpoint(app_,
flask.request.form["name"],
flask.request.form["url"],
300,
max(15, int(flask.request.form["ping_interval"])),
flask.request.form["comment"])
db.session.add(endpoint)
db.session.commit()
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
return flask.redirect(f"/app/{app_id}/edit", code=303)
static/style.css
@@ -191,6 +191,7 @@ input[type="password"]:not(:placeholder-shown) {
.app-uptime {
display: flex;
flex-flow: row-reverse nowrap;
overflow: hidden;
border-radius: calc(24px - 0.75rem);
}
@@ -216,6 +217,7 @@ input[type="password"]:not(:placeholder-shown) {
background: #546E7A;
height: 2rem;
flex: 1 0 auto;
outline: 1px solid #ffffff;
}
.uptime-bar-ok {
@@ -275,11 +277,11 @@ textarea {
.side-by-side {
display: flex;
gap: 1rem;
align-items: stretch;
align-items: center;
justify-content: space-around;
}
.side-by-side > button.extend {
.extend {
flex: 1 1 100%;
}
@@ -296,3 +298,18 @@ nav a:focus {
box-shadow: none;
text-decoration: underline;
}
.horizontal-form {
display: flex;
align-items: center;
gap: 1rem;
}
label {
color: #00796B;
display: block;
}
label > input {
width: 100%;
}
templates/app-editor.html
@@ -8,6 +8,10 @@
<form class="stacked-form" method="post" action="/app/{{ app.id }}/edit/{{ endpoint.id }}">
<input type="text" name="name" placeholder="Name" value="{{ endpoint.name }}">
<input type="url" name="url" placeholder="Ping address" value="{{ endpoint.address }}">
<div class="side-by-side">
<input class="extend" type="number" name="ping_interval" placeholder="Interval (seconds)" value="{{ endpoint.ping_interval }}" step="1" min="15">
seconds
</div>
<textarea name="comment" placeholder="Comment" rows="4">{{ endpoint.comment }}</textarea>
<div class="side-by-side">
<button type="submit" class="extend">Apply changes</button>
@@ -22,6 +26,10 @@
<form class="stacked-form" method="post" action="/app/{{ app.id }}/add-endpoint">
<input type="text" name="name" placeholder="Name">
<input type="url" name="url" placeholder="Ping address">
<div class="side-by-side">
<input class="extend" type="number" name="ping_interval" placeholder="Interval (seconds)" value="300" step="1" min="15">
seconds
</div>
<textarea name="comment" placeholder="Comment" rows="4"></textarea>
<button type="submit">Add</button>
</form>
templates/app.html
@@ -9,12 +9,47 @@
Manage endpoints
</a>
{% endif %}
<form style="margin-bottom: 1em;" class="horizontal-form">
<label class="extend">
Interval duration (minutes)
<input type="number" name="bar_duration" step="1" value="{{ bar_duration }}">
</label>
<label class="extend">
Time period (minutes)
<input type="number" name="time_period" step="1" value="{{ time_period }}">
</label>
<button type="submit">Change</button>
</form>
<div id="endpoint-list">
{% for endpoint in app.endpoints %}
<div class="endpoint-card">
<h2>{{ endpoint.name }}</h2>
<p>{{ endpoint.comment }}</p>
<div class="app-uptime">
<!-- {% set slice_size = bar_duration * 60 %}-->
<!-- {% set shown_seconds = 3600 %}-->
<!-- {% set num_slices = int(shown_seconds // slice_size) %}-->
<!-- {% for i in range(num_slices) %}-->
<!-- {% set slice_statuses = endpoint.statuses.filter(Status.time.between(fromtimestamp(now - i*slice_size), fromtimestamp(now - i*slice_size + slice_size))) %}-->
<!-- {% if not slice_statuses.count() %}-->
<!-- <div class="uptime-bar"></div>-->
<!-- {% elif is_ok(slice_statuses) %}-->
<!-- <div class="uptime-bar uptime-bar-ok"></div>-->
<!-- {% else %}-->
<!-- <div class="uptime-bar uptime-bar-down"></div>-->
<!-- {% endif %}-->
<!-- {{ slice_statuses.all() }}<br>-->
<!-- {% endfor %}-->
{% for slice in slices %}
{% if not slice %}
<div class="uptime-bar"></div>
{% elif is_ok(slice) %}
<div class="uptime-bar uptime-bar-ok"></div>
{% else %}
<div class="uptime-bar uptime-bar-down"></div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>