forked from libertytechforce/statusforce
Get the ball rolling
- Add templates - Add static assets - Add module setup - Add services to the page - Add incidents to the page - Add authentication to the admin section - Add Dockerfile and docker-compose.yamlmaster
parent
9e6cb91da9
commit
7802bb963e
|
@ -0,0 +1,4 @@
|
|||
__pycache__
|
||||
*.py[co]
|
||||
*.sqlite
|
||||
data/
|
|
@ -0,0 +1,6 @@
|
|||
FROM tiangolo/meinheld-gunicorn-flask
|
||||
LABEL org.opencontainers.image.authors="Liberty Tech Force"
|
||||
|
||||
RUN pip install --no-cache-dir flask_admin flask_sqlalchemy flask_login psycopg2-binary pymysql 'greenlet<0.5,>=0.4.5'
|
||||
COPY ./statusforce /app/statusforce
|
||||
COPY ./docker/main.py /app/
|
|
@ -0,0 +1,4 @@
|
|||
StatusForce
|
||||
===========
|
||||
|
||||
StatusForce is a simple, open source status page written in Python and Flask.
|
|
@ -0,0 +1,20 @@
|
|||
version: '3'
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- "8000:80"
|
||||
environment:
|
||||
- SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://statusforce:statusforce@db/statusforce?sslmode=disable
|
||||
- FLASK_SECRET_KEY=my-super-secret-key
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=statusforce
|
||||
- POSTGRES_PASSWORD=statusforce
|
||||
- POSTGRES_DB=statusforce
|
||||
volumes:
|
||||
- "./data:/var/lib/postgresql/data"
|
|
@ -0,0 +1,3 @@
|
|||
from statusforce.app import get_app
|
||||
|
||||
app = get_app()
|
|
@ -0,0 +1,37 @@
|
|||
[metadata]
|
||||
name = StatusForce
|
||||
version = 0.0.1
|
||||
author = Raoul Snyman
|
||||
author_email = raoul@libertytechforce.com
|
||||
description = Simple status page for system status
|
||||
long_description = file:README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://libertytechforce.com
|
||||
license = MIT
|
||||
classifiers =
|
||||
Development Status :: 3 - Alpha
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Topic :: Utilities
|
||||
keywords = website, status
|
||||
|
||||
[options]
|
||||
py_modules = statusforce
|
||||
python_requires = >=3.7
|
||||
install_requires =
|
||||
Flask
|
||||
Flask-Admin
|
||||
Flask-SQLAlchemy
|
||||
Flask-Login
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
The statusforce package
|
||||
"""
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
|
@ -1,17 +1,48 @@
|
|||
import os
|
||||
from flask import Flask
|
||||
from flask_admin import Admin
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from flask_login import LoginManager
|
||||
|
||||
from statusforce.db import db
|
||||
from statusforce.models import Service, Incident
|
||||
from statusforce.db import db, session
|
||||
from statusforce.models import Service, Incident, User
|
||||
from statusforce.views import AuthAdminIndexView, AuthModelView, main
|
||||
|
||||
|
||||
# Set up Flask application
|
||||
app = Flask(__name__)
|
||||
app.config['FLASK_ADMIN_SWATCH'] = 'materia'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
|
||||
def get_app():
|
||||
# Set up Flask application and initial config
|
||||
app = Flask(__name__)
|
||||
app.secret_key = b'super-secret-please-replace-me'
|
||||
app.config['FLASK_ADMIN_SWATCH'] = 'cosmo'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///statusforce.sqlite'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
db.init_app(app)
|
||||
admin = Admin(app, name='StatusForce', template_mode='bootstrap4')
|
||||
admin.add_view(ModelView(Service))
|
||||
admin.add_view(ModelView(Incident))
|
||||
# Pull configuration from environment variables
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith('FLASK_ADMIN_') or key.startswith('SQLALCHEMY_'):
|
||||
app.config[key] = value
|
||||
elif key == 'FLASK_SECRET_KEY':
|
||||
# Special case, apparently
|
||||
app.secret_key = value
|
||||
elif key.startswith('FLASK_'):
|
||||
app.config[key.replace('FLASK_', '')] = value
|
||||
|
||||
# Set up database
|
||||
db.init_app(app)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Set up the authentication
|
||||
login_manager = LoginManager(app)
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(user_id)
|
||||
|
||||
# Set up admin interface
|
||||
admin = Admin(app, name='StatusForce', index_view=AuthAdminIndexView(), template_mode='bootstrap4')
|
||||
admin.add_view(AuthModelView(Service, session))
|
||||
admin.add_view(AuthModelView(Incident, session))
|
||||
|
||||
# Finally, attache the blueprint
|
||||
app.register_blueprint(main)
|
||||
return app
|
||||
|
|
|
@ -3,12 +3,15 @@ from flask_sqlalchemy import SQLAlchemy
|
|||
db = SQLAlchemy()
|
||||
|
||||
# Pull out some attributes to make life easier
|
||||
session = db.session
|
||||
Model = db.Model
|
||||
Column = db.Column
|
||||
ForeignKey = db.ForeignKey
|
||||
Boolean = db.Boolean
|
||||
DateTime = db.DateTime
|
||||
Enum = db.Enum
|
||||
Integer = db.Integer
|
||||
String = db.String
|
||||
Text = db.Text
|
||||
relationship = db.relationship
|
||||
backref = db.backref
|
||||
|
|
|
@ -2,18 +2,50 @@ from datetime import datetime
|
|||
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
from statusforce.db import Model, Column, ForeignKey, DateTime, Integer, String
|
||||
from statusforce.db import Model, Column, ForeignKey, Boolean, DateTime, Enum, Integer, String, Text, \
|
||||
relationship, backref
|
||||
|
||||
|
||||
class Service(Model):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
status = Column(String, choices=['operational', 'unclear', 'offline'])
|
||||
status = Column(Enum('operational', 'unclear', 'offline', native_enum=False), default='operational')
|
||||
incidents = relationship('Incident', backref=backref('service', lazy=True))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Incident(Model):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
service_id = Column(Integer, ForeignKey('service.id'))
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
is_resolved = Column(Boolean, default=False)
|
||||
created = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated = Column(DateTime, unupdate=func.current_timestamp())
|
||||
updated = Column(DateTime, onupdate=func.current_timestamp())
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({})'.format(self.title, self.service.name)
|
||||
|
||||
|
||||
class User(Model):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
full_name = Column(String)
|
||||
email = Column(String, nullable=False)
|
||||
password = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name or self.email
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
|
@ -0,0 +1,119 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="260mm"
|
||||
height="260mm"
|
||||
version="1.1"
|
||||
viewBox="0 0 260 260"
|
||||
id="svg855"
|
||||
sodipodi:docname="ltf-logo.svg"
|
||||
inkscape:export-filename="/home/raoul/Nextcloud/Documents/LibertyTechForce/ltf-logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
||||
<defs
|
||||
id="defs859" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1015"
|
||||
id="namedview857"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.83140224"
|
||||
inkscape:cx="491.33858"
|
||||
inkscape:cy="491.33858"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg855" />
|
||||
<metadata
|
||||
id="metadata833">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(43.957 -17.442)"
|
||||
id="g853">
|
||||
<path
|
||||
d="m86.043 22.059c-39.82-5.3e-5 -79.258 7.7669-116.1 22.866l-4.4511 1.8241 0.37669 4.7952c6.6963 85.322 48.803 163.96 116.1 216.82l4.0744 3.201 4.0744-3.201c67.302-52.869 109.41-131.5 116.11-216.82l0.37616-4.7952-4.4505-1.8236c-36.847-15.1-76.286-22.868-116.11-22.868z"
|
||||
color="#000000"
|
||||
color-rendering="auto"
|
||||
dominant-baseline="auto"
|
||||
fill="none"
|
||||
image-rendering="auto"
|
||||
shape-rendering="auto"
|
||||
solid-color="#000000"
|
||||
stop-color="#000000"
|
||||
stroke="#00a9ff"
|
||||
stroke-linecap="round"
|
||||
stroke-width="9.2339"
|
||||
style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"
|
||||
id="path835" />
|
||||
<path
|
||||
d="m86.042 35.423c-36.485-5.3e-5 -72.627 6.8321-106.59 20.095 7.426 78.046 45.817 149.85 106.59 199.36 60.769-49.515 99.161-121.32 106.59-199.36-33.959-13.263-70.102-20.096-106.59-20.097z"
|
||||
color="#000000"
|
||||
color-rendering="auto"
|
||||
dominant-baseline="auto"
|
||||
fill="#00a9ff"
|
||||
fill-rule="evenodd"
|
||||
image-rendering="auto"
|
||||
shape-rendering="auto"
|
||||
solid-color="#000000"
|
||||
stop-color="#000000"
|
||||
style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"
|
||||
id="path837" />
|
||||
<g
|
||||
transform="matrix(.26382 0 0 .26382 -1336.7 -404.22)"
|
||||
fill="#fff"
|
||||
id="g851">
|
||||
<path
|
||||
d="m5352.5 1709.1s1.8115 11.734 1.7243 30.702c-0.09 19.529-2.5476 44.4-11.336 72.898-9.507 30.713-24.958 61.051-46.461 90.248-5.0966 6.9914-10.776 14.22-16.487 21.719-3.3072 4.3423-6.6851 8.8526-10.036 13.521-9.2086 12.678-18.137 26.635-25.058 41.958-7.6521 17.178-11.474 34.532-12.175 51.292-0.7637 18.134 2.0741 35.132 7.1133 50.426 6.0002 18.23 14.964 33.594 24.243 45.894 9.8042 13 19.931 22.51 27.18 28.626 7.3049 6.1634 11.869 9.1825 11.869 9.1825s-2.1535-4.2599-5.1401-11.919c-2.9974-7.6862-6.6513-18.653-8.9284-32.125-2.1545-12.754-2.9786-27.224-1.2734-42.978 1.4321-13.183 4.593-26.798 9.8393-40.4 1.5837-4.1002 3.3452-8.1707 5.278-12.199-0.066 8.8009 1.804 17.074 4.9134 24.585 4.0812 9.8702 10.178 18.189 16.49 24.848 6.6688 7.0385 13.557 12.188 18.487 15.499 4.9688 3.3369 8.073 4.9716 8.073 4.9716s-1.4649-2.3069-3.4964-6.4535c-2.0386-4.1616-4.5234-10.1-6.0723-17.393-1.4656-6.9054-2.0261-14.74-0.8662-23.269 0.9741-7.1374 3.1238-14.509 6.6922-21.873 3.2961-6.7927 7.7049-13.44 13.22-19.722 4.9702-5.7674 11.177-11.598 17.587-17.998 2.3746-2.3425 4.7684-4.7396 7.1344-7.219 4.0841-4.2799 8.1465-8.87 11.876-13.783 0.7474-0.9928 1.4695-1.9874 2.1672-2.9841 4.8101 10.333 11.033 20.599 18.854 30.556 5.0335 6.3636 10.363 12.257 15.653 17.726 3.0648 3.1687 6.143 6.2227 9.1896 9.2042 8.239 8.1514 16.144 15.547 22.695 22.964 15.3 16.97 25.1 36.162 29.978 54.892 4.6856 18.186 4.3259 34.074 2.5531 46.169-1.7328 11.823-4.3539 18.9-4.3539 18.9s6.7539-5.3156 15.259-16.217c8.5175-10.917 18.459-27.599 22.255-49.266 3.9891-22.479 0.9691-48.912-14.848-74.687-6.7764-10.884-14.884-20.641-23.058-29.438-2.9805-3.242-5.9635-6.3653-8.8761-9.3692-5.03-5.188-10.007-10.178-14.528-15.029-19.044-20.242-29.917-44.553-36.114-66.89-6.3714-24.38-7.141-41.046-6.0269-54.337 1.2586-15.015 8.7797-49.583 8.7797-49.583s-22.432 42.5-39.254 50.098c1.206-12.117 1.176-23.997 0.094-35.444-3.1047-33.196-14.642-59.919-25.291-78.338-10.556-18.259-19.52-27.968-19.52-27.968z"
|
||||
id="path839" />
|
||||
<g
|
||||
fill-rule="evenodd"
|
||||
id="g849">
|
||||
<ellipse
|
||||
cx="5390.9"
|
||||
cy="2209.8"
|
||||
rx="122.99"
|
||||
ry="42.108"
|
||||
id="ellipse841" />
|
||||
<ellipse
|
||||
cx="5390.9"
|
||||
cy="2253.7"
|
||||
rx="107.4"
|
||||
ry="24.427"
|
||||
id="ellipse843" />
|
||||
<path
|
||||
d="m5306.1 2195.9h169.48l-23.609 189.53h-122.26z"
|
||||
id="path845" />
|
||||
<ellipse
|
||||
cx="5390.9"
|
||||
cy="2385.2"
|
||||
rx="69.792"
|
||||
ry="15.873"
|
||||
id="ellipse847" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.3 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,22 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% import 'admin/lib.html' as lib with context %}
|
||||
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{{ lib.form_css() }}
|
||||
{% endblock head %}
|
||||
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
{% if current_user.is_authenticated %}
|
||||
<p class="lead">Use the menu items above to add services and incidents.</p>
|
||||
{% else %}
|
||||
{{lib.render_form(form)}}
|
||||
{% endif %}
|
||||
{% endblock body %}
|
||||
|
||||
{% block tail %}
|
||||
{{super()}}
|
||||
{{lib.form_js()}}
|
||||
{% endblock tail %}
|
|
@ -0,0 +1,128 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>System Status</title>
|
||||
<link href="{{url_for('static', filename='css/bootstrap.min.css')}}" rel="stylesheet">
|
||||
<link href="{{url_for('static', filename='css/bootstrap-icons.css')}}" rel="stylesheet">
|
||||
<link rel="icon" href="{{url_for('static', filename='img/ltf-logo.png')}}">
|
||||
<style>
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
text-anchor: middle;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.bd-placeholder-img-lg {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container px-5">
|
||||
<main>
|
||||
<div class="py-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{url_for('static', filename='img/ltf-logo.svg')}}" alt="" width="72" height="57">
|
||||
<h2>Systems Status</h2>
|
||||
<p class="lead">The current status of the Liberty Tech Force systems.</p>
|
||||
</div>
|
||||
{% if overall_status == 'operational' %}
|
||||
<div class="p-3 mb-5 bg-success text-white" style="border-radius: 3px;">
|
||||
<i class="bi-check-circle-fill" style="margin-right: 0.5rem;"></i> All systems are operational
|
||||
</div>
|
||||
{% elif overall_status == 'unclear' %}
|
||||
<div class="p-3 mb-5 bg-warning" style="border-radius: 3px;">
|
||||
<i class="bi-question-circle-fill" style="margin-right: 0.5rem;"></i> Some systems are experiencing problems
|
||||
</div>
|
||||
{% elif overall_status == 'offline' %}
|
||||
<div class="p-3 mb-5 bg-danger text-white" style="border-radius: 3px;">
|
||||
<i class="bi-exclamation-circle-fill" style="margin-right: 0.5rem;"></i> <strong>There is a major disruption of services</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if services | length > 0 %}
|
||||
<h2 class="mt-5 mb-3">Services</h2>
|
||||
<ul class="list-group">
|
||||
{% for service in services %}
|
||||
{% if service.status == 'operational' %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3">
|
||||
{% elif service.status == 'unclear' %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3 bg-warning" style="--bs-bg-opacity: 0.2">
|
||||
{% elif service.status == 'offline' %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3 bg-danger" style="--bs-bg-opacity: 0.2">
|
||||
{% endif %}
|
||||
{{service.name}}
|
||||
{% if service.status == 'operational' %}
|
||||
<span class="text-success">
|
||||
{% elif service.status == 'unclear' %}
|
||||
<span class="text-warning">
|
||||
{% elif service.status == 'offline' %}
|
||||
<span class="text-danger">
|
||||
{% endif %}
|
||||
{{service.status.title()}}
|
||||
|
||||
{% if service.status == 'operational' %}
|
||||
<i class="bi-check-circle-fill"></i>
|
||||
{% elif service.status == 'unclear' %}
|
||||
<i class="bi-question-circle-fill"></i>
|
||||
{% elif service.status == 'unclear' %}
|
||||
<i class="bi-exclamation-circle-fill"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if incidents | length > 0 %}
|
||||
<h2 class="mt-5 mb-3">Incidents</h2>
|
||||
{% endif %}
|
||||
{% for incident in incidents %}
|
||||
<div class="row">
|
||||
<div class="col-auto text-center flex-column d-none d-sm-flex">
|
||||
<div class="row h-50">
|
||||
{% if loop.first %}
|
||||
<div class="col"> </div>
|
||||
<div class="col"> </div>
|
||||
{% else %}
|
||||
<div class="col border-end border-2"> </div>
|
||||
<div class="col border-start border-2"> </div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- h5 class="m-2"><span class="rounded-circle bg-white border px-3 py-1"></span></h5 -->
|
||||
{% if incident.is_resolved %}
|
||||
<h5 class="m-2"><span class="bi-check-circle-fill text-success" style="font-size: 200%"></span></h5>
|
||||
{% else %}
|
||||
<h5 class="m-2"><span class="bi-exclamation-circle-fill text-danger" style="font-size: 200%"></span></h5>
|
||||
{% endif %}
|
||||
<div class="row h-50">
|
||||
{% if loop.last %}
|
||||
<div class="col"> </div>
|
||||
<div class="col"> </div>
|
||||
{% else %}
|
||||
<div class="col border-end border-2"> </div>
|
||||
<div class="col border-start border-2"> </div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col py-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="float-end">{{incident.created.strftime('%H:%M%z - %a %d %B, %Y')}}</div>
|
||||
<h4 class="card-title text-muted">{{incident.title}} - {{incident.service.name}}</h4>
|
||||
<p class="card-text text-muted">{{incident.description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</main>
|
||||
<footer class="my-5 pt-5 text-muted text-center text-small">
|
||||
<p class="mb-1">© 2021 <a href="https://libertytechforce.com">Liberty Tech Force</a> | Powered by <a href="https://git.libertytechforce.com/libertytechforce/statusforce">StatusForce</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
<!-- script src="js/bootstrap.bundle.min.js"></script -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,85 @@
|
|||
from flask import Blueprint, redirect, request, url_for, render_template
|
||||
from flask_admin import AdminIndexView, expose
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from flask_admin.helpers import validate_form_on_submit
|
||||
from flask_login import current_user, login_user, logout_user
|
||||
from werkzeug.security import check_password_hash
|
||||
from wtforms.form import Form
|
||||
from wtforms.fields import StringField, PasswordField
|
||||
from wtforms.validators import ValidationError, required
|
||||
|
||||
from statusforce.models import Service, Incident, User
|
||||
|
||||
# The blueprint for the main page
|
||||
main = Blueprint('main', __name__, url_prefix='/')
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
email = StringField(validators=[required()])
|
||||
password = PasswordField(validators=[required()])
|
||||
|
||||
def validate_login(self, field):
|
||||
user = self.get_user()
|
||||
if user is None or not check_password_hash(user.password, self.password.data):
|
||||
raise ValidationError('Unable to log user in')
|
||||
|
||||
def get_user(self):
|
||||
return User.query.filter_by(email=self.email.data).first()
|
||||
|
||||
|
||||
class AuthModelView(ModelView):
|
||||
"""
|
||||
A derivative ModelView that supports user authentication
|
||||
"""
|
||||
def is_accessible(self):
|
||||
return current_user.is_authenticated
|
||||
|
||||
|
||||
class AuthAdminIndexView(AdminIndexView):
|
||||
"""
|
||||
A derivative AdminIndexView that supports user authentication
|
||||
"""
|
||||
|
||||
@expose('/')
|
||||
def index(self):
|
||||
"""
|
||||
Override the index page implementation to check for user permissions, and redirect to the login page
|
||||
"""
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for('.login'))
|
||||
return super().index()
|
||||
|
||||
@expose('/login/', methods=('GET', 'POST'))
|
||||
def login(self):
|
||||
"""
|
||||
A page for the user to log in
|
||||
"""
|
||||
form = LoginForm(request.form)
|
||||
if validate_form_on_submit(form):
|
||||
user = form.get_user()
|
||||
login_user(user)
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('.index'))
|
||||
self._template_args['form'] = form
|
||||
return super().index()
|
||||
|
||||
@expose('/logout/')
|
||||
def logout(self):
|
||||
"""
|
||||
Log the user out of the system
|
||||
"""
|
||||
logout_user()
|
||||
return redirect(url_for('.index'))
|
||||
|
||||
|
||||
@main.route('')
|
||||
def index():
|
||||
"""
|
||||
Show the status page
|
||||
"""
|
||||
services = Service.query.all()
|
||||
incidents = Incident.query.order_by(Incident.created.desc()).limit(10).all()
|
||||
service_statuses = [service.status for service in services]
|
||||
overall_status = 'operational' if all(status == 'operational' for status in service_statuses) else \
|
||||
'offline' if all(status == 'offline' for status in service_statuses) else 'unclear'
|
||||
return render_template('index.html', services=services, incidents=incidents, overall_status=overall_status)
|
Loading…
Reference in New Issue