today       = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)
last_month  = (month_start - timedelta(days=1)).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])
    prs_last_month = len(filtered[
        (filtered["date"].dt.date >= last_month) &
        (filtered["date"].dt.date < month_start)
    ])
else:
    prs_today = prs_week = prs_month = prs_last_month = 0

# Podcast coverage: % of filtered PRs that have at least one podcast
prs_with_pod   = len(filtered[filtered["podcast_count"] > 0]) if not filtered.empty else 0
coverage_pct   = (prs_with_pod / len(filtered) * 100) if len(filtered) > 0 else 0
month_delta    = prs_month - prs_last_month
delta_sign     = "+" if month_delta >= 0 else ""
delta_color    = "#22c55e" if month_delta >= 0 else "#f43f5e"

def kpi(color, icon_path, label, value, sub=""):
    icon = _svg(icon_path, size=14, color={
        "blue":"#00b4d8","green":"#22c55e","amber":"#f59e0b","purple":"#a855f7","red":"#f43f5e"
    }[color])
    return (
        f'
'
        f'
{icon}{label}
'
        f'
{value}
'
        f'{"
" + sub + "
" if sub else ""}'
        f'
'
    )

c1, c2, c3, c4, c5 = st.columns(5)
with c1:
    st.markdown(kpi("blue",
        '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
        "Total Press Releases", f"{len(filtered):,}",
        f"since {date_from.strftime('%d %b %Y')}",
    ), unsafe_allow_html=True)
with c2:
    st.markdown(kpi("green",
        '<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"/>',
        "Podcast Coverage", f"{coverage_pct:.1f}%",
        f"{prs_with_pod} of {len(filtered)} PRs have a podcast",
    ), unsafe_allow_html=True)
with c3:
    st.markdown(kpi("amber",
        '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>',
        "This Month", f"{prs_month:,}",
        f'{delta_sign}{month_delta} vs last month',
    ), unsafe_allow_html=True)
with c4:
    st.markdown(kpi("purple",
        '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
        "This Week", f"{prs_week:,}",
        f"since {week_start.strftime('%d %b')}",
    ), unsafe_allow_html=True)
with c5:
    st.markdown(kpi("red",
        '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
        "Today", f"{prs_today:,}",
        today.strftime("%d %B %Y"),
    ), unsafe_allow_html=True)

st.markdown("
", unsafe_allow_html=True)
st.divider()

"""
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,
    NOTEBOOKLM_START_DATE,
    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",
)

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

ICO_NEWS    = _svg('<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('<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>', color="#51cf66")
ICO_CHART   = _svg('<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('<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('<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_MIC_SM  = _svg('<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=15, color="#f59e0b")
ICO_CLOCK   = _svg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', color="#94a3b8")
ICO_LOG     = _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>', color="#94a3b8")
ICO_LINK    = _svg('<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=14, color="#00b4d8")

def section_title(icon, text):
    st.markdown(
        f'
{icon}{text}
',
        unsafe_allow_html=True,
    )

# ── 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()]
    roi_rows = [dict(r) for r in db.get_roi_monthly_since(NOTEBOOKLM_START_DATE)]
    stats    = db.get_stats()
    log      = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, roi_rows, stats, log

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

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)

if not df_pod.empty:
    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")

# ── ROI pre-calculations ──────────────────────────────────────────────────────
NOTEBOOKLM_AED   = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED   # ~73.4 AED/month
NLM_START        = pd.Timestamp(NOTEBOOKLM_START_DATE)

df_roi = pd.DataFrame(roi_rows) if roi_rows else pd.DataFrame(
    columns=["month", "total", "en_count", "ar_count"]
)

if not df_roi.empty:
    # Cost basis: 1 podcast per press release @ AED 20,000
    # (PR cost = 0 AED; only the podcast production was charged)
    df_roi["old_cost"] = df_roi["pr_count"] * COST_PER_PODCAST_OLD_AED
    df_roi["new_cost"] = NOTEBOOKLM_AED   # flat monthly subscription
    df_roi["savings"]  = df_roi["old_cost"] - df_roi["new_cost"]

    total_prs_nlm   = int(df_roi["pr_count"].sum())
    total_eps_nlm   = int(df_roi["total_episodes"].sum())
    total_old_cost  = float(df_roi["old_cost"].sum())
    total_new_cost  = float(df_roi["new_cost"].sum())
    total_savings   = float(df_roi["savings"].sum())
    roi_pct         = (total_savings / total_old_cost * 100) if total_old_cost > 0 else 0
else:
    total_prs_nlm = total_eps_nlm = total_old_cost = total_new_cost = total_savings = roi_pct = 0

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    # DEWA logo — styled text (image blocked externally)
    st.markdown(
        '
DEWA
'
        '
Dubai Electricity & Water Authority
',
        unsafe_allow_html=True,
    )
    st.markdown("---")
    st.markdown("**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=(NLM_START.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 PR 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("""<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=34, color="#00b4d8")}'
    f'{DASHBOARD_TITLE}
',
    unsafe_allow_html=True,
)
st.markdown(
    f'
'
    f'Tracking all DEWA press releases and NotebookLM podcast ROI since {NOTEBOOKLM_START_DATE} — '
    f'{datetime.today().strftime("%d %B %Y")}
',
    unsafe_allow_html=True,
)
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
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

c1, c2, c3, c4, c5 = st.columns(5)
with c1:
    st.metric("Total Press Releases", f"{len(filtered):,}")
with c2:
    st.metric("Podcasts (since May 2025)", f"{total_prs_nlm:,}")
with c3:
    st.metric("Published Today", prs_today)
with c4:
    st.metric("This Week", prs_week)
with c5:
    st.metric("This Month", prs_month)

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2 — NotebookLM ROI  (auto-calculated from May 23, 2025)
# ═══════════════════════════════════════════════════════════════════════════════
section_title(ICO_MONEY, "NotebookLM ROI — Since 23 May 2025")

# Summary cards
sc1, sc2, sc3, sc4 = st.columns(4)
with sc1:
    st.markdown(
        f'
'
        f'
Old Cost (Traditional)
'
        f'
AED {total_old_cost:,.0f}
'
        f'
{total_prs_nlm} PRs × AED 20,000/podcast
'
        f'
',
        unsafe_allow_html=True,
    )
with sc2:
    months_active = len(df_roi)
    st.markdown(
        f'
'
        f'
New Cost (NotebookLM)
'
        f'
AED {total_new_cost:,.0f}
'
        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'
        f'
',
        unsafe_allow_html=True,
    )
with sc3:
    st.markdown(
        f'
'
        f'
Total Savings
'
        f'
AED {total_savings:,.0f}
'
        f'
Since {NOTEBOOKLM_START_DATE}
'
        f'
',
        unsafe_allow_html=True,
    )
with sc4:
    st.markdown(
        f'
'
        f'
Return on Investment
'
        f'
{roi_pct:.1f}%
'
        f'
Savings ÷ Old Cost
'
        f'
',
        unsafe_allow_html=True,
    )

st.markdown("
", unsafe_allow_html=True)

# ── Monthly detail ────────────────────────────────────────────────────────────
if not df_roi.empty:
    chart_col, table_col = st.columns([3, 2])

    with chart_col:
        # Grouped bar: old vs new cost per month
        df_chart = df_roi[["month", "old_cost", "new_cost"]].copy()
        df_chart_long = df_chart.melt(
            id_vars="month",
            value_vars=["old_cost", "new_cost"],
            var_name="Type", value_name="AED",
        )
        df_chart_long["Type"] = df_chart_long["Type"].map(
            {"old_cost": "Traditional (AED 20k/podcast)", "new_cost": "NotebookLM ($20/month)"}
        )
        fig_bar = px.bar(
            df_chart_long, x="month", y="AED", color="Type", barmode="group",
            color_discrete_map={
                "Traditional (AED 20k/podcast)": "#ff6b6b",
                "NotebookLM ($20/month)":         "#51cf66",
            },
            title="Monthly Cost: Traditional Podcast Production vs NotebookLM",
        )
        fig_bar.update_layout(
            paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
            font_color="white", legend_title_text="",
            xaxis=dict(showgrid=False, title=""),
            yaxis=dict(gridcolor="#1e293b", title="AED"),
            margin=dict(t=40, b=10), height=320,
        )
        st.plotly_chart(fig_bar, use_container_width=True)

        # PRs with podcasts per month
        fig_count = px.bar(
            df_roi, x="month", y="pr_count",
            color_discrete_sequence=["#f59e0b"],
            title="Press Releases with Podcasts per Month (since NotebookLM)",
        )
        fig_count.update_layout(
            paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
            font_color="white",
            xaxis=dict(showgrid=False, title=""),
            yaxis=dict(gridcolor="#1e293b", title="Press Releases"),
            margin=dict(t=40, b=10), height=280,
        )
        st.plotly_chart(fig_count, use_container_width=True)

    with table_col:
        st.markdown(
            '
'
            'Cost basis: AED 20,000 per PR podcast (EN+AR counted as one production).
'
            'NotebookLM: flat $20/month regardless of volume.
',
            unsafe_allow_html=True,
        )
        rows_html = ""
        for _, r in df_roi.iterrows():
            rows_html += (
                f'
'
                f'{r["month"]}	'
                f'{int(r["pr_count"])}	'
                f'{int(r["en_episodes"])}	'
                f'{int(r["ar_episodes"])}	'
                f'AED {r["old_cost"]:,.0f}	'
                f'AED {r["new_cost"]:,.0f}	'
                f'AED {r["savings"]:,.0f}	'
                f'
'
            )
        rows_html += (
            f'
'
            f'TOTAL	'
            f'{total_prs_nlm}	'
            f'{int(df_roi["en_episodes"].sum())}	'
            f'{int(df_roi["ar_episodes"].sum())}	'
            f'AED {total_old_cost:,.0f}	'
            f'AED {total_new_cost:,.0f}	'
            f'AED {total_savings:,.0f}	'
            f'
'
        )
        st.markdown(
            f'
'
            f'

'
            f'Month	PRs	EN eps	AR eps	'
            f'Old Cost	New Cost	Savings	'
            f'

'
            f'
{rows_html}
'
            f'
',
            unsafe_allow_html=True,
        )

st.divider()

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

def make_bar(df_g, x, y, title, color="#00b4d8"):
    fig = px.bar(df_g, 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, title=""),
        yaxis=dict(gridcolor="#1e293b"),
        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 data 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
# ═══════════════════════════════════════════════════════════════════════════════
section_title(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 "
    f"(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}**")
            desc = str(row.get("description") or "")
            if desc:
                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 '—'}`")
        with pc:
            pc_count = row.get("podcast_count", 0)
            st.markdown(
                ICO_MIC_SM + f" {pc_count}" if pc_count else "—",
                unsafe_allow_html=True,
            )
    st.divider()

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

    lang_filter = st.radio("Language", ["All", "English", "Arabic"], horizontal=True)
    df_pod_show = df_pod.copy()
    if lang_filter == "English":
        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
    elif 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(ICO_LINK + f" **Linked PR:** {pod['pr_title']}", unsafe_allow_html=True)
                if pod.get("url"):
                    st.markdown(f"[Open on RSS.com]({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,
)

+last_month  = (month_start - timedelta(days=1)).replace(day=1)

-    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])

+        (filtered["date"].dt.date >= last_month) &

+        (filtered["date"].dt.date < month_start)

-    prs_today = prs_week = prs_month = 0

+    prs_today = prs_week = prs_month = prs_last_month = 0

+# Podcast coverage: % of filtered PRs that have at least one podcast

+prs_with_pod   = len(filtered[filtered["podcast_count"] > 0]) if not filtered.empty else 0

+coverage_pct   = (prs_with_pod / len(filtered) * 100) if len(filtered) > 0 else 0

+month_delta    = prs_month - prs_last_month

+delta_sign     = "+" if month_delta >= 0 else ""

+delta_color    = "#22c55e" if month_delta >= 0 else "#f43f5e"

+def kpi(color, icon_path, label, value, sub=""):

+    icon = _svg(icon_path, size=14, color={

+        f'<div class="kpi-card {color}">'

+        f'{"
" + sub + "
" if sub else ""}'

-    st.metric("Total Press Releases", f"{len(filtered):,}")

+        "Total Press Releases", f"{len(filtered):,}",

+        f"since {date_from.strftime('%d %b %Y')}",

-    st.metric("Podcasts (since May 2025)", f"{total_prs_nlm:,}")

+        "Podcast Coverage", f"{coverage_pct:.1f}%",

+        f"{prs_with_pod} of {len(filtered)} PRs have a podcast",

-    st.metric("Published Today", prs_today)

+        "This Month", f"{prs_month:,}",

+        f'{delta_sign}{month_delta} vs last month',

+        f"since {week_start.strftime('%d %b')}",


Also clean up the ROI summary cards to use the new `.roi-card` class:


Edit

with sc1:
    st.markdown(
        f'
'
        f'
Old Cost (Traditional)
'
        f'
AED {total_old_cost:,.0f}
'
        f'
{total_prs_nlm} PRs × AED 20,000/podcast
'
        f'
',
        unsafe_allow_html=True,
    )
with sc2:
    months_active = len(df_roi)
    st.markdown(
        f'
'
        f'
New Cost (NotebookLM)
'
        f'
AED {total_new_cost:,.0f}
'
        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'
        f'
',
        unsafe_allow_html=True,
    )
with sc3:
    st.markdown(
        f'
'
        f'
Total Savings
'
        f'
AED {total_savings:,.0f}
'
        f'
Since {NOTEBOOKLM_START_DATE}
'
        f'
',
        unsafe_allow_html=True,
    )
with sc4:
    st.markdown(
        f'
'
        f'
Return on Investment
'
        f'
{roi_pct:.1f}%
'
        f'
Savings ÷ Old Cost
'
        f'
',
        unsafe_allow_html=True,
    )

months_active = len(df_roi)
with sc1:
    st.markdown(
        f'
'
        f'
Traditional Cost (if no NotebookLM)
'
        f'
AED {total_old_cost:,.0f}
'
        f'
{total_prs_nlm} PRs × AED {COST_PER_PODCAST_OLD_AED:,} per podcast
'
        f'
', unsafe_allow_html=True)
with sc2:
    st.markdown(
        f'
'
        f'
Actual Cost (NotebookLM)
'
        f'
AED {total_new_cost:,.0f}
'
        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'
        f'
', unsafe_allow_html=True)
with sc3:
    st.markdown(
        f'
'
        f'
Total Savings to Date
'
        f'
AED {total_savings:,.0f}
'
        f'
Since {NOTEBOOKLM_START_DATE}
'
        f'
', unsafe_allow_html=True)
with sc4:
    st.markdown(
        f'
'
        f'
Return on Investment
'
        f'
{roi_pct:.1f}%
'
        f'
Savings ÷ traditional cost
'
        f'
', unsafe_allow_html=True)


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

months_active = len(df_roi)
with sc1:
    st.markdown(
        f'
'
        f'
Traditional Cost (if no NotebookLM)
'
        f'
AED {total_old_cost:,.0f}
'
        f'
{total_prs_nlm} PRs × AED {COST_PER_PODCAST_OLD_AED:,} per podcast
'
        f'
', unsafe_allow_html=True)
with sc2:
    st.markdown(
        f'
'
        f'
Actual Cost (NotebookLM)
'
        f'
AED {total_new_cost:,.0f}
'
        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'
        f'
', unsafe_allow_html=True)
with sc3:
    st.markdown(
        f'
'
        f'
Total Savings to Date
'
        f'
AED {total_savings:,.0f}
'
        f'
Since {NOTEBOOKLM_START_DATE}
'
        f'
', unsafe_allow_html=True)
with sc4:
    st.markdown(
        f'
'
        f'
Return on Investment
'
        f'
{roi_pct:.1f}%
'
        f'
Savings ÷ traditional cost
'
        f'
', unsafe_allow_html=True)

"""
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,
    NOTEBOOKLM_START_DATE,
    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",
)

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

ICO_NEWS    = _svg('<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('<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>', color="#51cf66")
ICO_CHART   = _svg('<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('<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('<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_MIC_SM  = _svg('<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=15, color="#f59e0b")
ICO_CLOCK   = _svg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', color="#94a3b8")
ICO_LOG     = _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>', color="#94a3b8")
ICO_LINK    = _svg('<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=14, color="#00b4d8")

def section_title(icon, text):
    st.markdown(
        f'
{icon}{text}
',
        unsafe_allow_html=True,
    )

# ── 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()]
    roi_rows = [dict(r) for r in db.get_roi_monthly_since(NOTEBOOKLM_START_DATE)]
    stats    = db.get_stats()
    log      = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, roi_rows, stats, log

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

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)

if not df_pod.empty:
    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")

# ── ROI pre-calculations ──────────────────────────────────────────────────────
NOTEBOOKLM_AED   = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED   # ~73.4 AED/month
NLM_START        = pd.Timestamp(NOTEBOOKLM_START_DATE)

df_roi = pd.DataFrame(roi_rows) if roi_rows else pd.DataFrame(
    columns=["month", "total", "en_count", "ar_count"]
)

if not df_roi.empty:
    # Cost basis: 1 podcast per press release @ AED 20,000
    # (PR cost = 0 AED; only the podcast production was charged)
    df_roi["old_cost"] = df_roi["pr_count"] * COST_PER_PODCAST_OLD_AED
    df_roi["new_cost"] = NOTEBOOKLM_AED   # flat monthly subscription
    df_roi["savings"]  = df_roi["old_cost"] - df_roi["new_cost"]

    total_prs_nlm   = int(df_roi["pr_count"].sum())
    total_eps_nlm   = int(df_roi["total_episodes"].sum())
    total_old_cost  = float(df_roi["old_cost"].sum())
    total_new_cost  = float(df_roi["new_cost"].sum())
    total_savings   = float(df_roi["savings"].sum())
    roi_pct         = (total_savings / total_old_cost * 100) if total_old_cost > 0 else 0
else:
    total_prs_nlm = total_eps_nlm = total_old_cost = total_new_cost = total_savings = roi_pct = 0

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    # DEWA logo — styled text (image blocked externally)
    st.markdown(
        '
DEWA
'
        '
Dubai Electricity & Water Authority
',
        unsafe_allow_html=True,
    )
    st.markdown("---")
    st.markdown("**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=(NLM_START.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 PR 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("""<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=34, color="#00b4d8")}'
    f'{DASHBOARD_TITLE}
',
    unsafe_allow_html=True,
)
st.markdown(
    f'
'
    f'Tracking all DEWA press releases and NotebookLM podcast ROI since {NOTEBOOKLM_START_DATE} — '
    f'{datetime.today().strftime("%d %B %Y")}
',
    unsafe_allow_html=True,
)
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
today       = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)
last_month  = (month_start - timedelta(days=1)).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])
    prs_last_month = len(filtered[
        (filtered["date"].dt.date >= last_month) &
        (filtered["date"].dt.date < month_start)
    ])
else:
    prs_today = prs_week = prs_month = prs_last_month = 0

# Podcast coverage: % of filtered PRs that have at least one podcast
prs_with_pod   = len(filtered[filtered["podcast_count"] > 0]) if not filtered.empty else 0
coverage_pct   = (prs_with_pod / len(filtered) * 100) if len(filtered) > 0 else 0
month_delta    = prs_month - prs_last_month
delta_sign     = "+" if month_delta >= 0 else ""
delta_color    = "#22c55e" if month_delta >= 0 else "#f43f5e"

def kpi(color, icon_path, label, value, sub=""):
    icon = _svg(icon_path, size=14, color={
        "blue":"#00b4d8","green":"#22c55e","amber":"#f59e0b","purple":"#a855f7","red":"#f43f5e"
    }[color])
    return (
        f'
'
        f'
{icon}{label}
'
        f'
{value}
'
        f'{"
" + sub + "
" if sub else ""}'
        f'
'
    )

c1, c2, c3, c4, c5 = st.columns(5)
with c1:
    st.markdown(kpi("blue",
        '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
        "Total Press Releases", f"{len(filtered):,}",
        f"since {date_from.strftime('%d %b %Y')}",
    ), unsafe_allow_html=True)
with c2:
    st.markdown(kpi("green",
        '<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"/>',
        "Podcast Coverage", f"{coverage_pct:.1f}%",
        f"{prs_with_pod} of {len(filtered)} PRs have a podcast",
    ), unsafe_allow_html=True)
with c3:
    st.markdown(kpi("amber",
        '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>',
        "This Month", f"{prs_month:,}",
        f'{delta_sign}{month_delta} vs last month',
    ), unsafe_allow_html=True)
with c4:
    st.markdown(kpi("purple",
        '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
        "This Week", f"{prs_week:,}",
        f"since {week_start.strftime('%d %b')}",
    ), unsafe_allow_html=True)
with c5:
    st.markdown(kpi("red",
        '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
        "Today", f"{prs_today:,}",
        today.strftime("%d %B %Y"),
    ), unsafe_allow_html=True)

st.markdown("
", unsafe_allow_html=True)
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2 — NotebookLM ROI  (auto-calculated from May 23, 2025)
# ═══════════════════════════════════════════════════════════════════════════════
section_title(ICO_MONEY, "NotebookLM ROI — Since 23 May 2025")

# Summary cards
sc1, sc2, sc3, sc4 = st.columns(4)
with sc1:
    st.markdown(
        f'
'
        f'
Old Cost (Traditional)
'
        f'
AED {total_old_cost:,.0f}
'
        f'
{total_prs_nlm} PRs × AED 20,000/podcast
'
        f'
',
        unsafe_allow_html=True,
    )
with sc2:
    months_active = len(df_roi)
    st.markdown(
        f'
'
        f'
New Cost (NotebookLM)
'
        f'
AED {total_new_cost:,.0f}
'
        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'
        f'
',
        unsafe_allow_html=True,
    )
with sc3:
    st.markdown(
        f'
'
        f'
Total Savings
'
        f'
AED {total_savings:,.0f}
'
        f'
Since {NOTEBOOKLM_START_DATE}
'
        f'
',
        unsafe_allow_html=True,
    )
with sc4:
    st.markdown(
        f'
'
        f'
Return on Investment
'
        f'
{roi_pct:.1f}%
'
        f'
Savings ÷ Old Cost
'
        f'
',
        unsafe_allow_html=True,
    )

st.markdown("
", unsafe_allow_html=True)

# ── Monthly detail ────────────────────────────────────────────────────────────
if not df_roi.empty:
    chart_col, table_col = st.columns([3, 2])

    with chart_col:
        # Grouped bar: old vs new cost per month
        df_chart = df_roi[["month", "old_cost", "new_cost"]].copy()
        df_chart_long = df_chart.melt(
            id_vars="month",
            value_vars=["old_cost", "new_cost"],
            var_name="Type", value_name="AED",
        )
        df_chart_long["Type"] = df_chart_long["Type"].map(
            {"old_cost": "Traditional (AED 20k/podcast)", "new_cost": "NotebookLM ($20/month)"}
        )
        fig_bar = px.bar(
            df_chart_long, x="month", y="AED", color="Type", barmode="group",
            color_discrete_map={
                "Traditional (AED 20k/podcast)": "#ff6b6b",
                "NotebookLM ($20/month)":         "#51cf66",
            },
            title="Monthly Cost: Traditional Podcast Production vs NotebookLM",
        )
        fig_bar.update_layout(
            paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
            font_color="white", legend_title_text="",
            xaxis=dict(showgrid=False, title=""),
            yaxis=dict(gridcolor="#1e293b", title="AED"),
            margin=dict(t=40, b=10), height=320,
        )
        st.plotly_chart(fig_bar, use_container_width=True)

        # PRs with podcasts per month
        fig_count = px.bar(
            df_roi, x="month", y="pr_count",
            color_discrete_sequence=["#f59e0b"],
            title="Press Releases with Podcasts per Month (since NotebookLM)",
        )
        fig_count.update_layout(
            paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
            font_color="white",
            xaxis=dict(showgrid=False, title=""),
            yaxis=dict(gridcolor="#1e293b", title="Press Releases"),
            margin=dict(t=40, b=10), height=280,
        )
        st.plotly_chart(fig_count, use_container_width=True)

    with table_col:
        st.markdown(
            '
'
            'Cost basis: AED 20,000 per PR podcast (EN+AR counted as one production).
'
            'NotebookLM: flat $20/month regardless of volume.
',
            unsafe_allow_html=True,
        )
        rows_html = ""
        for _, r in df_roi.iterrows():
            rows_html += (
                f'
'
                f'{r["month"]}	'
                f'{int(r["pr_count"])}	'
                f'{int(r["en_episodes"])}	'
                f'{int(r["ar_episodes"])}	'
                f'AED {r["old_cost"]:,.0f}	'
                f'AED {r["new_cost"]:,.0f}	'
                f'AED {r["savings"]:,.0f}	'
                f'
'
            )
        rows_html += (
            f'
'
            f'TOTAL	'
            f'{total_prs_nlm}	'
            f'{int(df_roi["en_episodes"].sum())}	'
            f'{int(df_roi["ar_episodes"].sum())}	'
            f'AED {total_old_cost:,.0f}	'
            f'AED {total_new_cost:,.0f}	'
            f'AED {total_savings:,.0f}	'
            f'
'
        )
        st.markdown(
            f'
'
            f'

'
            f'Month	PRs	EN eps	AR eps	'
            f'Old Cost	New Cost	Savings	'
            f'

'
            f'
{rows_html}
'
            f'
',
            unsafe_allow_html=True,
        )

st.divider()

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

def make_bar(df_g, x, y, title, color="#00b4d8"):
    fig = px.bar(df_g, 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, title=""),
        yaxis=dict(gridcolor="#1e293b"),
        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 data 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
# ═══════════════════════════════════════════════════════════════════════════════
section_title(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 "
    f"(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}**")
            desc = str(row.get("description") or "")
            if desc:
                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 '—'}`")
        with pc:
            pc_count = row.get("podcast_count", 0)
            st.markdown(
                ICO_MIC_SM + f" {pc_count}" if pc_count else "—",
                unsafe_allow_html=True,
            )
    st.divider()

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

    lang_filter = st.radio("Language", ["All", "English", "Arabic"], horizontal=True)
    df_pod_show = df_pod.copy()
    if lang_filter == "English":
        df_pod_show = df_pod_show[df_pod_show["language"] == "en"]
    elif 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(ICO_LINK + f" **Linked PR:** {pod['pr_title']}", unsafe_allow_html=True)
                if pod.get("url"):
                    st.markdown(f"[Open on RSS.com]({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,
)

-        f'<div style="background:#1e293b;border-radius:10px;padding:18px 20px;border-left:4px solid #ff6b6b">'

+        f'<div class="roi-card" style="border-left:4px solid #f43f5e">'

+        f'
Traditional Cost (if no NotebookLM)
'

-        f'
{total_prs_nlm} PRs × AED 20,000/podcast
'

+        f'
{total_prs_nlm} PRs × AED {COST_PER_PODCAST_OLD_AED:,} per podcast
'

+        f'</div>', unsafe_allow_html=True)

-        f'<div style="background:#1e293b;border-radius:10px;padding:18px 20px;border-left:4px solid #51cf66">'

+        f'<div class="roi-card" style="border-left:4px solid #22c55e">'

-        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'

+        f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'

-        f'<div style="background:#1e293b;border-radius:10px;padding:18px 20px;border-left:4px solid #ffd43b">'

+        f'<div class="roi-card" style="border-left:4px solid #fbbf24">'

-        f'
Since {NOTEBOOKLM_START_DATE}
'

+        f'
Since {NOTEBOOKLM_START_DATE}
'

-        f'<div style="background:#1e293b;border-radius:10px;padding:18px 20px;border-left:4px solid #00b4d8">'

+        f'<div class="roi-card" style="border-left:4px solid #00b4d8">'

+        f'
Savings ÷ traditional cost
'


Bash

source venv/bin/activate && python -c "
import ast
with open('dashboard.py') as f: src = f.read()
try:
    ast.parse(src); print('Syntax OK')
except SyntaxError as e:
    print(f'Line {e.lineno}: {e.msg}')
"

Syntax check


Syntax OK


pkill -f "streamlit run dashboard.py" 2>/dev/null; sleep 1
source venv/bin/activate && streamlit run dashboard.py --server.headless true &
sleep 4 && curl -s -o /dev/null -w "%{http_code}" http://localhost:8501

Restart dashboard


Collecting usage statistics. To deactivate, set browser.gatherUsageStats to false.

/Users/khalidbinkalban/Projects/dewa-pr-tracker/venv/lib/python3.9/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
  warnings.warn(

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501
  Network URL: http://192.168.1.140:8501
  External URL: http://86.99.11.212:8501

  For better performance, install the Watchdog module:

  $ xcode-select --install
  $ pip install watchdog

200


Live at **http://localhost:8501**. Here's what changed:

**5 KPI cards — all unique now:**

| Card | Metric | Why it's useful |
|---|---|---|
| Blue | **Total Press Releases** | Volume in selected period |
| Green | **Podcast Coverage %** (96.2%) | Quality signal — what % of PRs got a podcast |
| Amber | **This Month** + delta vs last month | Trend momentum |
| Purple | **This Week** | Short-term activity |
| Red | **Today** | Live pulse |

**UI enhancements:**
- Dark `#0a0f1a` background (deeper than before)
- Custom KPI cards with colored left-border accents and SVG icons per card
- ROI cards use consistent `.roi-card` class with matching styling
- Sidebar styled with its own darker tone + hover effects on buttons
- Tab active state highlighted in `#00b4d8`
- Expanders styled to match the dark theme
- All text hierarchy refined (labels uppercase, values bold/large)


what is the use of Run Scraper now?


The **"Run scraper now"** button does exactly what the scheduled daily job does, but on demand — it:

1. **Opens a headless Chromium browser** → visits the DEWA news page → clicks through all years (2019–2026) → clicks "More" until all pages load → extracts every press release (title, date, URL, description)

2. **Fetches the RSS feed** from `media.rss.com/dewa-news/feed.xml` → parses all podcast episodes (title, date, MP3 URL, duration, EN/AR)

3. **Saves everything to the SQLite database** — new items are inserted, existing ones are updated (upsert), so no duplicates

4. **Logs the run** in the scrape history table (how many PRs/podcasts found, status, timestamp)

5. **You click "Refresh data"** after → dashboard reloads with the latest numbers

---

**When would you actually use it?**

| Situation | Use it? |
|---|---|
| DEWA just published a press release and you want it now | Yes |
| It's been more than a day since the last auto-scrape | Yes |
| You want to verify the latest podcast count before a meeting | Yes |
| The scheduler is running — just checking the dashboard | No, it auto-runs at 08:00 daily |

The **scheduler** (`python scheduler.py`) handles the daily 08:00 auto-run so you never need to press it manually in normal operation. The button is just a manual override.


great please convert it into a url link so i can share it with my team, using tailscale


tailscale ip --4 2>/dev/null || ifconfig tailscale0 2>/dev/null | grep "inet " | awk '{print $2}'

Get Tailscale IP address


Check Tailscale status and machine name


100.72.97.47  khalids-mac-mini  kkalban10@  macOS  -

# Funnel on:
#     - https://khalids-mac-mini.tailbd7b77.ts.net


Check existing Tailscale Funnel config


# Funnel on:
#     - https://khalids-mac-mini.tailbd7b77.ts.net

https://khalids-mac-mini.tailbd7b77.ts.net (Funnel on)
|-- / proxy http://127.0.0.1:8080


Funnel is already active but pointing to port 8080. I'll update it to point to Streamlit's port 8501 and configure Streamlit to bind on all interfaces:


pkill -f "streamlit run dashboard.py" 2>/dev/null; sleep 1
tailscale funnel reset 2>/dev/null; sleep 1
tailscale funnel --bg 8501 2>&1

Reset funnel and point it to Streamlit port 8501


Available on the internet:

https://khalids-mac-mini.tailbd7b77.ts.net/
|-- proxy http://127.0.0.1:8501

Funnel started and running in the background.
To disable the proxy, run: tailscale funnel --https=443 off


Now update the start script to bind on all interfaces and use a fixed port:


Write

#!/bin/bash
# DEWA PR Tracker — Start dashboard
cd "$(dirname "$0")"
source venv/bin/activate

echo ""
echo "  DEWA PR Tracker"
echo "  ─────────────────────────────────────────────────"
echo "  Local:    http://localhost:8501"
echo "  Team URL: https://khalids-mac-mini.tailbd7b77.ts.net"
echo "  ─────────────────────────────────────────────────"
echo ""

open http://localhost:8501

streamlit run dashboard.py \
  --server.port 8501 \
  --server.address 0.0.0.0 \
  --server.headless true \
  --browser.gatherUsageStats false


The file /Users/khalidbinkalban/Projects/dewa-pr-tracker/start.sh has been updated successfully.

#!/bin/bash
# DEWA PR Tracker — Start dashboard
cd "$(dirname "$0")"
source venv/bin/activate

echo ""
echo "  DEWA PR Tracker"
echo "  ─────────────────────────────────────────────────"
echo "  Local:    http://localhost:8501"
echo "  Team URL: https://khalids-mac-mini.tailbd7b77.ts.net"
echo "  ─────────────────────────────────────────────────"
echo ""

open http://localhost:8501

streamlit run dashboard.py \
  --server.port 8501 \
  --server.address 0.0.0.0 \
  --server.headless true \
  --browser.gatherUsageStats false

-echo "Starting DEWA PR Tracker dashboard..."

+echo "  ─────────────────────────────────────────────────"

+echo "  Local:    http://localhost:8501"

+echo "  Team URL: https://khalids-mac-mini.tailbd7b77.ts.net"

#!/bin/bash
# DEWA PR Tracker — Start dashboard
cd "$(dirname "$0")"
source venv/bin/activate
echo "Starting DEWA PR Tracker dashboard..."
open http://localhost:8501
streamlit run dashboard.py


Bash

source venv/bin/activate && streamlit run dashboard.py --server.port 8501 --server.address 0.0.0.0 --server.headless true --browser.gatherUsageStats false &
sleep 5 && curl -s -o /dev/null -w "Local: %{http_code}\n" http://localhost:8501

Start Streamlit bound to all interfaces


  You can now view your Streamlit app in your browser.

  URL: http://0.0.0.0:8501

  For better performance, install the Watchdog module:

  $ xcode-select --install
  $ pip install watchdog

Local: 200


curl -s -o /dev/null -w "Tailscale IP: %{http_code}\n" http://100.72.97.47:8501

Verify accessible via Tailscale IP


Tailscale IP: 200


Everything is live. Here's what your team gets:

---

**Share this URL with your team:**
```
https://khalids-mac-mini.tailbd7b77.ts.net
```

| Access method | URL | Who can use it |
|---|---|---|
| **Team link** (Tailscale Funnel) | `https://khalids-mac-mini.tailbd7b77.ts.net` | Anyone you share the link with — no Tailscale needed, works in any browser |
| **Your local** | `http://localhost:8501` | You only |
| **Tailscale VPN** | `http://100.72.97.47:8501` | Team members already on your Tailscale network |

**Important:** The link stays live as long as:
1. Your Mac is on and the Streamlit process is running
2. Tailscale is connected

To start everything in one command from now on:
```bash
cd ~/Projects/dewa-pr-tracker && ./start.sh
```


please make it white theme
mobile firneldy
and regenerate the url link for sharing one finished


Create Streamlit config directory


(Bash completed with no output)


Write

[theme]
base = "light"
primaryColor = "#0077b6"
backgroundColor = "#f8fafc"
secondaryBackgroundColor = "#ffffff"
textColor = "#0f172a"
font = "sans serif"

[server]
port = 8501
address = "0.0.0.0"
headless = true

[browser]
gatherUsageStats = false


File created successfully at: /Users/khalidbinkalban/Projects/dewa-pr-tracker/.streamlit/config.toml

[theme]
base = "light"
primaryColor = "#0077b6"
backgroundColor = "#f8fafc"
secondaryBackgroundColor = "#ffffff"
textColor = "#0f172a"
font = "sans serif"

[server]
port = 8501
address = "0.0.0.0"
headless = true

[browser]
gatherUsageStats = false


Now rewrite the full dashboard with a clean white theme and mobile-first responsive layout:


Now rewrite the full dashboard CSS and layout for light theme + mobile:


1	"""
2	DEWA PR Tracker — Streamlit Dashboard
3	Run:  streamlit run dashboard.py
4	"""
5	import math
6	import re as _re
7	from datetime import datetime, timedelta
8
9	import pandas as pd
10	import plotly.express as px
11	import plotly.graph_objects as go
12	import streamlit as st
13
14	import database as db
15	from config import (
16	    COST_PER_PODCAST_OLD_AED,
17	    NOTEBOOKLM_MONTHLY_USD,
18	    NOTEBOOKLM_START_DATE,
19	    USD_TO_AED,
20	    DASHBOARD_TITLE,
21	    PAGE_SIZE,
22	)
23
24	# ── Page setup ────────────────────────────────────────────────────────────────
25	st.set_page_config(
26	    page_title=DASHBOARD_TITLE,
27	    page_icon="📰",
28	    layout="wide",
29	    initial_sidebar_state="expanded",
30	)
31
32	# ── SVG helpers ───────────────────────────────────────────────────────────────
33	def _svg(paths, size=20, color="#00b4d8"):
34	    return (
35	        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '
36	        f'viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" '
37	        f'stroke-linecap="round" stroke-linejoin="round" '
38	        f'style="vertical-align:middle;margin-right:8px">{paths}</svg>'
39	    )
40
41	ICO_NEWS    = _svg('<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"/>')
42	ICO_MONEY   = _svg('<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>', color="#51cf66")
43	ICO_CHART   = _svg('<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")
44	ICO_LIST    = _svg('<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"/>')
45	ICO_MIC     = _svg('<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")
46	ICO_MIC_SM  = _svg('<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=15, color="#f59e0b")
47	ICO_CLOCK   = _svg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', color="#94a3b8")
48	ICO_LOG     = _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>', color="#94a3b8")
49	ICO_LINK    = _svg('<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=14, color="#00b4d8")
50
51	def section_title(icon, text):
52	    st.markdown(
53	        f'
{icon}{text}
',
55	        unsafe_allow_html=True,
56	    )
57
58	# ── CSS ───────────────────────────────────────────────────────────────────────
59	st.markdown("""
60

"""
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,
    NOTEBOOKLM_START_DATE,
    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",
)

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

ICO_NEWS    = _svg('<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('<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>', color="#51cf66")
ICO_CHART   = _svg('<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('<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('<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_MIC_SM  = _svg('<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=15, color="#f59e0b")
ICO_CLOCK   = _svg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', color="#94a3b8")
ICO_LOG     = _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>', color="#94a3b8")
ICO_LINK    = _svg('<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=14, color="#00b4d8")

def section_title(icon, text):
    st.markdown(
        f'
{icon}{text}
',
        unsafe_allow_html=True,
    )

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


Good — I have the full picture. Let me now write the complete updated dashboard:


"""
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,
    NOTEBOOKLM_START_DATE,
    USD_TO_AED,
    DASHBOARD_TITLE,
    PAGE_SIZE,
)

# ── Page setup ────────────────────────────────────────────────────────────────
st.set_page_config(
    page_title=DASHBOARD_TITLE,
    page_icon="📰",
    layout="wide",
    initial_sidebar_state="collapsed",   # collapsed by default on mobile
)

# ── SVG helpers ───────────────────────────────────────────────────────────────
def _svg(paths, size=16, color="#0077b6"):
    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '
        f'viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" '
        f'stroke-linecap="round" stroke-linejoin="round" '
        f'style="vertical-align:middle;margin-right:6px;flex-shrink:0">{paths}</svg>'
    )

# Icon paths
_P_DOC  = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>'
_P_MIC  = '<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"/>'
_P_COIN = '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>'
_P_BAR  = '<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"/>'
_P_LIST = '<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"/>'
_P_CAL  = '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>'
_P_LINK = '<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"/>'
_P_CLK  = '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>'
_P_WAVE = '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>'
_P_SYNC = '<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"/>'

def section_title(icon_path, text, icon_color="#0077b6"):
    st.markdown(
        f'
'
        f'{_svg(icon_path, size=18, color=icon_color)}{text}
',
        unsafe_allow_html=True,
    )

# ── CSS — light theme + mobile ────────────────────────────────────────────────
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()]
    roi_rows = [dict(r) for r in db.get_roi_monthly_since(NOTEBOOKLM_START_DATE)]
    stats    = db.get_stats()
    log      = [dict(r) for r in db.get_scrape_log()]
    return prs, pods, roi_rows, stats, log

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

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)
if not df_pod.empty:
    df_pod["date"] = pd.to_datetime(df_pod["date"], errors="coerce")

# ── ROI pre-calculations ──────────────────────────────────────────────────────
NOTEBOOKLM_AED = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED
NLM_START      = pd.Timestamp(NOTEBOOKLM_START_DATE)

df_roi = pd.DataFrame(roi_rows) if roi_rows else pd.DataFrame(
    columns=["month","pr_count","en_episodes","ar_episodes","total_episodes"]
)
if not df_roi.empty:
    df_roi["old_cost"] = df_roi["pr_count"] * COST_PER_PODCAST_OLD_AED
    df_roi["new_cost"] = NOTEBOOKLM_AED
    df_roi["savings"]  = df_roi["old_cost"] - df_roi["new_cost"]
    total_prs_nlm  = int(df_roi["pr_count"].sum())
    total_old_cost = float(df_roi["old_cost"].sum())
    total_new_cost = float(df_roi["new_cost"].sum())
    total_savings  = float(df_roi["savings"].sum())
    roi_pct        = (total_savings / total_old_cost * 100) if total_old_cost > 0 else 0
else:
    total_prs_nlm = total_old_cost = total_new_cost = total_savings = roi_pct = 0

# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
    st.markdown(
        '
DEWA
'
        '
Dubai Electricity & Water Authority
',
        unsafe_allow_html=True,
    )
    st.divider()

    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=(NLM_START.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(_P_DOC, size=30, color="#0077b6")}'
    f'
{DASHBOARD_TITLE}
'
    f'
'
    f'
'
    f'NotebookLM adoption since {NOTEBOOKLM_START_DATE}  ·  '
    f'{datetime.today().strftime("%d %B %Y")}
',
    unsafe_allow_html=True,
)
st.markdown("
", unsafe_allow_html=True)

# ═══════════════════════════════════════════════════════════════════════════════
# KPI CARDS
# ═══════════════════════════════════════════════════════════════════════════════
today       = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)
last_month  = (month_start - timedelta(days=1)).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])
    prs_last_month = len(filtered[
        (filtered["date"].dt.date >= last_month) &
        (filtered["date"].dt.date <  month_start)
    ])
else:
    prs_today = prs_week = prs_month = prs_last_month = 0

prs_with_pod = len(filtered[filtered["podcast_count"] > 0]) if not filtered.empty else 0
coverage_pct = (prs_with_pod / len(filtered) * 100) if len(filtered) > 0 else 0
month_delta  = prs_month - prs_last_month
d_cls        = "kpi-delta-pos" if month_delta >= 0 else "kpi-delta-neg"
d_sign       = "+" if month_delta >= 0 else ""

def kpi_card(color, icon_path, label, value, sub="", icon_color=None):
    clr = icon_color or {"blue":"#0077b6","green":"#16a34a","amber":"#d97706","purple":"#7c3aed","rose":"#e11d48"}[color]
    ico = _svg(icon_path, size=13, color=clr)
    return (
        f'
'
        f'
{ico}{label}
'
        f'
{value}
'
        f'{"
" + sub + "
" if sub else ""}'
        f'
'
    )

st.markdown(
    f'
'
    f'{kpi_card("blue",  _P_DOC,  "Total Press Releases", f"{len(filtered):,}", f"since {date_from.strftime(chr(37)+chr(100)+chr(32)+chr(37)+chr(98)+chr(32)+chr(37)+chr(89))}")}'
    f'{kpi_card("green", _P_MIC,  "Podcast Coverage",     f"{coverage_pct:.1f}%", f"{prs_with_pod} of {len(filtered)} PRs", "#16a34a")}'
    f'{kpi_card("amber", _P_CAL,  "This Month",           f"{prs_month:,}", f"{d_sign}{month_delta} vs last month", "#d97706")}'
    f'{kpi_card("purple",_P_WAVE, "This Week",            f"{prs_week:,}", f"since {week_start.strftime(chr(37)+chr(100)+chr(32)+chr(37)+chr(98))}", "#7c3aed")}'
    f'{kpi_card("rose",  _P_CLK,  "Today",               f"{prs_today:,}", today.strftime("%d %B %Y"), "#e11d48")}'
    f'
',
    unsafe_allow_html=True,
)
st.markdown("
", unsafe_allow_html=True)
st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# ROI — NotebookLM
# ═══════════════════════════════════════════════════════════════════════════════
section_title(_P_COIN, "NotebookLM ROI — Since 23 May 2025", "#16a34a")

months_active = len(df_roi)
st.markdown(
    f'
'
    f'
'
    f'
{_svg(_P_CLK, 13, "#e11d48")}Traditional Cost
'
    f'
AED {total_old_cost:,.0f}
'
    f'
{total_prs_nlm} PRs × AED {COST_PER_PODCAST_OLD_AED:,}/podcast
'
    f'
'
    f'
'
    f'
{_svg(_P_MIC, 13, "#16a34a")}NotebookLM Cost
'
    f'
AED {total_new_cost:,.0f}
'
    f'
{months_active} months × AED {NOTEBOOKLM_AED:.0f}/mo (${NOTEBOOKLM_MONTHLY_USD})
'
    f'
'
    f'
'
    f'
{_svg(_P_COIN, 13, "#d97706")}Total Savings
'
    f'
AED {total_savings:,.0f}
'
    f'
Since {NOTEBOOKLM_START_DATE}
'
    f'
'
    f'
'
    f'
{_svg(_P_BAR, 13, "#0077b6")}Return on Investment
'
    f'
{roi_pct:.1f}%
'
    f'
Savings ÷ traditional cost
'
    f'
'
    f'
',
    unsafe_allow_html=True,
)

# ── Charts + table ────────────────────────────────────────────────────────────
if not df_roi.empty:
    chart_col, table_col = st.columns([3, 2], gap="medium")

    PLOT_LAYOUT = dict(
        paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
        font_color="#0f172a", font_size=12,
        legend_title_text="",
        margin=dict(t=40, b=10, l=10, r=10),
        xaxis=dict(showgrid=False, title="", tickfont=dict(size=11)),
        yaxis=dict(gridcolor="#f1f5f9", title="AED", tickfont=dict(size=11)),
    )

    with chart_col:
        df_cl = df_roi[["month","old_cost","new_cost"]].melt(
            id_vars="month", var_name="Type", value_name="AED"
        )
        df_cl["Type"] = df_cl["Type"].map({
            "old_cost": "Traditional (AED 20k/podcast)",
            "new_cost": "NotebookLM ($20/month)"
        })
        fig1 = px.bar(df_cl, x="month", y="AED", color="Type", barmode="group",
            color_discrete_map={
                "Traditional (AED 20k/podcast)": "#fca5a5",
                "NotebookLM ($20/month)":         "#86efac",
            },
            title="Monthly Cost: Traditional vs NotebookLM",
        )
        fig1.update_layout(**PLOT_LAYOUT, height=300)
        st.plotly_chart(fig1, use_container_width=True)

        fig2 = px.bar(df_roi, x="month", y="pr_count",
            color_discrete_sequence=["#93c5fd"],
            title="Press Releases with Podcasts per Month",
        )
        fig2.update_layout(**{**PLOT_LAYOUT, "yaxis": dict(gridcolor="#f1f5f9", title="PRs", tickfont=dict(size=11))}, height=260)
        st.plotly_chart(fig2, use_container_width=True)

    with table_col:
        st.markdown(
            '
'
            'AED 20,000 per PR podcast production (EN + AR = one production unit). '
            'NotebookLM billed as flat $20/month.
',
            unsafe_allow_html=True,
        )
        rows_html = ""
        for _, r in df_roi.iterrows():
            rows_html += (
                f'
'
                f'{r["month"]}	'
                f'{int(r["pr_count"])}	'
                f'{int(r["en_episodes"])}	'
                f'{int(r["ar_episodes"])}	'
                f'AED {r["old_cost"]:,.0f}	'
                f'AED {r["new_cost"]:,.0f}	'
                f'AED {r["savings"]:,.0f}	'
                f'
'
            )
        rows_html += (
            f'
'
            f'TOTAL	'
            f'{total_prs_nlm}	'
            f'{int(df_roi["en_episodes"].sum())}	'
            f'{int(df_roi["ar_episodes"].sum())}	'
            f'AED {total_old_cost:,.0f}	'
            f'AED {total_new_cost:,.0f}	'
            f'AED {total_savings:,.0f}	'
            f'
'
        )
        st.markdown(
            f'
'
            f'

'
            f'Month	PRs	EN	AR	'
            f'Old Cost	New Cost	Savings	'
            f'

{rows_html}

'
            f'
',
            unsafe_allow_html=True,
        )

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# PUBLICATION TRENDS
# ═══════════════════════════════════════════════════════════════════════════════
section_title(_P_BAR, "Publication Trends", "#7c3aed")
tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])

TREND_LAYOUT = dict(
    paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
    font_color="#0f172a", margin=dict(t=36,b=10,l=10,r=10),
    xaxis=dict(showgrid=False, title=""),
    yaxis=dict(gridcolor="#f1f5f9"),
)

def trend_bar(df_g, x, y, title, color):
    fig = px.bar(df_g, x=x, y=y, title=title, color_discrete_sequence=[color])
    fig.update_layout(**TREND_LAYOUT)
    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(trend_bar(daily,"date","count","Press Releases per Day","#93c5fd"), use_container_width=True)
    else:
        st.info("No data yet.")

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(trend_bar(weekly,"week","count","Press Releases per Week","#a5b4fc"), 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(trend_bar(monthly,"month","count","Press Releases per Month","#c4b5fd"), use_container_width=True)
    else:
        st.info("No data yet.")

st.divider()

# ═══════════════════════════════════════════════════════════════════════════════
# PRESS RELEASES TABLE
# ═══════════════════════════════════════════════════════════════════════════════
section_title(_P_LIST, "Press Releases", "#0077b6")

search = st.text_input("Search", placeholder="Filter press releases by title…", label_visibility="collapsed")
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, end  = (page_num-1)*PAGE_SIZE, page_num*PAGE_SIZE
page_data   = display.iloc[start:end]

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

for _, row in page_data.iterrows():
    url   = row.get("url","")
    title = row.get("title","—")
    d     = row.get("date")
    date_str = d.strftime("%d %b %Y") if pd.notna(d) else "—"
    desc  = str(row.get("description") or "")
    desc_short = (desc[:150]+"…") if len(desc)>150 else desc
    pod_badge = (
        f'{_svg(_P_MIC,11,"#c2410c")} Podcast'
        if row.get("podcast_count",0) else ""
    )
    st.markdown(
        f'
'
        f'
'
        f'{title}'
        f'
'
        f'{date_str}{pod_badge}'
        f'

'
        f'{"
" + desc_short + "
" if desc_short else ""}'
        f'
',
        unsafe_allow_html=True,
    )

# ═══════════════════════════════════════════════════════════════════════════════
# PODCASTS
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
section_title(_P_MIC, "Podcasts", "#d97706")

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

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

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

    for _, pod in df_show.head(50).iterrows():
        with st.expander(f"{pod.get('title','—')}  ·  {pod.get('date','')}"):
            ca, cb = st.columns([3,1])
            with ca:
                if pod.get("pr_title"):
                    st.markdown(f"**Linked PR:** {pod['pr_title']}")
                if pod.get("url"):
                    st.markdown(f"[Open on RSS.com]({pod['url']})")
                if pod.get("audio_url"):
                    st.audio(pod["audio_url"])
            with cb:
                if pod.get("duration"):
                    st.metric("Duration", pod["duration"])
                st.metric("Saved", f"AED {COST_PER_PODCAST_OLD_AED:,}")

# ═══════════════════════════════════════════════════════════════════════════════
# SCRAPE LOG
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
with st.expander("Scrape history"):
    if scrape_log:
        st.dataframe(
            pd.DataFrame(scrape_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,
)