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

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

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

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

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

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

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

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

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

""", unsafe_allow_html=True)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

st.divider()

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

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

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

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

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

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

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

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

st.divider()

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

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

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

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

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

st.divider()

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

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

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

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

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

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

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

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

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

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

st.divider()

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

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

-                    unsafe_allow_html=True,

+                st.markdown(ICO_PODCAST + f" {pc_count}", unsafe_allow_html=True)


Bash

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

Syntax check dashboard again


<persisted-output>
Output too large (480.8KB). Full output saved to: /Users/khalidbinkalban/.claude/projects/-Users-khalidbinkalban/7e09fdd0-3bf7-4fd4-953f-b8434ec56a57/tool-results/b7va7ogpk.txt

Preview (first 2KB):
2026-04-04 08:59:25.683 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.683 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.811 WARNING streamlit:
  Warning: to view a Streamlit app on a browser, use Streamlit in a file and
  run it with the following command:

    streamlit run [FILE_NAME] [ARGUMENTS]
2026-04-04 08:59:25.811 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.811 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.811 WARNING streamlit.runtime.caching.cache_data_api: No runtime found, using MemoryCacheStorageManager
2026-04-04 08:59:25.812 WARNING streamlit.runtime.caching.cache_data_api: No runtime found, using MemoryCacheStorageManager
2026-04-04 08:59:25.812 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.812 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.812 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.813 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
...
</persisted-output>

2026-04-04 08:59:25.683 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.683 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.811 WARNING streamlit:
  Warning: to view a Streamlit app on a browser, use Streamlit in a file and
  run it with the following command:

    streamlit run [FILE_NAME] [ARGUMENTS]
2026-04-04 08:59:25.811 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.811 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.811 WARNING streamlit.runtime.caching.cache_data_api: No runtime found, using MemoryCacheStorageManager
2026-04-04 08:59:25.812 WARNING streamlit.runtime.caching.cache_data_api: No runtime found, using MemoryCacheStorageManager
2026-04-04 08:59:25.812 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.812 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.812 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.813 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.832 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.832 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.832 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.842 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.843 WARNING streamlit.runtime.state.session_state_proxy: Session state does not function when running a script without `streamlit run`
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.844 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.845 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.846 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.847 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be ignored when running in bare mode.
2026-04-04 08:59:25.858 WARNING streamlit.runtime.scriptrunner_utils.script_run_context: Thread 'MainThread': missing ScriptRunContext! This warning can be igno


No syntax errors — just Streamlit context warnings (expected outside browser). Now kill the old process and restart:


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

Restart Streamlit dashboard


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

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

  You can now view your Streamlit app in your browser.

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

  For better performance, install the Watchdog module:

  $ xcode-select --install
  $ pip install watchdog

200


Dashboard is back up at **http://localhost:8501**. Two things fixed:

1. **Crash fixed** — `max_value` on the podcast input is now dynamically set to `max(total_podcasts, 1000)` so it never crashes regardless of how many episodes exist
2. **Emojis replaced** — all section headers, buttons, and inline indicators now use inline SVG icons (Feather-style):
   - Header — newspaper SVG
   - ROI Calculator — coin/circle SVG (green)
   - Publication Trends — bar chart SVG (blue)
   - Press Releases — list SVG
   - Podcasts — microphone SVG (amber)
   - Podcast count in PR rows — inline mic SVG


Just make the calculation since May 23, 2025 when DEWA announced to use NotebookLM in their all press release podcasts

Check how many podcasts are compeleted monthly and do the details analysis taking into consideration before each podcast costs dewa 20,000 AED while consider the subscription of NotebookLM monthly and consider this the cost saving where we can check the total cost saving since we deployed NotebookLM

I can see in the top right corner something called deploy

and DEWA logo is not showing


source venv/bin/activate && python3 -c "
import database as db
import pandas as pd
db.init_db()
pods = [dict(r) for r in db.get_all_podcasts()]
df = pd.DataFrame(pods)
df['date'] = pd.to_datetime(df['date'], errors='coerce')
import re
df['language'] = df['title'].apply(lambda t: 'ar' if re.search(r'[\u0600-\u06FF]', str(t)) else 'en')

# Since NotebookLM launch
start = pd.Timestamp('2025-05-23')
df_nlm = df[df['date'] >= start].copy()
print('Total podcasts since May 23, 2025:', len(df_nlm))
print('English:', len(df_nlm[df_nlm.language=='en']))
print('Arabic:', len(df_nlm[df_nlm.language=='ar']))

df_nlm['month'] = df_nlm['date'].dt.to_period('M').astype(str)
monthly = df_nlm.groupby('month').size().reset_index(name='count')
print()
print(monthly.to_string())
"

Check podcast counts by month since May 23 2025


Total podcasts since May 23, 2025: 689
English: 344
Arabic: 345

      month  count
0   2025-05     16
1   2025-06     68
2   2025-07     63
3   2025-08     56
4   2025-09     82
5   2025-10    110
6   2025-11     82
7   2025-12     68
8   2026-01     60
9   2026-02     58
10  2026-03     24
11  2026-04      2


curl -s -o /dev/null -w "%{http_code}" "https://www.dewa.gov.ae/~/media/Images/Header/DEWA-Logo.ashx" && echo
curl -s -o /dev/null -w "%{http_code}" "https://www.dewa.gov.ae/-/media/Images/Header/DEWA-Logo.ashx" && echo

Check DEWA logo URL accessibility


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.

Note: The user's next message may contain a correction or preference. Pay close attention — if they explain what went wrong or how they'd prefer you to work, consider saving that to memory for future sessions.


[Request interrupted by user for tool use]


Just make the calculation since May 23, 2025 when DEWA announced to use NotebookLM in their
all press release podcasts

Check how many podcasts are compeleted monthly and do the details analysis taking into
consideration before each podcast costs dewa 20,000 AED while consider the subscription of
NotebookLM monthly and consider this the cost saving where we can check the total cost saving
since we deployed NotebookLM

I can see in the top right corner something called deploy

and DEWA logo is not showing


Good data. Now let me fix all three issues — ROI auto-calculation from May 23 2025, hide Deploy button, fix logo:


1	"""
2	DEWA PR Tracker — Configuration
3	"""
4
5	# ── URLs ──────────────────────────────────────────────────────────────────────
6	DEWA_NEWS_URL    = "https://www.dewa.gov.ae/en/about-us/media-publications/latest-news"
7	DEWA_PODCAST_URL = "https://www.dewa.gov.ae/en/about-us/media-publications/podcasts"
8	DEWA_BASE_URL    = "https://www.dewa.gov.ae"
9
10	# ── Database ──────────────────────────────────────────────────────────────────
11	DB_PATH = "dewa_tracker.db"
12
13	# ── ROI Settings ─────────────────────────────────────────────────────────────
14	COST_PER_PODCAST_OLD_AED = 20_000   # what a single podcast used to cost
15	NOTEBOOKLM_MONTHLY_USD   = 20       # current NotebookLM subscription
16	USD_TO_AED               = 3.67    # exchange rate
17
18	# ── Scraper ───────────────────────────────────────────────────────────────────
19	SCRAPER_HEADLESS     = True         # set False to watch the browser during debug
20	SCRAPER_TIMEOUT_MS   = 30_000
21	SCRAPER_WAIT_AFTER_LOAD_MS = 3_000  # wait for JS to finish rendering
22
23	# ── Dashboard ─────────────────────────────────────────────────────────────────
24	DASHBOARD_TITLE = "DEWA Press Release & Podcast Tracker"
25	PAGE_SIZE       = 25                # rows per page in the PR table
26

"""
DEWA PR Tracker — Configuration
"""

# ── URLs ──────────────────────────────────────────────────────────────────────
DEWA_NEWS_URL    = "https://www.dewa.gov.ae/en/about-us/media-publications/latest-news"
DEWA_PODCAST_URL = "https://www.dewa.gov.ae/en/about-us/media-publications/podcasts"
DEWA_BASE_URL    = "https://www.dewa.gov.ae"

# ── Database ──────────────────────────────────────────────────────────────────
DB_PATH = "dewa_tracker.db"

# ── ROI Settings ─────────────────────────────────────────────────────────────
COST_PER_PODCAST_OLD_AED = 20_000   # what a single podcast used to cost
NOTEBOOKLM_MONTHLY_USD   = 20       # current NotebookLM subscription
USD_TO_AED               = 3.67    # exchange rate

# ── Scraper ───────────────────────────────────────────────────────────────────
SCRAPER_HEADLESS     = True         # set False to watch the browser during debug
SCRAPER_TIMEOUT_MS   = 30_000
SCRAPER_WAIT_AFTER_LOAD_MS = 3_000  # wait for JS to finish rendering

# ── Dashboard ─────────────────────────────────────────────────────────────────
DASHBOARD_TITLE = "DEWA Press Release & Podcast Tracker"
PAGE_SIZE       = 25                # rows per page in the PR table


Edit

# ── ROI Settings ─────────────────────────────────────────────────────────────
COST_PER_PODCAST_OLD_AED = 20_000   # what a single podcast used to cost
NOTEBOOKLM_MONTHLY_USD   = 20       # current NotebookLM subscription
USD_TO_AED               = 3.67    # exchange rate

# ── ROI Settings ─────────────────────────────────────────────────────────────
COST_PER_PODCAST_OLD_AED = 20_000        # cost of a single podcast before NotebookLM
NOTEBOOKLM_MONTHLY_USD   = 20            # current NotebookLM subscription price
USD_TO_AED               = 3.67         # exchange rate
NOTEBOOKLM_START_DATE    = "2025-05-23"  # date DEWA announced NotebookLM adoption


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

# ── ROI Settings ─────────────────────────────────────────────────────────────
COST_PER_PODCAST_OLD_AED = 20_000        # cost of a single podcast before NotebookLM
NOTEBOOKLM_MONTHLY_USD   = 20            # current NotebookLM subscription price
USD_TO_AED               = 3.67         # exchange rate
NOTEBOOKLM_START_DATE    = "2025-05-23"  # date DEWA announced NotebookLM adoption

"""
DEWA PR Tracker — Configuration
"""

# ── URLs ──────────────────────────────────────────────────────────────────────
DEWA_NEWS_URL    = "https://www.dewa.gov.ae/en/about-us/media-publications/latest-news"
DEWA_PODCAST_URL = "https://www.dewa.gov.ae/en/about-us/media-publications/podcasts"
DEWA_BASE_URL    = "https://www.dewa.gov.ae"

# ── Database ──────────────────────────────────────────────────────────────────
DB_PATH = "dewa_tracker.db"

# ── ROI Settings ─────────────────────────────────────────────────────────────
COST_PER_PODCAST_OLD_AED = 20_000   # what a single podcast used to cost
NOTEBOOKLM_MONTHLY_USD   = 20       # current NotebookLM subscription
USD_TO_AED               = 3.67    # exchange rate

# ── Scraper ───────────────────────────────────────────────────────────────────
SCRAPER_HEADLESS     = True         # set False to watch the browser during debug
SCRAPER_TIMEOUT_MS   = 30_000
SCRAPER_WAIT_AFTER_LOAD_MS = 3_000  # wait for JS to finish rendering

# ── Dashboard ─────────────────────────────────────────────────────────────────
DASHBOARD_TITLE = "DEWA Press Release & Podcast Tracker"
PAGE_SIZE       = 25                # rows per page in the PR table

-COST_PER_PODCAST_OLD_AED = 20_000   # what a single podcast used to cost

-NOTEBOOKLM_MONTHLY_USD   = 20       # current NotebookLM subscription

-USD_TO_AED               = 3.67    # exchange rate

+COST_PER_PODCAST_OLD_AED = 20_000        # cost of a single podcast before NotebookLM

+NOTEBOOKLM_MONTHLY_USD   = 20            # current NotebookLM subscription price

+USD_TO_AED               = 3.67         # exchange rate

+NOTEBOOKLM_START_DATE    = "2025-05-23"  # date DEWA announced NotebookLM adoption


Now I need to add a database query for monthly podcast counts since the start date:


def get_podcasts_monthly_since(start_date: str):
    """Return monthly podcast counts from start_date onward, split by language."""
    with get_conn() as conn:
        return conn.execute(
            """
            SELECT
                substr(date,1,7) AS month,
                COUNT(*)         AS total,
                SUM(CASE WHEN language = 'en' THEN 1 ELSE 0 END) AS en_count,
                SUM(CASE WHEN language = 'ar' THEN 1 ELSE 0 END) AS ar_count
            FROM podcasts
            WHERE date >= ?
            GROUP BY month
            ORDER BY month
            """,
            (start_date,),
        ).fetchall()

def get_scrape_log():


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

def get_podcasts_monthly_since(start_date: str):
    """Return monthly podcast counts from start_date onward, split by language."""
    with get_conn() as conn:
        return conn.execute(
            """
            SELECT
                substr(date,1,7) AS month,
                COUNT(*)         AS total,
                SUM(CASE WHEN language = 'en' THEN 1 ELSE 0 END) AS en_count,
                SUM(CASE WHEN language = 'ar' THEN 1 ELSE 0 END) AS ar_count
            FROM podcasts
            WHERE date >= ?
            GROUP BY month
            ORDER BY month
            """,
            (start_date,),
        ).fetchall()

def get_scrape_log():

"""
DEWA PR Tracker — Database layer (SQLite)
"""
import sqlite3
from contextlib import contextmanager
from datetime import datetime
from config import DB_PATH

# ── Schema ────────────────────────────────────────────────────────────────────

SCHEMA = """
CREATE TABLE IF NOT EXISTS press_releases (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    title       TEXT    NOT NULL,
    url         TEXT    UNIQUE NOT NULL,
    date        TEXT,                    -- ISO-8601 (YYYY-MM-DD)
    category    TEXT,
    description TEXT,
    scraped_at  TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS podcasts (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    pr_id       INTEGER REFERENCES press_releases(id),
    title       TEXT    NOT NULL,
    url         TEXT    UNIQUE NOT NULL,
    audio_url   TEXT,
    date        TEXT,
    duration    TEXT,
    language    TEXT    DEFAULT 'en',
    scraped_at  TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS scrape_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    ran_at      TEXT NOT NULL,
    prs_found   INTEGER DEFAULT 0,
    pods_found  INTEGER DEFAULT 0,
    status      TEXT,
    notes       TEXT
);
"""

@contextmanager
def get_conn():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
        conn.commit()
    finally:
        conn.close()

def init_db():
    with get_conn() as conn:
        conn.executescript(SCHEMA)

# ── Upsert helpers ────────────────────────────────────────────────────────────

def upsert_press_release(title: str, url: str, date: str = None,
                          category: str = None, description: str = None) -> int:
    """Insert or update a press release. Returns the row id."""
    now = datetime.utcnow().isoformat()
    with get_conn() as conn:
        cur = conn.execute(
            """
            INSERT INTO press_releases (title, url, date, category, description, scraped_at)
            VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT(url) DO UPDATE SET
                title       = excluded.title,
                date        = excluded.date,
                category    = excluded.category,
                description = excluded.description,
                scraped_at  = excluded.scraped_at
            """,
            (title, url, date, category, description, now),
        )
        if cur.lastrowid:
            return cur.lastrowid
        row = conn.execute("SELECT id FROM press_releases WHERE url = ?", (url,)).fetchone()
        return row["id"]

def upsert_podcast(title: str, url: str, pr_id: int = None,
                   audio_url: str = None, date: str = None, duration: str = None,
                   language: str = "en") -> int:
    now = datetime.utcnow().isoformat()
    with get_conn() as conn:
        cur = conn.execute(
            """
            INSERT INTO podcasts (pr_id, title, url, audio_url, date, duration, language, scraped_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(url) DO UPDATE SET
                title     = excluded.title,
                audio_url = excluded.audio_url,
                date      = excluded.date,
                duration  = excluded.duration,
                language  = excluded.language,
                scraped_at= excluded.scraped_at
            """,
            (pr_id, title, url, audio_url, date, duration, language, now),
        )
        if cur.lastrowid:
            return cur.lastrowid
        row = conn.execute("SELECT id FROM podcasts WHERE url = ?", (url,)).fetchone()
        return row["id"]

def log_scrape(prs_found: int, pods_found: int, status: str, notes: str = ""):
    with get_conn() as conn:
        conn.execute(
            "INSERT INTO scrape_log (ran_at, prs_found, pods_found, status, notes) VALUES (?,?,?,?,?)",
            (datetime.utcnow().isoformat(), prs_found, pods_found, status, notes),
        )

# ── Query helpers ─────────────────────────────────────────────────────────────

def get_all_press_releases():
    with get_conn() as conn:
        return conn.execute(
            """
            SELECT
                pr.id,
                pr.title,
                pr.url,
                pr.date,
                pr.category,
                pr.description,
                pr.scraped_at,
                COUNT(p.id) AS podcast_count
            FROM press_releases pr
            LEFT JOIN podcasts p ON p.pr_id = pr.id
            GROUP BY pr.id
            ORDER BY pr.date DESC, pr.id DESC
            """
        ).fetchall()

def get_all_podcasts(language: str = None):
    with get_conn() as conn:
        if language:
            return conn.execute(
                """
                SELECT p.*, pr.title AS pr_title
                FROM podcasts p
                LEFT JOIN press_releases pr ON pr.id = p.pr_id
                WHERE p.language = ?
                ORDER BY p.date DESC, p.id DESC
                """,
                (language,),
            ).fetchall()
        return conn.execute(
            """
            SELECT p.*, pr.title AS pr_title
            FROM podcasts p
            LEFT JOIN press_releases pr ON pr.id = p.pr_id
            ORDER BY p.date DESC, p.id DESC
            """
        ).fetchall()

def get_counts_by_day():
    with get_conn() as conn:
        return conn.execute(
            "SELECT date, COUNT(*) as count FROM press_releases WHERE date IS NOT NULL GROUP BY date ORDER BY date"
        ).fetchall()

def get_counts_by_month():
    with get_conn() as conn:
        return conn.execute(
            """
            SELECT substr(date,1,7) AS month, COUNT(*) AS count
            FROM press_releases WHERE date IS NOT NULL
            GROUP BY month ORDER BY month
            """
        ).fetchall()

def get_scrape_log():
    with get_conn() as conn:
        return conn.execute(
            "SELECT * FROM scrape_log ORDER BY ran_at DESC LIMIT 50"
        ).fetchall()

def get_stats():
    with get_conn() as conn:
        total_prs    = conn.execute("SELECT COUNT(*) FROM press_releases").fetchone()[0]
        total_pods   = conn.execute("SELECT COUNT(*) FROM podcasts").fetchone()[0]
        last_scraped = conn.execute("SELECT MAX(scraped_at) FROM press_releases").fetchone()[0]
        return {"total_prs": total_prs, "total_podcasts": total_pods, "last_scraped": last_scraped}

+    """Return monthly podcast counts from start_date onward, split by language."""

+                substr(date,1,7) AS month,

+                COUNT(*)         AS total,

+                SUM(CASE WHEN language = 'en' THEN 1 ELSE 0 END) AS en_count,

+                SUM(CASE WHEN language = 'ar' THEN 1 ELSE 0 END) AS ar_count


Now rewrite the full dashboard with all fixes:


Now rewrite the full dashboard:


Write

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

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

import database as db
from config import (
    COST_PER_PODCAST_OLD_AED,
    NOTEBOOKLM_MONTHLY_USD,
    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 — hide Streamlit chrome (Deploy button, footer, menu) ─────────────────
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_podcasts_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:
    df_roi["old_cost"]   = df_roi["total"] * COST_PER_PODCAST_OLD_AED
    df_roi["new_cost"]   = NOTEBOOKLM_AED   # subscription per month (fixed)
    df_roi["savings"]    = df_roi["old_cost"] - df_roi["new_cost"]

    total_pods_nlm  = int(df_roi["total"].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_pods_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=(min_date, max_date),
        min_value=min_date,
        max_value=max_date,
    )

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

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

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

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

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

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

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

# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1 — Key Metrics
# ═══════════════════════════════════════════════════════════════════════════════
today       = datetime.today().date()
week_start  = today - timedelta(days=today.weekday())
month_start = today.replace(day=1)

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

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

st.divider()

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

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

st.markdown("
", unsafe_allow_html=True)

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

    with chart_col:
        # Grouped bar: old vs new cost per month
        df_chart = df_roi[["month", "old_cost", "new_cost"]].copy()
        df_chart_long = df_chart.melt(
            id_vars="month",
            value_vars=["old_cost", "new_cost"],
            var_name="Type",
            value_name="AED",
        )
        df_chart_long["Type"] = df_chart_long["Type"].map(
            {"old_cost": "Old Cost (Traditional)", "new_cost": "New Cost (NotebookLM)"}
        )
        fig_bar = px.bar(
            df_chart_long, x="month", y="AED", color="Type", barmode="group",
            color_discrete_map={
                "Old Cost (Traditional)": "#ff6b6b",
                "New Cost (NotebookLM)":  "#51cf66",
            },
            title="Monthly Cost Comparison: Traditional 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)

        # Podcast count per month
        fig_count = px.bar(
            df_roi, x="month", y="total",
            color_discrete_sequence=["#f59e0b"],
            title="Podcasts Produced 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="Podcasts"),
            margin=dict(t=40, b=10), height=280,
        )
        st.plotly_chart(fig_count, use_container_width=True)

    with table_col:
        st.markdown(
            '
'
            'Monthly breakdown — each row = 1 calendar month
',
            unsafe_allow_html=True,
        )
        rows_html = ""
        for _, r in df_roi.iterrows():
            rows_html += (
                f'
'
                f'{r["month"]}	'
                f'{int(r["total"])}	'
                f'{int(r["en_count"])}	'
                f'{int(r["ar_count"])}	'
                f'AED {r["old_cost"]:,.0f}	'
                f'AED {r["new_cost"]:,.0f}	'
                f'AED {r["savings"]:,.0f}	'
                f'
'
            )
        # Total row
        rows_html += (
            f'
'
            f'TOTAL	'
            f'{total_pods_nlm}	'
            f'{int(df_roi["en_count"].sum())}	'
            f'{int(df_roi["ar_count"].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	Podcasts	EN	AR	'
            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,
)