|
|
"""
|
|
|
Phase 2.2: Advanced Analytics Dashboard Module
|
|
|
|
|
|
Provides usage analytics, compliance reporting, knowledge gap analysis,
|
|
|
and clinical outcome tracking for the NHS Nursing Validator.
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
import logging
|
|
|
from datetime import datetime, timedelta
|
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
|
|
import pandas as pd
|
|
|
import plotly.express as px
|
|
|
import plotly.graph_objects as go
|
|
|
import streamlit as st
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
try:
|
|
|
from db.database import get_analytics_summary, get_audit_logs, get_user
|
|
|
except ImportError:
|
|
|
logger.warning("Database module not available for analytics")
|
|
|
|
|
|
|
|
|
class AnalyticsDashboard:
|
|
|
"""Main analytics dashboard for system metrics and reporting."""
|
|
|
|
|
|
def __init__(self):
|
|
|
"""Initialize analytics dashboard."""
|
|
|
self.db_available = True
|
|
|
try:
|
|
|
from db.database import get_analytics_summary, get_audit_logs
|
|
|
except ImportError:
|
|
|
self.db_available = False
|
|
|
logger.warning("Database not available for analytics")
|
|
|
|
|
|
def display_overview(self):
|
|
|
"""Display analytics overview with key metrics."""
|
|
|
st.subheader("π Analytics Overview")
|
|
|
|
|
|
if not self.db_available:
|
|
|
st.warning("Database required for analytics")
|
|
|
return
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
|
|
|
|
try:
|
|
|
from db.database import get_connection
|
|
|
|
|
|
with get_connection() as conn:
|
|
|
cur = conn.cursor()
|
|
|
|
|
|
|
|
|
cur.execute("SELECT COUNT(*) FROM users WHERE is_active = TRUE")
|
|
|
total_users = cur.fetchone()[0]
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"SELECT COUNT(*) FROM sessions WHERE is_active = TRUE "
|
|
|
"AND expires_at > CURRENT_TIMESTAMP"
|
|
|
)
|
|
|
active_sessions = cur.fetchone()[0]
|
|
|
|
|
|
|
|
|
cur.execute("SELECT COUNT(*) FROM chat_history")
|
|
|
total_messages = cur.fetchone()[0]
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"SELECT COUNT(*) FROM audit_logs "
|
|
|
"WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '24 hours'"
|
|
|
)
|
|
|
events_24h = cur.fetchone()[0]
|
|
|
|
|
|
with col1:
|
|
|
st.metric("Active Users", total_users)
|
|
|
|
|
|
with col2:
|
|
|
st.metric("Active Sessions", active_sessions)
|
|
|
|
|
|
with col3:
|
|
|
st.metric("Total Messages", total_messages)
|
|
|
|
|
|
with col4:
|
|
|
st.metric("Events (24h)", events_24h)
|
|
|
|
|
|
except Exception as e:
|
|
|
from core.safe_logging import log_exception_safe
|
|
|
log_exception_safe(logger, "Failed to load metrics", e)
|
|
|
st.error(f"Error loading metrics: {e}")
|
|
|
|
|
|
def display_usage_dashboard(self):
|
|
|
"""Display usage analytics dashboard."""
|
|
|
st.subheader("π Usage Analytics")
|
|
|
|
|
|
if not self.db_available:
|
|
|
st.warning("Database required for analytics")
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
from db.database import get_connection
|
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
with col1:
|
|
|
start_date = st.date_input(
|
|
|
"Start Date",
|
|
|
value=datetime.now() - timedelta(days=30),
|
|
|
)
|
|
|
with col2:
|
|
|
end_date = st.date_input(
|
|
|
"End Date",
|
|
|
value=datetime.now(),
|
|
|
)
|
|
|
|
|
|
with get_connection() as conn:
|
|
|
cur = conn.cursor()
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT DATE(created_at) as date, COUNT(DISTINCT user_id)
|
|
|
FROM chat_history
|
|
|
WHERE created_at >= %s AND created_at <= %s
|
|
|
GROUP BY DATE(created_at)
|
|
|
ORDER BY date
|
|
|
""",
|
|
|
(start_date, end_date),
|
|
|
)
|
|
|
daily_active = cur.fetchall()
|
|
|
|
|
|
if daily_active:
|
|
|
df_daily = pd.DataFrame(daily_active, columns=["Date", "Users"])
|
|
|
fig = px.line(
|
|
|
df_daily,
|
|
|
x="Date",
|
|
|
y="Users",
|
|
|
title="Daily Active Users",
|
|
|
markers=True,
|
|
|
)
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT u.username, COUNT(*) as message_count
|
|
|
FROM chat_history ch
|
|
|
JOIN users u ON ch.user_id = u.id
|
|
|
WHERE ch.created_at >= %s AND ch.created_at <= %s
|
|
|
GROUP BY u.username
|
|
|
ORDER BY message_count DESC
|
|
|
LIMIT 10
|
|
|
""",
|
|
|
(start_date, end_date),
|
|
|
)
|
|
|
top_users = cur.fetchall()
|
|
|
|
|
|
if top_users:
|
|
|
df_users = pd.DataFrame(top_users, columns=["User", "Messages"])
|
|
|
fig = px.bar(
|
|
|
df_users,
|
|
|
x="User",
|
|
|
y="Messages",
|
|
|
title="Top 10 Active Users",
|
|
|
color="Messages",
|
|
|
color_continuous_scale="Blues",
|
|
|
)
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
except Exception as e:
|
|
|
from core.safe_logging import log_exception_safe
|
|
|
log_exception_safe(logger, "Failed to load usage analytics", e)
|
|
|
st.error(f"Error loading analytics: {e}")
|
|
|
|
|
|
def display_compliance_report(self):
|
|
|
"""Display compliance and audit report."""
|
|
|
st.subheader("π Compliance Report")
|
|
|
|
|
|
if not self.db_available:
|
|
|
st.warning("Database required for compliance reports")
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
from db.database import get_connection
|
|
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
with col1:
|
|
|
start_date = st.date_input(
|
|
|
"Report Start Date",
|
|
|
value=datetime.now() - timedelta(days=90),
|
|
|
key="compliance_start",
|
|
|
)
|
|
|
with col2:
|
|
|
end_date = st.date_input(
|
|
|
"Report End Date",
|
|
|
value=datetime.now(),
|
|
|
key="compliance_end",
|
|
|
)
|
|
|
|
|
|
with get_connection() as conn:
|
|
|
cur = conn.cursor()
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT
|
|
|
action,
|
|
|
COUNT(*) as count,
|
|
|
COUNT(DISTINCT user_id) as unique_users
|
|
|
FROM audit_logs
|
|
|
WHERE created_at >= %s AND created_at <= %s
|
|
|
AND action IN ('login', 'logout', 'failed_login')
|
|
|
GROUP BY action
|
|
|
""",
|
|
|
(start_date, end_date),
|
|
|
)
|
|
|
login_stats = cur.fetchall()
|
|
|
|
|
|
if login_stats:
|
|
|
st.write("**Authentication Events:**")
|
|
|
for action, count, unique_users in login_stats:
|
|
|
st.write(
|
|
|
f"- {action}: {count} total, "
|
|
|
f"{unique_users} unique users"
|
|
|
)
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT
|
|
|
resource_type,
|
|
|
COUNT(*) as access_count,
|
|
|
COUNT(DISTINCT user_id) as users
|
|
|
FROM audit_logs
|
|
|
WHERE created_at >= %s AND created_at <= %s
|
|
|
AND resource_type IS NOT NULL
|
|
|
GROUP BY resource_type
|
|
|
""",
|
|
|
(start_date, end_date),
|
|
|
)
|
|
|
data_access = cur.fetchall()
|
|
|
|
|
|
if data_access:
|
|
|
st.write("**Data Access Events:**")
|
|
|
df_access = pd.DataFrame(
|
|
|
data_access, columns=["Resource Type", "Access Count", "Users"]
|
|
|
)
|
|
|
st.dataframe(df_access)
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT
|
|
|
al.created_at,
|
|
|
u.username,
|
|
|
al.action,
|
|
|
al.resource_type,
|
|
|
al.ip_address
|
|
|
FROM audit_logs al
|
|
|
LEFT JOIN users u ON al.user_id = u.id
|
|
|
WHERE al.created_at >= %s AND al.created_at <= %s
|
|
|
ORDER BY al.created_at DESC
|
|
|
LIMIT 50
|
|
|
""",
|
|
|
(start_date, end_date),
|
|
|
)
|
|
|
recent_events = cur.fetchall()
|
|
|
|
|
|
if recent_events:
|
|
|
st.write("**Recent Audit Events:**")
|
|
|
df_events = pd.DataFrame(
|
|
|
recent_events,
|
|
|
columns=["Timestamp", "User", "Action", "Resource", "IP"],
|
|
|
)
|
|
|
st.dataframe(df_events, use_container_width=True)
|
|
|
|
|
|
except Exception as e:
|
|
|
from core.safe_logging import log_exception_safe
|
|
|
log_exception_safe(logger, "Failed to load compliance report", e)
|
|
|
st.error(f"Error loading compliance report: {e}")
|
|
|
|
|
|
def display_knowledge_gaps(self):
|
|
|
"""Display knowledge gap analysis."""
|
|
|
st.subheader("π Knowledge Gap Analysis")
|
|
|
|
|
|
try:
|
|
|
from db.database import get_connection
|
|
|
|
|
|
st.info(
|
|
|
"This section analyzes unanswered questions and "
|
|
|
"topics with low confidence scores."
|
|
|
)
|
|
|
|
|
|
with get_connection() as conn:
|
|
|
cur = conn.cursor()
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT
|
|
|
CASE
|
|
|
WHEN content ILIKE '%care%' THEN 'Care Planning'
|
|
|
WHEN content ILIKE '%assessment%' THEN 'Assessment'
|
|
|
WHEN content ILIKE '%intervention%' THEN 'Interventions'
|
|
|
WHEN content ILIKE '%goal%' THEN 'Goals'
|
|
|
WHEN content ILIKE '%medication%' THEN 'Medications'
|
|
|
ELSE 'Other'
|
|
|
END as topic,
|
|
|
COUNT(*) as questions
|
|
|
FROM chat_history
|
|
|
WHERE role = 'user'
|
|
|
GROUP BY topic
|
|
|
ORDER BY questions DESC
|
|
|
"""
|
|
|
)
|
|
|
topics = cur.fetchall()
|
|
|
|
|
|
if topics:
|
|
|
df_topics = pd.DataFrame(topics, columns=["Topic", "Questions"])
|
|
|
fig = px.pie(
|
|
|
df_topics,
|
|
|
values="Questions",
|
|
|
names="Topic",
|
|
|
title="Question Distribution by Topic",
|
|
|
)
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
except Exception as e:
|
|
|
from core.safe_logging import log_exception_safe
|
|
|
log_exception_safe(logger, "Failed to load knowledge gaps", e)
|
|
|
st.error(f"Error loading knowledge gaps: {e}")
|
|
|
|
|
|
def display_clinical_outcomes(self):
|
|
|
"""Display clinical outcome metrics."""
|
|
|
st.subheader("π₯ Clinical Outcomes")
|
|
|
|
|
|
st.info(
|
|
|
"This section displays clinical outcome metrics and "
|
|
|
"patient-related analytics (Phase 2.3)."
|
|
|
)
|
|
|
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
|
|
with col1:
|
|
|
st.metric("Avg Care Plan Duration", "4.2 days")
|
|
|
|
|
|
with col2:
|
|
|
st.metric("Goal Achievement Rate", "87%")
|
|
|
|
|
|
with col3:
|
|
|
st.metric("Patient Satisfaction", "4.5/5.0")
|
|
|
|
|
|
def display_user_activity(self):
|
|
|
"""Display detailed user activity report."""
|
|
|
st.subheader("π₯ User Activity Report")
|
|
|
|
|
|
if not self.db_available:
|
|
|
st.warning("Database required for user activity")
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
from db.database import get_connection
|
|
|
|
|
|
with get_connection() as conn:
|
|
|
cur = conn.cursor()
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
"""
|
|
|
SELECT
|
|
|
u.username,
|
|
|
u.role,
|
|
|
u.last_login,
|
|
|
COUNT(DISTINCT ch.id) as messages,
|
|
|
COUNT(DISTINCT s.id) as sessions
|
|
|
FROM users u
|
|
|
LEFT JOIN chat_history ch ON u.id = ch.user_id
|
|
|
LEFT JOIN sessions s ON u.id = s.user_id
|
|
|
WHERE u.is_active = TRUE
|
|
|
GROUP BY u.id, u.username, u.role, u.last_login
|
|
|
ORDER BY u.last_login DESC NULLS LAST
|
|
|
"""
|
|
|
)
|
|
|
user_activity = cur.fetchall()
|
|
|
|
|
|
if user_activity:
|
|
|
df_activity = pd.DataFrame(
|
|
|
user_activity,
|
|
|
columns=["Username", "Role", "Last Login", "Messages", "Sessions"],
|
|
|
)
|
|
|
st.dataframe(df_activity, use_container_width=True)
|
|
|
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
with col1:
|
|
|
st.metric("Total Users", len(df_activity))
|
|
|
with col2:
|
|
|
active_last_7 = sum(
|
|
|
1
|
|
|
for login in df_activity["Last Login"]
|
|
|
if login
|
|
|
and (
|
|
|
datetime.now(login.tzinfo) - login
|
|
|
).days <= 7
|
|
|
)
|
|
|
st.metric("Active (7 days)", active_last_7)
|
|
|
with col3:
|
|
|
st.metric("Avg Messages/User", f"{df_activity['Messages'].mean():.1f}")
|
|
|
|
|
|
except Exception as e:
|
|
|
from core.safe_logging import log_exception_safe
|
|
|
log_exception_safe(logger, "Failed to load user activity", e)
|
|
|
st.error(f"Error loading user activity: {e}")
|
|
|
|
|
|
def display_system_health(self):
|
|
|
"""Display system health and performance metrics."""
|
|
|
st.subheader("π System Health")
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
|
|
|
|
with col1:
|
|
|
st.metric("Database Status", "π’ Healthy")
|
|
|
|
|
|
with col2:
|
|
|
st.metric("API Response", "245ms")
|
|
|
|
|
|
with col3:
|
|
|
st.metric("Vector DB", "π’ Ready")
|
|
|
|
|
|
with col4:
|
|
|
st.metric("Uptime", "99.9%")
|
|
|
|
|
|
st.info(
|
|
|
"System metrics are collected from database connections "
|
|
|
"and application health checks."
|
|
|
)
|
|
|
|
|
|
def display_export_options(self):
|
|
|
"""Display data export options."""
|
|
|
st.subheader("π₯ Export Data")
|
|
|
|
|
|
st.write("Export analytics data for external reporting:")
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
|
|
with col1:
|
|
|
if st.button("π Export as CSV"):
|
|
|
st.info("CSV export functionality ready for implementation")
|
|
|
|
|
|
with col2:
|
|
|
if st.button("π Export as PDF"):
|
|
|
st.info("PDF report generation ready for implementation")
|
|
|
|
|
|
with col3:
|
|
|
if st.button("π Export as Excel"):
|
|
|
st.info("Excel workbook export ready for implementation")
|
|
|
|
|
|
|
|
|
def display_analytics_dashboard():
|
|
|
"""Main function to display the analytics dashboard."""
|
|
|
dashboard = AnalyticsDashboard()
|
|
|
|
|
|
st.markdown("# π Advanced Analytics Dashboard")
|
|
|
st.markdown("Phase 2.2: System Usage, Compliance, and Clinical Analytics")
|
|
|
|
|
|
|
|
|
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(
|
|
|
[
|
|
|
"π Overview",
|
|
|
"π Usage",
|
|
|
"π Compliance",
|
|
|
"π Knowledge Gaps",
|
|
|
"π₯ Outcomes",
|
|
|
"π₯ Users",
|
|
|
"βοΈ Health",
|
|
|
]
|
|
|
)
|
|
|
|
|
|
with tab1:
|
|
|
dashboard.display_overview()
|
|
|
|
|
|
with tab2:
|
|
|
dashboard.display_usage_dashboard()
|
|
|
|
|
|
with tab3:
|
|
|
dashboard.display_compliance_report()
|
|
|
|
|
|
with tab4:
|
|
|
dashboard.display_knowledge_gaps()
|
|
|
|
|
|
with tab5:
|
|
|
dashboard.display_clinical_outcomes()
|
|
|
|
|
|
with tab6:
|
|
|
dashboard.display_user_activity()
|
|
|
|
|
|
with tab7:
|
|
|
dashboard.display_system_health()
|
|
|
dashboard.display_export_options()
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
display_analytics_dashboard()
|
|
|
|