|
| 1 | +import streamlit as st |
| 2 | +from database import add_url, get_urls, delete_url, stop_monitoring, start_monitoring, get_history |
| 3 | +from monitor import monitor_all |
| 4 | +import asyncio |
| 5 | +import pandas as pd |
| 6 | +import plotly.express as px |
| 7 | +import time |
| 8 | + |
| 9 | +# --- Page Config --- |
| 10 | +st.set_page_config(page_title="Website Monitoring Dashboard", layout="wide", initial_sidebar_state="expanded") |
| 11 | + |
| 12 | +# --- Modernized CSS Styling --- |
| 13 | +st.markdown(""" |
| 14 | + <style> |
| 15 | + /* Main App Styling */ |
| 16 | + .stApp { |
| 17 | + background-color: #0F1116; |
| 18 | + color: #FAFAFA; |
| 19 | + } |
| 20 | + /* Sidebar Styling */ |
| 21 | + [data-testid="stSidebar"] { |
| 22 | + background-color: #1A1C23; |
| 23 | + } |
| 24 | + [data-testid="stSidebar"] h2 { |
| 25 | + color: #00BFFF; /* Bright blue for sidebar headers */ |
| 26 | + } |
| 27 | + |
| 28 | + /* Main Title */ |
| 29 | + h1 { |
| 30 | + color: #00BFFF; |
| 31 | + text-align: center; |
| 32 | + font-weight: bold; |
| 33 | + } |
| 34 | + h2, h3 { |
| 35 | + color: #E0E0E0; |
| 36 | + } |
| 37 | + |
| 38 | + /* Status Card Styling */ |
| 39 | + .status-card { |
| 40 | + background-color: #1A1C23; |
| 41 | + padding: 20px; |
| 42 | + border-radius: 12px; |
| 43 | + border: 1px solid #2C2F38; |
| 44 | + text-align: center; |
| 45 | + transition: all 0.3s ease-in-out; |
| 46 | + height: 200px; /* Fixed height for uniform cards */ |
| 47 | + display: flex; |
| 48 | + flex-direction: column; |
| 49 | + justify-content: space-between; |
| 50 | + } |
| 51 | + .status-card:hover { |
| 52 | + transform: translateY(-5px); |
| 53 | + border-color: #00BFFF; |
| 54 | + box-shadow: 0 4px 20px rgba(0, 191, 255, 0.15); |
| 55 | + } |
| 56 | + |
| 57 | + /* Status Indicator Dot */ |
| 58 | + .status-dot { |
| 59 | + height: 20px; |
| 60 | + width: 20px; |
| 61 | + border-radius: 50%; |
| 62 | + display: inline-block; |
| 63 | + margin-bottom: 10px; |
| 64 | + animation: pulse 2s infinite; |
| 65 | + } |
| 66 | + |
| 67 | + /* URL Text Styling - CORRECTED for truncation */ |
| 68 | + .url-text { |
| 69 | + font-weight: bold; |
| 70 | + white-space: nowrap; /* Don't wrap */ |
| 71 | + overflow: hidden; /* Hide overflow */ |
| 72 | + text-overflow: ellipsis; /* Add ... */ |
| 73 | + font-size: 14px; |
| 74 | + } |
| 75 | + .status-text { |
| 76 | + font-size: 12px; |
| 77 | + color: #A0A0A0; |
| 78 | + } |
| 79 | + |
| 80 | + /* Button Styling inside Card */ |
| 81 | + .stButton>button { |
| 82 | + width: 100%; |
| 83 | + border-radius: 8px; |
| 84 | + border: 1px solid #00BFFF; |
| 85 | + background-color: transparent; |
| 86 | + color: #00BFFF; |
| 87 | + } |
| 88 | + .stButton>button:hover { |
| 89 | + background-color: #00BFFF; |
| 90 | + color: #1A1C23; |
| 91 | + border-color: #00BFFF; |
| 92 | + } |
| 93 | + |
| 94 | + /* Keyframe for Pulse Animation */ |
| 95 | + @keyframes pulse { |
| 96 | + 0% { box-shadow: 0 0 0 0 rgba(0, 191, 255, 0.4); } |
| 97 | + 70% { box-shadow: 0 0 0 10px rgba(0, 191, 255, 0); } |
| 98 | + 100% { box-shadow: 0 0 0 0 rgba(0, 191, 255, 0); } |
| 99 | + } |
| 100 | + </style> |
| 101 | +""", unsafe_allow_html=True) |
| 102 | + |
| 103 | + |
| 104 | +# --- Main Dashboard Title --- |
| 105 | +st.markdown("<h1>🌐 Website Monitoring Dashboard</h1>", unsafe_allow_html=True) |
| 106 | +st.markdown("---") |
| 107 | + |
| 108 | + |
| 109 | +# --- Session State Initialization --- |
| 110 | +if "urls" not in st.session_state: |
| 111 | + st.session_state.urls = get_urls() |
| 112 | + |
| 113 | +# --- Sidebar Controls --- |
| 114 | +with st.sidebar: |
| 115 | + st.header("🔧 Manage URLs") |
| 116 | + |
| 117 | + with st.form("add_url_form"): |
| 118 | + new_url = st.text_input("Add New URL", placeholder="https://example.com") |
| 119 | + submitted = st.form_submit_button("Add URL") |
| 120 | + if submitted and new_url.strip() != '': |
| 121 | + if add_url(new_url): |
| 122 | + st.toast(f"✅ URL '{new_url}' added!", icon="🎉") |
| 123 | + st.session_state.urls = get_urls() |
| 124 | + st.rerun() |
| 125 | + else: |
| 126 | + st.toast("⚠️ URL already exists!", icon="❗") |
| 127 | + |
| 128 | + urls_list = [u[1] for u in st.session_state.urls] |
| 129 | + if urls_list: |
| 130 | + st.header("⚙️ Global Controls") |
| 131 | + |
| 132 | + url_to_delete = st.selectbox("Select URL to Delete", options=urls_list) |
| 133 | + if st.button("Delete Selected URL", type="primary"): |
| 134 | + url_id = [u[0] for u in st.session_state.urls if u[1] == url_to_delete][0] |
| 135 | + stop_monitoring(url_id) |
| 136 | + delete_url(url_id) |
| 137 | + st.toast(f"🗑️ URL '{url_to_delete}' deleted!", icon="🗑️") |
| 138 | + st.session_state.urls = get_urls() |
| 139 | + st.rerun() |
| 140 | + |
| 141 | + if st.button("Start All Monitoring"): |
| 142 | + for row in st.session_state.urls: |
| 143 | + start_monitoring(row[0]) |
| 144 | + st.session_state.urls = get_urls() |
| 145 | + st.toast("🚀 Started monitoring all URLs!", icon="🚀") |
| 146 | + st.rerun() |
| 147 | + |
| 148 | + if st.button("Stop All Monitoring"): |
| 149 | + for row in st.session_state.urls: |
| 150 | + stop_monitoring(row[0]) |
| 151 | + st.session_state.urls = get_urls() |
| 152 | + st.toast("🛑 Stopped monitoring all URLs.", icon="🛑") |
| 153 | + st.rerun() |
| 154 | + |
| 155 | + |
| 156 | +# --- Auto-refresh Monitoring --- |
| 157 | +refresh_interval = 10 # seconds |
| 158 | +if "last_run" not in st.session_state: |
| 159 | + st.session_state.last_run = 0 |
| 160 | + |
| 161 | +if time.time() - st.session_state.last_run > refresh_interval: |
| 162 | + with st.spinner("Checking website statuses..."): |
| 163 | + asyncio.run(monitor_all()) |
| 164 | + st.session_state.last_run = time.time() |
| 165 | + st.session_state.urls = get_urls() |
| 166 | + # st.rerun() # <-- THIS LINE IS REMOVED TO FIX THE DOUBLE-CLICK ISSUE |
| 167 | + |
| 168 | + |
| 169 | +# --- Live Status Grid --- |
| 170 | +st.subheader("🖥️ Live Status") |
| 171 | +urls_data = st.session_state.urls |
| 172 | + |
| 173 | +if not urls_data: |
| 174 | + st.info("No URLs added yet. Add a URL from the sidebar to start monitoring.") |
| 175 | +else: |
| 176 | + cols = st.columns(4) |
| 177 | + for i, (url_id, url, status, is_monitoring) in enumerate(urls_data): |
| 178 | + with cols[i % 4]: |
| 179 | + # Logic for better visual feedback |
| 180 | + if is_monitoring: |
| 181 | + status_color = "#28a745" if status == 'UP' else "#ff4b4b" if status == 'DOWN' else "#6c757d" |
| 182 | + display_status = status |
| 183 | + else: |
| 184 | + status_color = "#6c757d" # Grey color for paused state |
| 185 | + display_status = "PAUSED" # Display "PAUSED" text |
| 186 | + |
| 187 | + history = get_history(url_id) |
| 188 | + last_resp_time = round(history[0][2], 2) if history else "N/A" |
| 189 | + tooltip = f"URL: {url}\nResponse Time: {last_resp_time} ms" |
| 190 | + |
| 191 | + st.markdown(f""" |
| 192 | + <div class="status-card" title="{tooltip}"> |
| 193 | + <div> |
| 194 | + <span class="status-dot" style="background-color: {status_color};"></span> |
| 195 | + <p class="url-text">{url}</p> |
| 196 | + <p class="status-text">{display_status}</p> |
| 197 | + </div> |
| 198 | + </div> |
| 199 | + """, unsafe_allow_html=True) |
| 200 | + |
| 201 | + # Button logic |
| 202 | + if is_monitoring: |
| 203 | + if st.button("Stop", key=f"stop_{url_id}"): |
| 204 | + stop_monitoring(url_id) |
| 205 | + st.session_state.urls = get_urls() # ✅ The fix |
| 206 | + st.toast(f"Monitoring stopped for {url}", icon="🛑") |
| 207 | + st.rerun() |
| 208 | + else: |
| 209 | + if st.button("Start", key=f"start_{url_id}"): |
| 210 | + start_monitoring(url_id) |
| 211 | + st.session_state.urls = get_urls() # ✅ The fix |
| 212 | + st.toast(f"Monitoring started for {url}", icon="🚀") |
| 213 | + st.rerun() |
| 214 | + |
| 215 | +st.markdown("---") |
| 216 | + |
| 217 | +# --- Analytics Dashboard with Tabs --- |
| 218 | +st.subheader("📊 Analytics Dashboard") |
| 219 | +agg_tab, ind_tab = st.tabs(["Aggregate Analytics", "Individual URL Analytics"]) |
| 220 | + |
| 221 | +with agg_tab: |
| 222 | + if not urls_data: |
| 223 | + st.warning("No data to display. Please add URLs.") |
| 224 | + else: |
| 225 | + agg_list = [] |
| 226 | + for url_id, url, _, _ in urls_data: |
| 227 | + hist = get_history(url_id) |
| 228 | + if hist: |
| 229 | + hist_df = pd.DataFrame(hist, columns=['Timestamp', 'Status', 'Response Time']) |
| 230 | + total = len(hist_df) |
| 231 | + up = len(hist_df[hist_df['Status'] == 'UP']) |
| 232 | + down = total - up |
| 233 | + avg_resp = round(hist_df['Response Time'].mean(), 2) |
| 234 | + uptime_percent = round((up / total) * 100, 2) if total > 0 else 0 |
| 235 | + agg_list.append([url, total, up, down, avg_resp, uptime_percent]) |
| 236 | + |
| 237 | + if agg_list: |
| 238 | + agg_df = pd.DataFrame(agg_list, columns=['URL', 'Total Checks', 'UP', 'DOWN', 'Avg Response (ms)', 'Uptime %']) |
| 239 | + st.dataframe(agg_df, width='stretch') |
| 240 | + |
| 241 | + fig_all = px.bar(agg_df, x='URL', y='Uptime %', color='Uptime %', |
| 242 | + color_continuous_scale=px.colors.sequential.Tealgrn, title="Uptime % of All URLs") |
| 243 | + fig_all.update_layout(plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font_color='white') |
| 244 | + st.plotly_chart(fig_all, width='stretch') |
| 245 | + else: |
| 246 | + st.info("No monitoring history available yet.") |
| 247 | + |
| 248 | +with ind_tab: |
| 249 | + if not urls_data: |
| 250 | + st.warning("No data to display. Please add URLs.") |
| 251 | + else: |
| 252 | + status_df = pd.DataFrame(urls_data, columns=['ID', 'URL', 'Status', 'Monitoring']) |
| 253 | + selected_url = st.selectbox("Select URL for Detailed Analysis", status_df['URL']) |
| 254 | + |
| 255 | + if selected_url: |
| 256 | + url_id = status_df[status_df['URL'] == selected_url]['ID'].values[0] |
| 257 | + history = get_history(url_id) |
| 258 | + |
| 259 | + if history: |
| 260 | + hist_df = pd.DataFrame(history, columns=['Timestamp', 'Status', 'Response Time']) |
| 261 | + hist_df['Timestamp'] = pd.to_datetime(hist_df['Timestamp']) |
| 262 | + |
| 263 | + total_checks = len(hist_df) |
| 264 | + up_checks = len(hist_df[hist_df['Status'] == 'UP']) |
| 265 | + uptime_percent = round((up_checks / total_checks) * 100, 2) if total_checks > 0 else 0 |
| 266 | + avg_response = round(hist_df['Response Time'].mean(), 2) |
| 267 | + |
| 268 | + kpi1, kpi2 = st.columns(2) |
| 269 | + kpi1.metric(label="✅ Uptime Percentage", value=f"{uptime_percent}%") |
| 270 | + kpi2.metric(label="⚡ Average Response Time", value=f"{avg_response} ms") |
| 271 | + |
| 272 | + fig1 = px.line(hist_df, x='Timestamp', y='Response Time', title=f"{selected_url} Response Time History", |
| 273 | + markers=True, template="plotly_dark") |
| 274 | + fig1.update_layout(plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)') |
| 275 | + st.plotly_chart(fig1, width='stretch') |
| 276 | + |
| 277 | + csv = hist_df.to_csv(index=False).encode('utf-8') |
| 278 | + st.download_button( |
| 279 | + label="⬇️ Download History as CSV", |
| 280 | + data=csv, |
| 281 | + file_name=f"{selected_url}_history.csv", |
| 282 | + mime='text/csv' |
| 283 | + ) |
| 284 | + else: |
| 285 | + st.info("No history available for this URL yet.") |
| 286 | + |
| 287 | + |
| 288 | + |
| 289 | + |
0 commit comments