1	"""
2	DEWA PR Tracker — Streamlit Dashboard
3	Run:  streamlit run dashboard.py
4	"""
5	import math
6	from datetime import datetime, timedelta
7
8	import pandas as pd
9	import plotly.express as px
10	import plotly.graph_objects as go
11	import streamlit as st
12
13	import database as db
14	from config import (
15	    COST_PER_PODCAST_OLD_AED,
16	    NOTEBOOKLM_MONTHLY_USD,
17	    USD_TO_AED,
18	    DASHBOARD_TITLE,
19	    PAGE_SIZE,
20	)
21
22	# ── Page setup ────────────────────────────────────────────────────────────────
23	st.set_page_config(
24	    page_title=DASHBOARD_TITLE,
25	    page_icon="📰",
26	    layout="wide",
27	    initial_sidebar_state="expanded",
28	)
29
30	# ── Custom CSS ────────────────────────────────────────────────────────────────
31	st.markdown("""
32
50	""", unsafe_allow_html=True)
51
52
53	# ── Data loading (cached 5 min) ───────────────────────────────────────────────
54	@st.cache_data(ttl=300)
55	def load_data():
56	    db.init_db()
57	    prs   = [dict(r) for r in db.get_all_press_releases()]
58	    pods  = [dict(r) for r in db.get_all_podcasts()]
59	    stats = db.get_stats()
60	    log   = [dict(r) for r in db.get_scrape_log()]
61	    return prs, pods, stats, log
62
63
64	prs, pods, stats, scrape_log = load_data()
65	df_pr  = pd.DataFrame(prs)
66	df_pod = pd.DataFrame(pods)
67
68	# Fix language field in podcasts (re-detect from title)
69	if not df_pod.empty and "title" in df_pod.columns:
70	    import re as _re
71	    df_pod["language"] = df_pod["title"].apply(
72	        lambda t: "ar" if _re.search(r'[\u0600-\u06FF]', str(t)) else "en"
73	    )
74
75	if not df_pr.empty:
76	    df_pr["date"] = pd.to_datetime(df_pr["date"], errors="coerce")
77	    df_pr["week"]  = df_pr["date"].dt.to_period("W").astype(str)
78	    df_pr["month"] = df_pr["date"].dt.to_period("M").astype(str)
79	    df_pr["year"]  = df_pr["date"].dt.year
80
81	if not df_pod.empty:
82	    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")
83	    df_pod["month"] = df_pod["date"].dt.to_period("M").astype(str)
84
85
86	# ── Sidebar ───────────────────────────────────────────────────────────────────
87	with st.sidebar:
88	    st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)
89	    st.title("Filters")
90
91	    # Date range
92	    if not df_pr.empty and df_pr["date"].notna().any():
93	        min_date = df_pr["date"].min().date()
94	        max_date = df_pr["date"].max().date()
95	    else:
96	        min_date = datetime(2020, 1, 1).date()
97	        max_date = datetime.today().date()
98
99	    date_from, date_to = st.date_input(
100	        "Date range",
101	        value=(min_date, max_date),
102	        min_value=min_date,
103	        max_value=max_date,
104	    )
105
106	    # Category filter
107	    categories = ["All"]
108	    if not df_pr.empty and "category" in df_pr.columns:
109	        cats = df_pr["category"].dropna().unique().tolist()
110	        categories += sorted(cats)
111	    selected_cat = st.selectbox("Category", categories)
112
113	    # Podcast filter
114	    has_podcast_filter = st.checkbox("Show only PRs with podcast", value=False)
115
116	    st.divider()
117	    if st.button("🔄 Refresh data", use_container_width=True):
118	        st.cache_data.clear()
119	        st.rerun()
120
121	    if st.button("▶ Run scraper now", use_container_width=True):
122	        with st.spinner("Scraping DEWA website…"):
123	            try:
124	                from scraper import run_scrape
125	                prs_n, pods_n = run_scrape()
126	                st.success(f"Done! {prs_n} PRs, {pods_n} podcasts saved.")
127	                st.cache_data.clear()
128	                st.rerun()
129	            except Exception as e:
130	                st.error(f"Scraper error: {e}")
131
132	    st.divider()
133	    st.caption(f"Last scraped: {stats.get('last_scraped', 'Never') or 'Never'}")
134
135
136	# ── Apply filters ─────────────────────────────────────────────────────────────
137	filtered = df_pr.copy()
138	if not filtered.empty:
139	    if filtered["date"].notna().any():
140	        filtered = filtered[
141	            (filtered["date"].dt.date >= date_from) &
142	            (filtered["date"].dt.date <= date_to)
143	        ]
144	    if selected_cat != "All":
145	        filtered = filtered[filtered["category"] == selected_cat]
146	    if has_podcast_filter:
147	        filtered = filtered[filtered["podcast_count"] > 0]
148
149
150	# ═══════════════════════════════════════════════════════════════════════════════
151	# HEADER
152	# ═══════════════════════════════════════════════════════════════════════════════
153	st.title(f"📰 {DASHBOARD_TITLE}")
154	st.markdown(f"*Tracking DEWA press releases and podcast ROI — {datetime.today().strftime('%d %B %Y')}*")
155	st.divider()
156
157
158	# ═══════════════════════════════════════════════════════════════════════════════
159	# ROW 1 — Key Metrics
160	# ═══════════════════════════════════════════════════════════════════════════════
161	col1, col2, col3, col4, col5 = st.columns(5)
162
163	total_prs  = len(filtered)
164	total_pods = stats.get("total_podcasts", 0)
165
166	today = datetime.today().date()
167	week_start  = today - timedelta(days=today.weekday())
168	month_start = today.replace(day=1)
169
170	if not filtered.empty and filtered["date"].notna().any():
171	    prs_today   = len(filtered[filtered["date"].dt.date == today])
172	    prs_week    = len(filtered[filtered["date"].dt.date >= week_start])
173	    prs_month   = len(filtered[filtered["date"].dt.date >= month_start])
174	else:
175	    prs_today = prs_week = prs_month = 0
176
177	with col1:
178	    st.metric("Total Press Releases", f"{total_prs:,}")
179	with col2:
180	    st.metric("Total Podcasts", f"{total_pods:,}")
181	with col3:
182	    st.metric("Published Today", prs_today)
183	with col4:
184	    st.metric("This Week", prs_week)
185	with col5:
186	    st.metric("This Month", prs_month)
187
188	st.divider()
189
190
191	# ═══════════════════════════════════════════════════════════════════════════════
192	# ROW 2 — ROI Calculator
193	# ═══════════════════════════════════════════════════════════════════════════════
194	st.subheader("💰 Podcast ROI Calculator")
195
196	notebooklm_aed = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED
197
198	# Calculate months since NotebookLM adoption (user can adjust in the UI)
199	c_left, c_right = st.columns([2, 1])
200	with c_right:
201	    months_using = st.number_input(
202	        "Months using NotebookLM", min_value=1, max_value=60, value=6
203	    )
204	    podcasts_in_period = st.number_input(
205	        "Podcasts produced in that period", min_value=0, max_value=500,
206	        value=total_pods
207	    )
208
209	old_cost  = podcasts_in_period * COST_PER_PODCAST_OLD_AED
210	new_cost  = months_using * notebooklm_aed
211	savings   = old_cost - new_cost
212	roi_pct   = (savings / old_cost * 100) if old_cost > 0 else 0
213
214	with c_left:
215	    r1, r2, r3 = st.columns(3)
216	    with r1:
217	        st.markdown("**Before — Traditional Production**")
218	        st.markdown(f"AED {old_cost:,.0f}", unsafe_allow_html=True)
219	        st.caption(f"{podcasts_in_period} podcast(s) × AED {COST_PER_PODCAST_OLD_AED:,} each")
220
221	    with r2:
222	        st.markdown("**After — NotebookLM**")
223	        st.markdown(f"AED {new_cost:,.0f}", unsafe_allow_html=True)
224	        st.caption(f"{months_using} month(s) × AED {notebooklm_aed:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD}/mo)")
225
226	    with r3:
227	        st.markdown("**Net Savings**")
228	        st.markdown(f"AED {savings:,.0f}", unsafe_allow_html=True)
229	        st.caption(f"ROI: {roi_pct:.1f}%")
230
231	    # ROI gauge
232	    fig_gauge = go.Figure(go.Indicator(
233	        mode="gauge+number+delta",
234	        value=roi_pct,
235	        title={"text": "ROI %"},
236	        delta={"reference": 0},
237	        gauge={
238	            "axis": {"range": [0, 100]},
239	            "bar": {"color": "#00c853"},
240	            "steps": [
241	                {"range": [0, 50], "color": "#1e2a3a"},
242	                {"range": [50, 80], "color": "#0d3b2e"},
243	                {"range": [80, 100], "color": "#0a4a35"},
244	            ],
245	            "threshold": {"line": {"color": "gold", "width": 4}, "value": 90},
246	        },
247	        number={"suffix": "%"},
248	    ))
249	    fig_gauge.update_layout(height=220, margin=dict(t=30, b=0, l=20, r=20),
250	                            paper_bgcolor="rgba(0,0,0,0)", font_color="white")
251	    st.plotly_chart(fig_gauge, use_container_width=True)
252
253	st.divider()
254
255
256	# ═══════════════════════════════════════════════════════════════════════════════
257	# ROW 3 — Publication Charts
258	# ═══════════════════════════════════════════════════════════════════════════════
259	st.subheader("📊 Publication Trends")
260	tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])
261
262	def make_bar(df_grouped, x, y, title, color="#00b4d8"):
263	    fig = px.bar(df_grouped, x=x, y=y, title=title,
264	                 color_discrete_sequence=[color])
265	    fig.update_layout(
266	        paper_bgcolor="rgba(0,0,0,0)",
267	        plot_bgcolor="rgba(0,0,0,0)",
268	        font_color="white",
269	        xaxis=dict(showgrid=False),
270	        yaxis=dict(gridcolor="#2a3a4a"),
271	        margin=dict(t=40, b=10),
272	    )
273	    return fig
274
275	with tab_daily:
276	    if not filtered.empty and filtered["date"].notna().any():
277	        daily = filtered.groupby(filtered["date"].dt.date).size().reset_index(name="count")
278	        daily.columns = ["date", "count"]
279	        st.plotly_chart(make_bar(daily, "date", "count", "Press Releases per Day"), use_container_width=True)
280	    else:
281	        st.info("No date data available yet. Run the scraper first.")
282
283	with tab_weekly:
284	    if not filtered.empty and "week" in filtered.columns and filtered["date"].notna().any():
285	        weekly = filtered.groupby("week").size().reset_index(name="count")
286	        st.plotly_chart(make_bar(weekly, "week", "count", "Press Releases per Week", "#4cc9f0"), use_container_width=True)
287	    else:
288	        st.info("No data yet.")
289
290	with tab_monthly:
291	    if not filtered.empty and "month" in filtered.columns and filtered["date"].notna().any():
292	        monthly = filtered.groupby("month").size().reset_index(name="count")
293	        st.plotly_chart(make_bar(monthly, "month", "count", "Press Releases per Month", "#7209b7"), use_container_width=True)
294	    else:
295	        st.info("No data yet.")
296
297	st.divider()
298
299
300	# ═══════════════════════════════════════════════════════════════════════════════
301	# ROW 4 — Press Releases Table
302	# ═══════════════════════════════════════════════════════════════════════════════
303	st.subheader("📋 Press Releases")
304
305	search = st.text_input("Search press releases", placeholder="Type to filter by title…")
306	if search:
307	    display = filtered[filtered["title"].str.contains(search, case=False, na=False)]
308	else:
309	    display = filtered.copy()
310
311	total_pages = max(1, math.ceil(len(display) / PAGE_SIZE))
312	page_num = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)
313	start = (page_num - 1) * PAGE_SIZE
314	end   = start + PAGE_SIZE
315	page_data = display.iloc[start:end]
316
317	st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases (page {page_num}/{total_pages})")
318
319	for _, row in page_data.iterrows():
320	    with st.container():
321	        tc, dc, pc = st.columns([5, 2, 1])
322	        with tc:
323	            url   = row.get("url", "")
324	            title = row.get("title", "—")
325	            st.markdown(f"**[{title}]({url})**" if url else f"**{title}**")
326	            if row.get("description"):
327	                st.caption(row["description"][:160] + "…" if len(str(row["description"])) > 160 else row["description"])
328	        with dc:
329	            d = row.get("date")
330	            st.markdown(f"`{d.strftime('%d %b %Y') if pd.notna(d) else '—'}`")
331	            if row.get("category"):
332	                st.caption(row["category"])
333	        with pc:
334	            pc_count = row.get("podcast_count", 0)
335	            if pc_count:
336	                st.markdown(f"🎙 {pc_count}")
337	            else:
338	                st.markdown("—")
339	    st.divider()
340
341
342	# ═══════════════════════════════════════════════════════════════════════════════
343	# ROW 5 — Podcasts Table
344	# ═══════════════════════════════════════════════════════════════════════════════
345	if not df_pod.empty:
346	    st.subheader("🎙 Podcasts")
347
348	    pod_lang_filter = st.radio(
349	        "Language", ["All", "English (EN)", "Arabic (AR)"],
350	        horizontal=True,
351	    )
352	    df_pod_show = df_pod.copy()
353	    if pod_lang_filter == "English (EN)":
354	        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
355	    elif pod_lang_filter == "Arabic (AR)":
356	        df_pod_show = df_pod_show[df_pod_show["language"] == "ar"]
357
358	    pod_search = st.text_input("Search podcasts", placeholder="Filter by title…")
359	    if pod_search:
360	        df_pod_show = df_pod_show[df_pod_show["title"].str.contains(pod_search, case=False, na=False)]
361
362	    st.caption(f"Showing {len(df_pod_show)} of {len(df_pod)} episodes")
363
364	    for _, pod in df_pod_show.head(100).iterrows():
365	        with st.expander(f"{pod.get('title', '—')}  |  {pod.get('date', '')}"):
366	            col_a, col_b = st.columns([3, 1])
367	            with col_a:
368	                if pod.get("pr_title"):
369	                    st.markdown(f"**Linked PR:** {pod['pr_title']}")
370	                if pod.get("url"):
371	                    st.markdown(f"[Open podcast page]({pod['url']})")
372	                if pod.get("audio_url"):
373	                    st.audio(pod["audio_url"])
374	            with col_b:
375	                if pod.get("duration"):
376	                    st.metric("Duration", pod["duration"])
377	                cost_saved = COST_PER_PODCAST_OLD_AED
378	                st.metric("Saved vs old cost", f"AED {cost_saved:,}")
379
380	st.divider()
381
382
383	# ═══════════════════════════════════════════════════════════════════════════════
384	# ROW 6 — Scrape Log
385	# ═══════════════════════════════════════════════════════════════════════════════
386	with st.expander("🔍 Scrape history"):
387	    if scrape_log:
388	        df_log = pd.DataFrame(scrape_log)
389	        st.dataframe(df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],
390	                     use_container_width=True)
391	    else:
392	        st.info("No scrape runs recorded yet.")
393
394	st.caption("DEWA PR Tracker · Built with Playwright + SQLite + Streamlit")
395

"""
DEWA PR Tracker — Streamlit Dashboard
Run:  streamlit run dashboard.py
"""
import math
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st

import database as db
from config import (
    COST_PER_PODCAST_OLD_AED,
    NOTEBOOKLM_MONTHLY_USD,
    USD_TO_AED,
    DASHBOARD_TITLE,
    PAGE_SIZE,
)

# ── Page setup ────────────────────────────────────────────────────────────────
st.set_page_config(
    page_title=DASHBOARD_TITLE,
    page_icon="📰",
    layout="wide",
    initial_sidebar_state="expanded",
)

# ── Custom CSS ────────────────────────────────────────────────────────────────
st.markdown("""

""", unsafe_allow_html=True)

# ── Data loading (cached 5 min) ───────────────────────────────────────────────
@st.cache_data(ttl=300)
def load_data():
    db.init_db()
    prs   = [dict(r) for r in db.get_all_press_releases()]
    pods  = [dict(r) for r in db.get_all_podcasts()]
    stats = db.get_stats()
    log   = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, stats, log

prs, pods, stats, scrape_log = load_data()
df_pr  = pd.DataFrame(prs)
df_pod = pd.DataFrame(pods)

# Fix language field in podcasts (re-detect from title)
if not df_pod.empty and "title" in df_pod.columns:
    import re as _re
    df_pod["language"] = df_pod["title"].apply(
        lambda t: "ar" if _re.search(r'[\u0600-\u06FF]', str(t)) else "en"
    )

if not df_pr.empty:
    df_pr["date"] = pd.to_datetime(df_pr["date"], errors="coerce")
    df_pr["week"]  = df_pr["date"].dt.to_period("W").astype(str)
    df_pr["month"] = df_pr["date"].dt.to_period("M").astype(str)
    df_pr["year"]  = df_pr["date"].dt.year

if not df_pod.empty:
    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")
    df_pod["month"] = df_pod["date"].dt.to_period("M").astype(str)

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)
    st.title("Filters")

    # Date range
    if not df_pr.empty and df_pr["date"].notna().any():
        min_date = df_pr["date"].min().date()
        max_date = df_pr["date"].max().date()
    else:
        min_date = datetime(2020, 1, 1).date()
        max_date = datetime.today().date()

    date_from, date_to = st.date_input(
        "Date range",
        value=(min_date, max_date),
        min_value=min_date,
        max_value=max_date,
    )

    # Category filter
    categories = ["All"]
    if not df_pr.empty and "category" in df_pr.columns:
        cats = df_pr["category"].dropna().unique().tolist()
        categories += sorted(cats)
    selected_cat = st.selectbox("Category", categories)

    # Podcast filter
    has_podcast_filter = st.checkbox("Show only PRs with podcast", value=False)

    st.divider()
    if st.button("🔄 Refresh data", use_container_width=True):
        st.cache_data.clear()
        st.rerun()

    if st.button("▶ Run scraper now", use_container_width=True):
        with st.spinner("Scraping DEWA website…"):
            try:
                from scraper import run_scrape
                prs_n, pods_n = run_scrape()
                st.success(f"Done! {prs_n} PRs, {pods_n} podcasts saved.")
                st.cache_data.clear()
                st.rerun()
            except Exception as e:
                st.error(f"Scraper error: {e}")

    st.divider()
    st.caption(f"Last scraped: {stats.get('last_scraped', 'Never') or 'Never'}")

# ── Apply filters ─────────────────────────────────────────────────────────────
filtered = df_pr.copy()
if not filtered.empty:
    if filtered["date"].notna().any():
        filtered = filtered[
            (filtered["date"].dt.date >= date_from) &
            (filtered["date"].dt.date <= date_to)
        ]
    if selected_cat != "All":
        filtered = filtered[filtered["category"] == selected_cat]
    if has_podcast_filter:
        filtered = filtered[filtered["podcast_count"] > 0]

# ═══════════════════════════════════════════════════════════════════════════════
# HEADER
# ═══════════════════════════════════════════════════════════════════════════════
st.title(f"📰 {DASHBOARD_TITLE}")
st.markdown(f"*Tracking DEWA press releases and podcast ROI — {datetime.today().strftime('%d %B %Y')}*")
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
col1, col2, col3, col4, col5 = st.columns(5)

total_prs  = len(filtered)
total_pods = stats.get("total_podcasts", 0)

today = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)

if not filtered.empty and filtered["date"].notna().any():
    prs_today   = len(filtered[filtered["date"].dt.date == today])
    prs_week    = len(filtered[filtered["date"].dt.date >= week_start])
    prs_month   = len(filtered[filtered["date"].dt.date >= month_start])
else:
    prs_today = prs_week = prs_month = 0

with col1:
    st.metric("Total Press Releases", f"{total_prs:,}")
with col2:
    st.metric("Total Podcasts", f"{total_pods:,}")
with col3:
    st.metric("Published Today", prs_today)
with col4:
    st.metric("This Week", prs_week)
with col5:
    st.metric("This Month", prs_month)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2 — ROI Calculator
# ═══════════════════════════════════════════════════════════════════════════════
st.subheader("💰 Podcast ROI Calculator")

notebooklm_aed = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED

# Calculate months since NotebookLM adoption (user can adjust in the UI)
c_left, c_right = st.columns([2, 1])
with c_right:
    months_using = st.number_input(
        "Months using NotebookLM", min_value=1, max_value=60, value=6
    )
    podcasts_in_period = st.number_input(
        "Podcasts produced in that period", min_value=0, max_value=500,
        value=total_pods
    )

old_cost  = podcasts_in_period * COST_PER_PODCAST_OLD_AED
new_cost  = months_using * notebooklm_aed
savings   = old_cost - new_cost
roi_pct   = (savings / old_cost * 100) if old_cost > 0 else 0

with c_left:
    r1, r2, r3 = st.columns(3)
    with r1:
        st.markdown("**Before — Traditional Production**")
        st.markdown(f"AED {old_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{podcasts_in_period} podcast(s) × AED {COST_PER_PODCAST_OLD_AED:,} each")

    with r2:
        st.markdown("**After — NotebookLM**")
        st.markdown(f"AED {new_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{months_using} month(s) × AED {notebooklm_aed:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD}/mo)")

    with r3:
        st.markdown("**Net Savings**")
        st.markdown(f"AED {savings:,.0f}", unsafe_allow_html=True)
        st.caption(f"ROI: {roi_pct:.1f}%")

    # ROI gauge
    fig_gauge = go.Figure(go.Indicator(
        mode="gauge+number+delta",
        value=roi_pct,
        title={"text": "ROI %"},
        delta={"reference": 0},
        gauge={
            "axis": {"range": [0, 100]},
            "bar": {"color": "#00c853"},
            "steps": [
                {"range": [0, 50], "color": "#1e2a3a"},
                {"range": [50, 80], "color": "#0d3b2e"},
                {"range": [80, 100], "color": "#0a4a35"},
            ],
            "threshold": {"line": {"color": "gold", "width": 4}, "value": 90},
        },
        number={"suffix": "%"},
    ))
    fig_gauge.update_layout(height=220, margin=dict(t=30, b=0, l=20, r=20),
                            paper_bgcolor="rgba(0,0,0,0)", font_color="white")
    st.plotly_chart(fig_gauge, use_container_width=True)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 3 — Publication Charts
# ═══════════════════════════════════════════════════════════════════════════════
st.subheader("📊 Publication Trends")
tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])

def make_bar(df_grouped, x, y, title, color="#00b4d8"):
    fig = px.bar(df_grouped, x=x, y=y, title=title,
                 color_discrete_sequence=[color])
    fig.update_layout(
        paper_bgcolor="rgba(0,0,0,0)",
        plot_bgcolor="rgba(0,0,0,0)",
        font_color="white",
        xaxis=dict(showgrid=False),
        yaxis=dict(gridcolor="#2a3a4a"),
        margin=dict(t=40, b=10),
    )
    return fig

with tab_daily:
    if not filtered.empty and filtered["date"].notna().any():
        daily = filtered.groupby(filtered["date"].dt.date).size().reset_index(name="count")
        daily.columns = ["date", "count"]
        st.plotly_chart(make_bar(daily, "date", "count", "Press Releases per Day"), use_container_width=True)
    else:
        st.info("No date data available yet. Run the scraper first.")

with tab_weekly:
    if not filtered.empty and "week" in filtered.columns and filtered["date"].notna().any():
        weekly = filtered.groupby("week").size().reset_index(name="count")
        st.plotly_chart(make_bar(weekly, "week", "count", "Press Releases per Week", "#4cc9f0"), use_container_width=True)
    else:
        st.info("No data yet.")

with tab_monthly:
    if not filtered.empty and "month" in filtered.columns and filtered["date"].notna().any():
        monthly = filtered.groupby("month").size().reset_index(name="count")
        st.plotly_chart(make_bar(monthly, "month", "count", "Press Releases per Month", "#7209b7"), use_container_width=True)
    else:
        st.info("No data yet.")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 4 — Press Releases Table
# ═══════════════════════════════════════════════════════════════════════════════
st.subheader("📋 Press Releases")

search = st.text_input("Search press releases", placeholder="Type to filter by title…")
if search:
    display = filtered[filtered["title"].str.contains(search, case=False, na=False)]
else:
    display = filtered.copy()

total_pages = max(1, math.ceil(len(display) / PAGE_SIZE))
page_num = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)
start = (page_num - 1) * PAGE_SIZE
end   = start + PAGE_SIZE
page_data = display.iloc[start:end]

st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases (page {page_num}/{total_pages})")

for _, row in page_data.iterrows():
    with st.container():
        tc, dc, pc = st.columns([5, 2, 1])
        with tc:
            url   = row.get("url", "")
            title = row.get("title", "—")
            st.markdown(f"**[{title}]({url})**" if url else f"**{title}**")
            if row.get("description"):
                st.caption(row["description"][:160] + "…" if len(str(row["description"])) > 160 else row["description"])
        with dc:
            d = row.get("date")
            st.markdown(f"`{d.strftime('%d %b %Y') if pd.notna(d) else '—'}`")
            if row.get("category"):
                st.caption(row["category"])
        with pc:
            pc_count = row.get("podcast_count", 0)
            if pc_count:
                st.markdown(f"🎙 {pc_count}")
            else:
                st.markdown("—")
    st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 5 — Podcasts Table
# ═══════════════════════════════════════════════════════════════════════════════
if not df_pod.empty:
    st.subheader("🎙 Podcasts")

    pod_lang_filter = st.radio(
        "Language", ["All", "English (EN)", "Arabic (AR)"],
        horizontal=True,
    )
    df_pod_show = df_pod.copy()
    if pod_lang_filter == "English (EN)":
        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
    elif pod_lang_filter == "Arabic (AR)":
        df_pod_show = df_pod_show[df_pod_show["language"] == "ar"]

    pod_search = st.text_input("Search podcasts", placeholder="Filter by title…")
    if pod_search:
        df_pod_show = df_pod_show[df_pod_show["title"].str.contains(pod_search, case=False, na=False)]

    st.caption(f"Showing {len(df_pod_show)} of {len(df_pod)} episodes")

    for _, pod in df_pod_show.head(100).iterrows():
        with st.expander(f"{pod.get('title', '—')}  |  {pod.get('date', '')}"):
            col_a, col_b = st.columns([3, 1])
            with col_a:
                if pod.get("pr_title"):
                    st.markdown(f"**Linked PR:** {pod['pr_title']}")
                if pod.get("url"):
                    st.markdown(f"[Open podcast page]({pod['url']})")
                if pod.get("audio_url"):
                    st.audio(pod["audio_url"])
            with col_b:
                if pod.get("duration"):
                    st.metric("Duration", pod["duration"])
                cost_saved = COST_PER_PODCAST_OLD_AED
                st.metric("Saved vs old cost", f"AED {cost_saved:,}")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 6 — Scrape Log
# ═══════════════════════════════════════════════════════════════════════════════
with st.expander("🔍 Scrape history"):
    if scrape_log:
        df_log = pd.DataFrame(scrape_log)
        st.dataframe(df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],
                     use_container_width=True)
    else:
        st.info("No scrape runs recorded yet.")

st.caption("DEWA PR Tracker · Built with Playwright + SQLite + Streamlit")


Two fixes needed: `max_value=500` crashes when total podcasts > 500, and replace all emojis with SVG icons. Full rewrite:


Write

"""
DEWA PR Tracker — Streamlit Dashboard
Run:  streamlit run dashboard.py
"""
import math
import re as _re
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st

import database as db
from config import (
    COST_PER_PODCAST_OLD_AED,
    NOTEBOOKLM_MONTHLY_USD,
    USD_TO_AED,
    DASHBOARD_TITLE,
    PAGE_SIZE,
)

# ── Page setup ────────────────────────────────────────────────────────────────
st.set_page_config(
    page_title=DASHBOARD_TITLE,
    page_icon="https://www.dewa.gov.ae/favicon.ico",
    layout="wide",
    initial_sidebar_state="expanded",
)

# ── SVG icon helpers ──────────────────────────────────────────────────────────
def svg_icon(path_d, size=20, color="#00b4d8", viewBox="0 0 24 24", stroke_width=2):
    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '
        f'viewBox="{viewBox}" fill="none" stroke="{color}" stroke-width="{stroke_width}" '
        f'stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:8px">'
        f'{path_d}</svg>'
    )

# Feather-style icon paths
ICO_NEWS      = svg_icon('<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 0-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/>')
ICO_MONEY     = svg_icon('<circle cx="12" cy="12" r="10"/><path d="M12 6v2m0 8v2m-4-7h8m-8 3h8"/>', color="#51cf66")
ICO_CHART     = svg_icon('<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', color="#4cc9f0")
ICO_LIST      = svg_icon('<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>')
ICO_MIC       = svg_icon('<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>', color="#f59e0b")
ICO_SEARCH    = svg_icon('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>', color="#94a3b8")
ICO_REFRESH   = svg_icon('<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>', color="#00b4d8")
ICO_PLAY      = svg_icon('<polygon points="5 3 19 12 5 21 5 3"/>', color="#00b4d8")
ICO_PODCAST   = svg_icon('<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>', size=16, color="#f59e0b")
ICO_LINK      = svg_icon('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>', size=16, color="#00b4d8")

def section_header(icon, text, size="h3"):
    tag = "h3" if size == "h3" else "h2"
    st.markdown(
        f'<{tag} style="display:flex;align-items:center;margin-bottom:0.5rem">'
        f'{icon}{text}</{tag}>',
        unsafe_allow_html=True,
    )

# ── Custom CSS ────────────────────────────────────────────────────────────────
st.markdown("""

""", unsafe_allow_html=True)

# ── Data loading (cached 5 min) ───────────────────────────────────────────────
@st.cache_data(ttl=300)
def load_data():
    db.init_db()
    prs   = [dict(r) for r in db.get_all_press_releases()]
    pods  = [dict(r) for r in db.get_all_podcasts()]
    stats = db.get_stats()
    log   = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, stats, log

prs, pods, stats, scrape_log = load_data()
df_pr  = pd.DataFrame(prs)
df_pod = pd.DataFrame(pods)

# Language detection from title
if not df_pod.empty and "title" in df_pod.columns:
    df_pod["language"] = df_pod["title"].apply(
        lambda t: "ar" if _re.search(r'[\u0600-\u06FF]', str(t)) else "en"
    )

if not df_pr.empty:
    df_pr["date"]  = pd.to_datetime(df_pr["date"], errors="coerce")
    df_pr["week"]  = df_pr["date"].dt.to_period("W").astype(str)
    df_pr["month"] = df_pr["date"].dt.to_period("M").astype(str)
    df_pr["year"]  = df_pr["date"].dt.year

if not df_pod.empty:
    df_pod["date"]  = pd.to_datetime(df_pod["date"], errors="coerce")
    df_pod["month"] = df_pod["date"].dt.to_period("M").astype(str)

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    try:
        st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)
    except Exception:
        st.markdown("**DEWA**")
    st.title("Filters")

    if not df_pr.empty and df_pr["date"].notna().any():
        min_date = df_pr["date"].min().date()
        max_date = df_pr["date"].max().date()
    else:
        min_date = datetime(2019, 1, 1).date()
        max_date = datetime.today().date()

    date_from, date_to = st.date_input(
        "Date range",
        value=(min_date, max_date),
        min_value=min_date,
        max_value=max_date,
    )

    categories = ["All"]
    if not df_pr.empty and "category" in df_pr.columns:
        cats = df_pr["category"].dropna().unique().tolist()
        categories += sorted(cats)
    selected_cat = st.selectbox("Category", categories)

    has_podcast_filter = st.checkbox("Show only PRs with podcast", value=False)

    st.divider()
    if st.button("Refresh data", use_container_width=True):
        st.cache_data.clear()
        st.rerun()

    if st.button("Run scraper now", use_container_width=True):
        with st.spinner("Scraping DEWA website…"):
            try:
                from scraper import run_scrape
                prs_n, pods_n = run_scrape()
                st.success(f"Done — {prs_n} PRs, {pods_n} podcasts saved.")
                st.cache_data.clear()
                st.rerun()
            except Exception as e:
                st.error(f"Scraper error: {e}")

    st.divider()
    st.caption(f"Last scraped: {stats.get('last_scraped', 'Never') or 'Never'}")

# ── Apply filters ─────────────────────────────────────────────────────────────
filtered = df_pr.copy()
if not filtered.empty:
    if filtered["date"].notna().any():
        filtered = filtered[
            (filtered["date"].dt.date >= date_from) &
            (filtered["date"].dt.date <= date_to)
        ]
    if selected_cat != "All":
        filtered = filtered[filtered["category"] == selected_cat]
    if has_podcast_filter:
        filtered = filtered[filtered["podcast_count"] > 0]

# ═══════════════════════════════════════════════════════════════════════════════
# HEADER
# ═══════════════════════════════════════════════════════════════════════════════
st.markdown(
    f'
'
    f'{svg_icon("""<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 0-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/>""", size=32, color="#00b4d8")}'
    f'{DASHBOARD_TITLE}
',
    unsafe_allow_html=True,
)
st.markdown(f'
Tracking DEWA press releases and podcast ROI — {datetime.today().strftime("%d %B %Y")}
', unsafe_allow_html=True)
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
col1, col2, col3, col4, col5 = st.columns(5)

total_prs  = len(filtered)
total_pods = stats.get("total_podcasts", 0)

today       = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)

if not filtered.empty and filtered["date"].notna().any():
    prs_today = len(filtered[filtered["date"].dt.date == today])
    prs_week  = len(filtered[filtered["date"].dt.date >= week_start])
    prs_month = len(filtered[filtered["date"].dt.date >= month_start])
else:
    prs_today = prs_week = prs_month = 0

with col1:
    st.metric("Total Press Releases", f"{total_prs:,}")
with col2:
    st.metric("Total Podcasts", f"{total_pods:,}")
with col3:
    st.metric("Published Today", prs_today)
with col4:
    st.metric("This Week", prs_week)
with col5:
    st.metric("This Month", prs_month)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2 — ROI Calculator
# ═══════════════════════════════════════════════════════════════════════════════
section_header(ICO_MONEY, "Podcast ROI Calculator")

notebooklm_aed  = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED
max_pod_value   = max(int(total_pods), 1000)   # never less than 1000 to avoid crash

c_left, c_right = st.columns([2, 1])
with c_right:
    months_using = st.number_input(
        "Months using NotebookLM",
        min_value=1, max_value=120, value=6,
    )
    podcasts_in_period = st.number_input(
        "Podcasts produced in that period",
        min_value=0,
        max_value=max_pod_value,
        value=min(int(total_pods), max_pod_value),
    )

old_cost = podcasts_in_period * COST_PER_PODCAST_OLD_AED
new_cost = months_using * notebooklm_aed
savings  = old_cost - new_cost
roi_pct  = (savings / old_cost * 100) if old_cost > 0 else 0

with c_left:
    r1, r2, r3 = st.columns(3)
    with r1:
        st.markdown("**Before — Traditional Production**")
        st.markdown(f"AED {old_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{podcasts_in_period} podcast(s) × AED {COST_PER_PODCAST_OLD_AED:,} each")

    with r2:
        st.markdown("**After — NotebookLM**")
        st.markdown(f"AED {new_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{months_using} month(s) × AED {notebooklm_aed:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD}/mo)")

    with r3:
        st.markdown("**Net Savings**")
        st.markdown(f"AED {savings:,.0f}", unsafe_allow_html=True)
        st.caption(f"ROI: {roi_pct:.1f}%")

    fig_gauge = go.Figure(go.Indicator(
        mode="gauge+number+delta",
        value=roi_pct,
        title={"text": "ROI %"},
        delta={"reference": 0},
        gauge={
            "axis": {"range": [0, 100]},
            "bar": {"color": "#00c853"},
            "steps": [
                {"range": [0,  50], "color": "#1e2a3a"},
                {"range": [50, 80], "color": "#0d3b2e"},
                {"range": [80,100], "color": "#0a4a35"},
            ],
            "threshold": {"line": {"color": "gold", "width": 4}, "value": 90},
        },
        number={"suffix": "%"},
    ))
    fig_gauge.update_layout(
        height=220,
        margin=dict(t=30, b=0, l=20, r=20),
        paper_bgcolor="rgba(0,0,0,0)",
        font_color="white",
    )
    st.plotly_chart(fig_gauge, use_container_width=True)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 3 — Publication Trends
# ═══════════════════════════════════════════════════════════════════════════════
section_header(ICO_CHART, "Publication Trends")
tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])

def make_bar(df_grouped, x, y, title, color="#00b4d8"):
    fig = px.bar(df_grouped, x=x, y=y, title=title, color_discrete_sequence=[color])
    fig.update_layout(
        paper_bgcolor="rgba(0,0,0,0)",
        plot_bgcolor="rgba(0,0,0,0)",
        font_color="white",
        xaxis=dict(showgrid=False),
        yaxis=dict(gridcolor="#2a3a4a"),
        margin=dict(t=40, b=10),
    )
    return fig

with tab_daily:
    if not filtered.empty and filtered["date"].notna().any():
        daily = filtered.groupby(filtered["date"].dt.date).size().reset_index(name="count")
        daily.columns = ["date", "count"]
        st.plotly_chart(make_bar(daily, "date", "count", "Press Releases per Day"), use_container_width=True)
    else:
        st.info("No date data available. Run the scraper first.")

with tab_weekly:
    if not filtered.empty and "week" in filtered.columns and filtered["date"].notna().any():
        weekly = filtered.groupby("week").size().reset_index(name="count")
        st.plotly_chart(make_bar(weekly, "week", "count", "Press Releases per Week", "#4cc9f0"), use_container_width=True)
    else:
        st.info("No data yet.")

with tab_monthly:
    if not filtered.empty and "month" in filtered.columns and filtered["date"].notna().any():
        monthly = filtered.groupby("month").size().reset_index(name="count")
        st.plotly_chart(make_bar(monthly, "month", "count", "Press Releases per Month", "#7209b7"), use_container_width=True)
    else:
        st.info("No data yet.")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 4 — Press Releases Table
# ═══════════════════════════════════════════════════════════════════════════════
section_header(ICO_LIST, "Press Releases")

search = st.text_input("Search press releases", placeholder="Filter by title…")
display = filtered[filtered["title"].str.contains(search, case=False, na=False)] if search else filtered.copy()

total_pages = max(1, math.ceil(len(display) / PAGE_SIZE))
page_num    = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)
start       = (page_num - 1) * PAGE_SIZE
end         = start + PAGE_SIZE
page_data   = display.iloc[start:end]

st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases  (page {page_num}/{total_pages})")

for _, row in page_data.iterrows():
    with st.container():
        tc, dc, pc = st.columns([5, 2, 1])
        with tc:
            url   = row.get("url", "")
            title = row.get("title", "—")
            st.markdown(f"**[{title}]({url})**" if url else f"**{title}**")
            if row.get("description"):
                desc = str(row["description"])
                st.caption(desc[:160] + "…" if len(desc) > 160 else desc)
        with dc:
            d = row.get("date")
            st.markdown(f"`{d.strftime('%d %b %Y') if pd.notna(d) else '—'}`")
            if row.get("category"):
                st.caption(row["category"])
        with pc:
            pc_count = row.get("podcast_count", 0)
            if pc_count:
                st.markdown(
                    f'{svg_icon("<path d=\\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\\"/><path d=\\"M19 10v2a7 7 0 0 1-14 0v-2\\"/><line x1=\\"12\\" y1=\\"19\\" x2=\\"12\\" y2=\\"23\\"/><line x1=\\"8\\" y1=\\"23\\" x2=\\"16\\" y2=\\"23\\" />", size=16, color="#f59e0b")} {pc_count}',
                    unsafe_allow_html=True,
                )
            else:
                st.markdown("—")
    st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 5 — Podcasts
# ═══════════════════════════════════════════════════════════════════════════════
if not df_pod.empty:
    section_header(ICO_MIC, "Podcasts")

    pod_lang_filter = st.radio(
        "Language", ["All", "English", "Arabic"],
        horizontal=True,
    )
    df_pod_show = df_pod.copy()
    if pod_lang_filter == "English":
        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
    elif pod_lang_filter == "Arabic":
        df_pod_show = df_pod_show[df_pod_show["language"] == "ar"]

    pod_search = st.text_input("Search podcasts", placeholder="Filter by title…")
    if pod_search:
        df_pod_show = df_pod_show[df_pod_show["title"].str.contains(pod_search, case=False, na=False)]

    st.caption(f"Showing {len(df_pod_show)} of {len(df_pod)} episodes")

    for _, pod in df_pod_show.head(100).iterrows():
        with st.expander(f"{pod.get('title', '—')}  |  {pod.get('date', '')}"):
            col_a, col_b = st.columns([3, 1])
            with col_a:
                if pod.get("pr_title"):
                    st.markdown(
                        f'{ICO_LINK} **Linked PR:** {pod["pr_title"]}',
                        unsafe_allow_html=True,
                    )
                if pod.get("url"):
                    st.markdown(f"[Open podcast page]({pod['url']})")
                if pod.get("audio_url"):
                    st.audio(pod["audio_url"])
            with col_b:
                if pod.get("duration"):
                    st.metric("Duration", pod["duration"])
                st.metric("Saved vs old cost", f"AED {COST_PER_PODCAST_OLD_AED:,}")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 6 — Scrape Log
# ═══════════════════════════════════════════════════════════════════════════════
with st.expander("Scrape history"):
    if scrape_log:
        df_log = pd.DataFrame(scrape_log)
        st.dataframe(
            df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],
            use_container_width=True,
        )
    else:
        st.info("No scrape runs recorded yet.")

st.markdown(
    '
'
    'DEWA PR Tracker · Playwright + SQLite + Streamlit
',
    unsafe_allow_html=True,
)


The file /Users/khalidbinkalban/Projects/dewa-pr-tracker/dashboard.py has been updated successfully.

"""
DEWA PR Tracker — Streamlit Dashboard
Run:  streamlit run dashboard.py
"""
import math
import re as _re
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st

import database as db
from config import (
    COST_PER_PODCAST_OLD_AED,
    NOTEBOOKLM_MONTHLY_USD,
    USD_TO_AED,
    DASHBOARD_TITLE,
    PAGE_SIZE,
)

# ── Page setup ────────────────────────────────────────────────────────────────
st.set_page_config(
    page_title=DASHBOARD_TITLE,
    page_icon="https://www.dewa.gov.ae/favicon.ico",
    layout="wide",
    initial_sidebar_state="expanded",
)

# ── SVG icon helpers ──────────────────────────────────────────────────────────
def svg_icon(path_d, size=20, color="#00b4d8", viewBox="0 0 24 24", stroke_width=2):
    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '
        f'viewBox="{viewBox}" fill="none" stroke="{color}" stroke-width="{stroke_width}" '
        f'stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:8px">'
        f'{path_d}</svg>'
    )

# Feather-style icon paths
ICO_NEWS      = svg_icon('<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 0-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/>')
ICO_MONEY     = svg_icon('<circle cx="12" cy="12" r="10"/><path d="M12 6v2m0 8v2m-4-7h8m-8 3h8"/>', color="#51cf66")
ICO_CHART     = svg_icon('<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', color="#4cc9f0")
ICO_LIST      = svg_icon('<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>')
ICO_MIC       = svg_icon('<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>', color="#f59e0b")
ICO_SEARCH    = svg_icon('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>', color="#94a3b8")
ICO_REFRESH   = svg_icon('<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>', color="#00b4d8")
ICO_PLAY      = svg_icon('<polygon points="5 3 19 12 5 21 5 3"/>', color="#00b4d8")
ICO_PODCAST   = svg_icon('<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>', size=16, color="#f59e0b")
ICO_LINK      = svg_icon('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>', size=16, color="#00b4d8")

def section_header(icon, text, size="h3"):
    tag = "h3" if size == "h3" else "h2"
    st.markdown(
        f'<{tag} style="display:flex;align-items:center;margin-bottom:0.5rem">'
        f'{icon}{text}</{tag}>',
        unsafe_allow_html=True,
    )

# ── Custom CSS ────────────────────────────────────────────────────────────────
st.markdown("""

""", unsafe_allow_html=True)

# ── Data loading (cached 5 min) ───────────────────────────────────────────────
@st.cache_data(ttl=300)
def load_data():
    db.init_db()
    prs   = [dict(r) for r in db.get_all_press_releases()]
    pods  = [dict(r) for r in db.get_all_podcasts()]
    stats = db.get_stats()
    log   = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, stats, log

prs, pods, stats, scrape_log = load_data()
df_pr  = pd.DataFrame(prs)
df_pod = pd.DataFrame(pods)

# Language detection from title
if not df_pod.empty and "title" in df_pod.columns:
    df_pod["language"] = df_pod["title"].apply(
        lambda t: "ar" if _re.search(r'[\u0600-\u06FF]', str(t)) else "en"
    )

if not df_pr.empty:
    df_pr["date"]  = pd.to_datetime(df_pr["date"], errors="coerce")
    df_pr["week"]  = df_pr["date"].dt.to_period("W").astype(str)
    df_pr["month"] = df_pr["date"].dt.to_period("M").astype(str)
    df_pr["year"]  = df_pr["date"].dt.year

if not df_pod.empty:
    df_pod["date"]  = pd.to_datetime(df_pod["date"], errors="coerce")
    df_pod["month"] = df_pod["date"].dt.to_period("M").astype(str)

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    try:
        st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)
    except Exception:
        st.markdown("**DEWA**")
    st.title("Filters")

    if not df_pr.empty and df_pr["date"].notna().any():
        min_date = df_pr["date"].min().date()
        max_date = df_pr["date"].max().date()
    else:
        min_date = datetime(2019, 1, 1).date()
        max_date = datetime.today().date()

    date_from, date_to = st.date_input(
        "Date range",
        value=(min_date, max_date),
        min_value=min_date,
        max_value=max_date,
    )

    categories = ["All"]
    if not df_pr.empty and "category" in df_pr.columns:
        cats = df_pr["category"].dropna().unique().tolist()
        categories += sorted(cats)
    selected_cat = st.selectbox("Category", categories)

    has_podcast_filter = st.checkbox("Show only PRs with podcast", value=False)

    st.divider()
    if st.button("Refresh data", use_container_width=True):
        st.cache_data.clear()
        st.rerun()

    if st.button("Run scraper now", use_container_width=True):
        with st.spinner("Scraping DEWA website…"):
            try:
                from scraper import run_scrape
                prs_n, pods_n = run_scrape()
                st.success(f"Done — {prs_n} PRs, {pods_n} podcasts saved.")
                st.cache_data.clear()
                st.rerun()
            except Exception as e:
                st.error(f"Scraper error: {e}")

    st.divider()
    st.caption(f"Last scraped: {stats.get('last_scraped', 'Never') or 'Never'}")

# ── Apply filters ─────────────────────────────────────────────────────────────
filtered = df_pr.copy()
if not filtered.empty:
    if filtered["date"].notna().any():
        filtered = filtered[
            (filtered["date"].dt.date >= date_from) &
            (filtered["date"].dt.date <= date_to)
        ]
    if selected_cat != "All":
        filtered = filtered[filtered["category"] == selected_cat]
    if has_podcast_filter:
        filtered = filtered[filtered["podcast_count"] > 0]

# ═══════════════════════════════════════════════════════════════════════════════
# HEADER
# ═══════════════════════════════════════════════════════════════════════════════
st.markdown(
    f'
'
    f'{svg_icon("""<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 0-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/>""", size=32, color="#00b4d8")}'
    f'{DASHBOARD_TITLE}
',
    unsafe_allow_html=True,
)
st.markdown(f'
Tracking DEWA press releases and podcast ROI — {datetime.today().strftime("%d %B %Y")}
', unsafe_allow_html=True)
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
col1, col2, col3, col4, col5 = st.columns(5)

total_prs  = len(filtered)
total_pods = stats.get("total_podcasts", 0)

today       = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)

if not filtered.empty and filtered["date"].notna().any():
    prs_today = len(filtered[filtered["date"].dt.date == today])
    prs_week  = len(filtered[filtered["date"].dt.date >= week_start])
    prs_month = len(filtered[filtered["date"].dt.date >= month_start])
else:
    prs_today = prs_week = prs_month = 0

with col1:
    st.metric("Total Press Releases", f"{total_prs:,}")
with col2:
    st.metric("Total Podcasts", f"{total_pods:,}")
with col3:
    st.metric("Published Today", prs_today)
with col4:
    st.metric("This Week", prs_week)
with col5:
    st.metric("This Month", prs_month)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2 — ROI Calculator
# ═══════════════════════════════════════════════════════════════════════════════
section_header(ICO_MONEY, "Podcast ROI Calculator")

notebooklm_aed  = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED
max_pod_value   = max(int(total_pods), 1000)   # never less than 1000 to avoid crash

c_left, c_right = st.columns([2, 1])
with c_right:
    months_using = st.number_input(
        "Months using NotebookLM",
        min_value=1, max_value=120, value=6,
    )
    podcasts_in_period = st.number_input(
        "Podcasts produced in that period",
        min_value=0,
        max_value=max_pod_value,
        value=min(int(total_pods), max_pod_value),
    )

old_cost = podcasts_in_period * COST_PER_PODCAST_OLD_AED
new_cost = months_using * notebooklm_aed
savings  = old_cost - new_cost
roi_pct  = (savings / old_cost * 100) if old_cost > 0 else 0

with c_left:
    r1, r2, r3 = st.columns(3)
    with r1:
        st.markdown("**Before — Traditional Production**")
        st.markdown(f"AED {old_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{podcasts_in_period} podcast(s) × AED {COST_PER_PODCAST_OLD_AED:,} each")

    with r2:
        st.markdown("**After — NotebookLM**")
        st.markdown(f"AED {new_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{months_using} month(s) × AED {notebooklm_aed:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD}/mo)")

    with r3:
        st.markdown("**Net Savings**")
        st.markdown(f"AED {savings:,.0f}", unsafe_allow_html=True)
        st.caption(f"ROI: {roi_pct:.1f}%")

    fig_gauge = go.Figure(go.Indicator(
        mode="gauge+number+delta",
        value=roi_pct,
        title={"text": "ROI %"},
        delta={"reference": 0},
        gauge={
            "axis": {"range": [0, 100]},
            "bar": {"color": "#00c853"},
            "steps": [
                {"range": [0,  50], "color": "#1e2a3a"},
                {"range": [50, 80], "color": "#0d3b2e"},
                {"range": [80,100], "color": "#0a4a35"},
            ],
            "threshold": {"line": {"color": "gold", "width": 4}, "value": 90},
        },
        number={"suffix": "%"},
    ))
    fig_gauge.update_layout(
        height=220,
        margin=dict(t=30, b=0, l=20, r=20),
        paper_bgcolor="rgba(0,0,0,0)",
        font_color="white",
    )
    st.plotly_chart(fig_gauge, use_container_width=True)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 3 — Publication Trends
# ═══════════════════════════════════════════════════════════════════════════════
section_header(ICO_CHART, "Publication Trends")
tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])

def make_bar(df_grouped, x, y, title, color="#00b4d8"):
    fig = px.bar(df_grouped, x=x, y=y, title=title, color_discrete_sequence=[color])
    fig.update_layout(
        paper_bgcolor="rgba(0,0,0,0)",
        plot_bgcolor="rgba(0,0,0,0)",
        font_color="white",
        xaxis=dict(showgrid=False),
        yaxis=dict(gridcolor="#2a3a4a"),
        margin=dict(t=40, b=10),
    )
    return fig

with tab_daily:
    if not filtered.empty and filtered["date"].notna().any():
        daily = filtered.groupby(filtered["date"].dt.date).size().reset_index(name="count")
        daily.columns = ["date", "count"]
        st.plotly_chart(make_bar(daily, "date", "count", "Press Releases per Day"), use_container_width=True)
    else:
        st.info("No date data available. Run the scraper first.")

with tab_weekly:
    if not filtered.empty and "week" in filtered.columns and filtered["date"].notna().any():
        weekly = filtered.groupby("week").size().reset_index(name="count")
        st.plotly_chart(make_bar(weekly, "week", "count", "Press Releases per Week", "#4cc9f0"), use_container_width=True)
    else:
        st.info("No data yet.")

with tab_monthly:
    if not filtered.empty and "month" in filtered.columns and filtered["date"].notna().any():
        monthly = filtered.groupby("month").size().reset_index(name="count")
        st.plotly_chart(make_bar(monthly, "month", "count", "Press Releases per Month", "#7209b7"), use_container_width=True)
    else:
        st.info("No data yet.")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 4 — Press Releases Table
# ═══════════════════════════════════════════════════════════════════════════════
section_header(ICO_LIST, "Press Releases")

search = st.text_input("Search press releases", placeholder="Filter by title…")
display = filtered[filtered["title"].str.contains(search, case=False, na=False)] if search else filtered.copy()

total_pages = max(1, math.ceil(len(display) / PAGE_SIZE))
page_num    = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)
start       = (page_num - 1) * PAGE_SIZE
end         = start + PAGE_SIZE
page_data   = display.iloc[start:end]

st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases  (page {page_num}/{total_pages})")

for _, row in page_data.iterrows():
    with st.container():
        tc, dc, pc = st.columns([5, 2, 1])
        with tc:
            url   = row.get("url", "")
            title = row.get("title", "—")
            st.markdown(f"**[{title}]({url})**" if url else f"**{title}**")
            if row.get("description"):
                desc = str(row["description"])
                st.caption(desc[:160] + "…" if len(desc) > 160 else desc)
        with dc:
            d = row.get("date")
            st.markdown(f"`{d.strftime('%d %b %Y') if pd.notna(d) else '—'}`")
            if row.get("category"):
                st.caption(row["category"])
        with pc:
            pc_count = row.get("podcast_count", 0)
            if pc_count:
                st.markdown(
                    f'{svg_icon("<path d=\\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\\"/><path d=\\"M19 10v2a7 7 0 0 1-14 0v-2\\"/><line x1=\\"12\\" y1=\\"19\\" x2=\\"12\\" y2=\\"23\\"/><line x1=\\"8\\" y1=\\"23\\" x2=\\"16\\" y2=\\"23\\" />", size=16, color="#f59e0b")} {pc_count}',
                    unsafe_allow_html=True,
                )
            else:
                st.markdown("—")
    st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 5 — Podcasts
# ═══════════════════════════════════════════════════════════════════════════════
if not df_pod.empty:
    section_header(ICO_MIC, "Podcasts")

    pod_lang_filter = st.radio(
        "Language", ["All", "English", "Arabic"],
        horizontal=True,
    )
    df_pod_show = df_pod.copy()
    if pod_lang_filter == "English":
        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
    elif pod_lang_filter == "Arabic":
        df_pod_show = df_pod_show[df_pod_show["language"] == "ar"]

    pod_search = st.text_input("Search podcasts", placeholder="Filter by title…")
    if pod_search:
        df_pod_show = df_pod_show[df_pod_show["title"].str.contains(pod_search, case=False, na=False)]

    st.caption(f"Showing {len(df_pod_show)} of {len(df_pod)} episodes")

    for _, pod in df_pod_show.head(100).iterrows():
        with st.expander(f"{pod.get('title', '—')}  |  {pod.get('date', '')}"):
            col_a, col_b = st.columns([3, 1])
            with col_a:
                if pod.get("pr_title"):
                    st.markdown(
                        f'{ICO_LINK} **Linked PR:** {pod["pr_title"]}',
                        unsafe_allow_html=True,
                    )
                if pod.get("url"):
                    st.markdown(f"[Open podcast page]({pod['url']})")
                if pod.get("audio_url"):
                    st.audio(pod["audio_url"])
            with col_b:
                if pod.get("duration"):
                    st.metric("Duration", pod["duration"])
                st.metric("Saved vs old cost", f"AED {COST_PER_PODCAST_OLD_AED:,}")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 6 — Scrape Log
# ═══════════════════════════════════════════════════════════════════════════════
with st.expander("Scrape history"):
    if scrape_log:
        df_log = pd.DataFrame(scrape_log)
        st.dataframe(
            df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],
            use_container_width=True,
        )
    else:
        st.info("No scrape runs recorded yet.")

st.markdown(
    '
'
    'DEWA PR Tracker · Playwright + SQLite + Streamlit
',
    unsafe_allow_html=True,
)

from datetime import datetime, timedelta

+    page_icon="https://www.dewa.gov.ae/favicon.ico",

+# ── SVG icon helpers ──────────────────────────────────────────────────────────

+def svg_icon(path_d, size=20, color="#00b4d8", viewBox="0 0 24 24", stroke_width=2):

+        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '

+        f'viewBox="{viewBox}" fill="none" stroke="{color}" stroke-width="{stroke_width}" '

+        f'stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:8px">'

+ICO_PLAY      = svg_icon('<polygon points="5 3 19 12 5 21 5 3"/>', color="#00b4d8")

+def section_header(icon, text, size="h3"):

+    tag = "h3" if size == "h3" else "h2"

+        f'<{tag} style="display:flex;align-items:center;margin-bottom:0.5rem">'

+        f'{icon}<span>{text}</span></{tag}>',

-        border-left: 4px solid #00b4d8;

-        background: linear-gradient(135deg, #0a3d2e, #0d5c3a);

-    .cost-old { color: #ff6b6b; font-size: 1.4rem; font-weight: bold; }

-    .cost-new { color: #51cf66; font-size: 1.4rem; font-weight: bold; }

-    .savings  { color: #ffd43b; font-size: 2rem; font-weight: bold; }

+    .cost-old  { color: #ff6b6b; font-size: 1.4rem; font-weight: bold; }

+    .cost-new  { color: #51cf66; font-size: 1.4rem; font-weight: bold; }

+    .savings   { color: #ffd43b; font-size: 2rem;   font-weight: bold; }

+    h1, h2, h3 { color: #e2e8f0 !important; }

+    .stMetric  { background: #1e293b; border-radius: 10px; padding: 12px; }

+    div[data-testid="stSidebar"] h1 { color: #00b4d8 !important; }

-# Fix language field in podcasts (re-detect from title)

if not df_pod.empty and "title" in df_pod.columns:

-    df_pr["date"] = pd.to_datetime(df_pr["date"], errors="coerce")

+    df_pr["date"]  = pd.to_datetime(df_pr["date"], errors="coerce")

df_pr["week"]  = df_pr["date"].dt.to_period("W").astype(str)

-    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")

+    df_pod["date"]  = pd.to_datetime(df_pod["date"], errors="coerce")

-    st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)

+        st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)

if not df_pr.empty and df_pr["date"].notna().any():

-        min_date = datetime(2020, 1, 1).date()

+        min_date = datetime(2019, 1, 1).date()

if not df_pr.empty and "category" in df_pr.columns:

selected_cat = st.selectbox("Category", categories)

-    if st.button("🔄 Refresh data", use_container_width=True):

+    if st.button("Refresh data", use_container_width=True):

-    if st.button("▶ Run scraper now", use_container_width=True):

+    if st.button("Run scraper now", use_container_width=True):

with st.spinner("Scraping DEWA website…"):

-                st.success(f"Done! {prs_n} PRs, {pods_n} podcasts saved.")

+                st.success(f"Done — {prs_n} PRs, {pods_n} podcasts saved.")

-st.markdown(f"*Tracking DEWA press releases and podcast ROI — {datetime.today().strftime('%d %B %Y')}*")

+    f'<h1 style="display:flex;align-items:center;color:#00b4d8">'

+st.markdown(f'
Tracking DEWA press releases and podcast ROI — {datetime.today().strftime("%d %B %Y")}
', unsafe_allow_html=True)

total_pods = stats.get("total_podcasts", 0)

week_start  = today - timedelta(days=today.weekday())

if not filtered.empty and filtered["date"].notna().any():

-    prs_today   = len(filtered[filtered["date"].dt.date == today])

-    prs_week    = len(filtered[filtered["date"].dt.date >= week_start])

-    prs_month   = len(filtered[filtered["date"].dt.date >= month_start])

+    prs_today = len(filtered[filtered["date"].dt.date == today])

+    prs_week  = len(filtered[filtered["date"].dt.date >= week_start])

+    prs_month = len(filtered[filtered["date"].dt.date >= month_start])

-st.subheader("💰 Podcast ROI Calculator")

+section_header(ICO_MONEY, "Podcast ROI Calculator")

-notebooklm_aed = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED

+notebooklm_aed  = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED

+max_pod_value   = max(int(total_pods), 1000)   # never less than 1000 to avoid crash

-# Calculate months since NotebookLM adoption (user can adjust in the UI)

-        "Months using NotebookLM", min_value=1, max_value=60, value=6

+        min_value=1, max_value=120, value=6,

-        "Podcasts produced in that period", min_value=0, max_value=500,

+        "Podcasts produced in that period",

+        value=min(int(total_pods), max_pod_value),

-old_cost  = podcasts_in_period * COST_PER_PODCAST_OLD_AED

-new_cost  = months_using * notebooklm_aed

-roi_pct   = (savings / old_cost * 100) if old_cost > 0 else 0

+old_cost = podcasts_in_period * COST_PER_PODCAST_OLD_AED

+new_cost = months_using * notebooklm_aed

+roi_pct  = (savings / old_cost * 100) if old_cost > 0 else 0

-    fig_gauge.update_layout(height=220, margin=dict(t=30, b=0, l=20, r=20),

-                            paper_bgcolor="rgba(0,0,0,0)", font_color="white")

def make_bar(df_grouped, x, y, title, color="#00b4d8"):

-    fig = px.bar(df_grouped, x=x, y=y, title=title,

-                 color_discrete_sequence=[color])

+    fig = px.bar(df_grouped, x=x, y=y, title=title, color_discrete_sequence=[color])

-        st.info("No date data available yet. Run the scraper first.")

+        st.info("No date data available. Run the scraper first.")

-search = st.text_input("Search press releases", placeholder="Type to filter by title…")

-    display = filtered[filtered["title"].str.contains(search, case=False, na=False)]

+search = st.text_input("Search press releases", placeholder="Filter by title…")

+display = filtered[filtered["title"].str.contains(search, case=False, na=False)] if search else filtered.copy()

total_pages = max(1, math.ceil(len(display) / PAGE_SIZE))

-page_num = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)

+page_num    = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)

+start       = (page_num - 1) * PAGE_SIZE

-st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases (page {page_num}/{total_pages})")

+st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases  (page {page_num}/{total_pages})")

-                st.caption(row["description"][:160] + "…" if len(str(row["description"])) > 160 else row["description"])

+                desc = str(row["description"])

+                st.caption(desc[:160] + "…" if len(desc) > 160 else desc)

-                st.markdown(f"🎙 {pc_count}")

+                    unsafe_allow_html=True,

+    section_header(ICO_MIC, "Podcasts")

-        "Language", ["All", "English (EN)", "Arabic (AR)"],

+        "Language", ["All", "English", "Arabic"],

-    if pod_lang_filter == "English (EN)":

df_pod_show = df_pod_show[df_pod_show["language"] == "en"]

-    elif pod_lang_filter == "Arabic (AR)":

df_pod_show = df_pod_show[df_pod_show["language"] == "ar"]

-                    st.markdown(f"**Linked PR:** {pod['pr_title']}")

+                        f'{ICO_LINK} **Linked PR:** {pod["pr_title"]}',

+                        unsafe_allow_html=True,

-                cost_saved = COST_PER_PODCAST_OLD_AED

-                st.metric("Saved vs old cost", f"AED {cost_saved:,}")

+                st.metric("Saved vs old cost", f"AED {COST_PER_PODCAST_OLD_AED:,}")

-        st.dataframe(df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],

-                     use_container_width=True)

+            df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],

-st.caption("DEWA PR Tracker · Built with Playwright + SQLite + Streamlit")

+    '<p style="color:#334155;text-align:center;font-size:0.8rem;margin-top:2rem">'

+    'DEWA PR Tracker · Playwright + SQLite + Streamlit</p>',

"""
DEWA PR Tracker — Streamlit Dashboard
Run:  streamlit run dashboard.py
"""
import math
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st

import database as db
from config import (
    COST_PER_PODCAST_OLD_AED,
    NOTEBOOKLM_MONTHLY_USD,
    USD_TO_AED,
    DASHBOARD_TITLE,
    PAGE_SIZE,
)

# ── Page setup ────────────────────────────────────────────────────────────────
st.set_page_config(
    page_title=DASHBOARD_TITLE,
    page_icon="📰",
    layout="wide",
    initial_sidebar_state="expanded",
)

# ── Custom CSS ────────────────────────────────────────────────────────────────
st.markdown("""

""", unsafe_allow_html=True)

# ── Data loading (cached 5 min) ───────────────────────────────────────────────
@st.cache_data(ttl=300)
def load_data():
    db.init_db()
    prs   = [dict(r) for r in db.get_all_press_releases()]
    pods  = [dict(r) for r in db.get_all_podcasts()]
    stats = db.get_stats()
    log   = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, stats, log

prs, pods, stats, scrape_log = load_data()
df_pr  = pd.DataFrame(prs)
df_pod = pd.DataFrame(pods)

# Fix language field in podcasts (re-detect from title)
if not df_pod.empty and "title" in df_pod.columns:
    import re as _re
    df_pod["language"] = df_pod["title"].apply(
        lambda t: "ar" if _re.search(r'[\u0600-\u06FF]', str(t)) else "en"
    )

if not df_pr.empty:
    df_pr["date"] = pd.to_datetime(df_pr["date"], errors="coerce")
    df_pr["week"]  = df_pr["date"].dt.to_period("W").astype(str)
    df_pr["month"] = df_pr["date"].dt.to_period("M").astype(str)
    df_pr["year"]  = df_pr["date"].dt.year

if not df_pod.empty:
    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")
    df_pod["month"] = df_pod["date"].dt.to_period("M").astype(str)

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    st.image("https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx", width=160)
    st.title("Filters")

    # Date range
    if not df_pr.empty and df_pr["date"].notna().any():
        min_date = df_pr["date"].min().date()
        max_date = df_pr["date"].max().date()
    else:
        min_date = datetime(2020, 1, 1).date()
        max_date = datetime.today().date()

    date_from, date_to = st.date_input(
        "Date range",
        value=(min_date, max_date),
        min_value=min_date,
        max_value=max_date,
    )

    # Category filter
    categories = ["All"]
    if not df_pr.empty and "category" in df_pr.columns:
        cats = df_pr["category"].dropna().unique().tolist()
        categories += sorted(cats)
    selected_cat = st.selectbox("Category", categories)

    # Podcast filter
    has_podcast_filter = st.checkbox("Show only PRs with podcast", value=False)

    st.divider()
    if st.button("🔄 Refresh data", use_container_width=True):
        st.cache_data.clear()
        st.rerun()

    if st.button("▶ Run scraper now", use_container_width=True):
        with st.spinner("Scraping DEWA website…"):
            try:
                from scraper import run_scrape
                prs_n, pods_n = run_scrape()
                st.success(f"Done! {prs_n} PRs, {pods_n} podcasts saved.")
                st.cache_data.clear()
                st.rerun()
            except Exception as e:
                st.error(f"Scraper error: {e}")

    st.divider()
    st.caption(f"Last scraped: {stats.get('last_scraped', 'Never') or 'Never'}")

# ── Apply filters ─────────────────────────────────────────────────────────────
filtered = df_pr.copy()
if not filtered.empty:
    if filtered["date"].notna().any():
        filtered = filtered[
            (filtered["date"].dt.date >= date_from) &
            (filtered["date"].dt.date <= date_to)
        ]
    if selected_cat != "All":
        filtered = filtered[filtered["category"] == selected_cat]
    if has_podcast_filter:
        filtered = filtered[filtered["podcast_count"] > 0]

# ═══════════════════════════════════════════════════════════════════════════════
# HEADER
# ═══════════════════════════════════════════════════════════════════════════════
st.title(f"📰 {DASHBOARD_TITLE}")
st.markdown(f"*Tracking DEWA press releases and podcast ROI — {datetime.today().strftime('%d %B %Y')}*")
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
col1, col2, col3, col4, col5 = st.columns(5)

total_prs  = len(filtered)
total_pods = stats.get("total_podcasts", 0)

today = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)

if not filtered.empty and filtered["date"].notna().any():
    prs_today   = len(filtered[filtered["date"].dt.date == today])
    prs_week    = len(filtered[filtered["date"].dt.date >= week_start])
    prs_month   = len(filtered[filtered["date"].dt.date >= month_start])
else:
    prs_today = prs_week = prs_month = 0

with col1:
    st.metric("Total Press Releases", f"{total_prs:,}")
with col2:
    st.metric("Total Podcasts", f"{total_pods:,}")
with col3:
    st.metric("Published Today", prs_today)
with col4:
    st.metric("This Week", prs_week)
with col5:
    st.metric("This Month", prs_month)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2 — ROI Calculator
# ═══════════════════════════════════════════════════════════════════════════════
st.subheader("💰 Podcast ROI Calculator")

notebooklm_aed = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED

# Calculate months since NotebookLM adoption (user can adjust in the UI)
c_left, c_right = st.columns([2, 1])
with c_right:
    months_using = st.number_input(
        "Months using NotebookLM", min_value=1, max_value=60, value=6
    )
    podcasts_in_period = st.number_input(
        "Podcasts produced in that period", min_value=0, max_value=500,
        value=total_pods
    )

old_cost  = podcasts_in_period * COST_PER_PODCAST_OLD_AED
new_cost  = months_using * notebooklm_aed
savings   = old_cost - new_cost
roi_pct   = (savings / old_cost * 100) if old_cost > 0 else 0

with c_left:
    r1, r2, r3 = st.columns(3)
    with r1:
        st.markdown("**Before — Traditional Production**")
        st.markdown(f"AED {old_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{podcasts_in_period} podcast(s) × AED {COST_PER_PODCAST_OLD_AED:,} each")

    with r2:
        st.markdown("**After — NotebookLM**")
        st.markdown(f"AED {new_cost:,.0f}", unsafe_allow_html=True)
        st.caption(f"{months_using} month(s) × AED {notebooklm_aed:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD}/mo)")

    with r3:
        st.markdown("**Net Savings**")
        st.markdown(f"AED {savings:,.0f}", unsafe_allow_html=True)
        st.caption(f"ROI: {roi_pct:.1f}%")

    # ROI gauge
    fig_gauge = go.Figure(go.Indicator(
        mode="gauge+number+delta",
        value=roi_pct,
        title={"text": "ROI %"},
        delta={"reference": 0},
        gauge={
            "axis": {"range": [0, 100]},
            "bar": {"color": "#00c853"},
            "steps": [
                {"range": [0, 50], "color": "#1e2a3a"},
                {"range": [50, 80], "color": "#0d3b2e"},
                {"range": [80, 100], "color": "#0a4a35"},
            ],
            "threshold": {"line": {"color": "gold", "width": 4}, "value": 90},
        },
        number={"suffix": "%"},
    ))
    fig_gauge.update_layout(height=220, margin=dict(t=30, b=0, l=20, r=20),
                            paper_bgcolor="rgba(0,0,0,0)", font_color="white")
    st.plotly_chart(fig_gauge, use_container_width=True)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 3 — Publication Charts
# ═══════════════════════════════════════════════════════════════════════════════
st.subheader("📊 Publication Trends")
tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])

def make_bar(df_grouped, x, y, title, color="#00b4d8"):
    fig = px.bar(df_grouped, x=x, y=y, title=title,
                 color_discrete_sequence=[color])
    fig.update_layout(
        paper_bgcolor="rgba(0,0,0,0)",
        plot_bgcolor="rgba(0,0,0,0)",
        font_color="white",
        xaxis=dict(showgrid=False),
        yaxis=dict(gridcolor="#2a3a4a"),
        margin=dict(t=40, b=10),
    )
    return fig

with tab_daily:
    if not filtered.empty and filtered["date"].notna().any():
        daily = filtered.groupby(filtered["date"].dt.date).size().reset_index(name="count")
        daily.columns = ["date", "count"]
        st.plotly_chart(make_bar(daily, "date", "count", "Press Releases per Day"), use_container_width=True)
    else:
        st.info("No date data available yet. Run the scraper first.")

with tab_weekly:
    if not filtered.empty and "week" in filtered.columns and filtered["date"].notna().any():
        weekly = filtered.groupby("week").size().reset_index(name="count")
        st.plotly_chart(make_bar(weekly, "week", "count", "Press Releases per Week", "#4cc9f0"), use_container_width=True)
    else:
        st.info("No data yet.")

with tab_monthly:
    if not filtered.empty and "month" in filtered.columns and filtered["date"].notna().any():
        monthly = filtered.groupby("month").size().reset_index(name="count")
        st.plotly_chart(make_bar(monthly, "month", "count", "Press Releases per Month", "#7209b7"), use_container_width=True)
    else:
        st.info("No data yet.")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 4 — Press Releases Table
# ═══════════════════════════════════════════════════════════════════════════════
st.subheader("📋 Press Releases")

search = st.text_input("Search press releases", placeholder="Type to filter by title…")
if search:
    display = filtered[filtered["title"].str.contains(search, case=False, na=False)]
else:
    display = filtered.copy()

total_pages = max(1, math.ceil(len(display) / PAGE_SIZE))
page_num = st.number_input("Page", min_value=1, max_value=total_pages, value=1, step=1)
start = (page_num - 1) * PAGE_SIZE
end   = start + PAGE_SIZE
page_data = display.iloc[start:end]

st.caption(f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases (page {page_num}/{total_pages})")

for _, row in page_data.iterrows():
    with st.container():
        tc, dc, pc = st.columns([5, 2, 1])
        with tc:
            url   = row.get("url", "")
            title = row.get("title", "—")
            st.markdown(f"**[{title}]({url})**" if url else f"**{title}**")
            if row.get("description"):
                st.caption(row["description"][:160] + "…" if len(str(row["description"])) > 160 else row["description"])
        with dc:
            d = row.get("date")
            st.markdown(f"`{d.strftime('%d %b %Y') if pd.notna(d) else '—'}`")
            if row.get("category"):
                st.caption(row["category"])
        with pc:
            pc_count = row.get("podcast_count", 0)
            if pc_count:
                st.markdown(f"🎙 {pc_count}")
            else:
                st.markdown("—")
    st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 5 — Podcasts Table
# ═══════════════════════════════════════════════════════════════════════════════
if not df_pod.empty:
    st.subheader("🎙 Podcasts")

    pod_lang_filter = st.radio(
        "Language", ["All", "English (EN)", "Arabic (AR)"],
        horizontal=True,
    )
    df_pod_show = df_pod.copy()
    if pod_lang_filter == "English (EN)":
        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
    elif pod_lang_filter == "Arabic (AR)":
        df_pod_show = df_pod_show[df_pod_show["language"] == "ar"]

    pod_search = st.text_input("Search podcasts", placeholder="Filter by title…")
    if pod_search:
        df_pod_show = df_pod_show[df_pod_show["title"].str.contains(pod_search, case=False, na=False)]

    st.caption(f"Showing {len(df_pod_show)} of {len(df_pod)} episodes")

    for _, pod in df_pod_show.head(100).iterrows():
        with st.expander(f"{pod.get('title', '—')}  |  {pod.get('date', '')}"):
            col_a, col_b = st.columns([3, 1])
            with col_a:
                if pod.get("pr_title"):
                    st.markdown(f"**Linked PR:** {pod['pr_title']}")
                if pod.get("url"):
                    st.markdown(f"[Open podcast page]({pod['url']})")
                if pod.get("audio_url"):
                    st.audio(pod["audio_url"])
            with col_b:
                if pod.get("duration"):
                    st.metric("Duration", pod["duration"])
                cost_saved = COST_PER_PODCAST_OLD_AED
                st.metric("Saved vs old cost", f"AED {cost_saved:,}")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 6 — Scrape Log
# ═══════════════════════════════════════════════════════════════════════════════
with st.expander("🔍 Scrape history"):
    if scrape_log:
        df_log = pd.DataFrame(scrape_log)
        st.dataframe(df_log[["ran_at", "prs_found", "pods_found", "status", "notes"]],
                     use_container_width=True)
    else:
        st.info("No scrape runs recorded yet.")

st.caption("DEWA PR Tracker · Built with Playwright + SQLite + Streamlit")


Bash

source venv/bin/activate && python -c "import dashboard" 2>&1

Syntax check dashboard


Error: Exit code 1
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/khalidbinkalban/Projects/dewa-pr-tracker/dashboard.py", line 364
    f'{svg_icon("<path d=\\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\\"/><path d=\\"M19 10v2a7 7 0 0 1-14 0v-2\\"/><line x1=\\"12\\" y1=\\"19\\" x2=\\"12\\" y2=\\"23\\"/><line x1=\\"8\\" y1=\\"23\\" x2=\\"16\\" y2=\\"23\\" />", size=16, color="#f59e0b")} {pc_count}',
                                                                                ^
SyntaxError: f-string expression part cannot include a backslash


Edit

        with pc:
            pc_count = row.get("podcast_count", 0)
            if pc_count:
                st.markdown(ICO_PODCAST + f" {pc_count}", unsafe_allow_html=True)
            else:
                st.markdown("—")