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

+    initial_sidebar_state="collapsed",   # collapsed by default on mobile

# ── SVG helpers ───────────────────────────────────────────────────────────────

-def _svg(paths, size=20, color="#00b4d8"):

+def _svg(paths, size=16, color="#0077b6"):

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'style="vertical-align:middle;margin-right:8px">{paths}</svg>'

+        f'style="vertical-align:middle;margin-right:6px;flex-shrink:0">{paths}</svg>'

+def section_title(icon_path, text, icon_color="#0077b6"):

-        f'<h3 style="display:flex;align-items:center;color:#e2e8f0;'

-        f'margin-bottom:.25rem;margin-top:1rem">{icon}{text}</h3>',

+        f'<h3 style="display:flex;align-items:center;color:#0f172a;'

+        f'margin-bottom:.5rem;margin-top:1.5rem;font-size:1.1rem;font-weight:700">'

+        f'{_svg(icon_path, size=18, color=icon_color)}{text}</h3>',

-# ── CSS ───────────────────────────────────────────────────────────────────────

+# ── CSS — light theme + mobile ────────────────────────────────────────────────

-    header, #MainMenu, footer      { display:none !important; visibility:hidden !important; }

+    header, #MainMenu, footer { display:none !important; visibility:hidden !important; }

-    body, .stApp                   { background:#0a0f1a; }

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

-    p, li, label                   { color:#94a3b8; }

-    .stDivider                     { border-color:#1e293b !important; }

+    html, body, .stApp        { background:#f0f4f8 !important; font-family:'Inter',sans-serif; }

+    h1,h2,h3,h4               { color:#0f172a !important; }

+    .stDivider                { border-color:#e2e8f0 !important; }

+    .block-container           { padding-top:1.5rem !important; padding-bottom:3rem !important; }

-    [data-testid="stSidebar"]      { background:#0d1424 !important; border-right:1px solid #1e293b; }

-    [data-testid="stSidebar"] *    { color:#94a3b8 !important; }

-    [data-testid="stSidebar"] .stButton > button {

-        background:#1e293b; border:1px solid #334155; color:#e2e8f0 !important;

-        border-radius:8px; transition:all .2s;

+    [data-testid="stSidebar"]                   { background:#ffffff !important; border-right:1px solid #e2e8f0; }

+    [data-testid="stSidebar"] label             { color:#64748b !important; font-size:.85rem; }

+    [data-testid="stSidebar"] .stButton>button  {

+        background:#f1f5f9; border:1px solid #e2e8f0; color:#0f172a;

+        border-radius:8px; font-weight:500; transition:all .15s; width:100%;

-    [data-testid="stSidebar"] .stButton > button:hover { background:#334155; border-color:#00b4d8; }

+    [data-testid="stSidebar"] .stButton>button:hover { background:#e0f2fe; border-color:#0077b6; color:#0077b6; }

-    /* ── KPI cards (custom HTML) ── */

+        grid-template-columns: repeat(5, 1fr);

+    @media (max-width:900px)  { .kpi-grid { grid-template-columns: repeat(3,1fr); } }

+    @media (max-width:600px)  { .kpi-grid { grid-template-columns: repeat(2,1fr); } }

+        transition:box-shadow .2s, border-color .2s;

-    .kpi-card:hover             { border-color:#334155; }

-        position:absolute; left:0; top:0; bottom:0;

-    .kpi-card.blue::before      { background:#00b4d8; }

-    .kpi-card.green::before     { background:#22c55e; }

-    .kpi-card.amber::before     { background:#f59e0b; }

-    .kpi-card.purple::before    { background:#a855f7; }

-    .kpi-card.red::before       { background:#f43f5e; }

+    .kpi-card:hover              { box-shadow:0 4px 12px rgba(0,0,0,.1); border-color:#bfdbfe; }

+    .kpi-card::before            { content:""; position:absolute; top:0; left:0; right:0; height:3px; border-radius:14px 14px 0 0; }

+    .kpi-card.blue::before       { background:#0077b6; }

+    .kpi-card.green::before      { background:#16a34a; }

+    .kpi-card.amber::before      { background:#d97706; }

+    .kpi-card.purple::before     { background:#7c3aed; }

+    .kpi-card.rose::before       { background:#e11d48; }

-        font-size:.72rem; font-weight:600; letter-spacing:.06em;

-        text-transform:uppercase; color:#64748b; margin-bottom:6px;

-        display:flex; align-items:center; gap:6px;

+        font-size:.68rem; font-weight:700; letter-spacing:.06em;

+        text-transform:uppercase; color:#94a3b8;

+        display:flex; align-items:center; gap:4px; margin-bottom:8px;

-        font-size:2rem; font-weight:800; color:#f1f5f9; line-height:1;

-    .kpi-sub   { font-size:.72rem; color:#475569; margin-top:5px; }

+    .kpi-value { font-size:1.9rem; font-weight:800; color:#0f172a; line-height:1; }

+    .kpi-sub   { font-size:.7rem; color:#94a3b8; margin-top:6px; line-height:1.4; }

+    .kpi-delta-pos { color:#16a34a; font-weight:600; }

+    .kpi-delta-neg { color:#e11d48; font-weight:600; }

+        grid-template-columns:repeat(4,1fr);

+    @media (max-width:900px)  { .roi-grid { grid-template-columns:repeat(2,1fr); } }

+    @media (max-width:500px)  { .roi-grid { grid-template-columns:1fr; } }

-        background:#111827; border:1px solid #1e293b;

-        border-radius:14px; padding:20px 22px;

-    .cost-old  { color:#f43f5e; font-size:1.6rem; font-weight:800; }

-    .cost-new  { color:#22c55e; font-size:1.6rem; font-weight:800; }

-    .savings   { color:#fbbf24; font-size:2rem;   font-weight:800; }

-    .roi-pct   { color:#00b4d8; font-size:2rem;   font-weight:800; }

+    .roi-card-label { font-size:.68rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#94a3b8; margin-bottom:8px; display:flex; align-items:center; gap:5px; }

+    .cost-old  { font-size:1.5rem; font-weight:800; color:#e11d48; }

+    .cost-new  { font-size:1.5rem; font-weight:800; color:#16a34a; }

+    .savings   { font-size:1.5rem; font-weight:800; color:#d97706; }

+    .roi-pct   { font-size:1.5rem; font-weight:800; color:#0077b6; }

+    .roi-sub   { font-size:.72rem; color:#94a3b8; margin-top:6px; line-height:1.5; }

-    table.roi-table { width:100%; border-collapse:collapse; font-size:.83rem; }

+    .tbl-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; }

+    table.roi-table { width:100%; border-collapse:collapse; font-size:.82rem; min-width:480px; }

-        background:#0a0f1a; color:#64748b; font-weight:600;

-        padding:9px 12px; text-align:right; border-bottom:1px solid #1e293b;

-        letter-spacing:.04em; text-transform:uppercase; font-size:.72rem;

+        background:#f8fafc; color:#64748b; font-weight:700;

+        padding:10px 12px; text-align:right;

+        border-bottom:2px solid #e2e8f0;

+        font-size:.68rem; letter-spacing:.05em; text-transform:uppercase;

table.roi-table th:first-child { text-align:left; }

-        padding:8px 12px; text-align:right;

-        border-bottom:1px solid #111827; color:#94a3b8;

+        padding:9px 12px; text-align:right;

+        border-bottom:1px solid #f1f5f9; color:#475569;

-    table.roi-table td:first-child  { text-align:left; color:#e2e8f0; font-weight:600; }

-    table.roi-table tr:hover td     { background:#1e293b66; }

+    table.roi-table td:first-child  { text-align:left; color:#0f172a; font-weight:600; }

+    table.roi-table tr:hover td     { background:#f8fafc; }

-        background:#0a0f1a; color:#fbbf24; font-weight:800;

-        border-top:2px solid #334155; font-size:.9rem;

+        background:#f0f9ff; color:#0077b6; font-weight:800;

+        border-top:2px solid #bae6fd; font-size:.88rem;

+    table.roi-table tr.total-row td:nth-child(5) { color:#e11d48; }

+    table.roi-table tr.total-row td:nth-child(6) { color:#16a34a; }

+    table.roi-table tr.total-row td:nth-child(7) { color:#d97706; }

-        font-size:1.8rem; font-weight:900; letter-spacing:.1em;

-        background:linear-gradient(90deg,#00b4d8,#0284c7);

-        -webkit-background-clip:text; -webkit-text-fill-color:transparent;

+        background:#ffffff; border:1px solid #e2e8f0; border-radius:12px;

+        padding:14px 16px; margin-bottom:8px;

-    .dewa-logo-sub { font-size:.6rem; color:#334155; letter-spacing:.1em; text-transform:uppercase; margin-top:2px; }

+    .pr-row:hover { box-shadow:0 2px 8px rgba(0,0,0,.08); }

+    .pr-title   { font-size:.95rem; font-weight:600; color:#0f172a; text-decoration:none; }

+    .pr-desc    { font-size:.78rem; color:#94a3b8; margin-top:3px; line-height:1.5; }

+    .pr-date    { font-size:.75rem; color:#64748b; font-weight:500; white-space:nowrap; }

+        display:inline-flex; align-items:center; gap:4px;

+        background:#fff7ed; color:#c2410c; border:1px solid #fed7aa;

+        border-radius:20px; padding:2px 10px; font-size:.7rem; font-weight:600;

+        background:#ffffff; border:1px solid #e2e8f0 !important;

+        border-radius:12px; margin-bottom:6px;

+    [data-testid="stExpander"] summary { color:#0f172a !important; font-weight:500; }

+    .js-plotly-plot .plotly { border-radius:12px; }

-    [data-testid="stTabs"] button          { color:#64748b !important; font-weight:500; }

-    [data-testid="stTabs"] button[aria-selected="true"] { color:#00b4d8 !important; border-bottom-color:#00b4d8 !important; }

+    [data-testid="stTabs"] [role="tab"]                       { color:#64748b !important; font-weight:500; font-size:.9rem; }

+    [data-testid="stTabs"] [role="tab"][aria-selected="true"] { color:#0077b6 !important; font-weight:700; }

-    [data-testid="stExpander"]             { background:#111827; border:1px solid #1e293b; border-radius:10px; }

-    [data-testid="stExpander"] summary     { color:#94a3b8 !important; }

+    .dewa-logo { font-size:1.5rem; font-weight:900; letter-spacing:.1em; color:#0077b6; }

+    .dewa-logo-sub { font-size:.6rem; color:#94a3b8; letter-spacing:.1em; text-transform:uppercase; margin-top:1px; }

+    /* ── Mobile: collapse sidebar toggle hint ── */

+        .block-container { padding-left:1rem !important; padding-right:1rem !important; }

+        .kpi-value       { font-size:1.5rem; }

+        .cost-old, .cost-new, .savings, .roi-pct { font-size:1.2rem; }

lambda t: "ar" if _re.search(r"[\u0600-\u06FF]", str(t)) else "en"

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

# ── ROI pre-calculations ──────────────────────────────────────────────────────

-NOTEBOOKLM_AED   = NOTEBOOKLM_MONTHLY_USD * USD_TO_AED   # ~73.4 AED/month

-NLM_START        = pd.Timestamp(NOTEBOOKLM_START_DATE)

+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", "total", "en_count", "ar_count"]

+    columns=["month","pr_count","en_episodes","ar_episodes","total_episodes"]

-    # 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["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_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

+    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

-    total_prs_nlm = total_eps_nlm = total_old_cost = total_new_cost = total_savings = roi_pct = 0

+    total_prs_nlm = total_old_cost = total_new_cost = total_savings = roi_pct = 0

# ── Sidebar ───────────────────────────────────────────────────────────────────

-    # DEWA logo — styled text (image blocked externally)

'
Dubai Electricity & Water Authority
',

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

+        min_value=min_date, max_value=max_date,

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

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

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

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

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

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

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

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

-# ── Apply PR filters ──────────────────────────────────────────────────────────

+# ── Apply filters ─────────────────────────────────────────────────────────────

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

-    f'<p style="color:#64748b;margin-top:4px;margin-bottom:1rem">'

-    f'Tracking all DEWA press releases and NotebookLM podcast ROI since {NOTEBOOKLM_START_DATE} — '

+    f'<div style="display:flex;align-items:center;gap:12px;margin-bottom:2px">'

+    f'<p style="color:#94a3b8;font-size:.82rem;margin:4px 0 0 38px">'

+    f'NotebookLM adoption since {NOTEBOOKLM_START_DATE}  ·  '

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

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

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

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

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

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"

+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(color, icon_path, label, value, sub=""):

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

+def kpi_card(color, icon_path, label, value, sub="", icon_color=None):

+    ico = _svg(icon_path, size=13, color=clr)

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

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

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

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

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

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

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

+    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")}'

-# ROW 2 — NotebookLM ROI  (auto-calculated from May 23, 2025)

-section_title(ICO_MONEY, "NotebookLM ROI — Since 23 May 2025")

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

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

-        f'
Traditional Cost (if no NotebookLM)
'

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

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

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

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

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

-        f'
Since {NOTEBOOKLM_START_DATE}
'

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

-        f'
Savings ÷ traditional cost
'

+    f'<div class="roi-card" style="border-top:3px solid #fecdd3">'

+    f'
{_svg(_P_CLK, 13, "#e11d48")}Traditional Cost
'

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

+    f'<div class="roi-card" style="border-top:3px solid #bbf7d0">'

+    f'
{_svg(_P_MIC, 13, "#16a34a")}NotebookLM Cost
'

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

+    f'<div class="roi-card" style="border-top:3px solid #fde68a">'

+    f'
{_svg(_P_COIN, 13, "#d97706")}Total Savings
'

+    f'<div class="roi-card" style="border-top:3px solid #bae6fd">'

+    f'
{_svg(_P_BAR, 13, "#0077b6")}Return on Investment
'

-# ── Monthly detail ────────────────────────────────────────────────────────────

+# ── Charts + table ────────────────────────────────────────────────────────────

-    chart_col, table_col = st.columns([3, 2])

+    chart_col, table_col = st.columns([3, 2], gap="medium")

+        paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",

+        font_color="#0f172a", font_size=12,

+        xaxis=dict(showgrid=False, title="", tickfont=dict(size=11)),

+        yaxis=dict(gridcolor="#f1f5f9", title="AED", tickfont=dict(size=11)),

-        # Grouped bar: old vs new cost per month

-        df_chart = df_roi[["month", "old_cost", "new_cost"]].copy()

-            value_vars=["old_cost", "new_cost"],

-            var_name="Type", value_name="AED",

+        df_cl = df_roi[["month","old_cost","new_cost"]].melt(

+            id_vars="month", 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)"}

-            df_chart_long, x="month", y="AED", color="Type", barmode="group",

+        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",

-                "Traditional (AED 20k/podcast)": "#ff6b6b",

+                "Traditional (AED 20k/podcast)": "#fca5a5",

+                "NotebookLM ($20/month)":         "#86efac",

-            title="Monthly Cost: Traditional Podcast Production vs NotebookLM",

+            title="Monthly Cost: Traditional vs NotebookLM",

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

+        fig1.update_layout(**PLOT_LAYOUT, height=300)

+        st.plotly_chart(fig1, use_container_width=True)

-            df_roi, x="month", y="pr_count",

-            color_discrete_sequence=["#f59e0b"],

-            title="Press Releases with Podcasts per Month (since NotebookLM)",

+        fig2 = px.bar(df_roi, x="month", y="pr_count",

+            color_discrete_sequence=["#93c5fd"],

+            title="Press Releases with Podcasts per Month",

-            yaxis=dict(gridcolor="#1e293b", title="Press Releases"),

-            margin=dict(t=40, b=10), height=280,

-        st.plotly_chart(fig_count, use_container_width=True)

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

-            '<p style="color:#94a3b8;font-size:.8rem;margin-bottom:.5rem">'

-            'Cost basis: AED 20,000 per PR podcast (EN+AR counted as one production).<br>'

-            'NotebookLM: flat $20/month regardless of volume.</p>',

+            '<p style="font-size:.78rem;color:#94a3b8;margin-bottom:8px">'

+            'AED 20,000 per PR podcast production (EN + AR = one production unit). '

+            'NotebookLM billed as flat $20/month.</p>',

-                f'AED {r["savings"]:,.0f}	'

+                f'AED {r["savings"]:,.0f}	'

-            f'<table class="roi-table">'

-            f'Month	PRs	EN eps	AR eps	'

+            f'<table class="roi-table"><thead><tr>'

-            f'<tbody>{rows_html}</tbody>'

+section_title(_P_BAR, "Publication Trends", "#7c3aed")

tab_daily, tab_weekly, tab_monthly = st.tabs(["Daily", "Weekly", "Monthly"])

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

+    paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",

+    xaxis=dict(showgrid=False, title=""),

+def trend_bar(df_g, x, y, title, color):

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

-        paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",

-        xaxis=dict(showgrid=False, title=""),

-        yaxis=dict(gridcolor="#1e293b"),

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

-        daily.columns = ["date", "count"]

-        st.plotly_chart(make_bar(daily, "date", "count", "Press Releases per Day"), use_container_width=True)

+        daily.columns = ["date","count"]

+        st.plotly_chart(trend_bar(daily,"date","count","Press Releases per Day","#93c5fd"), use_container_width=True)

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

-        st.plotly_chart(make_bar(weekly, "week", "count", "Press Releases per Week", "#4cc9f0"), use_container_width=True)

+        st.plotly_chart(trend_bar(weekly,"week","count","Press Releases per Week","#a5b4fc"), use_container_width=True)

-        st.plotly_chart(make_bar(monthly, "month", "count", "Press Releases per Month", "#7209b7"), use_container_width=True)

+        st.plotly_chart(trend_bar(monthly,"month","count","Press Releases per Month","#c4b5fd"), use_container_width=True)

+section_title(_P_LIST, "Press Releases", "#0077b6")

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

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

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

-start       = (page_num - 1) * PAGE_SIZE

+start, end  = (page_num-1)*PAGE_SIZE, page_num*PAGE_SIZE

-    f"Showing {start+1}–{min(end, len(display))} of {len(display)} press releases "

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

-        tc, dc, pc = st.columns([5, 2, 1])

-            title = row.get("title", "—")

-            st.markdown(f"**[{title}]({url})**" if url else f"**{title}**")

-            desc = str(row.get("description") or "")

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

-            st.markdown(f"`{d.strftime('%d %b %Y') if pd.notna(d) else '—'}`")

-            pc_count = row.get("podcast_count", 0)

-                ICO_MIC_SM + f" {pc_count}" if pc_count else "—",

-                unsafe_allow_html=True,

+    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

+        if row.get("podcast_count",0) else ""

+        f'<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap">'

+        f'<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">'

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

-    lang_filter = st.radio("Language", ["All", "English", "Arabic"], horizontal=True)

+    lang_filter = st.radio("Language", ["All","English","Arabic"], horizontal=True)

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

+        df_show = df_show[df_show["language"]=="en"]

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

+        df_show = df_show[df_show["language"]=="ar"]

-    pod_search = st.text_input("Search podcasts", placeholder="Filter by title…")

-            df_pod_show["title"].str.contains(pod_search, case=False, na=False)

+    pod_q = st.text_input("Search podcasts", placeholder="Filter by title…", label_visibility="collapsed")

+        df_show = df_show[df_show["title"].str.contains(pod_q, case=False, na=False)]

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

+    st.caption(f"Showing {len(df_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])

+    for _, pod in df_show.head(50).iterrows():

+        with st.expander(f"{pod.get('title','—')}  ·  {pod.get('date','')}"):

-                    st.markdown(ICO_LINK + f" **Linked PR:** {pod['pr_title']}", unsafe_allow_html=True)

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

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

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

-        df_log = pd.DataFrame(scrape_log)

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

+            pd.DataFrame(scrape_log)[["ran_at","prs_found","pods_found","status","notes"]],

-    '<p style="color:#1e293b;text-align:center;font-size:.75rem;margin-top:2rem">'

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

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

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

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

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


Bash

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

Syntax check


OK


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

Restart dashboard


  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

200


Confirm Funnel is active


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

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


Live. Share this link with your team:

```
https://khalids-mac-mini.tailbd7b77.ts.net
```

**What changed:**

**Light theme**
- White cards on `#f0f4f8` background
- All text in dark slate, muted labels in gray
- Soft pastel bar chart colors (light red, light green, light blue)
- Clean borders and subtle shadows instead of dark outlines

**Mobile friendly**
- KPI cards use CSS Grid — **5 columns on desktop → 3 on tablet → 2 on phone**
- ROI cards: **4 columns → 2 on tablet → 1 on phone**
- ROI table wrapped in a scrollable container so it doesn't break on small screens
- Sidebar collapses by default on mobile (tap the `>` arrow to open)
- Font sizes scale down on small screens
- Press release rows stack gracefully (title + date + badge wrap cleanly)

**Other improvements**
- PR rows are now styled cards with hover shadow — title is a direct clickable link
- Podcast badge (orange pill) shows inline next to the date in each PR row
- Streamlit config moved to `.streamlit/config.toml` so theme is enforced system-wide


the data will be refereshed and fitched from the source everytime?


**No** — opening the dashboard never triggers a scrape. Here's exactly what happens each time:

```
User opens dashboard
        │
        ▼
  Read from SQLite DB (local file)   ← fast, ~50ms
        │
        ▼
  st.cache_data (5 min memory cache) ← even faster on repeat visits
        │
        ▼
  Render charts + tables
```

The DEWA website is only ever touched in **two situations:**

| When | What happens |
|---|---|
| You click **"Run scraper now"** | Playwright opens Chromium, scrapes DEWA + RSS feed, saves to DB |
| **Scheduler runs** at 08:00 daily | Same as above, automatically |

---

**Right now the scheduler is NOT running** — it only runs while `python scheduler.py` is active in a terminal. So the data is currently from the last manual scrape.

To make it truly automatic, run the scheduler as a background service so it survives reboots:

```bash
# One-time setup — registers it as a macOS LaunchAgent
cd ~/Projects/dewa-pr-tracker
```

Want me to set that up as a proper macOS background service (LaunchAgent) so the daily 08:00 scrape runs automatically even after restarts?


make a count down for daily referesh


I'll inject a JavaScript countdown in the sidebar that ticks live to the next 08:00 Dubai time — no page reload needed:


130	    @media (max-width:900px)  { .roi-grid { grid-template-columns:repeat(2,1fr); } }
131	    @media (max-width:500px)  { .roi-grid { grid-template-columns:1fr; } }
132
133	    .roi-card {
134	        background:#ffffff;
135	        border:1px solid #e2e8f0;
136	        border-radius:14px;
137	        padding:18px 20px;
138	        box-shadow:0 1px 3px rgba(0,0,0,.06);
139	    }
140	    .roi-card-label { font-size:.68rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#94a3b8; margin-bottom:8px; display:flex; align-items:center; gap:5px; }
141	    .cost-old  { font-size:1.5rem; font-weight:800; color:#e11d48; }
142	    .cost-new  { font-size:1.5rem; font-weight:800; color:#16a34a; }
143	    .savings   { font-size:1.5rem; font-weight:800; color:#d97706; }
144	    .roi-pct   { font-size:1.5rem; font-weight:800; color:#0077b6; }

    @media (max-width:900px)  { .roi-grid { grid-template-columns:repeat(2,1fr); } }
    @media (max-width:500px)  { .roi-grid { grid-template-columns:1fr; } }

    .roi-card {
        background:#ffffff;
        border:1px solid #e2e8f0;
        border-radius:14px;
        padding:18px 20px;
        box-shadow:0 1px 3px rgba(0,0,0,.06);
    }
    .roi-card-label { font-size:.68rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#94a3b8; margin-bottom:8px; display:flex; align-items:center; gap:5px; }
    .cost-old  { font-size:1.5rem; font-weight:800; color:#e11d48; }
    .cost-new  { font-size:1.5rem; font-weight:800; color:#16a34a; }
    .savings   { font-size:1.5rem; font-weight:800; color:#d97706; }
    .roi-pct   { font-size:1.5rem; font-weight:800; color:#0077b6; }


204	    [data-testid="stTabs"] [role="tab"][aria-selected="true"] { color:#0077b6 !important; font-weight:700; }
205
206	    /* ── DEWA logo ── */
207	    .dewa-logo { font-size:1.5rem; font-weight:900; letter-spacing:.1em; color:#0077b6; }
208	    .dewa-logo-sub { font-size:.6rem; color:#94a3b8; letter-spacing:.1em; text-transform:uppercase; margin-top:1px; }
209
210	    /* ── Mobile: collapse sidebar toggle hint ── */
211	    @media (max-width:768px) {
212	        .block-container { padding-left:1rem !important; padding-right:1rem !important; }
213	        .kpi-value       { font-size:1.5rem; }
214	        .cost-old, .cost-new, .savings, .roi-pct { font-size:1.2rem; }
215	    }
216	</style>
217	""", unsafe_allow_html=True)
218
219
220	# ── Data loading (cached 5 min) ───────────────────────────────────────────────
221	@st.cache_data(ttl=300)
222	def load_data():
223	    db.init_db()

    [data-testid="stTabs"] [role="tab"][aria-selected="true"] { color:#0077b6 !important; font-weight:700; }

    /* ── DEWA logo ── */
    .dewa-logo { font-size:1.5rem; font-weight:900; letter-spacing:.1em; color:#0077b6; }
    .dewa-logo-sub { font-size:.6rem; color:#94a3b8; letter-spacing:.1em; text-transform:uppercase; margin-top:1px; }

    /* ── Mobile: collapse sidebar toggle hint ── */
    @media (max-width:768px) {
        .block-container { padding-left:1rem !important; padding-right:1rem !important; }
        .kpi-value       { font-size:1.5rem; }
        .cost-old, .cost-new, .savings, .roi-pct { font-size:1.2rem; }
    }
</style>
""", unsafe_allow_html=True)

# ── Data loading (cached 5 min) ───────────────────────────────────────────────
@st.cache_data(ttl=300)
def load_data():
    db.init_db()


270	        '
DEWA
'
271	        '
Dubai Electricity & Water Authority
',
272	        unsafe_allow_html=True,
273	    )
274	    st.divider()
275
276	    if not df_pr.empty and df_pr["date"].notna().any():
277	        min_date = df_pr["date"].min().date()
278	        max_date = df_pr["date"].max().date()
279	    else:
280	        min_date = datetime(2019, 1, 1).date()
281	        max_date = datetime.today().date()
282
283	    date_from, date_to = st.date_input(
284	        "Date range",
285	        value=(NLM_START.date(), max_date),
286	        min_value=min_date, max_value=max_date,
287	    )
288	    categories = ["All"]
289	    if not df_pr.empty and "category" in df_pr.columns:

'
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:


302	                from scraper import run_scrape
303	                prs_n, pods_n = run_scrape()
304	                st.success(f"Done — {prs_n} PRs, {pods_n} podcasts saved.")
305	                st.cache_data.clear()
306	                st.rerun()
307	            except Exception as e:
308	                st.error(f"Scraper error: {e}")
309	    st.divider()
310	    st.caption(f"Last scraped: {stats.get('last_scraped','Never') or 'Never'}")
311
312
313	# ── Apply filters ─────────────────────────────────────────────────────────────
314	filtered = df_pr.copy()
315	if not filtered.empty:
316	    if filtered["date"].notna().any():
317	        filtered = filtered[
318	            (filtered["date"].dt.date >= date_from) &
319	            (filtered["date"].dt.date <= date_to)
320	        ]
321	    if selected_cat != "All":
322	        filtered = filtered[filtered["category"] == selected_cat]
323	    if has_podcast_filter:
324	        filtered = filtered[filtered["podcast_count"] > 0]
325
326

                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]


Edit

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

st.divider()

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

    # ── Live countdown to next 08:00 Dubai time ───────────────────────────────
    st.markdown("""

    Next auto-refresh

    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
      fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
    </svg>
    --:--:--

Daily at 08:00 Dubai time

""", unsafe_allow_html=True)


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

st.divider()

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

    # ── Live countdown to next 08:00 Dubai time ───────────────────────────────
    st.markdown("""

    Next auto-refresh

    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
      fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
    </svg>
    --:--:--

Daily at 08:00 Dubai time

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

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

+    last_scraped = stats.get('last_scraped','Never') or 'Never'

+    st.caption(f"Last scraped: {last_scraped}")

+    # ── Live countdown to next 08:00 Dubai time ───────────────────────────────

+<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:10px;padding:12px 14px;margin-top:8px">

+  <div style="display:flex;align-items:center;gap:6px">

+    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"

+      fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">

+    var dubai = new Date(now.toLocaleString("en-US", {timeZone:"Asia/Dubai"}));

+    if (dubai >= next) next.setDate(next.getDate() + 1);   // already past 08:00 today

+    return next - dubai;   // ms remaining

+    return [h,m,sc].map(function(v){return String(v).padStart(2,'0');}).join(':');

+    var el = document.getElementById('cd-timer');


Bash

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

Restart dashboard


Done. At the bottom of the sidebar you'll now see a live countdown widget:

```
NEXT AUTO-REFRESH
⏱  14:32:07
Daily at 08:00 Dubai time
```

**How it works:**
- Pure JavaScript running in the browser — ticks every second with no server calls
- Calculates remaining time to the next **08:00 Asia/Dubai (UTC+4)** — correct even for team members in different time zones
- Turns **red** when under 5 minutes away
- Resets automatically after 08:00 passes and counts down to the next day


how many lines so far?


find /Users/khalidbinkalban/Projects/dewa-pr-tracker -name "*.py" -o -name "*.sh" -o -name "*.toml" | grep -v venv | xargs wc -l

Count lines of code