"""
HemoStat Dashboard Main Application
Real-time Streamlit dashboard for monitoring HemoStat container health system.
Displays live container metrics, active issues, remediation history, and event timeline
with auto-refresh every 5 seconds.
"""
import os
from datetime import datetime
from zoneinfo import ZoneInfo
import streamlit as st
from dotenv import load_dotenv
from agents.logger import HemoStatLogger
from dashboard.components import (
render_active_issues,
render_health_grid,
render_metrics_cards,
render_remediation_history,
render_timeline,
)
from dashboard.data_fetcher import (
get_active_containers,
get_all_events,
get_events_by_type,
get_false_alarm_count,
get_redis_client,
get_remediation_stats,
)
# Load environment variables
load_dotenv()
# Initialize logger
logger = HemoStatLogger.get_logger("dashboard")
# Page configuration
st.set_page_config(
page_title="HemoStat Dashboard",
layout="wide",
initial_sidebar_state="expanded",
)
# Custom CSS styling - clinical/medical + DevOps infrastructure theme
st.markdown("""
<style>
/* Primary colors: Medical blue + DevOps infrastructure orange */
:root {
--primary-blue: #0066cc;
--secondary-blue: #003d99;
--devops-orange: #ff6b35;
--accent-green: #00aa44;
--accent-red: #cc0000;
--accent-yellow: #ffaa00;
--neutral-light: #f5f7fa;
--neutral-dark: #1a1a1a;
}
/* Main container styling - subtle infrastructure grid pattern */
.main {
background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
background-image:
linear-gradient(0deg, transparent 24%, rgba(0, 102, 204, 0.02) 25%, rgba(0, 102, 204, 0.02) 26%, transparent 27%, transparent 74%, rgba(0, 102, 204, 0.02) 75%, rgba(0, 102, 204, 0.02) 76%, transparent 77%, transparent),
linear-gradient(90deg, transparent 24%, rgba(0, 102, 204, 0.02) 25%, rgba(0, 102, 204, 0.02) 26%, transparent 27%, transparent 74%, rgba(0, 102, 204, 0.02) 75%, rgba(0, 102, 204, 0.02) 76%, transparent 77%, transparent);
background-size: 50px 50px;
}
/* Sidebar styling - DevOps infrastructure gradient */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #003d99 0%, #0066cc 50%, #ff6b35 100%);
color: white;
}
[data-testid="stSidebar"] * {
color: white !important;
}
/* Headers - clean and professional with DevOps accent */
h1, h2, h3 {
color: #003d99;
font-weight: 600;
letter-spacing: -0.5px;
}
h1 {
border-bottom: 3px solid #ff6b35;
padding-bottom: 12px;
margin-bottom: 24px;
}
h2 {
border-left: 4px solid #ff6b35;
padding-left: 12px;
}
/* Metric cards - elevated with DevOps accent */
[data-testid="metric-container"] {
background: white;
border-radius: 12px;
border-left: 4px solid #ff6b35;
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.1);
padding: 20px;
transition: all 0.3s ease;
}
[data-testid="metric-container"]:hover {
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
transform: translateY(-2px);
border-left-color: #0066cc;
}
/* Data frames - infrastructure style */
[data-testid="stDataFrame"] {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 107, 53, 0.1);
}
/* Buttons - DevOps orange with blue accent */
button {
background: linear-gradient(135deg, #ff6b35 0%, #ff5722 100%) !important;
color: white !important;
border: none !important;
border-radius: 6px !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
button:hover {
background: linear-gradient(135deg, #ff5722 0%, #ff6b35 100%) !important;
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4) !important;
}
/* Containers and cards */
[data-testid="stContainer"] {
border-radius: 8px;
}
/* Expanders - DevOps infrastructure style */
[data-testid="stExpander"] {
border: 1px solid rgba(255, 107, 53, 0.2);
border-radius: 8px;
background: white;
}
[data-testid="stExpander"] summary {
color: #003d99;
font-weight: 500;
}
/* Tabs - modern with DevOps accent */
[data-testid="stTabs"] [role="tablist"] {
border-bottom: 2px solid rgba(255, 107, 53, 0.2);
}
[data-testid="stTabs"] [role="tab"] {
color: #666;
font-weight: 500;
border-bottom: 3px solid transparent;
transition: all 0.2s ease;
}
[data-testid="stTabs"] [role="tab"][aria-selected="true"] {
color: #ff6b35;
border-bottom-color: #ff6b35;
}
/* Status indicators - operational colors */
.status-healthy {
color: #00aa44;
font-weight: 600;
}
.status-unhealthy {
color: #cc0000;
font-weight: 600;
}
.status-warning {
color: #ffaa00;
font-weight: 600;
}
.status-deploying {
color: #ff6b35;
font-weight: 600;
}
/* Chart containers - infrastructure monitoring style */
[data-testid="stPlotlyContainer"] {
border-radius: 8px;
background: white;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 107, 53, 0.1);
}
/* Dividers - DevOps themed */
hr {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255, 107, 53, 0.3), transparent);
margin: 24px 0;
}
/* Text styling */
p, span, label {
color: #333;
line-height: 1.6;
}
/* Info boxes - infrastructure alerts */
[data-testid="stAlert"] {
border-radius: 8px;
border-left: 4px solid #ff6b35;
}
/* Code blocks - terminal style */
code {
background: #1a1a1a;
color: #00aa44;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
</style>
""", unsafe_allow_html=True)
# Initialize session state
if "auto_refresh_enabled" not in st.session_state:
auto_refresh_env = os.getenv("DASHBOARD_AUTO_REFRESH", "true").lower() == "true"
st.session_state.auto_refresh_enabled = auto_refresh_env
if "refresh_interval" not in st.session_state:
st.session_state.refresh_interval = int(os.getenv("DASHBOARD_REFRESH_INTERVAL", 5))
if "max_events" not in st.session_state:
st.session_state.max_events = int(os.getenv("DASHBOARD_MAX_EVENTS", 1000))
if "last_refresh" not in st.session_state:
st.session_state.last_refresh = None
if "manual_refresh_trigger" not in st.session_state:
st.session_state.manual_refresh_trigger = 0
[docs]
def check_redis_connection() -> bool:
"""
Test Redis connection and return status.
Returns:
bool: True if Redis is connected, False otherwise
"""
try:
client = get_redis_client()
client.ping()
return True
except Exception as e:
logger.error(f"Redis connection failed: {e}")
return False
[docs]
def render_live_content() -> None:
"""
Render auto-refreshing dashboard content.
Uses st.fragment with dynamic run_every interval tied to session state.
Fetches data from Redis and renders all dashboard tabs.
Tabs are outside fragment to preserve selection across refreshes.
"""
if not st.session_state.auto_refresh_enabled:
st.info("Auto-refresh is disabled. Click 'Refresh Now' to update.")
return
# Fetch data with fragment for auto-refresh
@st.fragment(run_every=st.session_state.refresh_interval) # type: ignore[attr-defined]
def fetch_data() -> tuple:
eastern = ZoneInfo("America/New_York")
st.session_state.last_refresh = datetime.now(eastern)
try:
with st.spinner("Loading data from Redis..."):
all_events = get_all_events(limit=st.session_state.max_events)
remediation_events = get_events_by_type(
"remediation_complete", limit=st.session_state.max_events
)
false_alarm_count = get_false_alarm_count()
active_containers = len(get_active_containers())
remediation_stats = get_remediation_stats()
return all_events, remediation_events, false_alarm_count, active_containers, remediation_stats
except Exception as e:
logger.error(f"Error fetching dashboard data: {e}")
st.error(f"Error loading dashboard data: {e}")
return [], [], 0, 0, {"success_rate": 0.0, "total_remediations": 0, "false_alarms": 0}
# Fetch data (will auto-refresh)
all_events, remediation_events, false_alarm_count, active_containers, remediation_stats = fetch_data()
# Metrics section (outside fragment)
st.subheader("Key Metrics")
render_metrics_cards(remediation_stats, false_alarm_count, active_containers)
# Tabs for different views (outside fragment to preserve tab state)
tab1, tab2, tab3, tab4 = st.tabs(
["🏥 Health Grid", "⚠️ Active Issues", "📊 History", "📈 Timeline"]
)
with tab1:
st.subheader("Container Health Grid")
render_health_grid(all_events)
with tab2:
st.subheader("Active Issues")
render_active_issues(all_events)
with tab3:
st.subheader("Remediation History")
render_remediation_history(remediation_events)
with tab4:
st.subheader("Event Timeline")
render_timeline(all_events, max_events=st.session_state.max_events)
[docs]
def main() -> None:
"""
Main dashboard application entry point.
Initializes the dashboard, renders sidebar, header, content, and footer.
"""
logger.info("Dashboard started")
# Render sidebar
render_sidebar()
# Render header
render_header()
# Render main content
render_live_content()
# Render footer
render_footer()
if __name__ == "__main__":
main()