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