+ update emblemed support in iframe and list view
This commit is contained in:
119
main.py
119
main.py
@@ -3,16 +3,17 @@ import requests
|
|||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time, math
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from flask import Flask, render_template, flash, redirect, url_for, request
|
from flask import Flask, render_template, flash, redirect, url_for, request, make_response
|
||||||
from flask_admin import Admin, AdminIndexView
|
from flask_admin import Admin, AdminIndexView, BaseView, expose
|
||||||
from flask_admin.contrib.sqla import ModelView
|
from flask_admin.contrib.sqla import ModelView
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
||||||
|
from flask_admin.menu import MenuLink
|
||||||
from flask_migrate import Migrate, upgrade
|
from flask_migrate import Migrate, upgrade
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
@@ -91,6 +92,7 @@ class Monitor(db.Model):
|
|||||||
url = db.Column(db.String(200), nullable=False)
|
url = db.Column(db.String(200), nullable=False)
|
||||||
keyword = db.Column(db.String(100), nullable=True)
|
keyword = db.Column(db.String(100), nullable=True)
|
||||||
port = db.Column(db.Integer, nullable=True)
|
port = db.Column(db.Integer, nullable=True)
|
||||||
|
position = db.Column(db.Integer, nullable=False, default=0, server_default='0')
|
||||||
status_override = db.Column(db.String(50), nullable=True)
|
status_override = db.Column(db.String(50), nullable=True)
|
||||||
status_override_message = db.Column(db.Text, nullable=True)
|
status_override_message = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ class UptimeLog(db.Model):
|
|||||||
"""Modell zum Protokollieren des Uptime-Status."""
|
"""Modell zum Protokollieren des Uptime-Status."""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
monitor_id = db.Column(db.Integer, db.ForeignKey('monitor.id'), nullable=False)
|
monitor_id = db.Column(db.Integer, db.ForeignKey('monitor.id'), nullable=False)
|
||||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
|
timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), index=True)
|
||||||
status_code = db.Column(db.Integer, nullable=True)
|
status_code = db.Column(db.Integer, nullable=True)
|
||||||
is_up = db.Column(db.Boolean, nullable=False)
|
is_up = db.Column(db.Boolean, nullable=False)
|
||||||
|
|
||||||
@@ -156,6 +158,14 @@ class SecureAdminIndexView(SecureView, AdminIndexView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedInfoView(SecureView, BaseView):
|
||||||
|
"""Eine gesicherte Admin-Ansicht, die den iFrame-Einbettungscode anzeigt."""
|
||||||
|
@expose('/')
|
||||||
|
def index(self):
|
||||||
|
# 'render' wird von BaseView bereitgestellt und injiziert den Admin-Kontext
|
||||||
|
return self.render('admin/embed_info.html')
|
||||||
|
|
||||||
|
|
||||||
class MonitorModelView(SecureModelView):
|
class MonitorModelView(SecureModelView):
|
||||||
"""Angepasste Admin-Ansicht für Monitore."""
|
"""Angepasste Admin-Ansicht für Monitore."""
|
||||||
form_overrides = {
|
form_overrides = {
|
||||||
@@ -188,11 +198,11 @@ class MonitorModelView(SecureModelView):
|
|||||||
'validators': [VOptional()],
|
'validators': [VOptional()],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
column_list = ('name', 'monitor_type', 'status_override', 'url', 'port')
|
column_list = ('name', 'monitor_type', 'position', 'status_override', 'url', 'port')
|
||||||
form_columns = ('name', 'monitor_type', 'url', 'port', 'keyword', 'status_override', 'status_override_message')
|
form_columns = ('name', 'monitor_type', 'position', 'url', 'port', 'keyword', 'status_override', 'status_override_message')
|
||||||
column_labels = dict(
|
column_labels = dict(
|
||||||
name='Name', monitor_type='Typ', url='URL/Host', port='Port', keyword='Keyword',
|
name='Name', monitor_type='Typ', url='URL/Host', port='Port', keyword='Keyword',
|
||||||
status_override='Manueller Status', status_override_message='Status-Nachricht'
|
position='Position', status_override='Manueller Status', status_override_message='Status-Nachricht'
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_model_change(self, form, model, is_created):
|
def on_model_change(self, form, model, is_created):
|
||||||
@@ -216,10 +226,17 @@ class MonitorModelView(SecureModelView):
|
|||||||
admin = Admin(app, name='Uptime Stats Admin', template_mode='bootstrap4', index_view=SecureAdminIndexView())
|
admin = Admin(app, name='Uptime Stats Admin', template_mode='bootstrap4', index_view=SecureAdminIndexView())
|
||||||
admin.add_view(MonitorModelView(Monitor, db.session, name="Monitore"))
|
admin.add_view(MonitorModelView(Monitor, db.session, name="Monitore"))
|
||||||
admin.add_view(SecureModelView(User, db.session, name="Administratoren", endpoint='user_admin'))
|
admin.add_view(SecureModelView(User, db.session, name="Administratoren", endpoint='user_admin'))
|
||||||
|
admin.add_view(EmbedInfoView(name='Einbettungs-Info', endpoint='embed-info'))
|
||||||
|
|
||||||
|
|
||||||
# --- Web-Routen ---
|
# --- Web-Routen ---
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_now():
|
||||||
|
"""Stellt das aktuelle UTC-Datum für alle Templates zur Verfügung."""
|
||||||
|
return {'now': datetime.now(timezone.utc)}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Öffentliche Statusseite."""
|
"""Öffentliche Statusseite."""
|
||||||
@@ -237,11 +254,72 @@ def index():
|
|||||||
UptimeLog,
|
UptimeLog,
|
||||||
(UptimeLog.monitor_id == latest_log_subquery.c.monitor_id) &
|
(UptimeLog.monitor_id == latest_log_subquery.c.monitor_id) &
|
||||||
(UptimeLog.timestamp == latest_log_subquery.c.max_timestamp)
|
(UptimeLog.timestamp == latest_log_subquery.c.max_timestamp)
|
||||||
).order_by(Monitor.name).all()
|
).order_by(Monitor.position, Monitor.name).all()
|
||||||
|
|
||||||
return render_template('index.html', monitors_with_status=monitors_with_status)
|
return render_template('index.html', monitors_with_status=monitors_with_status)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/status')
|
||||||
|
def status():
|
||||||
|
"""Erstellt eine für iFrames optimierte Status-Tabellenseite."""
|
||||||
|
latest_log_subquery = db.session.query(
|
||||||
|
UptimeLog.monitor_id,
|
||||||
|
func.max(UptimeLog.timestamp).label('max_timestamp')
|
||||||
|
).group_by(UptimeLog.monitor_id).subquery()
|
||||||
|
|
||||||
|
monitors_with_status = db.session.query(
|
||||||
|
Monitor,
|
||||||
|
UptimeLog
|
||||||
|
).outerjoin(
|
||||||
|
latest_log_subquery, Monitor.id == latest_log_subquery.c.monitor_id
|
||||||
|
).outerjoin(
|
||||||
|
UptimeLog,
|
||||||
|
(UptimeLog.monitor_id == latest_log_subquery.c.monitor_id) &
|
||||||
|
(UptimeLog.timestamp == latest_log_subquery.c.max_timestamp)
|
||||||
|
).order_by(Monitor.position, Monitor.name).all()
|
||||||
|
|
||||||
|
response = make_response(render_template('status.html', monitors_with_status=monitors_with_status))
|
||||||
|
response.headers.pop('X-Frame-Options', None)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route('/monitor/<int:monitor_id>')
|
||||||
|
def monitor_details(monitor_id: int):
|
||||||
|
"""Zeigt Detailinformationen und Statistiken für einen einzelnen Monitor."""
|
||||||
|
monitor = db.session.get(Monitor, monitor_id)
|
||||||
|
if not monitor:
|
||||||
|
return "Monitor nicht gefunden", 404
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# Statistik für 24 Stunden
|
||||||
|
start_24h = now - timedelta(hours=24)
|
||||||
|
logs_24h = db.session.scalars(
|
||||||
|
db.select(UptimeLog).where(
|
||||||
|
UptimeLog.monitor_id == monitor_id,
|
||||||
|
UptimeLog.timestamp >= start_24h
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
total_checks_24h = len(logs_24h)
|
||||||
|
up_checks_24h = sum(1 for log in logs_24h if log.is_up)
|
||||||
|
uptime_24h = (up_checks_24h / total_checks_24h * 100) if total_checks_24h > 0 else 100
|
||||||
|
|
||||||
|
# Statistik für 30 Tage
|
||||||
|
start_30d = now - timedelta(days=30)
|
||||||
|
logs_30d = db.session.scalars(
|
||||||
|
db.select(UptimeLog).where(
|
||||||
|
UptimeLog.monitor_id == monitor_id,
|
||||||
|
UptimeLog.timestamp >= start_30d
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
total_checks_30d = len(logs_30d)
|
||||||
|
up_checks_30d = sum(1 for log in logs_30d if log.is_up)
|
||||||
|
uptime_30d = (up_checks_30d / total_checks_30d * 100) if total_checks_30d > 0 else 100
|
||||||
|
|
||||||
|
return render_template('monitor_details.html', monitor=monitor,
|
||||||
|
uptime_24h=uptime_24h, uptime_30d=uptime_30d,
|
||||||
|
total_checks_24h=total_checks_24h, total_checks_30d=total_checks_30d)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
@@ -328,14 +406,14 @@ def check_monitors(monitor_id: Optional[int] = None):
|
|||||||
query = db.select(Monitor)
|
query = db.select(Monitor)
|
||||||
if monitor_id:
|
if monitor_id:
|
||||||
query = query.where(Monitor.id == monitor_id)
|
query = query.where(Monitor.id == monitor_id)
|
||||||
app.logger.info(f"[{datetime.now()}] Starte Prüfung für Monitor ID {monitor_id}...")
|
app.logger.info(f"[{datetime.now(timezone.utc)}] Starte Prüfung für Monitor ID {monitor_id}...")
|
||||||
else:
|
else:
|
||||||
app.logger.info(f"[{datetime.now()}] Starte periodische Überprüfung...")
|
app.logger.info(f"[{datetime.now(timezone.utc)}] Starte periodische Überprüfung...")
|
||||||
|
|
||||||
monitors = db.session.scalars(query).all()
|
monitors = db.session.scalars(query).all()
|
||||||
if not monitors:
|
if not monitors:
|
||||||
if not monitor_id:
|
if not monitor_id:
|
||||||
app.logger.info(f"[{datetime.now()}] Keine Monitore zur Überprüfung gefunden.")
|
app.logger.info(f"[{datetime.now(timezone.utc)}] Keine Monitore zur Überprüfung gefunden.")
|
||||||
return
|
return
|
||||||
|
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
@@ -348,7 +426,7 @@ def check_monitors(monitor_id: Optional[int] = None):
|
|||||||
db.session.add(log_entry)
|
db.session.add(log_entry)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
app.logger.info(f"[{datetime.now()}] Überprüfung abgeschlossen.")
|
app.logger.info(f"[{datetime.now(timezone.utc)}] Überprüfung abgeschlossen.")
|
||||||
|
|
||||||
|
|
||||||
def run_checks_periodically(interval: int = None):
|
def run_checks_periodically(interval: int = None):
|
||||||
@@ -374,17 +452,14 @@ def init_app():
|
|||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
app.logger.info("Führe Datenbank-Migrationen aus...")
|
app.logger.info("Führe Datenbank-Migrationen aus...")
|
||||||
migrations_dir = os.path.join(basedir, 'migrations')
|
|
||||||
ran_migrations = False
|
|
||||||
try:
|
try:
|
||||||
if os.path.isdir(migrations_dir):
|
# Wendet ausstehende Datenbank-Migrationen an.
|
||||||
upgrade()
|
# Dies aktualisiert das Schema auf den neuesten Stand.
|
||||||
ran_migrations = True
|
upgrade()
|
||||||
app.logger.info("Migrationen abgeschlossen.")
|
app.logger.info("Datenbank-Migrationen erfolgreich angewendet.")
|
||||||
else:
|
|
||||||
app.logger.warning("Kein migrations/-Ordner gefunden – überspringe Alembic upgrade.")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.warning(f"Alembic upgrade fehlgeschlagen: {e}")
|
app.logger.error(f"Anwenden der Datenbank-Migrationen fehlgeschlagen: {e}")
|
||||||
|
app.logger.info("Versuche, Tabellen mit db.create_all() zu erstellen, falls sie nicht existieren.")
|
||||||
|
|
||||||
# Sicherstellen, dass alle Tabellen existieren – Fallback für frische Setups ohne Migrations
|
# Sicherstellen, dass alle Tabellen existieren – Fallback für frische Setups ohne Migrations
|
||||||
insp = inspect(db.engine)
|
insp = inspect(db.engine)
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
"""Auto-detect model changes
|
||||||
|
|
||||||
|
Revision ID: fe9c4d25c5da
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-10-06 12:00:18.340503
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fe9c4d25c5da'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('monitor', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('position', sa.Integer(), server_default='0', nullable=False))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('monitor', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('position')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
11
templates/_footer.html
Normal file
11
templates/_footer.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<footer class="footer mt-auto py-3 bg-light border-top">
|
||||||
|
<div class="container text-center">
|
||||||
|
<span class="text-muted">
|
||||||
|
© {{ now.year }} <a href="https://rl-dev.de" target="_blank" class="text-decoration-none">rl-dev.de</a>
|
||||||
|
</span>
|
||||||
|
<span class="mx-2 text-muted">|</span>
|
||||||
|
<a href="https://git.rl-dev.de/Robin/Uptime-Stats" target="_blank" class="text-decoration-none text-muted">
|
||||||
|
<i class="bi bi-git"></i> Source Code
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
13
templates/_status_badge.html
Normal file
13
templates/_status_badge.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<td>
|
||||||
|
{% if monitor.status_override == 'MAINTENANCE' %}
|
||||||
|
<span class="badge bg-primary">Wartung <i class="bi bi-tools"></i></span>
|
||||||
|
{% elif monitor.status_override == 'DEGRADED' %}
|
||||||
|
<span class="badge bg-warning text-dark">Probleme <i class="bi bi-exclamation-triangle"></i></span>
|
||||||
|
{% elif last_log and last_log.is_up %}
|
||||||
|
<span class="badge bg-success status-badge">Up <i class="bi bi-check-circle"></i></span>
|
||||||
|
{% elif last_log and not last_log.is_up %}
|
||||||
|
<span class="badge bg-danger status-badge">Down <i class="bi bi-x-circle"></i></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Unbekannt <i class="bi bi-question-circle"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
16
templates/admin/embed_info.html
Normal file
16
templates/admin/embed_info.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'admin/master.html' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>iFrame-Einbettungscode</h1>
|
||||||
|
<p>Kopieren Sie den folgenden HTML-Code, um die Status-Tabelle auf einer anderen Seite oder in Ihrem Intranet einzubetten:</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body bg-light">
|
||||||
|
<pre><code><iframe src="{{ url_for('status', _external=True) }}" style="width: 100%; height: 400px; border: none;"></iframe></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4">
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="btn btn-primary"><i class="bi bi-arrow-left"></i> Zurück zum Dashboard</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@@ -1,78 +1,86 @@
|
|||||||
|
<!doctype html>
|
||||||
{% extends 'layout.html' %}
|
<html lang="de">
|
||||||
|
<head>
|
||||||
{% block title %}Statusübersicht - Uptime Stats{% endblock %}
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{% block content %}
|
<title>Uptime Status</title>
|
||||||
<div class="container">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<h1 class="mb-4">Statusübersicht</h1>
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<style>
|
||||||
{% if not monitors %}
|
.status-badge.bg-danger {
|
||||||
<div class="alert alert-info text-center">
|
animation: pulse-red 1.5s infinite;
|
||||||
<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>
|
@keyframes pulse-green {
|
||||||
</div>
|
0% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.7); }
|
||||||
{% else %}
|
70% { box-shadow: 0 0 0 10px rgba(25, 135, 84, 0); }
|
||||||
<div class="row">
|
100% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0); }
|
||||||
{% for monitor in monitors %}
|
}
|
||||||
<div class="col-lg-4 col-md-6 mb-4">
|
@keyframes pulse-red {
|
||||||
<div class="card h-100 shadow-sm">
|
0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); }
|
||||||
<div class="card-body">
|
70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); }
|
||||||
<h5 class="card-title d-flex justify-content-between">
|
100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
|
||||||
{{ monitor.name }}
|
}
|
||||||
|
</style>
|
||||||
{# Manuelle Statusanzeige priorisieren #}
|
</head>
|
||||||
{% if monitor.status_override == 'MAINTENANCE' %}
|
<body class="d-flex flex-column vh-100">
|
||||||
<span class="badge bg-primary status-badge">Wartung <i class="bi bi-tools"></i></span>
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
{% elif monitor.status_override == 'DEGRADED' %}
|
<div class="container">
|
||||||
<span class="badge bg-warning text-dark status-badge">Probleme <i class="bi bi-exclamation-triangle"></i></span>
|
<i class="bi bi-bar-chart-line-fill"></i> Uptime Status
|
||||||
{% elif monitor.status_override == 'OPERATIONAL' %}
|
</a>
|
||||||
<span class="badge bg-success status-badge">Funktionsfähig <i class="bi bi-check-circle"></i></span>
|
<div class="ms-auto">
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="btn btn-outline-light">
|
||||||
{# Automatische Statusanzeige, wenn kein manueller Status gesetzt ist #}
|
<i class="bi bi-person-circle"></i> Admin-Bereich
|
||||||
{% elif monitor.is_up == True %}
|
</a>
|
||||||
<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>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</nav>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
<main class="container mt-4 flex-grow-1">
|
||||||
|
<h1 class="mb-4">Statusübersicht</h1>
|
||||||
|
|
||||||
|
{% if not monitors_with_status %}
|
||||||
|
<div class="alert alert-info text-center">
|
||||||
|
<h4>Keine Monitore konfiguriert.</h4>
|
||||||
|
<p>Bitte loggen Sie sich in den <a href="{{ url_for('admin.index') }}" class="alert-link">Admin-Bereich</a> ein, um neue Monitore hinzuzufügen.</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Dienst</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Letzte Prüfung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for monitor, last_log in monitors_with_status %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('monitor_details', monitor_id=monitor.id) }}" class="text-decoration-none fw-bold">{{ monitor.name }}</a>
|
||||||
|
{% if monitor.status_override_message %}
|
||||||
|
<div class="text-muted fst-italic small">
|
||||||
|
<i class="bi bi-info-circle"></i> {{ monitor.status_override_message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% include '_status_badge.html' %}
|
||||||
|
<td>
|
||||||
|
{% if last_log %}
|
||||||
|
{{ last_log.timestamp.strftime('%d.%m.%Y %H:%M:%S UTC') }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Nie</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% include '_footer.html' %}
|
||||||
|
|
||||||
|
<script src="ht
|
||||||
|
</html>
|
78
templates/status.html
Normal file
78
templates/status.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Statusübersicht</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: transparent;
|
||||||
|
}
|
||||||
|
.status-badge.bg-success {
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
|
.status-badge.bg-danger {
|
||||||
|
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>
|
||||||
|
<div class="container-fluid">
|
||||||
|
{% if not monitors_with_status %}
|
||||||
|
<div class="alert alert-info text-center">
|
||||||
|
<h4>Keine Monitore konfiguriert.</h4>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Dienst</th>
|
||||||
|
<th class="text-center">Status</th>
|
||||||
|
<th>Letzte Prüfung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for monitor, last_log in monitors_with_status %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('monitor_details', monitor_id=monitor.id, _external=True) }}" target="_blank" class="text-decoration-none fw-bold">{{ monitor.name }}</a>
|
||||||
|
{% if monitor.status_override_message %}
|
||||||
|
<div class="text-muted fst-italic small">
|
||||||
|
<i class="bi bi-info-circle"></i> {{ monitor.status_override_message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% with %}
|
||||||
|
{% set monitor=monitor %}
|
||||||
|
{% set last_log=last_log %}
|
||||||
|
{% include '_status_badge.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
<td>
|
||||||
|
{% if last_log %}
|
||||||
|
{{ last_log.timestamp.strftime('%d.%m.%Y %H:%M:%S UTC') }}
|
||||||
|
{% else %}
|
||||||
|
Nie
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% include '_footer.html' %}
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
85
update.py
Normal file
85
update.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
|
def run_command(command, cwd=None):
|
||||||
|
"""
|
||||||
|
Führt einen Befehl aus, gibt seine Ausgabe in Echtzeit aus und prüft auf Fehler.
|
||||||
|
"""
|
||||||
|
print(f"\n>>> Führe aus: {' '.join(command)}")
|
||||||
|
print("-" * 40)
|
||||||
|
try:
|
||||||
|
# Stelle sicher, dass der Python-Interpreter des venv für Module verwendet wird
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
encoding='utf-8',
|
||||||
|
errors='replace', # Verhindert Fehler bei unerwarteten Zeichen
|
||||||
|
cwd=cwd,
|
||||||
|
bufsize=1 # Zeilen-gepuffert
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lese und drucke die Ausgabe Zeile für Zeile
|
||||||
|
for line in iter(process.stdout.readline, ''):
|
||||||
|
print(line, end='')
|
||||||
|
|
||||||
|
process.wait() # Warte auf das Ende des Prozesses
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
print("-" * 40)
|
||||||
|
print(f"FEHLER: Befehl schlug mit Exit-Code {process.returncode} fehl.")
|
||||||
|
print("-" * 40)
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("-" * 40)
|
||||||
|
print(">>> Befehl erfolgreich abgeschlossen.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"FEHLER: Befehl '{command[0]}' nicht gefunden. Ist Git/Python korrekt installiert und im PATH?")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ein unerwarteter Fehler ist aufgetreten: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Haupt-Update-Prozess."""
|
||||||
|
print("Starte den Update-Prozess der Anwendung...")
|
||||||
|
|
||||||
|
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Finde den Python-Interpreter des aktuellen Virtual Environments
|
||||||
|
python_executable = sys.executable
|
||||||
|
|
||||||
|
# Schritt 1: Neueste Änderungen von Git holen
|
||||||
|
if not run_command(["git", "pull"], cwd=project_root):
|
||||||
|
print("\nUpdate fehlgeschlagen: Konnte die neuesten Änderungen nicht von Git holen.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Schritt 2: Python-Abhängigkeiten installieren/aktualisieren
|
||||||
|
if not run_command([python_executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=project_root):
|
||||||
|
print("\nUpdate fehlgeschlagen: Konnte die Python-Abhängigkeiten nicht installieren.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Schritt 3: Datenbank-Migrationen erstellen (falls nötig)
|
||||||
|
# Dieser Befehl erkennt Modelländerungen und erstellt ein Migrationsskript.
|
||||||
|
# Er kann fehlschlagen, wenn es keine Änderungen gibt, das ist in Ordnung.
|
||||||
|
print("\nVersuche, Datenbank-Änderungen zu erkennen...")
|
||||||
|
run_command([python_executable, "-m", "flask", "--app", "main", "db", "migrate", "-m", "Auto-detect model changes"], cwd=project_root)
|
||||||
|
|
||||||
|
# Schritt 4: Datenbank-Migrationen anwenden
|
||||||
|
print("\nWende Datenbank-Migrationen an...")
|
||||||
|
if not run_command([python_executable, "-m", "flask", "--app", "main", "db", "upgrade"], cwd=project_root):
|
||||||
|
print("\nUpdate fehlgeschlagen: Konnte die Datenbank-Migrationen nicht anwenden.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n\n===================================================")
|
||||||
|
print("✅ Update erfolgreich abgeschlossen!")
|
||||||
|
print("Bitte starten Sie die Anwendung (main.py) neu, um die Änderungen zu übernehmen.")
|
||||||
|
print("===================================================")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Reference in New Issue
Block a user