uploade
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
281
data.md
Normal file
281
data.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
Selbst gehostetes Monitoring-Tool
|
||||||
|
Du betreibst Uptime Kuma auf deiner eigenen Infrastruktur
|
||||||
|
Libre Self-hosted
|
||||||
|
+3
|
||||||
|
uptime.kuma.pet
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
|
||||||
|
Verschiedene Monitor-Typen
|
||||||
|
Uptime Kuma unterstützt viele Arten von Überwachungen:
|
||||||
|
|
||||||
|
HTTP / HTTPS (Standard-Webseiten)
|
||||||
|
uptimekuma.org
|
||||||
|
+6
|
||||||
|
uptimekuma.org
|
||||||
|
+6
|
||||||
|
uptimekuma.org
|
||||||
|
+6
|
||||||
|
|
||||||
|
HTTP(s) mit Keyword-Abfragen (z. B. “enthält dieses Wort auf der Seite”)
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
mielke.de
|
||||||
|
+2
|
||||||
|
|
||||||
|
HTTP(s) JSON-Abfragen (z. B. API-Antworten)
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
GitHub
|
||||||
|
+3
|
||||||
|
|
||||||
|
TCP-Port-Checks (z. B. ein Dienst auf Port 22, 3306 etc.)
|
||||||
|
GitHub
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
mielke.de
|
||||||
|
+4
|
||||||
|
|
||||||
|
Ping (ICMP) Überwachung (Latenz, ob Host erreichbar)
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
mielke.de
|
||||||
|
+2
|
||||||
|
|
||||||
|
DNS-Eintragsüberwachung (z. B. überprüfe, ob DNS-A-Einträge sich ändern oder korrekt sind)
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
mielke.de
|
||||||
|
+2
|
||||||
|
|
||||||
|
Push-Monitor: Für Dienste, die nicht „standardmäßig“ abgefragt werden können, sendest du selbst einen „Heartbeat“ an Uptime Kuma.
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
Steam Game Server Überwachung
|
||||||
|
GitHub
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
|
||||||
|
Überwachung von Docker-Containern
|
||||||
|
uptime.kuma.pet
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
GitHub
|
||||||
|
+4
|
||||||
|
|
||||||
|
Datenbankmonitoring (z. B. MySQL / SQL)
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
GitHub
|
||||||
|
+4
|
||||||
|
|
||||||
|
Überwachungsintervalle & Wiederholungen
|
||||||
|
|
||||||
|
Minimales Intervall: 20 Sekunden
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
mielke.de
|
||||||
|
+4
|
||||||
|
|
||||||
|
Standardintervall typischerweise 60 Sekunden (kann angepasst werden)
|
||||||
|
uptimekuma.org
|
||||||
|
+1
|
||||||
|
|
||||||
|
Einstellung, wie oft Wiederholungen erfolgen müssen, bevor ein Ausfall gemeldet wird (Retries)
|
||||||
|
mielke.de
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
„Reverse Mode“ (Umgekehrter Modus): Der Monitor ist aktiv, wenn das Ziel nicht erreichbar ist, z. B. um eine Backup-Leitung zu überwachen.
|
||||||
|
mielke.de
|
||||||
|
|
||||||
|
Status-Dashboard & Visualisierung
|
||||||
|
|
||||||
|
Echtzeit-Statusanzeige aller Monitore im Dashboard
|
||||||
|
blog.matt-vdv.me
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
uptime.kuma.pet
|
||||||
|
+3
|
||||||
|
|
||||||
|
Graphen (z. B. Ping-Graph, Antwortzeiten) und historische Verlaufdaten
|
||||||
|
GitHub
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
mielke.de
|
||||||
|
+4
|
||||||
|
|
||||||
|
Uptime / Ausfallstatistiken über Zeiträume
|
||||||
|
uptime.kuma.pet
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
|
||||||
|
Statusseiten (Status Pages)
|
||||||
|
|
||||||
|
Erstelle eine oder mehrere öffentlich zugängliche Statusseiten, um Nutzern oder Kunden den Status deiner Services anzuzeigen.
|
||||||
|
mielke.de
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
GitHub
|
||||||
|
+4
|
||||||
|
|
||||||
|
Jede Statusseite kann individuell konfiguriert werden (Name, Slug, Monitore, Beschreibung)
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
mielke.de
|
||||||
|
+2
|
||||||
|
|
||||||
|
Statusseiten werden im Cache 5 Minuten gespeichert, und die Seite refresht sich alle 5 Minuten.
|
||||||
|
GitHub
|
||||||
|
|
||||||
|
Möglichkeit, Domains für Statusseiten zu definieren (z. B. status.deinedomain.tld)
|
||||||
|
GitHub
|
||||||
|
|
||||||
|
Öffentlich (ohne Auth) oder eingeschränkt via Basic Auth (je nach Einstellungen)
|
||||||
|
GitHub
|
||||||
|
+1
|
||||||
|
|
||||||
|
Benachrichtigungen / Alerts
|
||||||
|
|
||||||
|
Unterstützt über 90 Notification Channels (z. B. E-Mail (SMTP), Telegram, Discord, Slack, Signal, Webhook, etc.)
|
||||||
|
GitHub
|
||||||
|
+4
|
||||||
|
betterstack.com
|
||||||
|
+4
|
||||||
|
uptimekuma.org
|
||||||
|
+4
|
||||||
|
|
||||||
|
Testfunktion für Benachrichtigungskanäle in der Konfiguration
|
||||||
|
PikaPods Docs
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
Individuelle Auswahl je Monitor, über welchen Kanal benachrichtigt werden soll
|
||||||
|
mielke.de
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
Option (in Entwicklung / Diskussion), Benachrichtigungen nur bei Fehler („Only on Down“) statt auch bei Up oder „Recoveries“.
|
||||||
|
GitHub
|
||||||
|
|
||||||
|
Sicherheit & Authentifizierung
|
||||||
|
|
||||||
|
Zwei-Faktor-Authentifizierung (2FA) für das Uptime Kuma Dashboard
|
||||||
|
uptimekuma.org
|
||||||
|
|
||||||
|
Proxy-Unterstützung (z. B. Cloudflare, Caddy, HAProxy, Traefik, Nginx)
|
||||||
|
uptimekuma.org
|
||||||
|
+1
|
||||||
|
|
||||||
|
API Keys: Für Zugriff auf Prometheus-Metriken etc.
|
||||||
|
uptimekuma.org
|
||||||
|
+1
|
||||||
|
|
||||||
|
REST-API & Socket.io Schnittstellen
|
||||||
|
GitHub
|
||||||
|
|
||||||
|
Badge & öffentliche API-Daten
|
||||||
|
|
||||||
|
Generiere Badges (z. B. für Monitor-Status) für Webseiten / Statusseiten.
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
Öffentliche Statusseite-API-Endpunkte zur Abfrage von Statusdaten (bei veröffentlichten Statusseiten)
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
|
||||||
|
Prometheus-Metrik-Endpunkt (mit Auth / API-Key Option)
|
||||||
|
GitHub
|
||||||
|
+1
|
||||||
|
|
||||||
|
Internationalisierung / Anpassung
|
||||||
|
|
||||||
|
Mehr als 20 Sprachen unterstützt (u. a. Deutsch)
|
||||||
|
uptimekuma.org
|
||||||
|
|
||||||
|
Dunkelmodus / Hellmodus / Automatik je nach Systemtheme
|
||||||
|
uptimekuma.org
|
||||||
|
|
||||||
|
Anpassbare CSS / Layout-Anpassungen (über öffentliche Statusseite, Dashboard) (teilweise / mit Customizations)
|
||||||
|
|
||||||
|
Backup & Wiederherstellung
|
||||||
|
|
||||||
|
Möglichkeit, Daten und Konfiguration zu sichern / wiederherzustellen (Backup-Funktion)
|
||||||
|
blog.matt-vdv.me
|
||||||
|
|
||||||
|
Einstellungen für Proxy, Sicherheit, Notifications, allgemeine Einstellungen etc. gruppiert in Menüs
|
||||||
|
blog.matt-vdv.me
|
||||||
|
|
||||||
|
Reverse Proxy & Netzwerkintegration
|
||||||
|
|
||||||
|
Unterstützung, Uptime Kuma hinter einem Reverse Proxy zu betreiben (weitergeleitete Header etc.)
|
||||||
|
blog.matt-vdv.me
|
||||||
|
+3
|
||||||
|
uptimekuma.org
|
||||||
|
+3
|
||||||
|
mielke.de
|
||||||
|
+3
|
||||||
|
|
||||||
|
Umgang mit HTTP Basic Auth (z. B. wenn eine Webanwendung Grund-Auth nutzt)
|
||||||
|
Stack Overflow
|
||||||
|
|
||||||
|
Weitere Funktionen / kleine Zahnräder
|
||||||
|
|
||||||
|
„Ping Charts“ mit Interaktivität
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
blog.matt-vdv.me
|
||||||
|
+2
|
||||||
|
|
||||||
|
Filter, Suchfunktionen, Sortieren von Monitoren (Dashboard-Funktionalität) (teilweise vorhanden, aber mit Beschränkungen)
|
||||||
|
Reddit
|
||||||
|
+1
|
||||||
|
|
||||||
|
Status-Historie und Vorfälle (Incidents) in Statusseiten (wenn aktiviert)
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
Möglichkeit, Monitore zu gruppieren oder in Ordnern / Abschnitten darzustellen (Dashboard-Organisation)
|
||||||
|
|
||||||
|
Anbindung an Home Assistant (Integration) laut Erfahrungsberichten
|
||||||
|
mielke.de
|
||||||
|
|
||||||
|
API-Dokumentation (intern / inoffiziell)
|
||||||
|
|
||||||
|
Uptime Kuma hat eine API-Dokumentation im Wiki, inkl. Endpunkte für Push Monitore, Status Page, Metrics etc.
|
||||||
|
GitHub
|
||||||
|
+2
|
||||||
|
uptimekuma.org
|
||||||
|
+2
|
||||||
|
|
||||||
|
Hinweis: Diese API ist primär für die eigene App gedacht und kann zwischen Versionen brechen.
|
302
main.py
Normal file
302
main.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning, module='flask_admin.contrib')
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, flash, redirect, url_for, request
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_admin import Admin, AdminIndexView
|
||||||
|
from flask_admin.contrib.sqla import ModelView
|
||||||
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
||||||
|
from flask_migrate import Migrate, upgrade as upgrade_database
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, SubmitField, SelectField, TextAreaField
|
||||||
|
from wtforms.validators import DataRequired, EqualTo
|
||||||
|
|
||||||
|
# --- Initialisierung ---
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'uptime.db')
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
app.config['SECRET_KEY'] = 'eine-viel-sicherere-geheime-zeichenkette' # In Produktion ändern!
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
login_manager = LoginManager(app)
|
||||||
|
login_manager.login_view = 'login'
|
||||||
|
login_manager.login_message = 'Bitte loggen Sie sich ein, um auf diese Seite zuzugreifen.'
|
||||||
|
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
|
# --- Datenbankmodelle ---
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
"""Modell für Benutzer."""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(200), nullable=False)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
|
class Monitor(db.Model):
|
||||||
|
"""Modell für einen zu überwachenden Dienst."""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||||
|
monitor_type = db.Column(db.String(20), nullable=False, default='HTTP')
|
||||||
|
url = db.Column(db.String(200), nullable=False)
|
||||||
|
keyword = db.Column(db.String(100), nullable=True)
|
||||||
|
port = db.Column(db.Integer, nullable=True)
|
||||||
|
status_override = db.Column(db.String(50), nullable=True)
|
||||||
|
status_override_message = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
|
logs = db.relationship('UptimeLog', backref='monitor', lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Monitor {self.name}>'
|
||||||
|
|
||||||
|
class UptimeLog(db.Model):
|
||||||
|
"""Modell zum Protokollieren des Uptime-Status."""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
monitor_id = db.Column(db.Integer, db.ForeignKey('monitor.id'), nullable=False)
|
||||||
|
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
status_code = db.Column(db.Integer, nullable=True)
|
||||||
|
is_up = db.Column(db.Boolean, nullable=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<UptimeLog {self.monitor.name} - {self.timestamp} - {"Up" if self.is_up else "Down"}>'
|
||||||
|
|
||||||
|
# --- Login-Management ---
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
# --- Formulare ---
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField('Benutzername', validators=[DataRequired()])
|
||||||
|
password = PasswordField('Passwort', validators=[DataRequired()])
|
||||||
|
submit = SubmitField('Einloggen')
|
||||||
|
|
||||||
|
class ChangePasswordForm(FlaskForm):
|
||||||
|
current_password = PasswordField('Aktuelles Passwort', validators=[DataRequired()])
|
||||||
|
new_password = PasswordField('Neues Passwort', validators=[DataRequired()])
|
||||||
|
confirm_password = PasswordField('Neues Passwort bestätigen', validators=[DataRequired(), EqualTo('new_password', 'Die Passwörter müssen übereinstimmen.')])
|
||||||
|
submit = SubmitField('Passwort ändern')
|
||||||
|
|
||||||
|
# --- Gesicherter Admin-Bereich ---
|
||||||
|
|
||||||
|
class SecureModelView(ModelView):
|
||||||
|
def is_accessible(self):
|
||||||
|
return current_user.is_authenticated
|
||||||
|
|
||||||
|
def inaccessible_callback(self, name, **kwargs):
|
||||||
|
return redirect(url_for('login', next=request.url))
|
||||||
|
|
||||||
|
class SecureAdminIndexView(AdminIndexView):
|
||||||
|
def is_accessible(self):
|
||||||
|
return current_user.is_authenticated
|
||||||
|
|
||||||
|
def inaccessible_callback(self, name, **kwargs):
|
||||||
|
return redirect(url_for('login', next=request.url))
|
||||||
|
|
||||||
|
class MonitorModelView(SecureModelView):
|
||||||
|
# Dropdown für den Monitor-Typ und manuellen Status
|
||||||
|
form_overrides = {
|
||||||
|
'monitor_type': SelectField,
|
||||||
|
'status_override': SelectField,
|
||||||
|
'status_override_message': TextAreaField
|
||||||
|
}
|
||||||
|
form_args = {
|
||||||
|
'monitor_type': {
|
||||||
|
'label': 'Monitor-Typ',
|
||||||
|
'choices': [
|
||||||
|
('HTTP', 'HTTP(s)'),
|
||||||
|
('KEYWORD', 'HTTP(s) mit Keyword'),
|
||||||
|
('TCP', 'TCP-Port')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'status_override': {
|
||||||
|
'label': 'Manueller Status',
|
||||||
|
'choices': [
|
||||||
|
('', 'Automatisch'), # Leerer String für None
|
||||||
|
('MAINTENANCE', 'Wartungsarbeiten'),
|
||||||
|
('DEGRADED', 'Leistungsprobleme'),
|
||||||
|
('OPERATIONAL', 'Funktionsfähig (Manuell)')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
column_list = ('name', 'monitor_type', 'status_override', 'url', 'port')
|
||||||
|
form_columns = ('name', 'monitor_type', 'url', 'port', 'keyword', 'status_override', 'status_override_message')
|
||||||
|
column_labels = dict(name='Name', monitor_type='Typ', url='URL/Host', port='Port', keyword='Keyword', status_override='Manueller Status', status_override_message='Status-Nachricht')
|
||||||
|
|
||||||
|
def on_model_change(self, form, model, is_created):
|
||||||
|
if is_created:
|
||||||
|
print(f"Neuer Monitor erstellt: {model.name}. Starte initiale Prüfung.")
|
||||||
|
checker_thread = threading.Thread(target=check_monitors, kwargs={'monitor_id': model.id})
|
||||||
|
checker_thread.start()
|
||||||
|
super().on_model_change(form, model, is_created)
|
||||||
|
|
||||||
|
|
||||||
|
admin = Admin(app, name='Uptime Stats Admin', template_mode='bootstrap4', index_view=SecureAdminIndexView())
|
||||||
|
admin.add_view(MonitorModelView(Monitor, db.session, name="Monitore verwalten"))
|
||||||
|
admin.add_view(SecureModelView(User, db.session, name="Administratoren", endpoint='user_admin'))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Web-Routen ---
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
monitors_with_status = []
|
||||||
|
monitors = Monitor.query.order_by(Monitor.name).all()
|
||||||
|
for monitor in monitors:
|
||||||
|
last_log = UptimeLog.query.filter_by(monitor_id=monitor.id).order_by(UptimeLog.timestamp.desc()).first()
|
||||||
|
monitors_with_status.append({
|
||||||
|
'name': monitor.name,
|
||||||
|
'url': monitor.url,
|
||||||
|
'monitor_type': monitor.monitor_type,
|
||||||
|
'keyword': monitor.keyword,
|
||||||
|
'port': monitor.port,
|
||||||
|
'status_override': monitor.status_override,
|
||||||
|
'status_override_message': monitor.status_override_message,
|
||||||
|
'is_up': last_log.is_up if last_log else None,
|
||||||
|
'last_checked': last_log.timestamp if last_log else 'Nie'
|
||||||
|
})
|
||||||
|
return render_template('index.html', monitors=monitors_with_status)
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
form = LoginForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
|
if user is None or not user.check_password(form.password.data):
|
||||||
|
flash('Ungültiger Benutzername oder Passwort.', 'danger')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
login_user(user)
|
||||||
|
flash('Erfolgreich eingeloggt.', 'success')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
return render_template('login.html', form=form)
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash('Erfolgreich ausgeloggt.', 'info')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/change-password', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def change_password():
|
||||||
|
form = ChangePasswordForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if not current_user.check_password(form.current_password.data):
|
||||||
|
flash('Das aktuelle Passwort ist nicht korrekt.', 'danger')
|
||||||
|
else:
|
||||||
|
current_user.set_password(form.new_password.data)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Ihr Passwort wurde erfolgreich geändert.', 'success')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
return render_template('change_password.html', form=form)
|
||||||
|
|
||||||
|
# --- Uptime-Checker (Hintergrundprozess) ---
|
||||||
|
|
||||||
|
def check_monitors(monitor_id=None):
|
||||||
|
"""Überprüft den Status. Wenn monitor_id angegeben ist, nur diesen, sonst alle."""
|
||||||
|
with app.app_context():
|
||||||
|
if monitor_id:
|
||||||
|
monitors = Monitor.query.filter_by(id=monitor_id).all()
|
||||||
|
print(f"[{datetime.now()}] Starte initiale Prüfung für Monitor ID {monitor_id}...")
|
||||||
|
else:
|
||||||
|
monitors = Monitor.query.all()
|
||||||
|
if not monitors:
|
||||||
|
return
|
||||||
|
print(f"[{datetime.now()}] Starte periodische Überprüfung für {len(monitors)} Monitor(en)...")
|
||||||
|
|
||||||
|
if not monitors:
|
||||||
|
return
|
||||||
|
for monitor in monitors:
|
||||||
|
# Wenn ein manueller Status gesetzt ist, überspringe die automatische Prüfung.
|
||||||
|
if monitor.status_override:
|
||||||
|
print(f" - Monitor '{monitor.name}' hat manuellen Status '{monitor.status_override}'. Überspringe Prüfung.")
|
||||||
|
continue
|
||||||
|
is_up, status_code = False, None
|
||||||
|
|
||||||
|
if monitor.monitor_type in ['HTTP', 'KEYWORD']:
|
||||||
|
try:
|
||||||
|
response = requests.get(monitor.url, timeout=10)
|
||||||
|
status_code = response.status_code
|
||||||
|
if 200 <= status_code < 400:
|
||||||
|
if monitor.monitor_type == 'KEYWORD' and monitor.keyword:
|
||||||
|
if monitor.keyword in response.text:
|
||||||
|
is_up = True
|
||||||
|
else: # Standard HTTP
|
||||||
|
is_up = True
|
||||||
|
except requests.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif monitor.monitor_type == 'TCP':
|
||||||
|
try:
|
||||||
|
# For TCP, the 'url' field holds the host
|
||||||
|
host = monitor.url
|
||||||
|
port = monitor.port
|
||||||
|
if host and port:
|
||||||
|
with socket.create_connection((host, port), timeout=10) as sock:
|
||||||
|
is_up = True
|
||||||
|
# status_code is not applicable for TCP checks
|
||||||
|
except (socket.timeout, socket.error, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
log_entry = UptimeLog(monitor_id=monitor.id, status_code=status_code, is_up=is_up)
|
||||||
|
db.session.add(log_entry)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"[{datetime.now()}] Überprüfung abgeschlossen.")
|
||||||
|
|
||||||
|
|
||||||
|
def run_checks_periodically():
|
||||||
|
"""Führt die Überprüfungen in regelmäßigen Abständen aus."""
|
||||||
|
while True:
|
||||||
|
check_monitors()
|
||||||
|
# Warte 5 Minuten (300 Sekunden) bis zur nächsten Überprüfung
|
||||||
|
time.sleep(300)
|
||||||
|
|
||||||
|
# --- Initialisierung der App ---
|
||||||
|
|
||||||
|
def create_initial_user():
|
||||||
|
with app.app_context():
|
||||||
|
# db.create_all() is now handled by migrations
|
||||||
|
if User.query.first() is None:
|
||||||
|
print("Erstelle initialen Admin-Benutzer...")
|
||||||
|
initial_user = User(username='admin')
|
||||||
|
initial_user.set_password('admin123')
|
||||||
|
db.session.add(initial_user)
|
||||||
|
db.session.commit()
|
||||||
|
print("Benutzer 'admin' mit Passwort 'admin123' erstellt.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Apply database migrations automatically
|
||||||
|
with app.app_context():
|
||||||
|
upgrade_database()
|
||||||
|
|
||||||
|
create_initial_user()
|
||||||
|
|
||||||
|
checker_thread = threading.Thread(target=run_checks_periodically, daemon=True)
|
||||||
|
checker_thread.start()
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic,flask_migrate
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
113
migrations/env.py
Normal file
113
migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
47
readme.md
Normal file
47
readme.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Uptime Stats
|
||||||
|
|
||||||
|
Ein einfaches, in Python geschriebenes Tool zur Überwachung der Uptime von Websites, inspiriert von Uptime Kuma.
|
||||||
|
|
||||||
|
Dieses Projekt verwendet Flask, um eine Weboberfläche und einen Admin-Bereich bereitzustellen, und speichert alle Daten in einer SQLite-Datenbank.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Web-Dashboard:** Eine einfache Seite zur Anzeige des aktuellen Status aller überwachten Websites.
|
||||||
|
- **Admin-Bereich:** Ein passwortgeschützter Bereich (`/admin`) zum Hinzufügen, Bearbeiten und Löschen von zu überwachenden Websites.
|
||||||
|
- **SQLite-Datenbank:** Alle Konfigurationen und Uptime-Protokolle werden in einer einzigen `uptime.db`-Datei gespeichert. Es ist keine externe Datenbank erforderlich.
|
||||||
|
- **Periodische Überprüfungen:** Ein Hintergrundprozess überprüft alle 5 Minuten automatisch den Status der Websites.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Klonen Sie das Repository (oder laden Sie die Dateien herunter):**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd Uptime-Stats/Uptime-Stats
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Installieren Sie die Abhängigkeiten:**
|
||||||
|
Stellen Sie sicher, dass Sie Python 3 installiert haben. Erstellen Sie optional eine virtuelle Umgebung.
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
1. **Starten Sie die Anwendung:**
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
Die Anwendung wird standardmäßig auf `http://localhost:5000` ausgeführt.
|
||||||
|
|
||||||
|
2. **Einloggen und Websites hinzufügen:**
|
||||||
|
- Öffnen Sie die Login-Seite, die automatisch erscheint, wenn Sie auf den Admin-Bereich zugreifen wollen: [http://localhost:5000/admin](http://localhost:5000/admin)
|
||||||
|
- Loggen Sie sich mit den Standard-Anmeldedaten ein:
|
||||||
|
- **Benutzername:** `admin`
|
||||||
|
- **Passwort:** `admin123`
|
||||||
|
- Nach dem Login können Sie im Admin-Bereich Websites hinzufügen, bearbeiten oder löschen.
|
||||||
|
- Über den Menüpunkt "Passwort ändern" können Sie Ihr Passwort aktualisieren.
|
||||||
|
|
||||||
|
3. **Überprüfen Sie den Status:**
|
||||||
|
- Öffnen Sie die Hauptseite: [http://localhost:5000](http://localhost:5000)
|
||||||
|
- Die Seite zeigt den aktuellen Status der von Ihnen hinzugefügten Websites an.
|
||||||
|
- Die Statusüberprüfung findet alle 5 Minuten statt. Die Seite aktualisiert sich nicht automatisch; Sie müssen sie neu laden, um den neuesten Status zu sehen.
|
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
requests
|
||||||
|
Flask
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
Flask-Admin==1.6.1
|
||||||
|
Flask-Login
|
||||||
|
Flask-WTF
|
||||||
|
werkzeug
|
||||||
|
WTForms==3.0.1
|
||||||
|
Flask-Migrate
|
20
templates/admin/index.html
Normal file
20
templates/admin/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'admin/master.html' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h1>Willkommen im Admin-Bereich</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Dies ist der zentrale Verwaltungsbereich für Ihre Uptime-Statistiken.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Klicken Sie auf den Button unten, um direkt einen neuen Monitor zur Überwachung hinzuzufügen.
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-primary btn-lg my-3" href="{{ url_for('monitor.create_view', url=url_for('monitor.index_view')) }}">
|
||||||
|
<i class="bi bi-plus-circle"></i> Neuen Monitor hinzufügen
|
||||||
|
</a>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
Alternativ können Sie die Menüpunkte in der Navigationsleiste oben verwenden, um alle Monitore oder Benutzer anzuzeigen und zu bearbeiten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
43
templates/change_password.html
Normal file
43
templates/change_password.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
|
||||||
|
{% block title %}Passwort ändern - Uptime Stats{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-center mb-4">Passwort ändern</h2>
|
||||||
|
<form method="POST" action="{{ url_for('change_password') }}" novalidate>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.current_password.label(class="form-label") }}
|
||||||
|
{{ form.current_password(class="form-control is-invalid" if form.current_password.errors else "form-control") }}
|
||||||
|
{% for error in form.current_password.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.new_password.label(class="form-label") }}
|
||||||
|
{{ form.new_password(class="form-control is-invalid" if form.new_password.errors else "form-control") }}
|
||||||
|
{% for error in form.new_password.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.confirm_password.label(class="form-label") }}
|
||||||
|
{{ form.confirm_password(class="form-control is-invalid" if form.confirm_password.errors else "form-control") }}
|
||||||
|
{% for error in form.confirm_password.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
{{ form.submit(class_="btn btn-primary") }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
78
templates/index.html
Normal file
78
templates/index.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
|
||||||
|
{% block title %}Statusübersicht - Uptime Stats{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="mb-4">Statusübersicht</h1>
|
||||||
|
|
||||||
|
{% if not monitors %}
|
||||||
|
<div class="alert alert-info text-center">
|
||||||
|
<h4>Noch keine Monitore konfiguriert.</h4>
|
||||||
|
<p>Bitte fügen Sie im <a href="{{ url_for('admin.index') }}" class="alert-link">Admin-Bereich</a> einen Monitor hinzu, um mit der Überwachung zu beginnen.</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row">
|
||||||
|
{% for monitor in monitors %}
|
||||||
|
<div class="col-lg-4 col-md-6 mb-4">
|
||||||
|
<div class="card h-100 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title d-flex justify-content-between">
|
||||||
|
{{ monitor.name }}
|
||||||
|
|
||||||
|
{# Manuelle Statusanzeige priorisieren #}
|
||||||
|
{% if monitor.status_override == 'MAINTENANCE' %}
|
||||||
|
<span class="badge bg-primary status-badge">Wartung <i class="bi bi-tools"></i></span>
|
||||||
|
{% elif monitor.status_override == 'DEGRADED' %}
|
||||||
|
<span class="badge bg-warning text-dark status-badge">Probleme <i class="bi bi-exclamation-triangle"></i></span>
|
||||||
|
{% elif monitor.status_override == 'OPERATIONAL' %}
|
||||||
|
<span class="badge bg-success status-badge">Funktionsfähig <i class="bi bi-check-circle"></i></span>
|
||||||
|
|
||||||
|
{# Automatische Statusanzeige, wenn kein manueller Status gesetzt ist #}
|
||||||
|
{% elif monitor.is_up == True %}
|
||||||
|
<span class="badge bg-success status-badge">Up <i class="bi bi-check-circle"></i></span>
|
||||||
|
{% elif monitor.is_up == False %}
|
||||||
|
<span class="badge bg-danger status-badge">Down <i class="bi bi-x-circle"></i></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary status-badge">Unbekannt <i class="bi bi-question-circle"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
{# Status-Nachricht anzeigen, wenn vorhanden #}
|
||||||
|
{% if monitor.status_override_message %}
|
||||||
|
<div class="alert alert-info mt-2 p-2">
|
||||||
|
<small>{{ monitor.status_override_message }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="card-text">
|
||||||
|
{% if monitor.monitor_type == 'TCP' %}
|
||||||
|
<span class="text-muted text-break">{{ monitor.url }}:{{ monitor.port }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ monitor.url }}" target="_blank" class="text-muted text-break">{{ monitor.url }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<span class="badge rounded-pill bg-primary">{{ monitor.monitor_type }}</span>
|
||||||
|
{% if monitor.monitor_type == 'KEYWORD' and monitor.keyword %}
|
||||||
|
<span class="badge rounded-pill bg-light text-dark">Keyword: {{ monitor.keyword }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-muted">
|
||||||
|
<small>Zuletzt geprüft:
|
||||||
|
{% if monitor.last_checked != 'Nie' %}
|
||||||
|
{{ monitor.last_checked.strftime('%d.%m.%Y %H:%M:%S UTC') }}
|
||||||
|
{% else %}
|
||||||
|
Nie
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
94
templates/layout.html
Normal file
94
templates/layout.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}Uptime Stats{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #e9ecef; /* Hellerer Hintergrund für mehr Kontrast */
|
||||||
|
padding-bottom: 80px; /* Mehr Platz für den Footer */
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
box-shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
border-top: 3px solid #0d6efd; /* Akzentfarbe oben */
|
||||||
|
box-shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulsierende Animationen für Status */
|
||||||
|
.status-badge.bg-success {
|
||||||
|
border-top-color: #198754;
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
|
.status-badge.bg-danger {
|
||||||
|
border-top-color: #dc3545;
|
||||||
|
animation: pulse-red 1.5s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse-green {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.7); }
|
||||||
|
70% { box-shadow: 0 0 0 10px rgba(25, 135, 84, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0); }
|
||||||
|
}
|
||||||
|
@keyframes pulse-red {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); }
|
||||||
|
70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('index') }}"><i class="bi bi-bar-chart-line-fill"></i> Uptime Stats</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('admin.index') }}"><i class="bi bi-shield-lock"></i> Admin</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('change_password') }}"><i class="bi bi-key"></i> Passwort ändern</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('logout') }}"><i class="bi bi-box-arrow-right"></i> Logout</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('login') }}"><i class="bi bi-box-arrow-in-right"></i> Login</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer mt-auto py-3 bg-dark text-white fixed-bottom">
|
||||||
|
<div class="container d-flex justify-content-between align-items-center">
|
||||||
|
<span>Powered by <a href="https://rl-dev.de" target="_blank" class="text-light fw-bold">rl-dev.de</a></span>
|
||||||
|
<a href="https://git.rl-dev.de/Robin/Uptime-Stats" target="_blank" class="text-light fs-4">
|
||||||
|
<i class="bi bi-git"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
templates/login.html
Normal file
36
templates/login.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
|
||||||
|
{% block title %}Login - Uptime Stats{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-center mb-4">Admin Login</h2>
|
||||||
|
<form method="POST" action="{{ url_for('login') }}" novalidate>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.username.label(class="form-label") }}
|
||||||
|
{{ form.username(class="form-control is-invalid" if form.username.errors else "form-control") }}
|
||||||
|
{% for error in form.username.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-control is-invalid" if form.password.errors else "form-control") }}
|
||||||
|
{% for error in form.password.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
{{ form.submit(class_="btn btn-primary") }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user