roundabout,
created on Saturday, 6 April 2024, 13:16:00 (1712409360),
received on Sunday, 7 April 2024, 12:44:08 (1712493848)
Author identity: vlad <vlad.muntoiu@gmail.com>
d9b8489cff2e0a30067bb118b360346b8e46c269
.idea/workspace.xml
@@ -4,20 +4,17 @@
<option name="autoReloadType" value="SELECTIVE" /> </component> <component name="ChangeListManager"> <list default="true" id="411335b4-e813-41ad-9046-18b77b97ee46" name="Changes" comment=""><change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" /><change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" /><change afterPath="$PROJECT_DIR$/.idea/itec24.iml" afterDir="false" /><change afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" /><change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" /><change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" /><change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /><change afterPath="$PROJECT_DIR$/app.py" afterDir="false" /><change afterPath="$PROJECT_DIR$/static/style.css" afterDir="false" /><change afterPath="$PROJECT_DIR$/templates/dashboard.html" afterDir="false" /><change afterPath="$PROJECT_DIR$/templates/default.html" afterDir="false" /><change afterPath="$PROJECT_DIR$/templates/login.html" afterDir="false" /><change afterPath="$PROJECT_DIR$/templates/signup.html" afterDir="false" /><list default="true" id="411335b4-e813-41ad-9046-18b77b97ee46" name="Changes" comment="Initial commit"> <change afterPath="$PROJECT_DIR$/templates/app-editor.html" afterDir="false" /> <change afterPath="$PROJECT_DIR$/templates/app.html" afterDir="false" /> <change afterPath="$PROJECT_DIR$/templates/new-app.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$/static/style.css" beforeDir="false" afterPath="$PROJECT_DIR$/static/style.css" afterDir="false" /> <change beforePath="$PROJECT_DIR$/templates/dashboard.html" beforeDir="false" afterPath="$PROJECT_DIR$/templates/dashboard.html" afterDir="false" /> <change beforePath="$PROJECT_DIR$/templates/default.html" beforeDir="false" afterPath="$PROJECT_DIR$/templates/default.html" afterDir="false" /> <change beforePath="$PROJECT_DIR$/templates/login.html" beforeDir="false" afterPath="$PROJECT_DIR$/templates/login.html" afterDir="false" /> <change beforePath="$PROJECT_DIR$/templates/signup.html" beforeDir="false" afterPath="$PROJECT_DIR$/templates/signup.html" afterDir="false" /></list> <option name="SHOW_DIALOG" value="false" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -75,6 +72,15 @@
<option name="presentableId" value="Default" /> <updated>1712323797410</updated> </task> <task id="LOCAL-00001" summary="Initial commit"> <option name="closed" value="true" /> <created>1712333020964</created> <option name="number" value="00001" /> <option name="presentableId" value="LOCAL-00001" /> <option name="project" value="LOCAL" /> <updated>1712333020964</updated> </task> <option name="localTasksCounter" value="2" /><servers /> </component> <component name="Vcs.Log.Tabs.Properties">
@@ -88,4 +94,8 @@
</map> </option> </component> <component name="VcsManagerConfiguration"> <MESSAGE value="Initial commit" /> <option name="LAST_COMMIT_MESSAGE" value="Initial commit" /> </component></project>
app.py
@@ -1,8 +1,14 @@
import datetimeimport flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_migrate import Migrate from sqlalchemy.orm import declarative_base import httpx app = flask.Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = \ "postgresql://echo:1234@localhost:5432/echo"
@@ -18,11 +24,67 @@ with app.app_context():
password = db.Column(db.String(72), nullable=False) admin = db.Column(db.Boolean, nullable=False, default=False) applications = db.relationship("Application", back_populates="owner") def __init__(self, username, password, admin=False): self.username = username self.password = bcrypt.generate_password_hash(password).decode("utf-8") self.admin = admin class Application(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True, default=0) name = db.Column(db.String(64), unique=True, nullable=False) owner_name = db.Column(db.String(64), db.ForeignKey("user.username"), nullable=False) owner = db.relationship("User", back_populates="applications") endpoints = db.relationship("Endpoint", back_populates="application") def __init__(self, name, owner): self.name = name self.owner_name = owner.username class Endpoint(db.Model): id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True) application_name = db.Column(db.String(64), db.ForeignKey("application.name"), nullable=False) address = db.Column(db.String(2048), nullable=False) name = db.Column(db.String(64), nullable=False) comment = db.Column(db.String(2048), nullable=True) application = db.relationship("Application", back_populates="endpoints") def __init__(self, application_name): self.application_name = application_name Base = declarative_base() class Status(Base): __table_args = ( { "timescaledb_hypertable": { "time_column_name": "time", }, } ) __tablename__ = "status" id = db.Column(db.Integer, unique=True, nullable=False, autoincrement=True) endpoint_id = db.Column(db.Integer, nullable=False) time = db.Column(db.DateTime, index=True, default=datetime.datetime.utcnow, primary_key=True) status = db.Column(db.SmallInteger, nullable=False) endpoint = db.relationship("Endpoint", back_populates="statuses") def __init__(self, endpoint, status): self.endpoint_id = endpoint.id self.status = status def ping(endpoint): url = endpoint.address response = httpx.get(url) return response.status_code @app.context_processor def default():
@@ -33,7 +95,7 @@ def default():
@app.route("/") def dashboard(): return flask.render_template("dashboard.html")return flask.render_template("dashboard.html", apps=Application.query.all())@app.route("/login", methods=["GET"])
@@ -46,6 +108,50 @@ def signup():
return flask.render_template("signup.html") @app.route("/new-app", methods=["GET"]) def new_app(): if not flask.session.get("username"): return flask.redirect("/login", code=303) return flask.render_template("new-app.html") @app.route("/new-app", methods=["POST"]) def new_app_post(): if not flask.session.get("username"): return flask.redirect("/login", code=303) if Application.query.filter_by(name=flask.request.form["name"]).first(): flask.flash("Application already exists") return flask.redirect("/new-app", code=303) new_app_ = Application( flask.request.form["name"], db.session.get(User, flask.session["username"]), ) db.session.add(new_app_) db.session.commit() return flask.redirect("/", code=303) @app.route("/login", methods=["POST"]) def login_post(): user = db.session.get(User, flask.request.form["username"]) if not user: flask.flash("Username doesn't exist") return flask.redirect("/signup", code=303) if not bcrypt.check_password_hash(user.password, flask.request.form["password"]): flask.flash("Wrong password") return flask.redirect("/signup", code=303) flask.session["username"] = user.username return flask.redirect("/", code=303) @app.route("/logout") def logout(): flask.session.pop("username", None) return flask.redirect("/", code=303) @app.route("/signup", methods=["POST"]) def signup_post(): if flask.request.form["password"] != flask.request.form["password2"]:
@@ -54,6 +160,12 @@ def signup_post():
if db.session.get(User, flask.request.form["username"]): flask.flash("Username already exists") return flask.redirect("/signup", code=303) if len(flask.request.form["password"]) < 8: flask.flash("Password must be at least 8 characters") return flask.redirect("/signup", code=303) if len(flask.request.form["username"]) < 4: flask.flash("Username must be at least 4 characters") return flask.redirect("/signup", code=303)new_user = User( flask.request.form["username"],
@@ -68,3 +180,17 @@ def signup_post():
@app.route("/timeline/<endpoint_id>") def info(endpoint_id): return flask.render_template("timeline.html", endpoint=endpoint_id) @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_) @app.route("/app/<int:app_id>/edit") def app_editor(app_id): if flask.session.get("username") != db.session.get(Application, app_id).owner_name: flask.abort(403) app_ = db.session.get(Application, app_id) return flask.render_template("app-editor.html", app=app_)
static/style.css
@@ -1,7 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Lexend+Exa:wght@100..900&family=Lexend:wght@100..900&display=swap");html { font-size: 1.25rem;font-size: 1.125rem; color: #000000eb;} * {
@@ -26,18 +28,31 @@ nav {
flex-flow: row nowrap; justify-content: flex-start; align-items: center; padding: 8px 16px;padding: 0 16px;height: 64px; background-color: #000000; gap: 2ch; width: 100%; display: sticky;position: sticky; top: 0;} nav a { color: #ffffff; text-decoration: none; transition: color 0.25s cubic-bezier(0.33, 1, 0.68, 1); font-family: "Lexend Exa", "Lexend", sans-serif; font-size: 0.875rem; height: 100%; display: flex; align-items: center; border-top: 4px solid transparent; border-bottom: 4px solid transparent; } nav a.active { border-top: 4px solid #4DB6AC; color: #80CBC4;} nav a:hover {
@@ -59,14 +74,14 @@ nav a:hover {
main { padding: 16px; width: max(800px, 1vw);width: min(800px, 100vw);} #navi-spacer { flex: 1 0 auto; } input {input, textarea {padding: 0.5rem; border: 2px solid #009688; border-radius: 6px;
@@ -76,7 +91,7 @@ input {
background 0.25s cubic-bezier(0.33, 1, 0.68, 1); } button {button, .button {padding: 0.5rem; border: 2px solid #009688; border-radius: 6px;
@@ -85,26 +100,54 @@ button {
transition: box-shadow 0.25s cubic-bezier(0.33, 1, 0.68, 1), background 0.25s cubic-bezier(0.33, 1, 0.68, 1); font-weight: 575; font-family: "Lexend Exa", "Lexend", sans-serif; cursor: pointer; display: flex; gap: 0.25rem; align-items: center; text-decoration: none; justify-content: center;} h1 { font-weight: 850;font-weight: 900; font-size: 3rem; margin: 0.25em 0; color: #000000df; } h2 { font-weight: 767.5; font-size: 2rem; margin: 0.25em 0; color: #000000df;} :is(input, button):focus {h3 { font-weight: 767.5; font-size: 1.6rem; margin: 0.25em 0; color: #000000df; } * { transition: box-shadow 0.25s cubic-bezier(0.33, 1, 0.68, 1); } :focus {box-shadow: 0 0 0 4px #009688cc; outline: none; } button, input, select, textarea {button, .button, input, select, textarea {font-size: 1rem; } input:hover {:is(input, textarea):hover {background: #B2DFDB; } button:hover {:is(button, .button):hover {background: #00695C; }
@@ -130,4 +173,93 @@ input[type="password"]:not(:placeholder-shown) {
font-weight: bold; text-align: center; margin: 1rem 0; font-family: "Lexend Exa", "Lexend", sans-serif; } .app-card { width: 100%; border: 2px solid #000000aa; padding: 0.75rem; border-radius: 24px; } .app-card h2 { margin-top: 0; font-size: 1.45rem; font-weight: 750; } .app-uptime { display: flex; overflow: hidden; border-radius: calc(24px - 0.75rem); } .app-info { font-family: "Lexend Exa", "Lexend", sans-serif; font-style: italic; } .app-info-ok { color: #00C853; } .app-info-broken { color: #FFD600; } .app-info-down { color: #D50000; } .uptime-bar { background: #546E7A; height: 2rem; flex: 1 0 auto; } .uptime-bar-ok { background: #00C853; } .uptime-bar-broken { background: #FFD600; } .uptime-bar-down { background: #EF5350; } .subtitle { font-weight: 400; margin-top: 0; } iconify-icon { display: inline-block; width: 0.8lh; /* match parent line-height */ height: 0.8lh; font-size: 1.25em; } .quiet-link { color: inherit; text-decoration: none; font-weight: inherit; display: inline-block; } .quiet-link:has(.app-card) { display: block; border-radius: 24px; } .app-card:hover { border-color: #009688; background: #B2DFDB; } textarea { resize: vertical; appearance: none;}
templates/app-editor.html
@@ -0,0 +1,21 @@
{% extends "default.html" %} {% block title %}{{ app.name }} | Echo Tracker{% endblock %} {% block content %} <main> <h1>Editing {{ app.name }}</h1> {% for endpoint in app.endpoints %} <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="text" name="url" placeholder="URL" value="{{ endpoint.url }}"> <button type="submit">Save</button> <a class="button" href="/app/{{ app.id }}/delete/{{ endpoint.id }}">Delete</a> </form> {% endfor %} <form class="stacked-form" method="post" action="/app/{{ app.id }}/edit"> <input type="text" name="name" placeholder="Name"> <input type="text" name="url" placeholder="URL"> <textarea name="comment" placeholder="Comment" rows="4"></textarea> <button type="submit">Add</button> </form> </main> {% endblock %}
templates/app.html
@@ -0,0 +1,13 @@
{% extends "default.html" %} {% block title %}{{ app.name }} | Echo Tracker{% endblock %} {% block content %} <main> <h1>{{ app.name }}</h1> {% 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> Manage endpoints </a> {% endif %} </main> {% endblock %}
templates/dashboard.html
@@ -1,7 +1,55 @@
{% extends "default.html" %} {% block title %}Dashboard | Echo Tracker{% endblock %} {% set active = "dashboard" %}{% block content %} <main> <h1>Dashboard</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 %}
templates/default.html
@@ -6,20 +6,25 @@
<link rel="icon" href="/static/logo.svg" type="image/x-icon"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}{% endblock %}</title> <script src="https://code.iconify.design/iconify-icon/2.0.0/iconify-icon.min.js"></script></head> <body> <nav> <a href="/" id="navi-logo"><a href="/" id="navi-logo" {% if active == "dashboard" %}class="active"{% endif %}><img src="/static/logo.svg" alt=""> Dashboard </a> <a href="/my" {% if active == "my" %}class="active"{% endif %}> My Apps </a><div id="navi-spacer"></div> {% if not session.username %} <a href="/login"><a href="/login" {% if active == "login" %}class="active"{% endif %}>Log In </a> <a href="/signup"><a href="/signup" {% if active == "signup" %}class="active"{% endif %}>Sign Up </a> {% else %}
templates/login.html
@@ -1,5 +1,6 @@
{% extends "default.html" %} {% block title %}Log In | Echo Tracker{% endblock %} {% set active = "login" %}{% block content %} <main> <h1>Log In</h1>
templates/new-app.html
@@ -0,0 +1,18 @@
{% extends "default.html" %} {% block title %}Sign Up | Echo Tracker{% endblock %} {% block content %} <main> <h1>Create an App</h1> {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} <div class="alert">{{ message }}</div> {% endfor %} {% endif %} {% endwith %} <form class="stacked-form" method="post"> <input type="text" name="name" placeholder="App Name"> <button type="submit">Create</button> </form> </main> {% endblock %}
templates/signup.html
@@ -1,8 +1,10 @@
{% extends "default.html" %} {% block title %}Sign Up | Echo Tracker{% endblock %} {% set active = "signup" %}{% block content %} <main> <h1>Sign Up</h1> <h2 class="subtitle">Become a developer</h2>{% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %}