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 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 (
|
||||
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
||||
format_table_data, format_graph_data, format_team_data
|
||||
@@ -481,15 +481,52 @@ def login():
|
||||
session['username'] = user.username
|
||||
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')
|
||||
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')
|
||||
|
||||
return render_template('login.html', title='Login')
|
||||
|
||||
@app.route('/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()
|
||||
flash('You have been logged out.', 'info')
|
||||
return redirect(url_for('login'))
|
||||
@@ -1195,9 +1232,33 @@ def verify_2fa():
|
||||
session['username'] = user.username
|
||||
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')
|
||||
return redirect(url_for('home'))
|
||||
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')
|
||||
|
||||
return render_template('verify_2fa.html', title='Two-Factor Authentication')
|
||||
@@ -2047,6 +2108,72 @@ def system_admin_settings():
|
||||
total_users=total_users,
|
||||
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')
|
||||
@system_admin_required
|
||||
def system_admin_announcements():
|
||||
|
||||
@@ -15,7 +15,7 @@ try:
|
||||
from app import app, db
|
||||
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
||||
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
|
||||
FLASK_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -71,6 +71,7 @@ def run_all_migrations(db_path=None):
|
||||
migrate_to_company_model(db_path)
|
||||
migrate_work_config_data(db_path)
|
||||
migrate_task_system(db_path)
|
||||
migrate_system_events(db_path)
|
||||
|
||||
if FLASK_AVAILABLE:
|
||||
with app.app_context():
|
||||
@@ -678,6 +679,54 @@ def migrate_task_system(db_path):
|
||||
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():
|
||||
"""Handle data migration with Flask app context."""
|
||||
if not FLASK_AVAILABLE:
|
||||
@@ -847,6 +896,8 @@ def main():
|
||||
help='Run only company model migration')
|
||||
parser.add_argument('--basic', '-b', action='store_true',
|
||||
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()
|
||||
|
||||
@@ -876,6 +927,9 @@ def main():
|
||||
elif args.basic:
|
||||
run_basic_migrations(db_path)
|
||||
|
||||
elif args.system_events:
|
||||
migrate_system_events(db_path)
|
||||
|
||||
else:
|
||||
# Default: run all migrations
|
||||
run_all_migrations(db_path)
|
||||
|
||||
111
models.py
111
models.py
@@ -612,4 +612,113 @@ class Announcement(db.Model):
|
||||
def get_active_announcements_for_user(user):
|
||||
"""Get all active announcements visible to a specific user"""
|
||||
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">
|
||||
⚙️ System Settings
|
||||
</a>
|
||||
<a href="{{ url_for('system_admin_health') }}" class="btn btn-warning">
|
||||
🏥 System Health
|
||||
</a>
|
||||
</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 class="info-card">
|
||||
<h4>Database</h4>
|
||||
<p>SQLite</p>
|
||||
<p>PostgreSQL</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h4>System Admin Access</h4>
|
||||
@@ -155,9 +155,9 @@
|
||||
<p>Run diagnostic checks to identify potential issues with the system.</p>
|
||||
</div>
|
||||
<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
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user