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 %}