from datetime import datetime, timezone from html import escape import os import time import pandas as pd import streamlit as st from streamlit_autorefresh import st_autorefresh from database import init_db, get_latest_crawl_time from timezone_utils import ( INDONESIA_TIMEZONES, browser_offset_to_choice, browser_timezone_to_choice, get_default_timezone, get_timezone_label, get_timezone_name, parse_dt_with_source_tz, ) try: from dotenv import load_dotenv load_dotenv() except Exception: pass init_db() # ── Auto-start background crawler scheduler ────────────────────────────────── # Dipanggil sekali saat app pertama dijalankan. # Scheduler berjalan sebagai daemon thread — tidak perlu terminal terpisah. try: from crawler import ensure_scheduler_running ensure_scheduler_running() except Exception as _e: print(f"[app] Gagal memulai scheduler: {_e}") st.set_page_config( page_title="Dashboard Sentimen Twitter", layout="wide", page_icon="📊", initial_sidebar_state="expanded" ) # Initialize timezone selection in session state if "user_timezone" not in st.session_state: st.session_state.user_timezone = get_default_timezone() if "follow_device_timezone" not in st.session_state: st.session_state.follow_device_timezone = True def _get_query_param(name): try: value = st.query_params.get(name) except Exception: value = None if isinstance(value, list): return value[0] if value else None return value def _get_context_timezone(): try: return st.context.timezone except Exception: return None def _get_context_offset(): try: offset = st.context.timezone_offset except Exception: return None if offset is None: return None try: return str(-int(offset)) except (TypeError, ValueError): return None def _sync_timezone_from_browser(): browser_timezone = _get_context_timezone() or _get_query_param("browser_tz") browser_offset = _get_context_offset() or _get_query_param("browser_offset") mapped_timezone = ( browser_timezone_to_choice(browser_timezone) or browser_offset_to_choice(browser_offset) ) st.session_state.browser_timezone = browser_timezone st.session_state.browser_offset = browser_offset if st.session_state.get("follow_device_timezone", True): timezone_choice = mapped_timezone or get_default_timezone() if st.session_state.user_timezone != timezone_choice: st.session_state.user_timezone = timezone_choice _sync_timezone_from_browser() def get_refresh_interval(): try: latest_crawl = get_latest_crawl_time() if latest_crawl: timezone_choice = st.session_state.get("user_timezone", get_default_timezone()) latest_time = parse_dt_with_source_tz( [latest_crawl], timezone_choice, os.getenv("APP_TIMEZONE", "Asia/Jakarta") ).iloc[0] now = pd.Timestamp.now(tz=get_timezone_name(timezone_choice)).tz_localize(None) time_diff = (now - latest_time).total_seconds() return 30000 if time_diff < 300 else 60000 except Exception: pass return 60000 refresh_interval = get_refresh_interval() st_autorefresh( interval=refresh_interval, key="auto_refresh_dashboard" ) def _check_crawler_alive(): from datetime import timedelta from crawler import NRT_INTERVAL_MINUTES, get_crawler_state threshold = timedelta(minutes=max((NRT_INTERVAL_MINUTES * 2) + 1, 3)) def parse_dt(value): if not value: return None try: parsed = datetime.fromisoformat(value) if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc) except Exception: return None state = get_crawler_state() if not state: return False updated_at = parse_dt(state.get("updated_at")) heartbeat_at = parse_dt(state.get("heartbeat_at")) now = datetime.now(timezone.utc) if ( state.get("is_running", False) and updated_at is not None and now - updated_at <= threshold ): return True return ( state.get("service_active", False) and heartbeat_at is not None and now - heartbeat_at <= threshold ) st.session_state._scheduler_started = _check_crawler_alive() try: from crawler import NRT_INTERVAL_MINUTES st.session_state._scheduler_interval = NRT_INTERVAL_MINUTES except Exception: st.session_state._scheduler_interval = 0 st.markdown(""" """, unsafe_allow_html=True) if "page" not in st.session_state: st.session_state.page = "crawling" if st.session_state.get("current_page"): st.session_state.page = st.session_state.current_page st.session_state.scroll_to_top = True del st.session_state["current_page"] if ( st.session_state.get("scroll_to_top") or st.session_state.get("_last_rendered_page") != st.session_state.page ): st.session_state.scroll_to_top = False st.session_state._last_rendered_page = st.session_state.page with st.sidebar: # ── HEADER ────────────────────────────────────────────────── st.markdown("""