WWW service status tracker

Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 app.py

View raw Download
text/x-script.python • 13.05 kiB
Python script, ASCII text executable
        
            
1
import datetime
2
3
import celery
4
import flask
5
import sqlalchemy
6
from flask_sqlalchemy import SQLAlchemy
7
from flask_bcrypt import Bcrypt
8
from flask_migrate import Migrate
9
10
from sqlalchemy.orm import declarative_base
11
from celery import shared_task
12
import httpx
13
14
app = flask.Flask(__name__)
15
from celery import Celery, Task
16
17
18
def celery_init_app(app_) -> Celery:
19
class FlaskTask(Task):
20
def __call__(self, *args: object, **kwargs: object) -> object:
21
with app_.app_context():
22
return self.run(*args, **kwargs)
23
24
celery_app = Celery(app_.name, task_cls=FlaskTask)
25
celery_app.config_from_object(app_.config["CELERY"])
26
celery_app.set_default()
27
app_.extensions["celery"] = celery_app
28
return celery_app
29
30
31
app.config.from_mapping(
32
CELERY=dict(
33
broker_url="redis://localhost:6379",
34
result_backend="redis://localhost:6379"
35
),
36
)
37
celery_app = celery_init_app(app)
38
39
app.config["SQLALCHEMY_DATABASE_URI"] = \
40
"postgresql://echo:1234@localhost:5432/echo"
41
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
42
"connect_args": {
43
"options": "-c timezone=utc"
44
}
45
}
46
db = SQLAlchemy(app)
47
bcrypt = Bcrypt(app)
48
migrate = Migrate(app, db)
49
app.config["SESSION_TYPE"] = "filesystem"
50
app.config["SECRET_KEY"] = "super secret"
51
52
with (app.app_context()):
53
class User(db.Model):
54
username = db.Column(db.String(64), unique=True, nullable=False, primary_key=True)
55
password = db.Column(db.String(72), nullable=False)
56
admin = db.Column(db.Boolean, nullable=False, default=False)
57
58
applications = db.relationship("Application", back_populates="owner")
59
60
def __init__(self, username, password, admin=False):
61
self.username = username
62
self.password = bcrypt.generate_password_hash(password).decode("utf-8")
63
self.admin = admin
64
65
class Application(db.Model):
66
id = db.Column(db.Integer, primary_key=True, autoincrement=True, unique=True, default=0)
67
name = db.Column(db.String(64), unique=True, nullable=False)
68
owner_name = db.Column(db.String(64), db.ForeignKey("user.username"), nullable=False)
69
70
owner = db.relationship("User", back_populates="applications")
71
72
endpoints = db.relationship("Endpoint", back_populates="application")
73
74
def __init__(self, name, owner):
75
self.name = name
76
self.owner_name = owner.username
77
78
class Endpoint(db.Model):
79
id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True)
80
application_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False)
81
address = db.Column(db.String(2048), nullable=False)
82
name = db.Column(db.String(64), nullable=False)
83
comment = db.Column(db.String(2048), nullable=True)
84
ping_interval = db.Column(db.Integer, default=300, nullable=False)
85
buggy = db.Column(db.Boolean, default=False)
86
create_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
87
88
application = db.relationship("Application", back_populates="endpoints")
89
statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic")
90
91
def __init__(self, application, name, address, ping_interval, comment=""):
92
self.application_id = application.id
93
self.name = name
94
self.address = address
95
self.comment = comment
96
self.ping_interval = ping_interval
97
98
99
class Status(db.Model):
100
id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True)
101
endpoint_id = db.Column(db.Integer, db.ForeignKey("endpoint.id"), nullable=False)
102
time = db.Column(db.DateTime, default=datetime.datetime.utcnow)
103
104
status = db.Column(db.SmallInteger, nullable=False)
105
buggy = db.Column(db.Boolean, default=False)
106
endpoint = db.relationship("Endpoint", back_populates="statuses")
107
108
def __init__(self, endpoint_id, status, buggy):
109
self.endpoint_id = endpoint_id
110
self.status = status
111
self.buggy = buggy
112
113
114
@celery.shared_task(name="ping")
115
def ping(id, address, next_ping):
116
if not db.session.get(Endpoint, id):
117
return
118
elif db.session.get(Endpoint, id).buggy:
119
buggy = True
120
else:
121
buggy = False
122
url = address
123
print(f"Pinging {url}")
124
response = httpx.get(url, verify=False)
125
reading = Status(id, response.status_code, buggy)
126
last_reading = db.session.query(Status).filter_by(endpoint_id=id).order_by(Status.time.desc()).first()
127
db.session.add(reading)
128
db.session.commit()
129
130
# Schedule the next ping
131
ping.apply_async(args=(id, address, next_ping), countdown=next_ping)
132
133
@celery.shared_task(name="ping_all")
134
def ping_all():
135
endpoints = Endpoint.query.all()
136
for endpoint in endpoints:
137
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
138
139
140
task = ping_all.delay()
141
142
print()
143
print()
144
print(task)
145
print()
146
print()
147
148
149
@app.context_processor
150
def default():
151
return {
152
"session": flask.session,
153
}
154
155
156
@app.route("/")
157
def dashboard():
158
return flask.render_template("dashboard.html", apps=Application.query.all())
159
160
161
@app.route("/my")
162
def my_apps():
163
if not flask.session.get("username"):
164
return flask.redirect("/login", code=303)
165
return flask.render_template("my-apps.html", apps=db.session.query(Application).filter_by(owner_name=flask.session["username"]).all())
166
167
168
@app.route("/login", methods=["GET"])
169
def login():
170
return flask.render_template("login.html")
171
172
173
@app.route("/signup", methods=["GET"])
174
def signup():
175
return flask.render_template("signup.html")
176
177
178
@app.route("/new-app", methods=["GET"])
179
def new_app():
180
if not flask.session.get("username"):
181
return flask.redirect("/login", code=303)
182
return flask.render_template("new-app.html")
183
184
185
@app.route("/new-app", methods=["POST"])
186
def new_app_post():
187
if not flask.session.get("username"):
188
return flask.redirect("/login", code=303)
189
if Application.query.filter_by(name=flask.request.form["name"]).first():
190
flask.flash("Application already exists")
191
return flask.redirect("/new-app", code=303)
192
193
new_app_ = Application(
194
flask.request.form["name"],
195
db.session.get(User, flask.session["username"]),
196
)
197
db.session.add(new_app_)
198
db.session.commit()
199
return flask.redirect("/", code=303)
200
201
202
@app.route("/login", methods=["POST"])
203
def login_post():
204
user = db.session.get(User, flask.request.form["username"])
205
if not user:
206
flask.flash("Username doesn't exist")
207
return flask.redirect("/signup", code=303)
208
if not bcrypt.check_password_hash(user.password, flask.request.form["password"]):
209
flask.flash("Wrong password")
210
return flask.redirect("/signup", code=303)
211
212
flask.session["username"] = user.username
213
return flask.redirect("/", code=303)
214
215
216
@app.route("/logout")
217
def logout():
218
flask.session.pop("username", None)
219
return flask.redirect("/", code=303)
220
221
222
@app.route("/signup", methods=["POST"])
223
def signup_post():
224
if flask.request.form["password"] != flask.request.form["password2"]:
225
flask.flash("Passwords do not match")
226
return flask.redirect("/signup", code=303)
227
if db.session.get(User, flask.request.form["username"]):
228
flask.flash("Username already exists")
229
return flask.redirect("/signup", code=303)
230
if len(flask.request.form["password"]) < 8:
231
flask.flash("Password must be at least 8 characters")
232
return flask.redirect("/signup", code=303)
233
if len(flask.request.form["username"]) < 4:
234
flask.flash("Username must be at least 4 characters")
235
return flask.redirect("/signup", code=303)
236
237
new_user = User(
238
flask.request.form["username"],
239
flask.request.form["password"],
240
)
241
db.session.add(new_user)
242
db.session.commit()
243
flask.session["username"] = new_user.username
244
return flask.redirect("/", code=303)
245
246
247
# UTC filter
248
@app.template_filter("utc")
249
def utc_filter(timestamp):
250
return datetime.datetime.utcfromtimestamp(timestamp)
251
252
253
@app.route("/app/<int:app_id>/")
254
def app_info(app_id):
255
app_ = db.session.get(Application, app_id)
256
257
time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * (i+1)),
258
datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * i))
259
for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)), 0, -1)]
260
261
slice_results = {}
262
all_results = []
263
264
for endpoint in app_.endpoints:
265
slice_results[endpoint.id] = []
266
267
for slice_ in time_slices:
268
slice_results[endpoint.id].append(
269
(
270
db.session.query(Status).filter(
271
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
272
Status.time >= slice_[0],
273
Status.time < slice_[1])).all(),
274
slice_
275
)
276
)
277
278
for endpoint in app_.endpoints:
279
all_results.extend(db.session.query(Status).filter(
280
sqlalchemy.and_(Status.endpoint_id == endpoint.id,
281
Status.time >= datetime.datetime.utcnow() - datetime.timedelta(minutes=10),
282
Status.time < datetime.datetime.utcnow())).all())
283
284
return flask.render_template("app.html", app=app_, sorted=sorted, list=list,
285
sorting=lambda x: x.time, reverse=True,
286
is_ok=lambda x: all(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
287
for status in x), and_=sqlalchemy.and_,
288
is_partial=lambda x: any(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307)
289
for status in x),
290
bar_duration=int(flask.request.args.get("bar_duration", 30)), int=int, Status=Status,
291
time_period=int(flask.request.args.get("time_period", 1440)),
292
now=round(datetime.datetime.utcnow().timestamp()), func=sqlalchemy.func,
293
reversed=reversed, fromtimestamp=datetime.datetime.utcfromtimestamp,
294
slices=slice_results, bugs=lambda x: any(status.buggy for status in x),
295
all_results=all_results)
296
297
298
@app.route("/app/<int:app_id>/edit/")
299
def app_editor(app_id):
300
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
301
flask.abort(403)
302
app_ = db.session.get(Application, app_id)
303
return flask.render_template("app-editor.html", app=app_)
304
305
306
@app.route("/app/<int:app_id>/edit/<int:endpoint_id>", methods=["POST"])
307
def endpoint_edit(app_id, endpoint_id):
308
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
309
flask.abort(403)
310
endpoint = db.session.get(Endpoint, endpoint_id)
311
if flask.request.form.get("delete") == "delete":
312
statuses = db.session.query(Status).filter_by(endpoint_id=endpoint_id).all()
313
for status in statuses:
314
db.session.delete(status)
315
db.session.delete(endpoint)
316
db.session.commit()
317
else:
318
endpoint.name = flask.request.form["name"]
319
endpoint.address = flask.request.form["url"]
320
endpoint.ping_interval = max(15, int(flask.request.form["ping_interval"]))
321
endpoint.comment = flask.request.form["comment"]
322
db.session.commit()
323
return flask.redirect(f"/app/{app_id}/edit", code=303)
324
325
326
@app.route("/app/<int:app_id>/report/<int:endpoint_id>")
327
def endpoint_report(app_id, endpoint_id):
328
endpoint = db.session.get(Endpoint, endpoint_id)
329
endpoint.buggy = True
330
db.session.commit()
331
return flask.redirect(f"/app/{app_id}", code=303)
332
333
334
@app.route("/app/<int:app_id>/fix/<int:endpoint_id>")
335
def endpoint_fix(app_id, endpoint_id):
336
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
337
flask.abort(403)
338
endpoint = db.session.get(Endpoint, endpoint_id)
339
endpoint.buggy = False
340
db.session.commit()
341
return flask.redirect(f"/app/{app_id}", code=303)
342
343
344
@app.route("/app/<int:app_id>/add-endpoint", methods=["POST"])
345
def app_add_endpoint(app_id):
346
if flask.session.get("username") != db.session.get(Application, app_id).owner_name:
347
flask.abort(403)
348
app_ = db.session.get(Application, app_id)
349
endpoint = Endpoint(app_,
350
flask.request.form["name"],
351
flask.request.form["url"],
352
max(15, int(flask.request.form["ping_interval"])),
353
flask.request.form["comment"])
354
db.session.add(endpoint)
355
db.session.commit()
356
357
ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval)
358
359
return flask.redirect(f"/app/{app_id}/edit", code=303)
360
361