Add system health and system event logging.

This commit is contained in:
2025-07-04 08:46:06 +02:00
parent e31401a939
commit 52d3400728
6 changed files with 878 additions and 6 deletions

129
app.py
View File

@@ -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():

View File

@@ -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)

109
models.py
View File

@@ -613,3 +613,112 @@ class Announcement(db.Model):
"""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)]
# 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'
}

View File

@@ -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>

View 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 %}

View File

@@ -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>