Add system health and system event logging.
This commit is contained in:
129
app.py
129
app.py
@@ -1,5 +1,5 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
|
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
|
||||||
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement
|
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent
|
||||||
from data_formatting import (
|
from data_formatting import (
|
||||||
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
||||||
format_table_data, format_graph_data, format_team_data
|
format_table_data, format_graph_data, format_team_data
|
||||||
@@ -481,15 +481,52 @@ def login():
|
|||||||
session['username'] = user.username
|
session['username'] = user.username
|
||||||
session['role'] = user.role.value
|
session['role'] = user.role.value
|
||||||
|
|
||||||
|
# Log successful login
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'user_login',
|
||||||
|
f'User {user.username} logged in successfully',
|
||||||
|
'auth',
|
||||||
|
'info',
|
||||||
|
user_id=user.id,
|
||||||
|
company_id=user.company_id,
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
user_agent=request.headers.get('User-Agent')
|
||||||
|
)
|
||||||
|
|
||||||
flash('Login successful!', 'success')
|
flash('Login successful!', 'success')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
|
# Log failed login attempt
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'login_failed',
|
||||||
|
f'Failed login attempt for username: {username}',
|
||||||
|
'auth',
|
||||||
|
'warning',
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
user_agent=request.headers.get('User-Agent')
|
||||||
|
)
|
||||||
|
|
||||||
flash('Invalid username or password', 'error')
|
flash('Invalid username or password', 'error')
|
||||||
|
|
||||||
return render_template('login.html', title='Login')
|
return render_template('login.html', title='Login')
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
|
# Log logout event before clearing session
|
||||||
|
if 'user_id' in session:
|
||||||
|
user = User.query.get(session['user_id'])
|
||||||
|
if user:
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'user_logout',
|
||||||
|
f'User {user.username} logged out',
|
||||||
|
'auth',
|
||||||
|
'info',
|
||||||
|
user_id=user.id,
|
||||||
|
company_id=user.company_id,
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
user_agent=request.headers.get('User-Agent')
|
||||||
|
)
|
||||||
|
|
||||||
session.clear()
|
session.clear()
|
||||||
flash('You have been logged out.', 'info')
|
flash('You have been logged out.', 'info')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
@@ -1195,9 +1232,33 @@ def verify_2fa():
|
|||||||
session['username'] = user.username
|
session['username'] = user.username
|
||||||
session['role'] = user.role.value
|
session['role'] = user.role.value
|
||||||
|
|
||||||
|
# Log successful 2FA login
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'user_login_2fa',
|
||||||
|
f'User {user.username} logged in successfully with 2FA',
|
||||||
|
'auth',
|
||||||
|
'info',
|
||||||
|
user_id=user.id,
|
||||||
|
company_id=user.company_id,
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
user_agent=request.headers.get('User-Agent')
|
||||||
|
)
|
||||||
|
|
||||||
flash('Login successful!', 'success')
|
flash('Login successful!', 'success')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
else:
|
else:
|
||||||
|
# Log failed 2FA attempt
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'2fa_failed',
|
||||||
|
f'Failed 2FA verification for user {user.username}',
|
||||||
|
'auth',
|
||||||
|
'warning',
|
||||||
|
user_id=user.id,
|
||||||
|
company_id=user.company_id,
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
user_agent=request.headers.get('User-Agent')
|
||||||
|
)
|
||||||
|
|
||||||
flash('Invalid verification code. Please try again.', 'error')
|
flash('Invalid verification code. Please try again.', 'error')
|
||||||
|
|
||||||
return render_template('verify_2fa.html', title='Two-Factor Authentication')
|
return render_template('verify_2fa.html', title='Two-Factor Authentication')
|
||||||
@@ -2047,6 +2108,72 @@ def system_admin_settings():
|
|||||||
total_users=total_users,
|
total_users=total_users,
|
||||||
total_system_admins=total_system_admins)
|
total_system_admins=total_system_admins)
|
||||||
|
|
||||||
|
@app.route('/system-admin/health')
|
||||||
|
@system_admin_required
|
||||||
|
def system_admin_health():
|
||||||
|
"""System Admin: System health check and event log"""
|
||||||
|
# Get system health summary
|
||||||
|
health_summary = SystemEvent.get_system_health_summary()
|
||||||
|
|
||||||
|
# Get recent events (last 7 days)
|
||||||
|
recent_events = SystemEvent.get_recent_events(days=7, limit=100)
|
||||||
|
|
||||||
|
# Get events by severity for quick stats
|
||||||
|
errors = SystemEvent.get_events_by_severity('error', days=7, limit=20)
|
||||||
|
warnings = SystemEvent.get_events_by_severity('warning', days=7, limit=20)
|
||||||
|
|
||||||
|
# System metrics
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Database connection test
|
||||||
|
db_healthy = True
|
||||||
|
db_error = None
|
||||||
|
try:
|
||||||
|
db.session.execute('SELECT 1')
|
||||||
|
except Exception as e:
|
||||||
|
db_healthy = False
|
||||||
|
db_error = str(e)
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'database_check_failed',
|
||||||
|
f'Database health check failed: {str(e)}',
|
||||||
|
'system',
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Application uptime (approximate based on first event)
|
||||||
|
first_event = SystemEvent.query.order_by(SystemEvent.timestamp.asc()).first()
|
||||||
|
uptime_start = first_event.timestamp if first_event else now
|
||||||
|
uptime_duration = now - uptime_start
|
||||||
|
|
||||||
|
# Recent activity stats
|
||||||
|
today = now.date()
|
||||||
|
today_events = SystemEvent.query.filter(
|
||||||
|
func.date(SystemEvent.timestamp) == today
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Log the health check
|
||||||
|
SystemEvent.log_event(
|
||||||
|
'system_health_check',
|
||||||
|
f'System health check performed by {session.get("username", "unknown")}',
|
||||||
|
'system',
|
||||||
|
'info',
|
||||||
|
user_id=session.get('user_id'),
|
||||||
|
ip_address=request.remote_addr,
|
||||||
|
user_agent=request.headers.get('User-Agent')
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template('system_admin_health.html',
|
||||||
|
title='System Health Check',
|
||||||
|
health_summary=health_summary,
|
||||||
|
recent_events=recent_events,
|
||||||
|
errors=errors,
|
||||||
|
warnings=warnings,
|
||||||
|
db_healthy=db_healthy,
|
||||||
|
db_error=db_error,
|
||||||
|
uptime_duration=uptime_duration,
|
||||||
|
today_events=today_events)
|
||||||
|
|
||||||
@app.route('/system-admin/announcements')
|
@app.route('/system-admin/announcements')
|
||||||
@system_admin_required
|
@system_admin_required
|
||||||
def system_admin_announcements():
|
def system_admin_announcements():
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ try:
|
|||||||
from app import app, db
|
from app import app, db
|
||||||
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
||||||
Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType,
|
Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType,
|
||||||
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement)
|
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent)
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
FLASK_AVAILABLE = True
|
FLASK_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -71,6 +71,7 @@ def run_all_migrations(db_path=None):
|
|||||||
migrate_to_company_model(db_path)
|
migrate_to_company_model(db_path)
|
||||||
migrate_work_config_data(db_path)
|
migrate_work_config_data(db_path)
|
||||||
migrate_task_system(db_path)
|
migrate_task_system(db_path)
|
||||||
|
migrate_system_events(db_path)
|
||||||
|
|
||||||
if FLASK_AVAILABLE:
|
if FLASK_AVAILABLE:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
@@ -678,6 +679,54 @@ def migrate_task_system(db_path):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_system_events(db_path):
|
||||||
|
"""Create system_event table for activity logging."""
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if system_event table exists
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='system_event'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
print("Creating system_event table...")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE system_event (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
event_type VARCHAR(50) NOT NULL,
|
||||||
|
event_category VARCHAR(30) NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
severity VARCHAR(20) DEFAULT 'info',
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INTEGER,
|
||||||
|
company_id INTEGER,
|
||||||
|
event_metadata TEXT,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent TEXT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES user (id),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES company (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Add an initial system event if Flask is available
|
||||||
|
if FLASK_AVAILABLE:
|
||||||
|
# We'll add the initial event after the table is created
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO system_event (event_type, event_category, description, severity, timestamp)
|
||||||
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
""", ('system_migration', 'system', 'SystemEvent table created and initialized', 'info'))
|
||||||
|
print("Added initial system event")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("System events migration completed successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during system events migration: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def migrate_data():
|
def migrate_data():
|
||||||
"""Handle data migration with Flask app context."""
|
"""Handle data migration with Flask app context."""
|
||||||
if not FLASK_AVAILABLE:
|
if not FLASK_AVAILABLE:
|
||||||
@@ -847,6 +896,8 @@ def main():
|
|||||||
help='Run only company model migration')
|
help='Run only company model migration')
|
||||||
parser.add_argument('--basic', '-b', action='store_true',
|
parser.add_argument('--basic', '-b', action='store_true',
|
||||||
help='Run only basic table migrations')
|
help='Run only basic table migrations')
|
||||||
|
parser.add_argument('--system-events', '-s', action='store_true',
|
||||||
|
help='Run only system events migration')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -876,6 +927,9 @@ def main():
|
|||||||
elif args.basic:
|
elif args.basic:
|
||||||
run_basic_migrations(db_path)
|
run_basic_migrations(db_path)
|
||||||
|
|
||||||
|
elif args.system_events:
|
||||||
|
migrate_system_events(db_path)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Default: run all migrations
|
# Default: run all migrations
|
||||||
run_all_migrations(db_path)
|
run_all_migrations(db_path)
|
||||||
|
|||||||
109
models.py
109
models.py
@@ -613,3 +613,112 @@ class Announcement(db.Model):
|
|||||||
"""Get all active announcements visible to a specific user"""
|
"""Get all active announcements visible to a specific user"""
|
||||||
announcements = Announcement.query.filter_by(is_active=True).all()
|
announcements = Announcement.query.filter_by(is_active=True).all()
|
||||||
return [ann for ann in announcements if ann.is_visible_to_user(user)]
|
return [ann for ann in announcements if ann.is_visible_to_user(user)]
|
||||||
|
|
||||||
|
# System Event model for logging system activities
|
||||||
|
class SystemEvent(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
event_type = db.Column(db.String(50), nullable=False) # e.g., 'login', 'logout', 'user_created', 'system_error'
|
||||||
|
event_category = db.Column(db.String(30), nullable=False) # e.g., 'auth', 'user_management', 'system', 'error'
|
||||||
|
description = db.Column(db.Text, nullable=False)
|
||||||
|
severity = db.Column(db.String(20), default='info') # 'info', 'warning', 'error', 'critical'
|
||||||
|
timestamp = db.Column(db.DateTime, default=datetime.now, nullable=False)
|
||||||
|
|
||||||
|
# Optional associations
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=True)
|
||||||
|
|
||||||
|
# Additional metadata (JSON string)
|
||||||
|
event_metadata = db.Column(db.Text, nullable=True) # Store additional event data as JSON
|
||||||
|
|
||||||
|
# IP address and user agent for security tracking
|
||||||
|
ip_address = db.Column(db.String(45), nullable=True) # IPv6 compatible
|
||||||
|
user_agent = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = db.relationship('User', backref='system_events')
|
||||||
|
company = db.relationship('Company', backref='system_events')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<SystemEvent {self.event_type}: {self.description[:50]}>'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_event(event_type, description, event_category='system', severity='info',
|
||||||
|
user_id=None, company_id=None, event_metadata=None, ip_address=None, user_agent=None):
|
||||||
|
"""Helper method to log system events"""
|
||||||
|
event = SystemEvent(
|
||||||
|
event_type=event_type,
|
||||||
|
event_category=event_category,
|
||||||
|
description=description,
|
||||||
|
severity=severity,
|
||||||
|
user_id=user_id,
|
||||||
|
company_id=company_id,
|
||||||
|
event_metadata=event_metadata,
|
||||||
|
ip_address=ip_address,
|
||||||
|
user_agent=user_agent
|
||||||
|
)
|
||||||
|
db.session.add(event)
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
# Log to application logger if DB logging fails
|
||||||
|
import logging
|
||||||
|
logging.error(f"Failed to log system event: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_recent_events(days=7, limit=100):
|
||||||
|
"""Get recent system events from the last N days"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
since = datetime.now() - timedelta(days=days)
|
||||||
|
return SystemEvent.query.filter(
|
||||||
|
SystemEvent.timestamp >= since
|
||||||
|
).order_by(SystemEvent.timestamp.desc()).limit(limit).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_events_by_severity(severity, days=7, limit=50):
|
||||||
|
"""Get events by severity level"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
since = datetime.now() - timedelta(days=days)
|
||||||
|
return SystemEvent.query.filter(
|
||||||
|
SystemEvent.timestamp >= since,
|
||||||
|
SystemEvent.severity == severity
|
||||||
|
).order_by(SystemEvent.timestamp.desc()).limit(limit).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_system_health_summary():
|
||||||
|
"""Get a summary of system health based on recent events"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
last_24h = now - timedelta(hours=24)
|
||||||
|
last_week = now - timedelta(days=7)
|
||||||
|
|
||||||
|
# Count events by severity in last 24h
|
||||||
|
recent_errors = SystemEvent.query.filter(
|
||||||
|
SystemEvent.timestamp >= last_24h,
|
||||||
|
SystemEvent.severity.in_(['error', 'critical'])
|
||||||
|
).count()
|
||||||
|
|
||||||
|
recent_warnings = SystemEvent.query.filter(
|
||||||
|
SystemEvent.timestamp >= last_24h,
|
||||||
|
SystemEvent.severity == 'warning'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Count total events in last week
|
||||||
|
weekly_events = SystemEvent.query.filter(
|
||||||
|
SystemEvent.timestamp >= last_week
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Get most recent error
|
||||||
|
last_error = SystemEvent.query.filter(
|
||||||
|
SystemEvent.severity.in_(['error', 'critical'])
|
||||||
|
).order_by(SystemEvent.timestamp.desc()).first()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'errors_24h': recent_errors,
|
||||||
|
'warnings_24h': recent_warnings,
|
||||||
|
'total_events_week': weekly_events,
|
||||||
|
'last_error': last_error,
|
||||||
|
'health_status': 'healthy' if recent_errors == 0 else 'issues' if recent_errors < 5 else 'critical'
|
||||||
|
}
|
||||||
@@ -183,6 +183,9 @@
|
|||||||
<a href="{{ url_for('system_admin_settings') }}" class="btn btn-primary">
|
<a href="{{ url_for('system_admin_settings') }}" class="btn btn-primary">
|
||||||
⚙️ System Settings
|
⚙️ System Settings
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('system_admin_health') }}" class="btn btn-warning">
|
||||||
|
🏥 System Health
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
579
templates/system_admin_health.html
Normal file
579
templates/system_admin_health.html
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="header-section">
|
||||||
|
<h1>🏥 System Health Check</h1>
|
||||||
|
<p class="subtitle">System diagnostics and event monitoring</p>
|
||||||
|
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Health Status -->
|
||||||
|
<div class="health-status-section">
|
||||||
|
<h2>🔍 System Status</h2>
|
||||||
|
<div class="health-cards">
|
||||||
|
<div class="health-card {% if health_summary.health_status == 'healthy' %}healthy{% elif health_summary.health_status == 'issues' %}warning{% else %}critical{% endif %}">
|
||||||
|
<div class="health-icon">
|
||||||
|
{% if health_summary.health_status == 'healthy' %}
|
||||||
|
✅
|
||||||
|
{% elif health_summary.health_status == 'issues' %}
|
||||||
|
⚠️
|
||||||
|
{% else %}
|
||||||
|
❌
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="health-info">
|
||||||
|
<h3>Overall Health</h3>
|
||||||
|
<p class="health-status">{{ health_summary.health_status|title }}</p>
|
||||||
|
<small>
|
||||||
|
{% if health_summary.health_status == 'healthy' %}
|
||||||
|
All systems running normally
|
||||||
|
{% elif health_summary.health_status == 'issues' %}
|
||||||
|
Minor issues detected
|
||||||
|
{% else %}
|
||||||
|
Critical issues require attention
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="health-card {% if db_healthy %}healthy{% else %}critical{% endif %}">
|
||||||
|
<div class="health-icon">
|
||||||
|
{% if db_healthy %}✅{% else %}❌{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="health-info">
|
||||||
|
<h3>Database</h3>
|
||||||
|
<p class="health-status">{% if db_healthy %}Connected{% else %}Error{% endif %}</p>
|
||||||
|
<small>
|
||||||
|
{% if db_healthy %}
|
||||||
|
PostgreSQL connection active
|
||||||
|
{% else %}
|
||||||
|
{{ db_error }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="health-card info">
|
||||||
|
<div class="health-icon">⏱️</div>
|
||||||
|
<div class="health-info">
|
||||||
|
<h3>Uptime</h3>
|
||||||
|
<p class="health-status">{{ uptime_duration.days }}d {{ uptime_duration.seconds//3600 }}h {{ (uptime_duration.seconds//60)%60 }}m</p>
|
||||||
|
<small>Since first recorded event</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="stats-section">
|
||||||
|
<h2>📊 Event Statistics</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card {% if health_summary.errors_24h > 0 %}error{% endif %}">
|
||||||
|
<h3>{{ health_summary.errors_24h }}</h3>
|
||||||
|
<p>Errors (24h)</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card {% if health_summary.warnings_24h > 0 %}warning{% endif %}">
|
||||||
|
<h3>{{ health_summary.warnings_24h }}</h3>
|
||||||
|
<p>Warnings (24h)</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ today_events }}</h3>
|
||||||
|
<p>Events Today</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ health_summary.total_events_week }}</h3>
|
||||||
|
<p>Events This Week</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Errors -->
|
||||||
|
{% if errors %}
|
||||||
|
<div class="events-section error-section">
|
||||||
|
<h2>🚨 Recent Errors</h2>
|
||||||
|
<div class="events-list">
|
||||||
|
{% for error in errors %}
|
||||||
|
<div class="event-item error">
|
||||||
|
<div class="event-header">
|
||||||
|
<span class="event-type">{{ error.event_type }}</span>
|
||||||
|
<span class="event-time">{{ error.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-description">{{ error.description }}</div>
|
||||||
|
{% if error.user %}
|
||||||
|
<div class="event-meta">User: {{ error.user.username }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if error.company %}
|
||||||
|
<div class="event-meta">Company: {{ error.company.name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Recent Warnings -->
|
||||||
|
{% if warnings %}
|
||||||
|
<div class="events-section warning-section">
|
||||||
|
<h2>⚠️ Recent Warnings</h2>
|
||||||
|
<div class="events-list">
|
||||||
|
{% for warning in warnings %}
|
||||||
|
<div class="event-item warning">
|
||||||
|
<div class="event-header">
|
||||||
|
<span class="event-type">{{ warning.event_type }}</span>
|
||||||
|
<span class="event-time">{{ warning.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-description">{{ warning.description }}</div>
|
||||||
|
{% if warning.user %}
|
||||||
|
<div class="event-meta">User: {{ warning.user.username }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if warning.company %}
|
||||||
|
<div class="event-meta">Company: {{ warning.company.name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- System Event Log -->
|
||||||
|
<div class="events-section">
|
||||||
|
<h2>📋 System Event Log (Last 7 Days)</h2>
|
||||||
|
<div class="events-controls">
|
||||||
|
<button class="filter-btn active" data-filter="all">All Events</button>
|
||||||
|
<button class="filter-btn" data-filter="auth">Authentication</button>
|
||||||
|
<button class="filter-btn" data-filter="user_management">User Management</button>
|
||||||
|
<button class="filter-btn" data-filter="system">System</button>
|
||||||
|
<button class="filter-btn" data-filter="error">Errors</button>
|
||||||
|
</div>
|
||||||
|
<div class="events-list" id="eventsList">
|
||||||
|
{% for event in recent_events %}
|
||||||
|
<div class="event-item {{ event.severity }} {{ event.event_category }}" data-category="{{ event.event_category }}" data-severity="{{ event.severity }}">
|
||||||
|
<div class="event-header">
|
||||||
|
<span class="event-type">{{ event.event_type }}</span>
|
||||||
|
<span class="event-category-badge">{{ event.event_category }}</span>
|
||||||
|
<span class="event-time">{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-description">{{ event.description }}</div>
|
||||||
|
{% if event.user %}
|
||||||
|
<div class="event-meta">User: {{ event.user.username }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.company %}
|
||||||
|
<div class="event-meta">Company: {{ event.company.name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.ip_address %}
|
||||||
|
<div class="event-meta">IP: {{ event.ip_address }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Error Details -->
|
||||||
|
{% if health_summary.last_error %}
|
||||||
|
<div class="events-section error-section">
|
||||||
|
<h2>🔍 Last Critical Error</h2>
|
||||||
|
<div class="last-error-details">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="error-header">
|
||||||
|
<h3>{{ health_summary.last_error.event_type }}</h3>
|
||||||
|
<span class="error-time">{{ health_summary.last_error.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="error-description">{{ health_summary.last_error.description }}</div>
|
||||||
|
{% if health_summary.last_error.event_metadata %}
|
||||||
|
<div class="error-metadata">
|
||||||
|
<strong>Additional Details:</strong>
|
||||||
|
<pre>{{ health_summary.last_error.event_metadata }}</pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border-left: 4px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.healthy {
|
||||||
|
border-left-color: #28a745;
|
||||||
|
background: #f8fff9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.warning {
|
||||||
|
border-left-color: #ffc107;
|
||||||
|
background: #fffdf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.critical {
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
background: #fff8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.info {
|
||||||
|
border-left-color: #17a2b8;
|
||||||
|
background: #f8fcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-info h3 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.healthy .health-status {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.warning .health-status {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.critical .health-status {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-card.info .health-status {
|
||||||
|
color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-info small {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.error {
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
background: #fff8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning {
|
||||||
|
border-left-color: #ffc107;
|
||||||
|
background: #fffdf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.error h3 {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning h3 {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-section {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background: white;
|
||||||
|
color: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #adb5bd;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item.error {
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
background: #fff8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item.warning {
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
background: #fffdf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item.critical {
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-type {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-category-badge {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-description {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section {
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-section {
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-error-details {
|
||||||
|
background: #fff8f8;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-time {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-description {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-metadata {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-metadata pre {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.health-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Event filtering functionality
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const filterButtons = document.querySelectorAll('.filter-btn');
|
||||||
|
const eventItems = document.querySelectorAll('.event-item');
|
||||||
|
|
||||||
|
filterButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const filter = this.dataset.filter;
|
||||||
|
|
||||||
|
// Update active button
|
||||||
|
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
// Filter events
|
||||||
|
eventItems.forEach(item => {
|
||||||
|
if (filter === 'all') {
|
||||||
|
item.style.display = 'block';
|
||||||
|
} else if (filter === 'error') {
|
||||||
|
item.style.display = item.classList.contains('error') || item.classList.contains('critical') ? 'block' : 'none';
|
||||||
|
} else {
|
||||||
|
item.style.display = item.dataset.category === filter ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<h4>Database</h4>
|
<h4>Database</h4>
|
||||||
<p>SQLite</p>
|
<p>PostgreSQL</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<h4>System Admin Access</h4>
|
<h4>System Admin Access</h4>
|
||||||
@@ -155,9 +155,9 @@
|
|||||||
<p>Run diagnostic checks to identify potential issues with the system.</p>
|
<p>Run diagnostic checks to identify potential issues with the system.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="danger-actions">
|
<div class="danger-actions">
|
||||||
<button class="btn btn-warning" onclick="alert('System health check functionality would be implemented here.')">
|
<a href="{{ url_for('system_admin_health') }}" class="btn btn-warning">
|
||||||
Run Health Check
|
Run Health Check
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user