roundabout,
created on Sunday, 7 April 2024, 06:31:02 (1712471462),
received on Sunday, 7 April 2024, 12:44:08 (1712493848)
Author identity: vlad <vlad.muntoiu@gmail.com>
f656a9a49843e3d263f83d42f89b765f69ccd8da
.idea/workspace.xml
@@ -4,10 +4,14 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="411335b4-e813-41ad-9046-18b77b97ee46" name="Changes" comment="Do it">
<list default="true" id="411335b4-e813-41ad-9046-18b77b97ee46" name="Changes" comment="Fix minute recording bug">
<change afterPath="$PROJECT_DIR$/static/bugs.svg" afterDir="false" />
<change afterPath="$PROJECT_DIR$/static/logo.svg" afterDir="false" />
<change afterPath="$PROJECT_DIR$/templates/my-apps.html" afterDir="false" />
<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$/templates/app-editor.html" beforeDir="false" afterPath="$PROJECT_DIR$/templates/app-editor.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/static/style.css" beforeDir="false" afterPath="$PROJECT_DIR$/static/style.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/templates/app.html" beforeDir="false" afterPath="$PROJECT_DIR$/templates/app.html" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -40,12 +44,13 @@
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "main",
"last_opened_file_path": "/home/vlad/PycharmProjects/itec24/static",
"last_opened_file_path": "/home/vlad/PycharmProjects/itec24/templates",
"settings.editor.selected.configurable": "preferences.pluginManager"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/templates" />
<recent name="$PROJECT_DIR$/static" />
</key>
</component>
@@ -105,7 +110,15 @@
<option name="project" value="LOCAL" />
<updated>1712464824601</updated>
</task>
<option name="localTasksCounter" value="6" />
<task id="LOCAL-00006" summary="Fix minute recording bug">
<option name="closed" value="true" />
<created>1712465435852</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1712465435852</updated>
</task>
<option name="localTasksCounter" value="7" />
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
@@ -125,6 +138,7 @@
<MESSAGE value="Endpoint management" />
<MESSAGE value="Save ping results" />
<MESSAGE value="Do it" />
<option name="LAST_COMMIT_MESSAGE" value="Do it" />
<MESSAGE value="Fix minute recording bug" />
<option name="LAST_COMMIT_MESSAGE" value="Fix minute recording bug" />
</component>
</project>
app.py
@@ -49,7 +49,7 @@ migrate = Migrate(app, db)
app.config["SESSION_TYPE"] = "filesystem"
app.config["SECRET_KEY"] = "super secret"
with app.app_context():
with (app.app_context()):
class User(db.Model):
username = db.Column(db.String(64), unique=True, nullable=False, primary_key=True)
password = db.Column(db.String(72), nullable=False)
@@ -83,6 +83,7 @@ with app.app_context():
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)
create_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
application = db.relationship("Application", back_populates="endpoints")
statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic")
@@ -122,6 +123,7 @@ with app.app_context():
print(f"Pinging {url}")
response = httpx.get(url, verify=False)
reading = Status(id, response.status_code, buggy)
last_reading = db.session.query(Status).filter_by(endpoint_id=id).order_by(Status.time.desc()).first()
db.session.add(reading)
db.session.commit()
@@ -156,6 +158,13 @@ def dashboard():
return flask.render_template("dashboard.html", apps=Application.query.all())
@app.route("/my")
def my_apps():
if not flask.session.get("username"):
return flask.redirect("/login", code=303)
return flask.render_template("my-apps.html", apps=db.session.query(Application).filter_by(owner_name=flask.session["username"]).all())
@app.route("/login", methods=["GET"])
def login():
return flask.render_template("login.html")
@@ -247,25 +256,43 @@ def app_info(app_id):
time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * (i+1)),
datetime.datetime.utcnow() - datetime.timedelta(minutes=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())
for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)), 0, -1)]
slice_results = {}
all_results = []
for endpoint in app_.endpoints:
slice_results[endpoint.id] = []
for slice_ in time_slices:
slice_results[endpoint.id].append(
(
db.session.query(Status).filter(
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
Status.time >= slice_[0],
Status.time < slice_[1])).all(),
slice_
)
)
for endpoint in app_.endpoints:
all_results.extend(db.session.query(Status).filter(
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
Status.time >= datetime.datetime.utcnow() - datetime.timedelta(minutes=10),
Status.time < datetime.datetime.utcnow())).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_,
is_partial=lambda x: any(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
for status in x),
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)
slices=slice_results, bugs=lambda x: any(status.buggy for status in x),
all_results=all_results)
@app.route("/app/<int:app_id>/edit/")
@@ -296,6 +323,24 @@ def endpoint_edit(app_id, endpoint_id):
return flask.redirect(f"/app/{app_id}/edit", code=303)
@app.route("/app/<int:app_id>/report/<int:endpoint_id>")
def endpoint_report(app_id, endpoint_id):
endpoint = db.session.get(Endpoint, endpoint_id)
endpoint.buggy = True
db.session.commit()
return flask.redirect(f"/app/{app_id}", code=303)
@app.route("/app/<int:app_id>/fix/<int:endpoint_id>")
def endpoint_fix(app_id, endpoint_id):
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
flask.abort(403)
endpoint = db.session.get(Endpoint, endpoint_id)
endpoint.buggy = False
db.session.commit()
return flask.redirect(f"/app/{app_id}", code=303)
@app.route("/app/<int:app_id>/add-endpoint", methods=["POST"])
def app_add_endpoint(app_id):
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
static/bugs.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#ffffff" d="M10 3h4v11h-4zm0 18v-4h4v4z"/></svg>
static/logo.svg
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg5"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:document-units="px"
showgrid="false"
borderlayer="false"
inkscape:showpageshadow="true"
units="px"
width="1920px"
inkscape:zoom="10.918519"
inkscape:cx="19.920285"
inkscape:cy="27.751018"
inkscape:current-layer="layer1"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs2" />
<g
inkscape:label="Strat 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#00c853;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:22.1;paint-order:stroke markers fill"
id="path1"
cx="3.7041669"
cy="3.7041664"
r="2.1166666" />
<circle
style="fill:#ffd600;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:22.1;paint-order:stroke markers fill"
id="circle2"
cx="8.9958334"
cy="3.7041664"
r="2.1166666" />
<circle
style="fill:#ef5350;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:22.1;paint-order:stroke markers fill"
id="circle3"
cx="3.7041669"
cy="8.9958334"
r="2.1166666" />
<circle
style="fill:#00c853;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:22.1;paint-order:stroke markers fill"
id="circle4"
cx="8.9958334"
cy="8.9958334"
r="2.1166666" />
</g>
<metadata
id="metadata2277">
<rdf:RDF>
<cc:Work
rdf:about="">
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>
static/style.css
@@ -191,7 +191,7 @@ input[type="password"]:not(:placeholder-shown) {
.app-uptime {
display: flex;
flex-flow: row-reverse nowrap;
flex-flow: row nowrap;
overflow: hidden;
border-radius: calc(24px - 0.75rem);
}
@@ -218,6 +218,11 @@ input[type="password"]:not(:placeholder-shown) {
height: 2rem;
flex: 1 0 auto;
outline: 1px solid #ffffff;
color: #ffffff;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
}
.uptime-bar-ok {
@@ -313,3 +318,27 @@ label {
label > input {
width: 100%;
}
.endpoint-header {
display: flex;
gap: 1rem;
justify-content: space-between;
align-items: center;
}
.endpoint-header > h2 {
margin: 0;
}
.action-buttons {
display: flex;
gap: 1rem;
align-items: center;
}
.uptime-bar-buggy {
background-image: url("/static/bugs.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
templates/app.html
@@ -3,6 +3,16 @@
{% block content %}
<main>
<h1>{{ app.name }}</h1>
<h2 class="subtitle">
{% if is_ok(all_results) %}
Operational
{% elif is_partial(all_results) %}
Unstable
{% else %}
Down
{% endif %}
</h2>
<p>Owner: {{ app.owner_name }}</p>
{% if session.get("username") == app.owner_name %}
<a class="button" style="width:100%; margin: 1em 0;" tabindex="0" href="/app/{{ app.id }}/edit">
<iconify-icon icon="mdi:pencil"></iconify-icon>
@@ -23,31 +33,51 @@
<div id="endpoint-list">
{% for endpoint in app.endpoints %}
<div class="endpoint-card">
<h2>{{ endpoint.name }}</h2>
<div class="endpoint-header">
<h2>{{ endpoint.name }}</h2>
<div class="action-buttons">
{% if not endpoint.buggy %}
<a href="/app/{{ app.id }}/report/{{ endpoint.id }}" class="button">
<iconify-icon icon="mdi:bug"></iconify-icon>
Report malfunction
</a>
{% elif app.owner_name == session.get("username") %}
<a href="/app/{{ app.id }}/fix/{{ endpoint.id }}" class="button">
<iconify-icon icon="mdi:tools"></iconify-icon>
Mark as fixed
</a>
{% endif %}
<a href="{{ endpoint.address }}" class="button">
<iconify-icon icon="mdi:send"></iconify-icon>
Access
</a>
</div>
</div>
{% if endpoint.buggy %}
<p class="endpoint-info endpoint-info-down">
Malfunctioning
</p>
{% elif is_ok(slices[endpoint.id][-1][0]) %}
Operational
{% elif is_partial(slices[endpoint.id][-1][0]) %}
Unstable
{% else %}
Down
{% endif %}
<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>
{% for slice in slices[endpoint.id] %}
{% if not slice[0] %}
<div class="uptime-bar
{% elif is_ok(slice[0]) %}
<div class="uptime-bar uptime-bar-ok
{% else %}
<div class="uptime-bar uptime-bar-down"></div>
<div class="uptime-bar uptime-bar-down
{% endif %}
{% if bugs(slice[0]) %}
uptime-bar-buggy
{% endif %}
" title="{{ slice[1][0].strftime('%Y-%m-%d %H:%M:%S') }} – {{ slice[1][1].strftime('%Y-%m-%d %H:%M:%S') }}"></div>
{% endfor %}
</div>
</div>
templates/my-apps.html
@@ -0,0 +1,55 @@
{% extends "default.html" %}
{% block title %}My Apps | Echo Tracker{% endblock %}
{% set active = "my" %}
{% block content %}
<main>
<h1>My Apps</h1>
{% if session.get("username") %}
<a class="button" style="width:100%; margin: 1em 0;" tabindex="0" href="/new-app">
<iconify-icon icon="mdi:plus"></iconify-icon>
Create an app
</a>
{% endif %}
<!--
<div class="app-card">
<h2>Application Name</h2>
<p class="app-info app-info-ok">
Operational
</p>
<div class="app-uptime">
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-down"></div>
<div class="uptime-bar uptime-bar-broken"></div>
<div class="uptime-bar uptime-bar-broken"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-broken"></div>
<div class="uptime-bar uptime-bar-broken"></div>
<div class="uptime-bar uptime-bar-broken"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-down"></div>
<div class="uptime-bar uptime-bar-down"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
<div class="uptime-bar uptime-bar-ok"></div>
</div>
</div>
-->
{% for app in apps %}
<a href="/app/{{ app.id }}" class="quiet-link">
<div class="app-card">
<h2>{{ app.name }}</h2>
</div>
</a>
{% endfor %}
</main>
{% endblock %}