     391 /Users/khalidbinkalban/Projects/dewa-pr-tracker/scraper.py
      26 /Users/khalidbinkalban/Projects/dewa-pr-tracker/config.py
      15 /Users/khalidbinkalban/Projects/dewa-pr-tracker/.streamlit/config.toml
     222 /Users/khalidbinkalban/Projects/dewa-pr-tracker/database.py
     694 /Users/khalidbinkalban/Projects/dewa-pr-tracker/dashboard.py
      28 /Users/khalidbinkalban/Projects/dewa-pr-tracker/scheduler.py
      20 /Users/khalidbinkalban/Projects/dewa-pr-tracker/start.sh
    1396 total


**1,396 lines** across 7 files:

| File | Lines |
|---|---|
| `dashboard.py` | 694 |
| `scraper.py` | 391 |
| `database.py` | 222 |
| `scheduler.py` | 28 |
| `config.py` | 26 |
| `start.sh` | 20 |
| `.streamlit/config.toml` | 15 |
| **Total** | **1,396** |

+172 lines since last count, mainly from the white theme CSS, mobile responsive grid, and the JS countdown widget.


how can i know how persons viewed my link created here>


Tailscale Funnel has no built-in analytics. The simplest option that fits what's already built — add a **visitor counter directly into the SQLite DB**. Every new browser session gets logged (timestamp + approximate location from timezone). Here's what I'll build:

- Log every new visit (time, session ID)
- Show a **"Who's viewing"** panel in the dashboard — total views, unique sessions, views per day chart


Edit

SCHEMA = """
CREATE TABLE IF NOT EXISTS press_releases (

SCHEMA = """
CREATE TABLE IF NOT EXISTS visitors (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT    NOT NULL,
    visited_at TEXT    NOT NULL,
    user_agent TEXT
);

CREATE TABLE IF NOT EXISTS press_releases (


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

SCHEMA = """
CREATE TABLE IF NOT EXISTS visitors (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT    NOT NULL,
    visited_at TEXT    NOT NULL,
    user_agent TEXT
);

CREATE TABLE IF NOT EXISTS press_releases (

"""
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_roi_monthly_since(start_date: str):
    """
    Monthly ROI breakdown since start_date.
    Cost unit = per press release (each PR had one podcast @ AED 20,000).
    Podcast episodes (EN + AR) are counted separately for reference.
    """
    with get_conn() as conn:
        return conn.execute(
            """
            SELECT
                substr(pr.date, 1, 7)                              AS month,
                COUNT(DISTINCT pr.id)                              AS pr_count,
                SUM(CASE WHEN p.language = 'en' THEN 1 ELSE 0 END) AS en_episodes,
                SUM(CASE WHEN p.language = 'ar' THEN 1 ELSE 0 END) AS ar_episodes,
                COUNT(p.id)                                        AS total_episodes
            FROM press_releases pr
            LEFT JOIN podcasts p ON p.pr_id = pr.id
            WHERE pr.date >= ?
            GROUP BY month
            ORDER BY month
            """,
            (start_date,),
        ).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}

+    id         INTEGER PRIMARY KEY AUTOINCREMENT,

id          INTEGER PRIMARY KEY AUTOINCREMENT,


def log_scrape(prs_found: int, pods_found: int, status: str, notes: str = ""):

def log_visit(session_id: str, user_agent: str = ""):
    with get_conn() as conn:
        exists = conn.execute(
            "SELECT 1 FROM visitors WHERE session_id = ?", (session_id,)
        ).fetchone()
        if not exists:
            conn.execute(
                "INSERT INTO visitors (session_id, visited_at, user_agent) VALUES (?,?,?)",
                (session_id, datetime.utcnow().isoformat(), user_agent or ""),
            )
            return True   # new visit
    return False  # already logged

def get_visitor_stats():
    with get_conn() as conn:
        total   = conn.execute("SELECT COUNT(*) FROM visitors").fetchone()[0]
        today   = conn.execute(
            "SELECT COUNT(*) FROM visitors WHERE visited_at >= date('now')"
        ).fetchone()[0]
        by_day  = conn.execute(
            "SELECT substr(visited_at,1,10) AS day, COUNT(*) AS visits "
            "FROM visitors GROUP BY day ORDER BY day DESC LIMIT 30"
        ).fetchall()
        recent  = conn.execute(
            "SELECT visited_at, user_agent FROM visitors ORDER BY visited_at DESC LIMIT 20"
        ).fetchall()
    return {"total": total, "today": today, "by_day": by_day, "recent": recent}

def log_scrape(prs_found: int, pods_found: int, status: str, notes: str = ""):


def log_visit(session_id: str, user_agent: str = ""):
    with get_conn() as conn:
        exists = conn.execute(
            "SELECT 1 FROM visitors WHERE session_id = ?", (session_id,)
        ).fetchone()
        if not exists:
            conn.execute(
                "INSERT INTO visitors (session_id, visited_at, user_agent) VALUES (?,?,?)",
                (session_id, datetime.utcnow().isoformat(), user_agent or ""),
            )
            return True   # new visit
    return False  # already logged

def get_visitor_stats():
    with get_conn() as conn:
        total   = conn.execute("SELECT COUNT(*) FROM visitors").fetchone()[0]
        today   = conn.execute(
            "SELECT COUNT(*) FROM visitors WHERE visited_at >= date('now')"
        ).fetchone()[0]
        by_day  = conn.execute(
            "SELECT substr(visited_at,1,10) AS day, COUNT(*) AS visits "
            "FROM visitors GROUP BY day ORDER BY day DESC LIMIT 30"
        ).fetchall()
        recent  = conn.execute(
            "SELECT visited_at, user_agent FROM visitors ORDER BY visited_at DESC LIMIT 20"
        ).fetchall()
    return {"total": total, "today": today, "by_day": by_day, "recent": recent}

def log_scrape(prs_found: int, pods_found: int, status: str, notes: str = ""):

"""
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 visitors (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT    NOT NULL,
    visited_at TEXT    NOT NULL,
    user_agent TEXT
);

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_roi_monthly_since(start_date: str):
    """
    Monthly ROI breakdown since start_date.
    Cost unit = per press release (each PR had one podcast @ AED 20,000).
    Podcast episodes (EN + AR) are counted separately for reference.
    """
    with get_conn() as conn:
        return conn.execute(
            """
            SELECT
                substr(pr.date, 1, 7)                              AS month,
                COUNT(DISTINCT pr.id)                              AS pr_count,
                SUM(CASE WHEN p.language = 'en' THEN 1 ELSE 0 END) AS en_episodes,
                SUM(CASE WHEN p.language = 'ar' THEN 1 ELSE 0 END) AS ar_episodes,
                COUNT(p.id)                                        AS total_episodes
            FROM press_releases pr
            LEFT JOIN podcasts p ON p.pr_id = pr.id
            WHERE pr.date >= ?
            GROUP BY month
            ORDER BY month
            """,
            (start_date,),
        ).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}

+def log_visit(session_id: str, user_agent: str = ""):

+            "SELECT 1 FROM visitors WHERE session_id = ?", (session_id,)

+                "INSERT INTO visitors (session_id, visited_at, user_agent) VALUES (?,?,?)",

+                (session_id, datetime.utcnow().isoformat(), user_agent or ""),

+        total   = conn.execute("SELECT COUNT(*) FROM visitors").fetchone()[0]

+            "SELECT COUNT(*) FROM visitors WHERE visited_at >= date('now')"

+            "SELECT substr(visited_at,1,10) AS day, COUNT(*) AS visits "

+            "FROM visitors GROUP BY day ORDER BY day DESC LIMIT 30"

+            "SELECT visited_at, user_agent FROM visitors ORDER BY visited_at DESC LIMIT 20"

+    return {"total": total, "today": today, "by_day": by_day, "recent": recent}


Now add visitor logging + analytics panel to the dashboard:


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 math
import re as _re
import uuid
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from streamlit.runtime.scriptrunner import get_script_run_ctx


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

import math
import re as _re
import uuid
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from streamlit.runtime.scriptrunner import get_script_run_ctx

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

    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)

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

+from streamlit.runtime.scriptrunner import get_script_run_ctx


Now add visitor tracking right after `st.set_page_config` and before the SVG helpers:


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

# ── Visitor tracking — log each new browser session once ─────────────────────
def _get_session_id():
    ctx = get_script_run_ctx()
    if ctx:
        return ctx.session_id
    return str(uuid.uuid4())

if "visit_logged" not in st.session_state:
    db.init_db()
    sid = _get_session_id()
    db.log_visit(sid)
    st.session_state["visit_logged"] = True

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


# ── Visitor tracking — log each new browser session once ─────────────────────
def _get_session_id():
    ctx = get_script_run_ctx()
    if ctx:
        return ctx.session_id
    return str(uuid.uuid4())

if "visit_logged" not in st.session_state:
    db.init_db()
    sid = _get_session_id()
    db.log_visit(sid)
    st.session_state["visit_logged"] = True

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

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

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from streamlit.runtime.scriptrunner import get_script_run_ctx

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

    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)

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

+# ── Visitor tracking — log each new browser session once ─────────────────────

+if "visit_logged" not in st.session_state:

+    st.session_state["visit_logged"] = True


Now add the visitor analytics panel before the scrape log section:


# ═══════════════════════════════════════════════════════════════════════════════
# SCRAPE LOG
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
with st.expander("Scrape history"):

# ═══════════════════════════════════════════════════════════════════════════════
# VISITOR ANALYTICS
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
_P_EYE   = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'
_P_USERS = '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>'
section_title(_P_EYE, "Dashboard Views", "#7c3aed")

vstats = db.get_visitor_stats()
v1, v2, v3 = st.columns(3)
with v1:
    st.markdown(
        f'
'
        f'
{_svg(_P_USERS,13,"#7c3aed")}Total Sessions
'
        f'
{vstats["total"]:,}
'
        f'
unique visitors since launch
'
        f'
', unsafe_allow_html=True,
    )
with v2:
    st.markdown(
        f'
'
        f'
{_svg(_P_EYE,13,"#0077b6")}Today
'
        f'
{vstats["today"]:,}
'
        f'
{today.strftime("%d %B %Y")}
'
        f'
', unsafe_allow_html=True,
    )
with v3:
    last_visit = vstats["recent"][0]["visited_at"][:16].replace("T", " ") if vstats["recent"] else "—"
    st.markdown(
        f'
'
        f'
{_svg(_P_CLK,13,"#16a34a")}Last Visit
'
        f'
{last_visit}
'
        f'
UTC time
'
        f'
', unsafe_allow_html=True,
    )

st.markdown("
", unsafe_allow_html=True)

if vstats["by_day"]:
    df_visits = pd.DataFrame([dict(r) for r in vstats["by_day"]])
    df_visits.columns = ["date", "visits"]
    df_visits = df_visits.sort_values("date")
    fig_v = px.bar(df_visits, x="date", y="visits",
        title="Daily Views (last 30 days)",
        color_discrete_sequence=["#a78bfa"],
    )
    fig_v.update_layout(
        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", title="Sessions"),
        height=220,
    )
    st.plotly_chart(fig_v, use_container_width=True)

with st.expander("Recent visits"):
    if vstats["recent"]:
        df_rec = pd.DataFrame([dict(r) for r in vstats["recent"]])
        df_rec.columns = ["Visited At (UTC)", "User Agent"]
        df_rec["Visited At (UTC)"] = df_rec["Visited At (UTC)"].str[:19].str.replace("T"," ")
        st.dataframe(df_rec, use_container_width=True, hide_index=True)
    else:
        st.info("No visits recorded yet.")

# ═══════════════════════════════════════════════════════════════════════════════
# SCRAPE LOG
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
with st.expander("Scrape history"):


# ═══════════════════════════════════════════════════════════════════════════════
# VISITOR ANALYTICS
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
_P_EYE   = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'
_P_USERS = '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>'
section_title(_P_EYE, "Dashboard Views", "#7c3aed")

vstats = db.get_visitor_stats()
v1, v2, v3 = st.columns(3)
with v1:
    st.markdown(
        f'
'
        f'
{_svg(_P_USERS,13,"#7c3aed")}Total Sessions
'
        f'
{vstats["total"]:,}
'
        f'
unique visitors since launch
'
        f'
', unsafe_allow_html=True,
    )
with v2:
    st.markdown(
        f'
'
        f'
{_svg(_P_EYE,13,"#0077b6")}Today
'
        f'
{vstats["today"]:,}
'
        f'
{today.strftime("%d %B %Y")}
'
        f'
', unsafe_allow_html=True,
    )
with v3:
    last_visit = vstats["recent"][0]["visited_at"][:16].replace("T", " ") if vstats["recent"] else "—"
    st.markdown(
        f'
'
        f'
{_svg(_P_CLK,13,"#16a34a")}Last Visit
'
        f'
{last_visit}
'
        f'
UTC time
'
        f'
', unsafe_allow_html=True,
    )

st.markdown("
", unsafe_allow_html=True)

if vstats["by_day"]:
    df_visits = pd.DataFrame([dict(r) for r in vstats["by_day"]])
    df_visits.columns = ["date", "visits"]
    df_visits = df_visits.sort_values("date")
    fig_v = px.bar(df_visits, x="date", y="visits",
        title="Daily Views (last 30 days)",
        color_discrete_sequence=["#a78bfa"],
    )
    fig_v.update_layout(
        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", title="Sessions"),
        height=220,
    )
    st.plotly_chart(fig_v, use_container_width=True)

with st.expander("Recent visits"):
    if vstats["recent"]:
        df_rec = pd.DataFrame([dict(r) for r in vstats["recent"]])
        df_rec.columns = ["Visited At (UTC)", "User Agent"]
        df_rec["Visited At (UTC)"] = df_rec["Visited At (UTC)"].str[:19].str.replace("T"," ")
        st.dataframe(df_rec, use_container_width=True, hide_index=True)
    else:
        st.info("No visits recorded yet.")

# ═══════════════════════════════════════════════════════════════════════════════
# SCRAPE LOG
# ═══════════════════════════════════════════════════════════════════════════════
st.divider()
with st.expander("Scrape history"):

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

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from streamlit.runtime.scriptrunner import get_script_run_ctx

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
)

# ── Visitor tracking — log each new browser session once ─────────────────────
def _get_session_id():
    ctx = get_script_run_ctx()
    if ctx:
        return ctx.session_id
    return str(uuid.uuid4())

if "visit_logged" not in st.session_state:
    db.init_db()
    sid = _get_session_id()
    db.log_visit(sid)
    st.session_state["visit_logged"] = True

# ── 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()

    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)

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

+section_title(_P_EYE, "Dashboard Views", "#7c3aed")

+        f'<div class="kpi-card purple" style="height:auto;padding:16px 18px">'

+        f'
{_svg(_P_USERS,13,"#7c3aed")}Total Sessions
'

+        f'
unique visitors since launch
'

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

+        f'<div class="kpi-card blue" style="height:auto;padding:16px 18px">'

+        f'
{today.strftime("%d %B %Y")}
'

+    last_visit = vstats["recent"][0]["visited_at"][:16].replace("T", " ") if vstats["recent"] else "—"

+        f'<div class="kpi-card green" style="height:auto;padding:16px 18px">'

+    df_visits = pd.DataFrame([dict(r) for r in vstats["by_day"]])

+    df_visits.columns = ["date", "visits"]

+    df_visits = df_visits.sort_values("date")

+    fig_v = px.bar(df_visits, x="date", y="visits",

+        title="Daily Views (last 30 days)",

+        color_discrete_sequence=["#a78bfa"],

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

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

+        yaxis=dict(gridcolor="#f1f5f9", title="Sessions"),

+    st.plotly_chart(fig_v, use_container_width=True)

+        df_rec = pd.DataFrame([dict(r) for r in vstats["recent"]])

+        df_rec.columns = ["Visited At (UTC)", "User Agent"]

+        df_rec["Visited At (UTC)"] = df_rec["Visited At (UTC)"].str[:19].str.replace("T"," ")

+        st.dataframe(df_rec, use_container_width=True, hide_index=True)

+        st.info("No visits recorded yet.")


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


Live at **https://khalids-mac-mini.tailbd7b77.ts.net**

---

Scroll down to the **"Dashboard Views"** section. Here's what it tracks:

| Widget | What it shows |
|---|---|
| **Total Sessions** | Every unique person who opened the link (counted once per browser tab/session) |
| **Today** | Views on the current day |
| **Last Visit** | Timestamp of the most recent visitor |
| **Daily Views chart** | Bar chart of sessions per day — last 30 days |
| **Recent visits** (expandable) | Timestamp list of last 20 visits |

**How it counts:** Each browser session gets a unique ID from Streamlit's runtime. The first time a session loads, it's written to the DB — refreshing the same tab doesn't count twice, but opening a new tab or a different team member opening the link does.

**Limitation:** No IP addresses or names — just session counts and timestamps. If you want to identify *who* specifically viewed it, you'd need to add a login page.


please remove Dashboard Views


Edit

import math
import re as _re
import uuid
from datetime import datetime, timedelta

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from streamlit.runtime.scriptrunner import get_script_run_ctx

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