Skip to content

Commit 58a00b0

Browse files
Update for Streamlit deployment
1 parent 0accb14 commit 58a00b0

File tree

7 files changed

+421
-0
lines changed

7 files changed

+421
-0
lines changed

.github/workflows/deploy.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Streamlit Deployment
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
deploy:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout repo
14+
uses: actions/checkout@v3
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.11'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
26+
- name: Test Streamlit run
27+
run: |
28+
streamlit run app.py --server.headless true &
29+
sleep 10
30+
pkill -f streamlit
31+
32+
# Deployment step is handled automatically when repo is connected to Streamlit Community Cloud
33+
34+

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
venv/
2+
__pycache__/
3+
*.pyc
4+
monitor.db

app.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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+

data/monitoring.db

20 KB
Binary file not shown.

database.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import sqlite3
2+
from datetime import datetime
3+
4+
conn = sqlite3.connect("data/monitoring.db", check_same_thread=False)
5+
c = conn.cursor()
6+
7+
# Create tables
8+
c.execute("""
9+
CREATE TABLE IF NOT EXISTS urls (
10+
id INTEGER PRIMARY KEY AUTOINCREMENT,
11+
url TEXT UNIQUE,
12+
status TEXT,
13+
monitoring INTEGER DEFAULT 1
14+
)
15+
""")
16+
17+
c.execute("""
18+
CREATE TABLE IF NOT EXISTS history (
19+
id INTEGER PRIMARY KEY AUTOINCREMENT,
20+
url_id INTEGER,
21+
timestamp DATETIME,
22+
status TEXT,
23+
response_time REAL,
24+
FOREIGN KEY(url_id) REFERENCES urls(id)
25+
)
26+
""")
27+
conn.commit()
28+
29+
def add_url(url):
30+
try:
31+
c.execute("INSERT INTO urls (url, status) VALUES (?, ?)", (url, 'UNKNOWN'))
32+
conn.commit()
33+
return True
34+
except:
35+
return False
36+
37+
# database.py
38+
def delete_url(url_id):
39+
c.execute("DELETE FROM urls WHERE id=?", (url_id,))
40+
c.execute("DELETE FROM history WHERE url_id=?", (url_id,))
41+
conn.commit()
42+
43+
44+
def get_urls():
45+
c.execute("SELECT * FROM urls")
46+
return c.fetchall()
47+
48+
def update_status(url_id, status, response_time):
49+
c.execute("UPDATE urls SET status=? WHERE id=?", (status, url_id))
50+
c.execute("INSERT INTO history (url_id, timestamp, status, response_time) VALUES (?, ?, ?, ?)",
51+
(url_id, datetime.now(), status, response_time))
52+
conn.commit()
53+
54+
def stop_monitoring(url_id):
55+
c.execute("UPDATE urls SET monitoring=0 WHERE id=?", (url_id,))
56+
conn.commit()
57+
58+
def start_monitoring(url_id):
59+
c.execute("UPDATE urls SET monitoring=1 WHERE id=?", (url_id,))
60+
conn.commit()
61+
62+
def get_history(url_id):
63+
c.execute("SELECT timestamp, status, response_time FROM history WHERE url_id=? ORDER BY timestamp DESC", (url_id,))
64+
return c.fetchall()

0 commit comments

Comments
 (0)