# cython: language_level=3
# ws_package/controllers/activity_controller.py
import asyncio
import datetime
import os
import platform
import threading
from pathlib import Path
import subprocess
from tzlocal import get_localzone, reload_localzone
# NOTE: Do NOT add `from datetime import datetime, timedelta` here — it would
# shadow the `datetime` module imported above and break every `datetime.datetime`
# / `datetime.timedelta` usage throughout the file.
from datetime import date
from collections import defaultdict
from typing import List, Tuple, Optional
from sqlalchemy import select, delete, and_, func, desc, or_
from ws_package.models.base import async_session
from ws_package.services.activity_service import ActivityService
from ws_package.services.mode_service import FixedService
from ws_package.entities.user_entity import UserEntity
from ws_package.entities.mode_entity import ModeEntity
from ws_package.entities.activity_entity import LocationEntity
from ws_package.entities.org_entity import OrgEntity
from ws_package.entities.s3bucket_entity import S3BucketEntity
from ws_package.entities.screenshot_entity import ScreenshotEntity
from ws_package.configs import config
from ws_package.utils.tracker_status import get_tracker_status, is_tracking_enabled

from ws_package.services.user_service import UserService
from ws_package.controllers.server_controller import ServerController
from ws_package.controllers.status_controller import StatusController
from ws_package.utils.system import get_ip, get_os_version, guid, precise_sleep
from ws_package.utils.browser_url_detector import get_browser_url_with_fallback
from ws_package.log.writer import LogWriter
from ws_package.utils import logs
from ws_package.utils.api_log_policy import (
    api_name_from_url,
    log_api_response_summary,
    log_testing_request_headers,
)
from ws_package.services.api_request_service import APIRequest
from ws_package.utils.db_lock_manager import db_lock_manager
from ws_package.exceptions.custom_exceptions import (
    FileSystemError, ValidationError, APIError, NetworkError, 
    ScreenshotError, ActivityMonitoringError
)
from ws_package.entities.activity_entity import ActivityPercentageEntity
from ws_package.repositories.activity_percentage_repo import activity_percentage_manager
from ws_package.services.activity_percentage_service import activity_percentage_service
from ws_package.services.db_writer_service import db_writer_service
from ws_package.utils.ipc_server import is_ipc_ready
from ws_package.utils.db_write_queue import db_write_queue, WriteOperationType
from ws_package.utils.s3 import s3_detail, S3sync, s3_detail_for_upload
from ws_package.utils.system_wake_detector import get_system_wake_detector
from ws_package.utils.build_mode import is_silent_build
from ws_package.services.weekly_limit_notification_service import get_weekly_limit_notification_service

import re
import unicodedata
import time
import base64
import sys
import psutil
import zoneinfo
import requests
import json
import traceback
import tempfile
import shutil

try:
    from ws_package.monitoring.windows import Window
    from ws_package.models.base import Appmonitor, FixedMode, OrgConfigurations, Screenshots, ActivityInterval, Notes

    if platform.system() == "Windows":
        import winreg
        from ws_package.monitoring.listeners_windows import DeviceListener
        from ws_package.monitoring.time_monitor import OptimizedWindowsTimeMonitor
    else:
        from ws_package.monitoring.listeners import DeviceListener
except ImportError:
    raise RuntimeError("Error in importing Activity Controller file either in listeners or windows")

from ws_package.controllers.screenshot_controller import ScreenshotController
from ws_package.managers import TimezoneManager, ScreenshotManager, ActivityManager, DataManager

from ws_package.controllers.activity_screenshot_mixin import ActivityScreenshotMixin
from ws_package.controllers.activity_break_mixin import ActivityBreakMixin
from ws_package.controllers.activity_idle_mixin import ActivityIdleMixin
from ws_package.controllers.activity_timezone_mixin import ActivityTimezoneMixin
from ws_package.controllers.activity_interval_mixin import ActivityIntervalMixin
from ws_package.controllers.activity_push_mixin import ActivityPushMixin



logger = LogWriter.file(__name__)


def _to_int(val, default=0):
    """Cast an API or DB value to int safely.

    API responses and older DB rows may return numeric fields as strings (e.g.
    "2") or omit them entirely (None).  Calling int() on None raises TypeError;
    calling it on a non-numeric string raises ValueError.  Both are caught here
    and replaced with `default` (0 = feature disabled) so no caller needs to
    guard individually.

    Defined at module level (not nested inside methods) so Cython can compile
    activity_controller.py without closure-related issues.
    """
    try:
        return int(val)
    except (TypeError, ValueError):
        return default


def _get_electron_comm_lazy():
    """Lazy import to avoid circular imports. Returns the global Electron communicator instance."""
    from ws_package.utils.electron_comm import get_electron_comm
    return get_electron_comm()


class ActivityController(
    ActivityScreenshotMixin,
    ActivityBreakMixin,
    ActivityIdleMixin,
    ActivityTimezoneMixin,
    ActivityIntervalMixin,
    ActivityPushMixin,
):
    """
    Orchestrates activity monitoring. See mixin classes for implementation.

    Inherits from:
    - ActivityScreenshotMixin: Screenshot capture and upload
    - ActivityBreakMixin: Break detection and interval processing
    - ActivityIdleMixin: Idle detection, popup handling, and idle intervals
    - ActivityTimezoneMixin: Timezone detection, validation, and normalization
    - ActivityIntervalMixin: Interval grouping, session management, and bulk payloads
    - ActivityPushMixin: Push/sync loop, DB-to-server upload, and data cleanup

    Attributes:
        last_count (int): Counter for tracking application restarts
        interval (int): Activity sampling interval in seconds (default: 600)
        screenshot_interval (int): Screenshot capture interval in seconds (default: 1)
        status (bool): Main activity tracking status
        main_start_date (Optional[datetime]): Start time for current tracking session
        main_end_date (Optional[datetime]): End time for current tracking session
        local_timezone (str): Local timezone identifier
        system_unique_id (str): Unique device identifier

    Example:
        >>> controller = ActivityController(last_count=1)
        >>> await controller.start_device_activity(user_entity, stop_event)
    """

    def __init__(self, last_count: int) -> None:
        """
        Initialize ActivityController with configuration and managers.
        
        Args:
            last_count (int): Counter value for tracking application restarts.
                             Used to identify gaps in activity monitoring.
                             
        Example:
            >>> controller = ActivityController(last_count=1)
        """
        self.last_count: int = last_count
        self._last_counter_bump_at: Optional[datetime.datetime] = None
        self._bump_counter_on_next_monitoring_start: bool = False
        # Removed individual db_write_lock - now using centralized db_lock_manager
        self.appVersion = "7.0"
        self.idle_time_limit_in_seconds: int = 3600  # Default 60 minutes, will be updated from API
        
        # Track background async tasks to prevent silent failures
        
        self._background_tasks = set()
        # Initialize managers
        self.timezone_manager = TimezoneManager()
        self.screenshot_manager = ScreenshotManager(last_count)
        self.activity_manager = ActivityManager()
        self.data_manager = DataManager()
        
        # Initialize services
        self.user_service = UserService()
        self.status_controller = StatusController()
        self.fixed_mode = FixedService()
        self.activity_service = ActivityService()
        self.server_controller = ServerController()
        
        # Configuration
        self.interval: int = 600
        
        # Cache for stealth mode API calls to prevent excessive API requests
        self._last_stealth_api_call: Optional[datetime.datetime] = None
        self._cached_stealth_response: Optional[dict] = None  # Cache the actual response data
        self._stealth_api_cache_interval: int = 300  # 5 minutes - only refresh stealth mode every 5 minutes
        self.screenshot_interval: int = 1
        self.slot: int = int(self.interval / 60)
        self.appandurlstatus: int = 3
        self.app_monitor_interval_seconds: int = 30  # Interval for checking active apps/windows (in seconds)
        # Get session gap threshold with graceful fallback (config may not exist)
        try:
            session_gap_minutes = config('session_gap_threshold_minutes', 'ActivityMonitor', dtype='int')
            if session_gap_minutes is None or session_gap_minutes <= 0:
                session_gap_minutes = 60  # Default to 1 hour between sessions
        except (ValueError, TypeError, AttributeError):
            # Config value not found or invalid - use default
            session_gap_minutes = 60  # Default to 1 hour between sessions
        self.session_gap_threshold_minutes: int = int(session_gap_minutes)
        self.session_gap_threshold_seconds: int = session_gap_minutes * 60
        self.s3_screenshot_path: str = config('s3_screenshot_path', 'Sync')
        self.s3_logs_path: str = config('s3_logs_path', 'Sync')
        self.has_on_demand: bool = False
        # Break status - set from Electron via ws_engine
        self.is_break: int = 0
        self.break_id = 0
        self.break_start_time: Optional[datetime.datetime] = None  # Track when current break started
        self.break_project_id: Optional[str] = None  # Store project_id when break starts (for break interval payload)
        self.break_task_id: Optional[str] = None  # Store task_id when break starts (for break interval payload)
        self._last_break_queue_flush: Optional[datetime.datetime] = None  # Track last queue flush time
        self._last_break_stop_event_time: Optional[datetime.datetime] = None  # Track when break_stop_event was last called to prevent duplicate API calls
        self._break_stop_time: Optional[datetime.datetime] = None  # When break stopped; device loop uses this to trim next interval start so we don't save 17:15-17:20 when stop was 17:17
        self._last_work_stop_time: Optional[datetime.datetime] = None  # Work segment end (last interval end); break start is this + 1s
        self._last_idle_popup_trigger_time: Optional[datetime.datetime] = None  # Track when idle popup was last triggered to prevent duplicates
        self._idle_popup_cooldown_seconds: int = 300  # 5 minutes cooldown between idle popup triggers
        self._idle_popup_active: bool = False  # Track if idle popup is currently active
        self._idle_popup_triggered_at: Optional[datetime.datetime] = None  # Track when idle popup was triggered (to check if interval started while popup was active)
        self._last_idle_resolution_time: Optional[datetime.datetime] = None  # Track when last idle period was resolved (Continue/Stop/Reassign) to prevent accumulation across multiple idle periods
        self._api_pause_active: bool = False  # Track if API calls should be paused (for zero activity or idle popup)
        self._api_pause_cycles: int = 0  # Consecutive pause cycles; resets when a push succeeds (max = _MAX_API_PAUSE_CYCLES)
        self._MAX_API_PAUSE_CYCLES: int = 3  # After this many consecutive pauses, force a push regardless of idle state
        self._idle_popup_timeout_seconds: int = 3600  # 60 minutes timeout for unhandled idle popup (auto-stop)
        self._held_idle_intervals: list = []  # Track held idle intervals (single idle interval held until next non-idle or popup handled)
        # Slot where user handled idle popup / resumed work — only this slot gets minimum activity (not prior idle slots)
        self._idle_resolving_slot: Optional[Tuple[datetime.datetime, datetime.datetime]] = None
        
        # Active app tracking for UI display
        self._current_active_app: Optional[str] = None
        self._current_active_app_path: Optional[str] = None
        self._current_active_app_icon: Optional[str] = None
        self._last_active_app_check_time: Optional[datetime.datetime] = None
        
        # In-memory tracking to prevent duplicate interval creation with microsecond-different timestamps
        # Stores tuples of (counter, start_date_rounded_to_seconds, end_date_rounded_to_seconds)
        self._creating_intervals: set = set()  # Track intervals currently being created
        self._creating_activity_percentages: set = set()  # Track activity percentage records currently being created
        
        # Time settings
        self.limit_start: datetime.time = datetime.datetime.strptime("00:00:00", "%H:%M:%S").time()
        self.limit_end: datetime.time = datetime.datetime.strptime("23:59:59", "%H:%M:%S").time()
        self.no_of_days: List[str] = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        
        # State variables
        self.main_start_date: Optional[datetime.datetime] = None
        self.main_end_date: Optional[datetime.datetime] = None
        # When set (after break end), payload "start" for today uses this instead of start_date (new session start time)
        self.current_session_start_time: Optional[datetime.datetime] = None
        # Canonical activity/log session start — pinned when empty open session is posted; interval uploads must match
        self._activity_log_payload_start: Optional[datetime.datetime] = None
        
        # Check persistent tracker status from WebSocket commands
        tracker_status = get_tracker_status()
        if tracker_status == 'stop':
            self.status: bool = False
            self.force_stop = True
            logger.info("[Tracker] Persistent status is 'stop' - tracking will not start until 'start' command received")
        else:
            self.status: bool = True
            self.force_stop = False
            logger.info("[Tracker] Persistent status is 'start' - tracking enabled")
        
        # Keep timezone mismatch separate from generic datetime/time integrity errors.
        self._timezone_mismatch = False
        self._datetime_mismatch = False
        self._time_error_type = None
        
        self.tracking_scenario = None
        self.mode = None
        self.org_api_timezone = None
        self.limit_action: Optional[str] = None  # "just_notify" or "stop_timer"
        # From organization settings API; 0=checks ON, 1=checks OFF. None until org settings applied.
        self.timezone_preference: Optional[int] = None
        self.limit_stopped_date: Optional[datetime.date] = None  # Track when timer was stopped due to limit

        # Log upload tracking for more frequent uploads
        self.last_log_upload_time: Optional[datetime.datetime] = None
        self.log_upload_interval_minutes: int = 60 
        

        # Writable screenshot dir (frozen Windows cannot use cwd-relative "screenshots").
        from ws_package.utils.system import resolve_screenshot_directory
        screenshot_folder = config('save_dir', 'Screenshot') or "screenshots"
        try:
            self.save_dir = resolve_screenshot_directory(screenshot_folder)
        except (PermissionError, OSError) as e:
            logger.error("Failed to create screenshot directory: %s", e)
            raise FileSystemError(
                f"Failed to create screenshot directory for folder {screenshot_folder!r}",
                context={"folder": screenshot_folder, "error": str(e)},
            )
        except Exception as e:
            logger.error("Unexpected error creating screenshot directory: %s", e)
            raise FileSystemError(
                f"Unexpected error creating screenshot directory for folder {screenshot_folder!r}",
                context={"folder": screenshot_folder, "error": str(e)},
            )

        self.img_format = config('img_format', 'Screenshot') or 'JPEG'
        self.img_quality = 100
        self.blur_intensity = 1
        self.img_prefix = ""
        self.org_id = None
        self.notification_status = 1
        self.start_date = datetime.datetime.now()
        # Get current directory with safe fallback for PyInstaller frozen apps
        try:
            self.current_directory = os.getcwd()
        except (FileNotFoundError, OSError):
            # Fallback to Application Support directory or home directory if cwd doesn't exist
            if sys.platform == "darwin":
                self.current_directory = str(Path.home() / "Library/Application Support/Workstatus")
            else:
                self.current_directory = str(Path.home())
        self.system_unique_id = platform.node() if platform.system() == "Windows" else str(guid())
        self.Flag = False
        self.flag2 = False
        # Track processed last min intervals to prevent reprocessing (defensive programming)
        self._processed_last_min_intervals = set()
        
        # Initialize ScreenshotController
        self.screenshot_controller = ScreenshotController(
            org_id=self.org_id,
            save_dir=self.save_dir,
            img_format=self.img_format,
            img_prefix=self.img_prefix,
            img_quality=self.img_quality,
            blur_intensity=self.blur_intensity,
            last_count=self.last_count,
            s3_screenshot_path=self.s3_screenshot_path,
            screenshot_interval=self.screenshot_interval,
            interval=self.interval,
            limit_start=self.limit_start,
            limit_end=self.limit_end,
            no_of_days=self.no_of_days
        )

        # Memory and performance monitoring
        self.process = psutil.Process()
        self.memory_limit_mb = 500  # Set memory limit
        self._memory_check_counter = 0
        self.device_listener = DeviceListener(60)  # 60 seconds = 1 minute
        
        # Project and task IDs from Electron (set when timer starts or changed on running timer)
        self.current_project_id: Optional[str] = None
        self.current_task_id: Optional[str] = None
        # When project/task is changed on running timer, segment start = now (used as payload "start" for next send)
        self.current_segment_start_time: Optional[datetime.datetime] = None
        self._project_switch_in_progress: bool = False
        self._session_close_lock = threading.RLock()

        self.user_entity: UserEntity = None
        self._user_entity_for_wake = None  # Store user_entity for wake callback access
        
        # Register sleep/wake callbacks. On Linux, skip the sleep callback: SystemWakeDetector
        # uses CPU/memory idle heuristics and _check_linux_activity() is a stub (always False),
        # so idle desktop time is misreported as sleep and stops pynput listeners.
        try:
            wake_detector = get_system_wake_detector()
            if wake_detector:
                if platform.system() == "Linux":
                    logger.info(
                        "[SLEEP] Skipping device listener sleep callback on Linux "
                        "(idle-based sleep detection would stop input listeners incorrectly)"
                    )
                else:
                    wake_detector.register_sleep_callback(
                        "device_listener_sleep",
                        self.device_listener.handle_system_sleep
                    )
                    logger.info("[SLEEP] Registered sleep callback for %s", platform.system())

                # Register wake callback to restart listeners
                wake_detector.register_wake_callback(
                    "device_listener_wake",
                    self.device_listener.handle_system_wake
                )
                logger.info("[WAKE] Registered wake callback for %s", platform.system())
                
                # Register wake callback to update S3 bucket config if new day
                wake_detector.register_wake_callback(
                    "s3_bucket_update_wake",
                    self._handle_wake_s3_update
                )
                logger.info("[WAKE] Registered S3 bucket update wake callback for %s", platform.system())

                wake_detector.register_wake_callback(
                    "activity_session_wake",
                    self._handle_wake_session_counter,
                )
                logger.info("[WAKE] Registered session counter wake callback for %s", platform.system())
        except Exception as e:
            logger.warning("Could not register sleep/wake callbacks: %s", e)

    def _handle_wake_session_counter(self) -> None:
        """After system sleep, next timer/monitoring start must use a new counter."""
        self._bump_counter_on_next_monitoring_start = True
        logger.info(
            "[WAKE] System wake — counter will increment on next monitoring start "
            "(stop→sleep→start and missed-stop cases)"
        )

    def _get_electron_comm(self):
        """Return the global Electron communicator instance (lazy-loaded)."""
        return _get_electron_comm_lazy()

    def _send_to_electron_if_connected(self, channel: str, data: dict) -> bool:
        """Send message to Electron if communicator is available and connected. Returns True if sent."""
        electron_comm = self._get_electron_comm()
        if not electron_comm or not electron_comm.is_connected():
            return False
        return electron_comm.send_to_electron(channel, data)
    
    def _is_system_sleeping(self) -> bool:
        """
        Check if system is currently sleeping.
        Returns True if system is sleeping, False otherwise.
        """
        try:
            wake_detector = get_system_wake_detector()
            if wake_detector:
                stats = wake_detector.get_stats()
                last_sleep_time = stats.get('last_sleep_time')
                last_wake_time = stats.get('last_wake_time')
                
                # If we have a sleep time and no wake time after it, system is sleeping
                if last_sleep_time:
                    # Convert datetime to timestamp if needed
                    if isinstance(last_sleep_time, datetime.datetime):
                        sleep_timestamp = last_sleep_time.timestamp()
                    else:
                        sleep_timestamp = last_sleep_time
                    
                    current_timestamp = time.time()
                    time_since_sleep = current_timestamp - sleep_timestamp
                    
                    # Check if system is actually responsive using non-blocking OS calls.
                    # The previous macOS path used blocking osascript (up to 1 s) inside
                    # an async coroutine, stalling the entire event loop every minute.
                    system_is_responsive = False
                    try:
                        if platform.system() == "Darwin":
                            # os.getloadavg() is a non-blocking syscall — succeeds only
                            # when the kernel is responsive (system is awake).
                            os.getloadavg()
                            system_is_responsive = True
                        elif platform.system() == "Windows":
                            # GetForegroundWindow() is non-blocking
                            try:
                                import win32gui
                                hwnd = win32gui.GetForegroundWindow()
                                if hwnd:
                                    system_is_responsive = True
                            except ImportError:
                                pass
                        else:
                            # Linux: getloadavg() is always available
                            os.getloadavg()
                            system_is_responsive = True
                    except Exception:
                        # If we can't query the system, assume it might be sleeping
                        pass
                    
                    # If system is responsive, it's definitely awake
                    if system_is_responsive:
                        logger.debug("[ACTIVITY] System is responsive (can query frontmost app) - system is awake")
                        return False
                    
                    # FIX: If sleep was detected more than 3 minutes ago and no new sleep,
                    # assume system is awake (wake detection may have missed it)
                    # Reduced from 10 minutes to 3 minutes for faster recovery
                    if time_since_sleep > 180:  # 3 minutes
                        logger.debug("[ACTIVITY] Sleep detected %s minutes ago - assuming awake", time_since_sleep/60)
                        return False
                    
                    # Otherwise, check if wake time is after sleep time
                    if last_wake_time is None:
                        return True
                    
                    # Convert wake time to timestamp if needed
                    if isinstance(last_wake_time, datetime.datetime):
                        wake_timestamp = last_wake_time.timestamp()
                    else:
                        wake_timestamp = last_wake_time
                    
                    if wake_timestamp < sleep_timestamp:
                        return True
            return False
        except Exception as e:
            logger.debug("Could not check system sleep state: %s", e)
            return False

    def boundary_time(self, interval):
        """
        Returns how many seconds are left until the next aligned interval boundary.
        
        Example:
        if now = 16:41:33 and interval=300 (5 min),
        it returns seconds until 16:45:00.
        """
        now = datetime.datetime.now()
        now_ts = int(now.timestamp())
        remainder = now_ts % interval
        return interval - remainder if remainder != 0 else 0

    @staticmethod
    def _seconds_until_datetime(target: datetime.datetime) -> float:
        """Seconds until ``target``; 0 if target is None or already passed."""
        if target is None:
            return 0.0
        return max(0.0, (target - datetime.datetime.now()).total_seconds())

    def increment_session_counter(self, reason: str = "new_session") -> int:
        """Bump last_count for a new timer session and sync dependent managers."""
        old_counter = self.last_count
        self.last_count = old_counter + 1
        self._last_counter_bump_at = datetime.datetime.now()
        self._bump_counter_on_next_monitoring_start = False
        self.start_date = datetime.datetime.now().replace(microsecond=0)
        self.current_session_start_time = None
        self._activity_log_payload_start = None
        screenshot_ctrl = getattr(self, 'screenshot_controller', None)
        if screenshot_ctrl is not None:
            screenshot_ctrl.last_count = self.last_count
        logger.info(
            "=== SESSION COUNTER === Incremented %s -> %s (%s); intervals use counter=%s",
            old_counter,
            self.last_count,
            reason,
            self.last_count + 1,
        )
        return self.last_count

    def should_skip_counter_bump_dedup(self, within_seconds: int = 12) -> bool:
        """Skip duplicate bump when the same start wave retried IPC within a few seconds."""
        if getattr(self, '_bump_counter_on_next_monitoring_start', False):
            return False
        last_bump = getattr(self, '_last_counter_bump_at', None)
        if last_bump is None:
            return False
        return (datetime.datetime.now() - last_bump).total_seconds() < within_seconds

    async def has_recent_intervals_on_write_counter(self, within_seconds: int = 45) -> bool:
        """True if the current write counter already has intervals ended very recently (start retry)."""
        DB_OPERATION_TIMEOUT = 5.0
        write_counter = self.last_count + 1
        cutoff = datetime.datetime.now() - datetime.timedelta(seconds=within_seconds)
        with db_lock_manager.read_lock():
            async with async_session() as session:
                try:
                    query = (
                        select(ActivityInterval.id)
                        .where(
                            ActivityInterval.counter == write_counter,
                            ActivityInterval.end_time >= cutoff,
                        )
                        .limit(1)
                    )
                    result = await asyncio.wait_for(
                        session.execute(query),
                        timeout=DB_OPERATION_TIMEOUT,
                    )
                    return result.first() is not None
                except asyncio.TimeoutError:
                    logger.error(
                        "Timeout checking recent intervals for counter=%s",
                        write_counter,
                    )
                    return False
                except Exception as e:
                    logger.error("Error checking recent intervals: %s", e)
                    return False

    def set_project_task_ids(
        self,
        project_id: Optional[str],
        task_id: Optional[str],
        *,
        update_segment_start: bool = True,
    ) -> None:
        """
        Set the current project ID and task ID received from Electron.
        When update_segment_start is True (default), sets current_segment_start_time to now
        for in-flight payload filtering. Project switches use project_switch_event instead.
        """
        self.current_project_id = str(project_id) if project_id is not None else None
        self.current_task_id = str(task_id) if task_id is not None else None
        if update_segment_start:
            self.current_segment_start_time = datetime.datetime.now()
        logger.info(
            "ActivityController: Set project_id=%s, task_id=%s, segment_start=%s",
            self.current_project_id,
            self.current_task_id,
            self.current_segment_start_time if update_segment_start else "(unchanged)",
        )

    async def _trim_open_partial_at_project_switch(
        self, switch_time: datetime.datetime
    ):
        """
        When switching again before the slot boundary, trim the open partial interval
        (e.g. 18:42:14-18:45) so it ends at switch_time instead of using a future end_time
        as the start of a bogus closing interval.
        """
        switch_time = switch_time.replace(microsecond=0)
        write_counter = self.last_count + 1
        try:
            with db_lock_manager.write_lock():
                async with async_session() as session:
                    query = (
                        select(ActivityInterval)
                        .where(
                            ActivityInterval.counter == write_counter,
                            ActivityInterval.is_uploaded.is_(False),
                            ActivityInterval.is_break == 0,
                            ActivityInterval.start_time <= switch_time,
                            ActivityInterval.end_time > switch_time,
                        )
                        .order_by(desc(ActivityInterval.id))
                        .limit(1)
                    )
                    result = await session.execute(query)
                    interval = result.scalar_one_or_none()
                    if interval is None:
                        return None
                    old_end = interval.end_time
                    interval.end_time = switch_time
                    await session.commit()
                    logger.info(
                        "Trimmed open partial for project switch (counter=%s): %s to %s (was to %s)",
                        write_counter,
                        interval.start_time,
                        switch_time,
                        old_end,
                    )
                    return interval
        except Exception as e:
            logger.error(
                "Failed to trim partial interval at project switch: %s", e, exc_info=True
            )
            return None

    async def _try_finalize_post_switch_slot_interval(
        self, user_entity: UserEntity
    ) -> bool:
        """
        After project switch, the first interval for the new task (e.g. 10:02–10:05) must
        not be written at switch time — only when the slot boundary is reached (10:05).

        Called from the device loop each iteration; creates the row once ``now >= slot_end``.
        """
        session_start = self.current_session_start_time
        if session_start is None or not self.status:
            return False

        session_start = session_start.replace(microsecond=0)
        now = datetime.datetime.now().replace(microsecond=0)
        interval_minutes = int(self.interval / 60)
        slot_start = self._align_to_interval_boundary(session_start)
        slot_end = slot_start + datetime.timedelta(minutes=interval_minutes)

        if now < slot_end:
            return False

        last_end = await self.activity_service.get_last_activity_interval()
        interval_start = session_start
        if last_end is not None and last_end > interval_start:
            interval_start = last_end.replace(microsecond=0)

        if interval_start >= slot_end:
            return False

        existing = await self.activity_service.check_existing_interval(
            interval_start, slot_end, exact=True
        )
        if existing:
            return False

        current_break_id = getattr(self, "break_id", 0) or 0
        try:
            activity_level = await self.get_activity_level_from_db(
                interval_start, slot_end
            )
        except Exception as e:
            logger.warning(
                "Error getting activity for deferred post-switch interval: %s", e
            )
            activity_level = {"mouse": "0.0", "average": "0.0", "keyboard": "0.0"}

        await self.activity_service.save_activity_log(
            activity_level,
            interval_start,
            slot_end,
            self.last_count + 1,
            self.get_today_weekday(),
            break_id=current_break_id,
            is_idle=False,
        )
        await db_writer_service.process_immediately()
        logger.info(
            "Deferred post-switch interval saved at slot boundary: %s to %s (counter=%s, "
            "session_start=%s)",
            interval_start,
            slot_end,
            self.last_count + 1,
            session_start,
        )
        return True

    async def _create_segment_dummy_entry(self, segment_start: datetime.datetime) -> None:
        """Create is_uploaded=True marker interval at a new session boundary (project switch, etc.)."""
        empty_activity_level = {"mouse": "1", "keyboard": "0.0", "average": "0.0"}
        current_break_id = getattr(self, "break_id", 0) or 0
        try:
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    query = select(ActivityInterval).where(
                        and_(
                            ActivityInterval.start_time == segment_start,
                            ActivityInterval.end_time == segment_start,
                            ActivityInterval.counter == self.last_count + 1,
                        )
                    ).limit(1)
                    result = await session.execute(query)
                    if result.scalars().first():
                        logger.info(
                            "Segment dummy entry already exists for start=%s counter=%s",
                            segment_start,
                            self.last_count + 1,
                        )
                        return
        except Exception as e:
            logger.warning("Error checking segment dummy entry: %s", e)

        await self.activity_service.save_first_activity_log(
            empty_activity_level,
            segment_start,
            segment_start,
            self.last_count + 1,
            self.get_today_weekday(),
            break_id=current_break_id,
        )
        processed_count = await db_writer_service.process_immediately()
        logger.info(
            "Created segment dummy entry at %s (counter=%s), flushed %s writes",
            segment_start,
            self.last_count + 1,
            processed_count,
        )
    
    def get_os_type_caps(self) -> str:
        platform_name = platform.system()
        if platform_name == "Darwin":
                return "Mac"
        return platform_name

    def handle_s3_time_error(self, error_message: str):
        """Handle S3 RequestTimeTooSkewed (clock skew vs AWS). Log only — not a timezone mismatch."""
        try:
            logger.warning(
                "S3 RequestTimeTooSkewed (system clock skew vs AWS): %s — "
                "not showing timezone mismatch; upload will retry after time sync",
                error_message,
            )
        except Exception as e:
            logger.error("Error handling S3 time error: %s", e)

    async def inrange(self, start_date, end_date):
        if start_date is None or end_date is None:
            logger.info("Start date or end date is None")
            return 
        start_date_y = start_date.time()
        end_date_y = end_date.time()
        if self.limit_start is None or self.limit_end is None or self.no_of_days is None:
            logger.info("No limit start or end time or no of days")
            return True
        if self.limit_start <= start_date_y <= self.limit_end and self.limit_start <= end_date_y <= self.limit_end and self.get_weekday(str(start_date)) in self.no_of_days:
            logger.info("In range of tracking senerio")
            return True
        logger.info("Out of range of tracking senerio")
        return False

    @staticmethod
    def _normalize_hhmmss(value) -> Optional[str]:
        """Normalize API time strings to HH:MM:SS for strptime."""
        if value is None:
            return None
        text = str(value).strip()
        if not text:
            return None
        for fmt in ("%H:%M:%S", "%H:%M"):
            try:
                parsed = datetime.datetime.strptime(text, fmt).time()
                return parsed.strftime("%H:%M:%S")
            except ValueError:
                continue
        return None

    def _parse_fixed_mode_window(self, response: dict):
        """
        Read fixed-mode (stealth) start/end from API — same for silent and visible builds.

        Prefer start_time/end_time; fall back to limit_start/limit_end; else all-day.
        """
        raw_start = response.get("start_time")
        if raw_start is None:
            raw_start = response.get("limit_start")
        raw_end = response.get("end_time")
        if raw_end is None:
            raw_end = response.get("limit_end")

        s_limit_start = self._normalize_hhmmss(raw_start)
        s_limit_end = self._normalize_hhmmss(raw_end)

        if s_limit_start is None or s_limit_end is None:
            logger.warning(
                "[STEALTH] Fixed-mode window missing/invalid (start=%r end=%r) — using all-day fallback",
                raw_start,
                raw_end,
            )
            return "00:00:00", "23:59:59"

        return s_limit_start, s_limit_end
    
    async def update_tracking_duration(self, user_entity:UserEntity):
        """
        Update tracking duration from stealth mode API.
        Uses caching to prevent excessive API calls - only refreshes every 5 minutes.
        """
        try:
            # Check if we should refresh from API (cache expired or first call)
            should_refresh = False
            current_time = datetime.datetime.now()
            
            if self._last_stealth_api_call is None:
                # First call - always refresh
                should_refresh = True
                logger.debug("[STEALTH] First stealth mode API call - fetching from server")
            else:
                # Check if cache has expired
                time_since_last_call = (current_time - self._last_stealth_api_call).total_seconds()
                if time_since_last_call >= self._stealth_api_cache_interval:
                    should_refresh = True
                    logger.debug("[STEALTH] Cache expired (%ss >= %ss) - refreshing from server", int(time_since_last_call), self._stealth_api_cache_interval)
                else:
                    logger.debug("[STEALTH] Using cached stealth mode data (%ss < %ss)", int(time_since_last_call), self._stealth_api_cache_interval)
            
            response = None
            api_call_made = False
            
            if should_refresh:
                # Make API call
                api_call_made = True
                response = await self.fixed_mode.get_fixed_slots(user_entity)
                # Update cache only on successful API call
                if response is not None:
                    self._last_stealth_api_call = current_time
                    self._cached_stealth_response = response
                    logger.debug("[STEALTH] API call successful, cached response")
                else:
                    # API call failed - fall through to database fallback
                    logger.warning("[STEALTH] API call failed, will try database fallback")
            else:
                # Use cached response
                response = self._cached_stealth_response
                if response is not None:
                    logger.debug("[STEALTH] Using cached stealth mode response")
                else:
                    # No cached response available - this shouldn't happen, but try database
                    logger.debug("[STEALTH] No cached response available, trying database")
                    api_call_made = True  # Treat as if we tried API and it failed
            
            if response:
                # Fixed-mode schedule from stealth API — same path for silent and visible builds.
                # Only create/upload intervals inside start_time–end_time on no_of_days.
                s_limit_start, s_limit_end = self._parse_fixed_mode_window(response)

                self.limit_start = datetime.datetime.strptime(s_limit_start, "%H:%M:%S").time()
                self.limit_end = datetime.datetime.strptime(s_limit_end, "%H:%M:%S").time()
                self.no_of_days = response.get('no_of_days')
                self.mode = response.get('mode')
                self.tracking_scenario = response.get('tracking_scenario')

                logger.info(
                    "updated tracking scenerio (fixed mode) %s %s %s mode=%s",
                    self.limit_start,
                    self.limit_end,
                    self.no_of_days,
                    self.mode,
                )

                self.screenshot_controller.limit_start = self.limit_start
                self.screenshot_controller.limit_end = self.limit_end
                self.screenshot_controller.no_of_days = self.no_of_days

                logger.info("updated Screenshot Controller %s %s %s", self.limit_start, self.limit_end, self.no_of_days)

                await self.fixed_mode.save_fixed_mode(ModeEntity(limit_start=s_limit_start, limit_end=s_limit_end, day_list=self.no_of_days, mode=self.mode, tracking=self.tracking_scenario))
            elif api_call_made:
                # Only try database if we actually made an API call and it failed
                # Don't try database if we're just using cached data
                mode_entity = await self.fixed_mode.getting_old_data()
                logger.warning("Failed to fetch start time from API Response, retrieving from Database")
                if mode_entity:
                    self.limit_start = datetime.datetime.strptime(mode_entity.limit_start, "%H:%M:%S").time()
                    self.limit_end = datetime.datetime.strptime(mode_entity.limit_end, "%H:%M:%S").time()
                    self.no_of_days = mode_entity.day_list
                    self.mode = mode_entity.mode
                    self.tracking_scenario = mode_entity.tracking
                    logger.info("updated tracking scenerio from database%s %s %s", self.limit_start, self.limit_end, self.no_of_days)
                    self.screenshot_controller.limit_start = self.limit_start
                    self.screenshot_controller.limit_end = self.limit_end
                    self.screenshot_controller.no_of_days = self.no_of_days
                else:
                    logger.info("No tracking scenerio found")
        except Exception as e:
            mode_entity = await self.fixed_mode.getting_old_data()
            logger.warning("Failed to fetch start time from API, retrieving from Database")
        
            if mode_entity:
                self.limit_start = datetime.datetime.strptime(mode_entity.limit_start, "%H:%M:%S").time()
                self.limit_end = datetime.datetime.strptime(mode_entity.limit_end, "%H:%M:%S").time()
                self.no_of_days = mode_entity.day_list
                self.mode = mode_entity.mode
                self.tracking_scenario = mode_entity.tracking
                logger.info("updated tracking scenerio from database%s %s %s", self.limit_start, self.limit_end, self.no_of_days)
            else:
                logger.info("No tracking scenerio found")

    async def midnight(self, user_entity: UserEntity, last_count):
        # Use centralized read lock for database read operations
        # Database operation timeout constant
        DB_OPERATION_TIMEOUT = 5.0
        
        with db_lock_manager.read_lock():
            async with async_session() as session:
                query = select(ActivityInterval).where(ActivityInterval.counter == last_count+1).order_by(ActivityInterval.start_time.desc()).limit(1)
                try:
                    result = await asyncio.wait_for(
                        session.scalars(query),
                        timeout=DB_OPERATION_TIMEOUT
                    )
                    last_entry = result.first()  # Fetch the first (most recent) entry
                    if last_entry:
                        last_entry = last_entry.start_time
                    else:
                        last_entry = None
                except asyncio.TimeoutError:
                    logger.error("Database query timed out after %ss in midnight", DB_OPERATION_TIMEOUT)
                    last_entry = None
                except Exception as e:
                    logger.exception("Error getting last interval: %s", e)
                    last_entry = None
        
        # Guard clause: return early if user_entity is None
        if user_entity is None:
            logger.warning("Cannot execute midnight operations: user_entity is None")
            return
        
        # S3 bucket update removed from midnight - now handled on app startup and wake
        # After releasing the lock, do the rest
        await self.update_interval(user_entity) # update Org settings in every interval
        await self.ensure_s3_credentials_fresh(user_entity)
        logger.debug("ORG check at every interval")
    
    def decrypt(self, encrypted_key) -> str:
        """Decrypt encrypted key with improved error handling."""
        if not encrypted_key or not isinstance(encrypted_key, str):
            raise ValidationError("Encrypted key must be a non-empty string", context={"encrypted_key": str(encrypted_key)[:50]})
            
        try:
            position_of_dollar = encrypted_key.find('$')
            if position_of_dollar == -1:
                raise ValidationError("Invalid encrypted key format: missing '$' delimiter", context={"encrypted_key": encrypted_key[:50]})
                
            new_string = encrypted_key[position_of_dollar + 1:]
            if not new_string:
                raise ValidationError("Invalid encrypted key format: empty data after '$'", context={"encrypted_key": encrypted_key[:50]})
                
            new_string = new_string[::-1]
            decrypted_data = base64.b64decode(new_string).decode()
            return decrypted_data
            
        except ValueError as e:
            logger.error("Value error in decryption: %s", e)
            raise ValidationError(f"Invalid base64 data in encrypted key: {e}", context={"encrypted_key": encrypted_key[:50]})
        except UnicodeDecodeError as e:
            logger.error("Unicode decode error in decryption: %s", e)
            raise ValidationError(f"Invalid UTF-8 data in encrypted key: {e}", context={"encrypted_key": encrypted_key[:50]})
        except Exception as e:
            logger.error("Unexpected error in decryption: %s", e)
            raise ValidationError(f"Unexpected error decrypting key: {e}", context={"encrypted_key": encrypted_key[:50]})
    
    def _handle_wake_s3_update(self):
        """
        Handle system wake event - check if new day and update S3 bucket config.
        This is called when system wakes from sleep, not at midnight.
        """
        try:
            # This is a synchronous callback, so we need to check if user_entity is available
            # and schedule async operation if needed
            if not hasattr(self, '_user_entity_for_wake') or self._user_entity_for_wake is None:
                logger.debug("[WAKE] No user_entity available for S3 bucket update on wake")
                return
            
            user_entity = self._user_entity_for_wake
            
            # Check if it's a new day by comparing with last activity interval date
            try:
                # Try to get event loop
                loop = asyncio.get_event_loop()
                if loop.is_running():
                    # Schedule async operation
                    asyncio.create_task(self._check_and_update_s3_on_wake(user_entity))
                else:
                    # Run async operation
                    asyncio.run(self._check_and_update_s3_on_wake(user_entity))
            except RuntimeError:
                # No event loop, create one
                asyncio.run(self._check_and_update_s3_on_wake(user_entity))
        except Exception as e:
            logger.error("[WAKE] Error handling S3 bucket update on wake: %s", e, exc_info=True)
    
    async def _check_and_update_s3_on_startup(self, user_entity):
        """
        Check if it's a new day on app startup and update S3 bucket config if needed.
        Called when app starts.
        """
        try:
            # Use centralized read lock for database read operations
            DB_OPERATION_TIMEOUT = 5.0
            
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    query = select(ActivityInterval).where(
                        ActivityInterval.counter == self.last_count + 1
                    ).order_by(ActivityInterval.start_time.desc()).limit(1)
                    try:
                        result = await asyncio.wait_for(
                            session.scalars(query),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        last_entry = result.first()
                        if last_entry:
                            last_entry_date = last_entry.start_time.date()
                        else:
                            last_entry_date = None
                    except asyncio.TimeoutError:
                        logger.error("Database query timed out after %ss in startup S3 check", DB_OPERATION_TIMEOUT)
                        last_entry_date = None
                    except Exception as e:
                        logger.exception("Error getting last interval on startup: %s", e)
                        last_entry_date = None
            
            # Check if it's a new day or STS credentials expired
            current_date = datetime.datetime.now().date()
            credentials_stale = await self._s3_credentials_need_refresh(user_entity)
            if last_entry_date is None or last_entry_date != current_date or credentials_stale:
                if credentials_stale:
                    logger.info("[STARTUP] STS credentials expired — refreshing S3 bucket config")
                elif last_entry_date is None or last_entry_date != current_date:
                    logger.info("[STARTUP] New day detected (last: %s, current: %s) - updating S3 bucket config", last_entry_date, current_date)
                await self.ensure_s3_credentials_fresh(user_entity)
            else:
                logger.debug("[STARTUP] Same day (last: %s, current: %s) - no S3 bucket update needed", last_entry_date, current_date)
        except Exception as e:
            logger.error("[STARTUP] Error checking/updating S3 bucket on startup: %s", e, exc_info=True)
    
    async def _check_and_update_s3_on_wake(self, user_entity):
        """
        Check if it's a new day and update S3 bucket config if needed.
        Called on app wake from sleep.
        """
        try:
            # Use centralized read lock for database read operations
            DB_OPERATION_TIMEOUT = 5.0
            
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    query = select(ActivityInterval).where(
                        ActivityInterval.counter == self.last_count + 1
                    ).order_by(ActivityInterval.start_time.desc()).limit(1)
                    try:
                        result = await asyncio.wait_for(
                            session.scalars(query),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        last_entry = result.first()
                        if last_entry:
                            last_entry_date = last_entry.start_time.date()
                        else:
                            last_entry_date = None
                    except asyncio.TimeoutError:
                        logger.error("Database query timed out after %ss in wake S3 check", DB_OPERATION_TIMEOUT)
                        last_entry_date = None
                    except Exception as e:
                        logger.exception("Error getting last interval on wake: %s", e)
                        last_entry_date = None
            
            # Check if it's a new day or STS credentials expired
            current_date = datetime.datetime.now().date()
            credentials_stale = await self._s3_credentials_need_refresh(user_entity)
            if last_entry_date is None or last_entry_date != current_date or credentials_stale:
                if credentials_stale:
                    logger.info("[WAKE] STS credentials expired — refreshing S3 bucket config")
                elif last_entry_date is None or last_entry_date != current_date:
                    logger.info("[WAKE] New day detected (last: %s, current: %s) - updating S3 bucket config", last_entry_date, current_date)
                await self.ensure_s3_credentials_fresh(user_entity)
            else:
                logger.debug("[WAKE] Same day (last: %s, current: %s) - no S3 bucket update needed", last_entry_date, current_date)
        except Exception as e:
            logger.error("[WAKE] Error checking/updating S3 bucket on wake: %s", e, exc_info=True)
    
    async def _s3_credentials_need_refresh(self, user_entity: UserEntity) -> bool:
        """True when stored STS credentials are missing or past expire_time."""
        if user_entity is None:
            return True
        from ws_package.utils.s3 import s3_credentials_expired

        existing = await self.server_controller.server_service.get_s3bucket(user_entity.org_id)
        return existing is None or s3_credentials_expired(existing)

    async def ensure_s3_credentials_fresh(self, user_entity: UserEntity) -> S3BucketEntity | None:
        """Refresh S3 credentials from sts/keys when expire_time has passed."""
        return await self.server_controller.ensure_fresh_s3_bucket(user_entity)

    async def get_bucket_in_every_interval(self, user_entity: UserEntity) -> S3BucketEntity | None:
        """Refresh S3 bucket credentials from the remote API when needed.

        Calls sts/keys only when credentials are missing or ``expire_time`` has passed.
        The underlying HTTP call is synchronous (``requests``).  It is offloaded
        to the thread-pool executor so the event loop is never blocked by network
        latency or retry back-off, regardless of how long the server takes to
        respond.
        """
        return await self.ensure_s3_credentials_fresh(user_entity)

    async def update_interval(self, user_entity: UserEntity):
        """Refresh org settings (interval, screenshot frequency, limits) from the remote API.

        Called at every interval boundary.  The underlying HTTP call is
        synchronous (``requests``); it is offloaded to the thread-pool executor
        so the event loop is never blocked during network I/O or retry sleeps.
        """
        if user_entity is None:
            logger.warning("Cannot update interval: user_entity is None")
            return

        self.user_service.application_org_config_url = config('application_org_config_url')
        sd_token = user_entity.sd_token
        org_id = user_entity.org_id
        user_id = user_entity.user_id
        self.user_service.headers = {
            'SDToken': str(sd_token),
            'OrgID': str(org_id),
            'Authorization': f'Bearer {sd_token}',
        }
        payload = {"user_id": user_id, "org_id": org_id}

        try:
            # Offload the blocking HTTP call to a thread-pool executor.
            loop = asyncio.get_event_loop()
            response_data = await loop.run_in_executor(
                None,
                self.user_service.fetch_user_org,
                payload,
            )

            if response_data is None:
                logger.warning(
                    "No org settings from API (rate limit or error) — skipping update_interval apply"
                )
                await self._load_org_settings_from_db()
                return

            # Extract weekly_limit data if present
            weekly_limit_data = response_data.get("weekly_limit")
            if weekly_limit_data:
                # Add weekly_limit fields to response_data for OrgEntity
                response_data["weekly_limit"] = weekly_limit_data
                response_data["org_week_start_day"] = weekly_limit_data.get("org_week_start_day")
                response_data["worked_total_hours_week"] = weekly_limit_data.get("worked_total_hours_week")
                response_data["worked_total_hours_week_leaving_today"] = weekly_limit_data.get("worked_total_hours_week_leaving_today")
                response_data["daily_limit"] = weekly_limit_data.get("daily_limit")
                response_data["weekly_limit_hours"] = weekly_limit_data.get("weekly_limit")
                response_data["worked_total_hours_today"] = weekly_limit_data.get("worked_total_hours_today")
                # limit_action lives inside weekly_limit — must be extracted here, not outside
                response_data["limit_action"] = weekly_limit_data.get("limit_action")
            else:
                # API did not return weekly_limit; leave limit_action unset so OrgEntity uses its default
                response_data.setdefault("limit_action", None)
            
            # Extract idle_time_limit_in_seconds from API response
            if "idle_time_limit_in_seconds" in response_data:
                response_data["idle_time_limit_in_seconds"] = response_data.get("idle_time_limit_in_seconds", 3600)
            else:
                response_data["idle_time_limit_in_seconds"] = 3600  # Default fallback

            from ws_package.entities.org_entity import normalize_timezone_preference

            response_data["timezone_preference"] = normalize_timezone_preference(
                response_data.get("timezone_preference", 1)
            )

            org_entity = OrgEntity(**response_data)
        except Exception as e:
            logger.exception("Error fetching user org: %s", e)
            response_data = None

        if response_data is not None:

            # saving into local DB again for the new values    
            await self.user_service.save_user_org(org_entity)
                 
            if response_data.get("timeInterval") == "5 Min.":
                self.interval = 300
            elif response_data.get("timeInterval") == "15 Min.":
                self.interval = 900
            else:
                self.interval = 600
            
            # Cast to int — module-level _to_int() handles None / string values safely.
            self.screenshot_interval = _to_int(response_data.get("ssfrequencystatus"), default=0)
            self.appandurlstatus = _to_int(response_data.get("appandurlstatus"), default=0)
            self.blur_intensity = _to_int(response_data.get("screenshotblurstatus"), default=0)
            self.notification_status = 0
            self.org_api_timezone = response_data.get("user_timezone")
            self.limit_action = response_data.get("limit_action")
            self._apply_org_timezone_preference(
                response_data.get("timezone_preference", 1),
                user_entity,
            )

            # Critical diagnostic: log the org tracking flags at INFO so we can confirm
            # whether screenshots and app/URL tracking are enabled at the org level.
            # ssfrequencystatus=0 → screenshots OFF; appandurlstatus=0 → tracking OFF.
            logger.info(
                "[update_interval] org settings — ssfrequencystatus=%s, appandurlstatus=%s, "
                "screenshotblurstatus=%s, timeInterval=%s, timezone_preference=%s",
                self.screenshot_interval, self.appandurlstatus,
                self.blur_intensity, response_data.get("timeInterval"),
                self.timezone_preference,
            )

            self.screenshot_controller.screenshot_interval = self.screenshot_interval
            
            # Store idle_time_limit_in_seconds from API
            self.idle_time_limit_in_seconds = org_entity.idle_time_limit_in_seconds or 3600
            logger.info("Updated idle_time_limit_in_seconds from API: %s seconds", self.idle_time_limit_in_seconds)
            self.screenshot_controller.blur_intensity = self.blur_intensity
            self.screenshot_controller.interval = self.interval
            self.screenshot_controller.status = self.status
            
            # Check limits and handle notifications/timer stopping
            await self._check_and_handle_limits(user_entity, org_entity)

    def _apply_org_timezone_preference(self, preference, user_entity: UserEntity = None) -> None:
        """Apply timezone_preference from org settings and toggle time monitor if it flips."""
        from ws_package.entities.org_entity import normalize_timezone_preference

        previous = getattr(self, "timezone_preference", None)
        normalized = normalize_timezone_preference(preference)
        self.timezone_preference = normalized
        if user_entity is not None:
            user_entity.timezone_preference = normalized
        if getattr(self, "user_entity", None) is not None:
            self.user_entity.timezone_preference = normalized

        if previous is None or int(previous) == int(normalized):
            return

        logger.info(
            "[TimezonePref] org timezone_preference changed %s → %s",
            previous,
            normalized,
        )
        time_monitor = getattr(getattr(self, "activity_manager", None), "time_monitor", None)
        if not time_monitor:
            return
        try:
            if normalized == 0:
                time_monitor.start_monitoring()
                logger.info("[TimezonePref] Time monitor started after org preference enabled checks")
            else:
                time_monitor.stop_monitoring()
                logger.info("[TimezonePref] Time monitor stopped after org preference disabled checks")
        except Exception as e:
            logger.warning("[TimezonePref] Failed to toggle time monitor: %s", e)

    async def _load_org_settings_from_db(self):
        """Load organization settings from local database as fallback."""
        try:
            # Use centralized read lock
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    query = select(OrgConfigurations).order_by(OrgConfigurations.id.desc()).limit(1)
                    result = await session.scalars(query)
                    record = result.first()
                    
                    if record:
                        logger.info("Loaded org settings from DB: timezone=%s", record.user_timezone)
                        self.org_api_timezone = record.user_timezone
                        # Also restore other critical settings if needed
                        if record.timeInterval == "5 Min.":
                            self.interval = 300
                        elif record.timeInterval == "15 Min.":
                            self.interval = 900
                        else:
                            self.interval = 600
                            
                        # Cast to int — module-level _to_int() handles None / string values safely.
                        self.screenshot_interval = _to_int(record.ssfrequencystatus)
                        self.appandurlstatus = _to_int(record.appandurlstatus)
                        self.blur_intensity = _to_int(record.screenshotblurstatus)
                        self._apply_org_timezone_preference(
                            getattr(record, "timezone_preference", 1),
                            getattr(self, "user_entity", None),
                        )

                        # Update screenshot controller
                        self.screenshot_controller.screenshot_interval = self.screenshot_interval
                        self.screenshot_controller.blur_intensity = self.blur_intensity
                        self.screenshot_controller.interval = self.interval
                        
                        return True
            return False
        except Exception as e:
            logger.error("Error loading org settings from DB: %s", e)
            return False

    async def _check_and_handle_limits(self, user_entity: UserEntity, org_entity: OrgEntity):
        """
        Check if daily or weekly limits are exceeded and handle accordingly.
        
        Args:
            user_entity: User entity for API calls
            org_entity: Organization entity with limit data
        """
        if is_silent_build():
            return
        try:
            notification_service = get_weekly_limit_notification_service()
            
            # Check if limits are exceeded
            limits_check = notification_service.check_limits_exceeded(
                daily_limit=org_entity.daily_limit,
                weekly_limit_hours=org_entity.weekly_limit_hours,
                worked_total_hours_today=org_entity.worked_total_hours_today,
                worked_total_hours_week=org_entity.worked_total_hours_week
            )
            
            daily_exceeded = limits_check.get('daily_exceeded', False)
            weekly_exceeded = limits_check.get('weekly_exceeded', False)
            
            if daily_exceeded or weekly_exceeded:
                # Prioritize daily limit if both are exceeded, otherwise use the one that's exceeded
                limit_type = "daily" if daily_exceeded else "weekly"
                
                # Log limit exceeded
                logger.info("")
                logger.info("=" * 80)
                logger.info("*** %s LIMIT EXCEEDED ***", limit_type.upper())
                logger.info("=" * 80)
                logger.info("Daily Limit: %s", org_entity.daily_limit)
                logger.info("Weekly Limit: %s", org_entity.weekly_limit_hours)
                logger.info("Worked Today: %s", org_entity.worked_total_hours_today)
                logger.info("Worked This Week: %s", org_entity.worked_total_hours_week)
                logger.info("Limit Action: %s", self.limit_action)
                logger.info("=" * 80)
                logger.info("")
                
                # Report to API (in background thread to avoid blocking)
                def report_in_background():
                    try:
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        loop.run_until_complete(
                            notification_service.report_limit_exceeded(
                                user_entity=user_entity,
                                limit_type=limit_type,
                                daily_limit=org_entity.daily_limit,
                                weekly_limit_hours=org_entity.weekly_limit_hours,
                                worked_total_hours_today=org_entity.worked_total_hours_today,
                                worked_total_hours_week=org_entity.worked_total_hours_week,
                                limit_action=self.limit_action,
                                device_id=self.system_unique_id
                            )
                        )
                    except Exception as e:
                        logger.error("Error in background limit notification: %s", e)
                    finally:
                        loop.close()
                
                report_thread = threading.Thread(target=report_in_background, name="WeeklyLimitNotification")
                report_thread.daemon = True
                report_thread.start()
                
                # Handle timer stopping if limit_action is "stop_timer"
                if self.limit_action == "stop_timer":
                    today = datetime.datetime.now().date()
                    
                    # Only stop if we haven't already stopped today (or date changed)
                    if self.limit_stopped_date != today:
                        logger.info("")
                        logger.info("=" * 80)
                        logger.info("*** STOPPING TIMER DUE TO LIMIT EXCEEDED ***")
                        logger.info("=" * 80)
                        logger.info("Limit Action: %s", self.limit_action)
                        logger.info("Limit Type: %s", limit_type)
                        logger.info("Timer will remain stopped until date changes or system restarts")
                        logger.info("=" * 80)
                        logger.info("")
                        
                        self.status = False
                        self.limit_stopped_date = today
                        self.force_stop = True
                        
                        # Also stop screenshot controller
                        if hasattr(self.screenshot_controller, 'status'):
                            self.screenshot_controller.status = False
                else:
                    # just_notify - continue tracking, just log
                    logger.info("")
                    logger.info("=" * 80)
                    logger.info("*** LIMIT EXCEEDED - NOTIFICATION ONLY ***")
                    logger.info("=" * 80)
                    logger.info("Limit Action: %s", self.limit_action)
                    logger.info("Tracking will continue as normal")
                    logger.info("=" * 80)
                    logger.info("")
        
        except Exception as e:
            logger.error("Error checking and handling limits (fail-safe): %s", e)

    def _send_subscription_expired_popup(self, _response=None):
        """Send subscription-expired popup to Electron (same message for token expired or plan deactivated). All OSes."""
        try:
            from ws_package.utils.user_error_categories import enrich_subscription_payload

            category_fields = enrich_subscription_payload(_response)
            error_data = {
                'title': f"{category_fields['userCategory']} — {category_fields['userCategoryTitle']}",
                'message': "Your account is currently inactive (deleted, deactivated, or subscription expired).\nPlease contact your organization administrator to restore access.",
                'planStatus': _response.get('planStatus') if _response else None,
                'isdeleted': _response.get('isdeleted') if _response else None,
                'isdeactivated': _response.get('isdeactivated') if _response else None,
                'timestamp': datetime.datetime.now().isoformat(),
                **category_fields,
            }
            if self._send_to_electron_if_connected('subscription_expired', error_data):
                logger.info("Sent subscription expiration error to Electron: %s", error_data)
        except Exception as e:
            logger.error("Failed to send subscription expiration error to Electron: %s", e)

    async def start_screenshot_activity(self, user_entity: UserEntity, stop_event=None):
        await precise_sleep(10)
        logger.info("Starting screenshot activity with interval: %s and ss frequency: %s blur intensity: %s status: %s limit start: %s limit end: %s no of days: %s", self.interval, self.screenshot_interval, self.blur_intensity, self.status, self.limit_start, self.limit_end, self.no_of_days)
        await self.screenshot_controller.start_screenshot_activity(
            user_entity,
            self.limit_start,
            self.limit_end,
            self.no_of_days,
            self.screenshot_interval,
            self.interval,
            self.blur_intensity,
            self.status,
            stop_event
        )
    
    async def start_device_activity(self, user_entity: UserEntity, stop_event: Optional[asyncio.Event] = None) -> None:
        self.user_entity = user_entity
        """
        Start the main device activity monitoring loop.
        
        This is the primary entry point for activity monitoring. It delegates to the
        internal implementation while providing a clean public interface.
        
        Args:
            user_entity (UserEntity): User information and authentication details.
            stop_event (Optional[asyncio.Event]): Event to signal monitoring should stop.
                                                 Defaults to None.
                                                 
        Example:
            >>> controller = ActivityController(last_count=1)
            >>> stop_event = asyncio.Event()
            >>> await controller.start_device_activity(user_entity, stop_event)
        """
        # Store user_entity for wake callback access
        self._user_entity_for_wake = user_entity
        
        
        # Check if it's a new day on app startup and update S3 bucket config if needed
        await self._check_and_update_s3_on_startup(user_entity)
        
        # Check persistent tracker status before starting
        if not is_tracking_enabled():
            logger.warning("=" * 80)
            logger.warning("[Tracker] Tracking is disabled (status='stop') - device activity thread will wait")
            logger.warning("[Tracker] WebSocket connection is active - waiting for 'start' command")
            logger.warning("=" * 80)
            # Keep force_stop and status as False, but continue to start the thread
            # The thread will check status and wait until it's enabled
            self.force_stop = True
            self.status = False
            # Continue to start the thread - it will wait when status is False
        
        # Start the minute-by-minute rolling average timer
        minute_timer_task = asyncio.create_task(self.__start_minute_activity_timer(stop_event))
        self._background_tasks.add(minute_timer_task)
        minute_timer_task.add_done_callback(self._background_tasks.discard)
        
        # ActivityPercentage queue processor removed - using universal db_write_queue instead
        
        # Start the app monitor flush task for batching
        await self.activity_service.start_app_monitor_flush_task()
        logger.info("Started app monitor flush task")
        
        try:
            # Start the main device activity monitoring
            await self.__start_device_activity(user_entity, stop_event)
        finally:
            # Ensure the minute timer and all background tasks are cancelled when main loop exits
            minute_timer_task.cancel()
            # queue_processor_task.cancel()
            # queue_processor_task removed - using universal db_write_queue instead
            # Cancel all background tasks
            current_loop = asyncio.get_running_loop()
            for task in list(self._background_tasks):
                if not task.done():
                    try:
                        # Only cancel tasks that belong to the current event loop
                        task_loop = task.get_loop()
                        if task_loop == current_loop:
                            task.cancel()
                    except Exception as e:
                        logger.debug("Error cancelling background task: %s", e)
            
            logger.info("Device activity monitoring stopped")
    
    # Timer warning monitoring has been moved to Electron client-side
    # This method is no longer needed - reminder logic is now handled entirely in the Electron renderer

    async def getStartingSlot(self) -> Tuple[datetime.datetime, datetime.datetime]:
        """
        Calculate the starting time slot for activity monitoring.
        
        This method determines the appropriate start and end times for the first
        activity monitoring interval based on the current time and configured
        interval duration.
        
        Returns:
            Tuple[datetime.datetime, datetime.datetime]: Start and end times for
            the initial monitoring slot.
            
        Example:
            >>> controller = ActivityController(last_count=1)
            >>> start_time, end_time = await controller.getStartingSlot()
            >>> print(f"Monitoring from {start_time} to {end_time}")
        """
        last_minute = 50
        if int(self.interval/60) == 15:
            last_minute = 45
        if int(self.interval/60) ==5:
            last_minute = 55
        start_time=datetime.datetime.now()
        self.tzmain = start_time
        minute=start_time.minute
        if minute<last_minute:
            end_time=start_time.replace(minute=((int(self.interval/60)-(minute%int(self.interval/60)))+minute),second=00, microsecond=00)
        else:
            hr=start_time.hour
            if hr == 23:
                # Use timedelta to handle month/year rollovers correctly
                next_day = start_time + datetime.timedelta(days=1)
                logger.debug("Day rollover detected: %s -> %s", start_time.day, next_day.day)
                end_time = next_day.replace(minute=00, hour=00, second=00, microsecond=00)
                return start_time, end_time
            end_time=start_time.replace(minute=00,hour=hr+1,second=00, microsecond=00)
        return start_time,end_time

    async def getCurrentSlot(self):
        start_date = datetime.datetime.now()
        self.tzmain = start_date
        minute = start_date.minute

        # Round down to the nearest 10-minute interval
        rounded_minute = (minute // int(self.interval/60)) * int(self.interval/60)

        # Create the end date with the rounded minute
        end_date = start_date.replace(minute=rounded_minute, second=0, microsecond=0)
        return end_date

    def _align_to_interval_boundary(self, time: datetime.datetime) -> datetime.datetime:
        """
        Align a given time to the nearest interval boundary (round down).
        
        For example, with 10-minute intervals:
        - 16:42:30 -> 16:40:00
        - 16:40:00 -> 16:40:00
        - 16:39:59 -> 16:30:00
        
        Args:
            time: The datetime to align
            
        Returns:
            datetime aligned to interval boundary
        """
        interval_minutes = int(self.interval / 60)
        minute = time.minute
        rounded_minute = (minute // interval_minutes) * interval_minutes
        aligned_time = time.replace(minute=rounded_minute, second=0, microsecond=0)
        return aligned_time

    def is_browser(self, app_name):
        # Normalize and clean string
        normalized_name = unicodedata.normalize("NFKD", app_name)
        cleaned_name = ''.join(c for c in normalized_name if c.isprintable())
        cleaned_name = re.sub(r'[\u200B-\u200D\uFEFF]', '', cleaned_name)  # Remove zero-width chars
        
        # Convert to lowercase for case-insensitive matching
        cleaned_name_lower = cleaned_name.lower()

        browsers = ['Google Chrome', 'Mozilla Firefox', 'Safari', 'Microsoft Edge', 'Opera', 'Msedge', 'Chrome', 'Firefox', 'Brave Browser']
        # Case-insensitive matching
        return any(browser.lower() in cleaned_name_lower for browser in browsers)

    async def __start_minute_activity_timer(self, stop_event):
        """Start minute-by-minute activity rolling average timer"""
        logger.info("Starting minute-by-minute activity rolling average timer")
        
        while not stop_event.is_set():
            try:
                # Wait for 1 minute
                await precise_sleep(60, stop_event)
                
                if stop_event.is_set():
                    break
                
                # CRITICAL: Continue activity calculation when on break (break_id != 0) even if status=False
                # This ensures activity percentages are calculated and stored during break sessions
                current_break_id = getattr(self, 'break_id', 0) or 0
                if not self.status and current_break_id == 0:
                    logger.debug("[Tracker] Activity calculation skipped - tracking is stopped")
                    continue
                
                # Skip activity calculation if system is sleeping
                if self._is_system_sleeping():
                    logger.info("[ACTIVITY] System is sleeping - skipping activity calculation and storage")
                    continue
                
                # Get current activity level for the past minute
                
                minute_activity_level, _ = await self.device_listener.get_activity_level(60)
                
                # Store rolling averages in activity_percentage table (updates latest row)
                # Pass interval_minutes to ensure minute counter is capped correctly
                await activity_percentage_service.calculate_and_store_rolling_average(
                    keyboard_percentage=float(minute_activity_level.get('keyboard', 0)),
                    mouse_percentage=float(minute_activity_level.get('mouse', 0)),
                    average_percentage=float(minute_activity_level.get('average', 0)),
                    interval_minutes=int(self.interval / 60)  # 5, 10, or 15 minutes
                )
                
                # logger.info(f"Minute activity rolled up - Keyboard: {minute_activity_level.get('keyboard')}%, "
                #           f"Mouse: {minute_activity_level.get('mouse')}%, "
                #           f"Average: {minute_activity_level.get('average')}%")  # DISABLED to reduce logging overhead - keyboard/mouse events
                
            except Exception as e:
                logger.exception("Error in minute activity timer: %s", e)
                # Continue running even if there's an error
                continue
        
        logger.info("Minute activity timer stopped")

    async def __start_device_activity(self, user_entity:UserEntity, stop_event):

        # Update start_date to actual timer start time (when user clicks Start)
        self.start_date = datetime.datetime.now().replace(microsecond=0)
        # Clear session start so payload "start" uses start_date (full timer start, not post-break)
        self.current_session_start_time = None
        self._activity_log_payload_start = None

        # CRITICAL: Ensure status is True when device activity starts (unless force_stop is set)
        # This ensures intervals are saved immediately, even before status thread updates it
        # This is especially important after break stops when monitoring resumes
        # Only set to True if force_stop is not True (tracking is not explicitly disabled)
        if (not hasattr(self, 'status') or self.status is False) and not getattr(self, 'force_stop', False):
            # Set status to True as fallback - status thread will update it later based on API response
            self.status = True
            logger.info("Set status=True as fallback when starting device activity (status thread will update it based on API response)")
        
        # CRITICAL: Always restart device listeners when device activity starts
        # This ensures listeners are working after breaks, restarts, or any other scenario
        # Reset the tracking resumed flag so the loop can also check and log
        if hasattr(self, 'device_listener') and self.device_listener:
            try:
                # Reset flag so the loop can also handle it
                self._tracking_resumed_logged = False
                
                logger.info("Restarting device listeners at start of device activity...")
                self.device_listener.restart_listeners()
                logger.info("Device listeners restarted at start of device activity - keyboard and mouse events should now be logged")
            except Exception as e:
                logger.error("Error restarting device listeners at start of device activity: %s", e, exc_info=True)
        
        # Start the platform-appropriate time monitor when device activity begins.
        # Windows: OptimizedWindowsTimeMonitor (registry-based).
        # macOS/Linux: CrossPlatformTimeMonitor (monotonic-drift).
        if (
            hasattr(self, 'activity_manager')
            and self.activity_manager.time_monitor
            and self._timezone_checks_enabled(user_entity)
        ):
            try:
                logger.info("Starting time monitor (%s)...", platform.system())
                def _on_time_error(error_type=None):
                    if not self._timezone_checks_enabled():
                        logger.info(
                            "Ignoring time-monitor event because timezone_preference=1 (checks off)"
                        )
                        return
                    # Only true timezone changes block tracking; clock drift is informational.
                    self._time_error_type = error_type or "datetime_error"
                    if self._time_error_type == "timezone_error":
                        self._timezone_mismatch = True
                        logger.error(
                            "Timezone change detected by time monitor — tracking will stop"
                        )
                    else:
                        logger.warning(
                            "System clock drift detected by time monitor — "
                            "not treating as timezone mismatch"
                        )
                self.activity_manager.time_monitor.set_on_time_error_callback(_on_time_error)
                self.activity_manager.time_monitor.start_monitoring()
                logger.info("Time monitor started — timezone and datetime changes will be detected")
            except Exception as e:
                logger.error("Error starting time monitor: %s", e, exc_info=True)
        elif hasattr(self, 'activity_manager') and self.activity_manager.time_monitor:
            logger.info(
                "Timezone/time monitor disabled by login timezone_preference=1"
            )
            try:
                self.activity_manager.time_monitor.stop_monitoring()
            except Exception as e:
                logger.warning("Could not stop disabled time monitor: %s", e)
        
        # CRITICAL FIX: Ensure org settings (especially timezone) are loaded
        try:
            logger.info("Fetching latest org settings before starting activity loop...")
            logger.debug("[Timing][Python] Fetching org settings in __start_device_activity")
            await self.update_interval(user_entity)
            logger.debug("[Timing][Python] Org settings fetched. Timezone: %s", self.org_api_timezone)
            
            # If timezone is still missing (e.g. offline or update_interval failed), try DB
            if self.org_api_timezone is None:
                logger.warning("org_api_timezone is None after update_interval, trying local DB...")
                logger.debug("[Timing][Python] org_api_timezone is None, trying DB...")
                await self._load_org_settings_from_db()
                
            if self.org_api_timezone is None:
                logger.error("CRITICAL: org_api_timezone is still None. Timezone checks may fail!")
        except Exception as e:
            logger.error("Error initializing org settings: %s", e)
            # Try DB as last resort
            await self._load_org_settings_from_db()

        # Reset Flag to False for new session to ensure getStartingSlot() is used on first iteration
        self.Flag = False
        logger.info("Timer started at: %s, appsandurlstatus: %s, interval: %s, Flag reset to False for new session", self.start_date, self.appandurlstatus, self.interval)

        if user_entity is not None and getattr(self, "is_break", 0) != 1:
            try:
                work_session_start = self._normalize_activity_log_start(self.start_date)
                self.start_date = work_session_start
                await self.push_open_session_activity_log(
                    user_entity,
                    start_time=work_session_start,
                    break_id=None,
                )
            except Exception as e:
                logger.error("Error posting open work session activity/log at monitoring start: %s", e, exc_info=True)

        if user_entity is not None:
            try:
                from ws_package.log.writer import enable_hourly_log_upload
                enable_hourly_log_upload(user_entity, user_entity.org_id, self.s3_logs_path)
                logger.info("Hourly log upload enabled through logging thread")
            except Exception as e:
                logger.error("Failed to enable hourly log upload: %s", e)
        else:
            logger.warning("Skipping hourly log upload initialization: user_entity is None")

        app_listener = Window()
        last_app_monitor_time = datetime.datetime.now()

        # No complex time monitoring needed - using S3 error detection

        async def monitor_active_apps():
            nonlocal last_app_monitor_time
            logger.info(
                "[AppMonitor] monitor_active_apps started — OS: %s, appandurlstatus: %s, interval: %ss",
                self.get_os_type(), self.appandurlstatus, self.app_monitor_interval_seconds,
            )
            while True:
                # Always sleep first to ensure proper interval spacing
                await asyncio.sleep(self.app_monitor_interval_seconds)

                # Skip monitoring if tracking is stopped OR app/URL tracking is disabled.
                # Guard against None (can occur if update_interval() hasn't run yet).
                if not self.status or not self.appandurlstatus or self.appandurlstatus <= 0:
                    # Log at INFO on first skip, then DEBUG to avoid repeated noise.
                    # appandurlstatus=0 means org admin disabled app/URL tracking.
                    logger.info(
                        "[AppMonitor] Skipping cycle — status=%s, appandurlstatus=%s "
                        "(0 = tracking disabled by org; check org settings in dashboard)",
                        self.status, self.appandurlstatus,
                    )
                    # Subsequent skips at debug level until the value changes
                    while (not self.status or not self.appandurlstatus or self.appandurlstatus <= 0):
                        logger.debug(
                            "[AppMonitor] Still disabled — status=%s, appandurlstatus=%s",
                            self.status, self.appandurlstatus,
                        )
                        await asyncio.sleep(self.app_monitor_interval_seconds)
                    logger.info("[AppMonitor] Re-enabled — status=%s, appandurlstatus=%s", self.status, self.appandurlstatus)
                    continue

                try:
                        active_app_url = app_listener.get_active_windows()
                        logger.debug(
                            "[AppMonitor] OS: %s, appandurlstatus: %s, windows: %s",
                            self.get_os_type(), self.appandurlstatus,
                            len(active_app_url) if active_app_url else 0,
                        )
                        if (self.get_os_type() == "linux") and (self.appandurlstatus == 3 or self.appandurlstatus == 2) and len(active_app_url)>0:
                            # Apps + URLs mode (2) or full mode (3): resolve URL from browser DB.
                            app_name = active_app_url[0]['active_app']
                            url = None  # Will be set by URL detection

                            if self.is_browser(app_name):
                                url_result = await get_browser_url_with_fallback(
                                    tab_title=app_name,
                                )

                                if url_result and url_result.strip() and url_result != "New Tab":
                                    url = url_result.strip()
                                    logger.info("[Linux] URL detected via comprehensive method: %s", url)
                                else:
                                    url = "New Tab"
                                    logger.info("[Linux] All URL detection methods failed, using 'New Tab'")
                            else:
                                # Not a browser, use app name as URL
                                url = app_name

                            active_app_url = [{
                                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                                "module": url,
                                "active_app": app_name,
                                "url": url,
                            }]

                        elif (self.get_os_type() == "linux") and self.appandurlstatus == 1 and len(active_app_url) > 0:
                            # Apps-only mode on Linux: record app name, skip URL resolution.
                            app_name = active_app_url[0]['active_app']
                            active_app_url = [{
                                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                                "module": app_name,
                                "active_app": app_name,
                                "url": app_name,
                            }]
                        elif (self.get_os_type() == "windows") and (self.appandurlstatus == 3 or self.appandurlstatus == 2) and len(active_app_url)>0:
                            # Apps + URLs mode (2) or full mode (3): resolve URL from browser DB.
                            row = active_app_url[0]
                            i2t = row.get('url') or row.get('active_app', '')
                            display_name = row.get('app_display_name') or row.get('active_app', '')

                            # get_active_windows_ms() already reads Chrome History — reuse URL when present
                            if i2t.startswith(('http://', 'https://')):
                                pass
                            elif self.is_browser(display_name) or self.is_browser(i2t):
                                url_result = await get_browser_url_with_fallback(
                                    tab_title=display_name if self.is_browser(display_name) else i2t,
                                )
                                if url_result and url_result.strip() and url_result != "New Tab":
                                    i2t = url_result.strip()
                                else:
                                    i2t = "New Tab"

                            active_app_url = [{
                                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                                "module": row.get('active_app', i2t),
                                "active_app": row.get('active_app', i2t),
                                "url": i2t,
                            }]

                        elif (self.get_os_type() == "windows") and self.appandurlstatus == 1 and len(active_app_url) > 0:
                            # Apps-only mode on Windows: record app name, skip URL resolution.
                            app_name = active_app_url[0]['active_app']
                            active_app_url = [{
                                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                                "module": app_name,
                                "active_app": app_name,
                                "url": app_name,
                            }]

                        elif (self.get_os_type() == "darwin") and (self.appandurlstatus >= 1) and len(active_app_url)>0:
                            # Initialize i2t with default value
                            i2t = active_app_url[0]['active_app']  # Default to the app name

                            detected_app = active_app_url[0]['active_app']

                            if self.is_browser(detected_app):
                                # Browser-database URL detection only.
                                url_result = await get_browser_url_with_fallback(
                                    tab_title=detected_app,
                                )
                                if url_result and url_result.strip() and url_result != "New Tab":
                                    i2t = url_result.strip()
                                else:
                                    if self.appandurlstatus == 1:
                                        i2t = detected_app
                                        logger.debug("[macOS] Apps only mode: Using browser name '%s' instead of 'New Tab'", i2t)
                                    else:
                                        i2t = "New Tab"

                            active_app_url = [{
                                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                                "module": i2t,
                                "active_app": i2t,
                                "url": i2t,
                            }]

                        if self.status == False:
                            await self.activity_service.save_app_monitors(active_app_url, self.last_count+1, True) # Sleep + Shutdown case + Correct timezone
                        else:
                            await self.activity_service.save_app_monitors(active_app_url, self.last_count+1, False)

                        # Update the last time we checked for active apps
                        last_app_monitor_time = datetime.datetime.now()
                except Exception as e:
                    logger.exception("Error in monitoring active apps: %s", e)
                    # Update timestamp even on error to prevent rapid retries
                    last_app_monitor_time = datetime.datetime.now()
        
        # Create and track the monitor_active_apps task to prevent silent failures
        monitor_task = asyncio.create_task(monitor_active_apps())
        self._background_tasks.add(monitor_task)
        
        def task_done_callback(task):
            """Handle task completion and log any exceptions"""
            self._background_tasks.discard(task)
            if task.done():
                try:
                    task.result()  # This will raise any exception that occurred
                except asyncio.CancelledError:
                    # Task was cancelled (expected during shutdown), don't log as error
                    logger.debug("Background task monitor_active_apps was cancelled")
                except Exception as e:
                    logger.error("Background task monitor_active_apps failed: %s", e, exc_info=True)
        
        monitor_task.add_done_callback(task_done_callback)
        
        # Monitor active app changes for UI display (real-time updates)
        async def monitor_active_app_for_ui():
            """Monitor active app changes and send updates to Electron for UI display."""
            while not stop_event.is_set():
                try:
                    # Skip if tracking is stopped or on break
                    if not self.status or (getattr(self, 'break_id', 0) != 0 or self.is_break != 0):
                        await asyncio.sleep(2)  # Check every 2 seconds
                        continue
                    
                    # Get current active window (title only — skip Chrome History SQLite reads)
                    active_windows = app_listener.get_active_windows(
                        fetch_browser_url=False,
                        include_printers=False,
                    )
                    
                    if active_windows and len(active_windows) > 0:
                        current_app_data = active_windows[0]
                        current_app_name = current_app_data.get('app_display_name') or current_app_data.get('active_app', 'Unknown')
                        current_app_path = current_app_data.get('app_path')
                        
                        # Check if app has changed
                        if current_app_name != self._current_active_app:
                            logger.debug("Active app changed: %s -> %s", self._current_active_app, current_app_name)
                            
                            # Extract icon
                            app_icon = None
                            try:
                                app_icon = app_listener.get_app_icon(current_app_name, current_app_path)
                            except Exception as e:
                                logger.debug("Failed to extract icon for %s: %s", current_app_name, e)
                            
                            # Update tracked values
                            self._current_active_app = current_app_name
                            self._current_active_app_path = current_app_path
                            self._current_active_app_icon = app_icon
                            
                            # Send update to Electron
                            await self._send_active_app_update_to_electron(
                                app_name=current_app_name,
                                app_icon=app_icon
                            )
                    
                    # Poll every 8s — frequent foreground-window checks add hook/IO overhead on Windows
                    await asyncio.sleep(8)
                    
                except Exception as e:
                    logger.error("Error in monitor_active_app_for_ui: %s", e, exc_info=True)
                    await asyncio.sleep(2)  # Wait before retrying
        
        # Create and track the active app UI monitoring task
        active_app_ui_task = asyncio.create_task(monitor_active_app_for_ui())
        self._background_tasks.add(active_app_ui_task)
        
        def active_app_ui_task_done_callback(task):
            """Handle active app UI task completion"""
            self._background_tasks.discard(task)
            if task.done():
                try:
                    task.result()
                except asyncio.CancelledError:
                    logger.debug("Background task monitor_active_app_for_ui was cancelled")
                except Exception as e:
                    logger.error("Background task monitor_active_app_for_ui failed: %s", e, exc_info=True)
        
        active_app_ui_task.add_done_callback(active_app_ui_task_done_callback)
        
        # Create first dummy entry when timer starts (is_uploaded=True to mark session start, not to be uploaded)
        empty_activity_level =  {'mouse': '1.0', 'average': '0.0', 'keyboard': '0.0'} #for not consider idle if 2nd interval is idle as program consider 2 consicutive idle interval
        # CRITICAL: Read is_break at the time of saving to ensure current value is used
        current_break_id = getattr(self, 'break_id', 0) or 0  # Default to 0 if not available
        
        # CRITICAL: Check if first dummy entry already exists to prevent duplicates
        # This can happen if __start_device_activity is called multiple times (e.g., from deferred messages)
        try:
            # First, flush queue to ensure any pending writes are visible
            await db_writer_service.process_immediately()
            await asyncio.sleep(0.1)  # Small delay to ensure commit is visible
            
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    # Check for exact match: start_time == end_time == self.start_date AND counter matches
                    # This identifies the first dummy entry (which has start_time == end_time)
                    query = select(ActivityInterval).where(
                        and_(
                            ActivityInterval.start_time == self.start_date,
                            ActivityInterval.end_time == self.start_date,
                            ActivityInterval.counter == self.last_count + 1
                        )
                    ).limit(1)
                    result = await session.execute(query)
                    existing_dummy = result.scalars().first()
                    
                    if existing_dummy:
                        logger.info("First dummy entry already exists for start_date=%s, counter=%s - skipping duplicate creation", self.start_date, self.last_count+1)
                        logger.info("Existing dummy entry: start_time=%s, end_time=%s, counter=%s, is_uploaded=%s", existing_dummy.start_time, existing_dummy.end_time, existing_dummy.counter, existing_dummy.is_uploaded)
                        first_entry = existing_dummy
                    else:
                        logger.info("Creating first dummy entry: start_date=%s, counter=%s, is_uploaded=True, break_id=%s, will be saved as is_break=%s", self.start_date, self.last_count+1, current_break_id, current_break_id if current_break_id else 0)
                        logger.debug("[Timing][Python] Creating first dummy entry with break_id=%s...", current_break_id)
                        first_entry = await self.activity_service.save_first_activity_log(empty_activity_level, self.start_date, self.start_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id)
                        logger.debug("[Timing][Python] First dummy entry created")
        except Exception as e:
            # If check fails, proceed with creation (fail-safe to ensure functionality continues)
            logger.warning("Error checking for existing first dummy entry: %s - proceeding with creation", e)
            logger.info("Creating first dummy entry: start_date=%s, counter=%s, is_uploaded=True, break_id=%s, will be saved as is_break=%s", self.start_date, self.last_count+1, current_break_id, current_break_id if current_break_id else 0)
            logger.debug("[Timing][Python] Creating first dummy entry with break_id=%s...", current_break_id)
            first_entry = await self.activity_service.save_first_activity_log(empty_activity_level, self.start_date, self.start_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id)
            logger.debug("[Timing][Python] First dummy entry created")

        logger.info("First dummy entry created successfully: start_time=%s, end_time=%s, counter=%s, is_uploaded=%s", first_entry.start_time if first_entry else 'None', first_entry.end_time if first_entry else 'None', first_entry.counter if first_entry else 'None', first_entry.is_uploaded if first_entry else 'None')
        
        # Flush the queue immediately to ensure first entry is committed to database
        try:
            logger.debug("[Timing][Python] Flushing DB write queue...")
            processed_count = await db_writer_service.process_immediately()
            logger.debug("[Timing][Python] DB write queue flushed")
            logger.info("Flushed %s queued database writes - first dummy entry should now be in database", processed_count)

        except Exception as e:
            logger.error("Error flushing database write queue after first entry: %s", e, exc_info=True)

        while not stop_event.is_set():

            logger.info("****************************************************************has_on_demand_screenshot: %s*********************************************************", self.has_on_demand)
            # Wait if tracking is stopped (via WebSocket command or persistent status)
            # CRITICAL: Continue monitoring when on break (break_id != 0) even if status=False
            # This ensures intervals are created in real-time during break sessions
            current_break_id = getattr(self, 'break_id', 0) or 0
            while not self.status and current_break_id != 0 and not stop_event.is_set():
                logger.info("=" * 80)
                logger.info("[Tracker] Tracking is STOPPED - waiting for 'start' command via WebSocket...")
                logger.info("[Tracker] Activity monitoring paused - WebSocket connection remains active")
                logger.info("=" * 80)
                await asyncio.sleep(5)
                # Re-check break_id in case break started while waiting
                current_break_id = getattr(self, 'break_id', 0) or 0
            
            # If stop_event was set while waiting, exit
            if stop_event.is_set():
                break
            
            # Log when tracking resumes or continues during break
            if self.status and not getattr(self, '_tracking_resumed_logged', False):
                logger.info("=" * 80)
                logger.info("[Tracker] ✓ Tracking RESUMED - activity monitoring active")
                logger.info("=" * 80)
                self._tracking_resumed_logged = True
                
                # CRITICAL: Always restart device listeners after break ends
                # This ensures keyboard/mouse events are captured when tracking resumes
                # Listeners may have been stopped during break_stop_event or other operations
                if hasattr(self, 'device_listener') and self.device_listener:
                    try:
                        # Check current state for logging
                        stop_listeners = getattr(self.device_listener, 'stop_listeners', True)
                        mouse_active = getattr(self.device_listener, 'mouse_listener_active', False)
                        keyboard_active = getattr(self.device_listener, 'keyboard_listener_active', False)
                        mouse_verified = getattr(self.device_listener, 'mouse_listener_verified', False)
                        keyboard_verified = getattr(self.device_listener, 'keyboard_listener_verified', False)
                        
                        logger.info(
                            "Device listener state after break - stop_listeners: %s, "
                            "Mouse active: %s, Keyboard active: %s, "
                            "Mouse verified: %s, Keyboard verified: %s",
                            stop_listeners, mouse_active, keyboard_active,
                            mouse_verified, keyboard_verified)
                        
                        # ALWAYS restart listeners after break to ensure they're working
                        # This is necessary because listeners may have been stopped during break_stop_event
                        logger.info("Restarting device listeners after break to ensure activity tracking works...")
                        self.device_listener.restart_listeners()
                        logger.info("Device listeners restarted successfully - keyboard and mouse events should now be logged")
                    except Exception as e:
                        logger.error("Error restarting device listeners after break: %s", e, exc_info=True)
                        # Try to restart anyway as fallback
                        try:
                            logger.info("Attempting fallback restart of device listeners...")
                            self.device_listener.restart_listeners()
                            logger.info("Fallback restart succeeded")
                        except Exception as fallback_error:
                            from ws_package.utils.error_codes import ErrorCode
                            logger.error("%s: %s", ErrorCode.DEVICE_LISTENER_DEAD, fallback_error, exc_info=True)
                            self._send_to_electron_if_connected("tracking_error", {
                                "code": str(ErrorCode.DEVICE_LISTENER_DEAD),
                                "message": (
                                    "Keyboard and mouse activity tracking has stopped and could not "
                                    "be restarted. Your activity may not be recorded accurately. "
                                    "On macOS, grant Accessibility access in System Settings → "
                                    "Privacy & Security → Accessibility. Then restart Workstatus."
                                ),
                                "recoverable": False,
                                "source": "activity",
                            })
            elif not self.status and current_break_id != 0:
                # On break - monitoring continues even though status is False
                if not getattr(self, '_break_monitoring_logged', False):
                    logger.info("=" * 80)
                    logger.info("[Break] ✓ Monitoring CONTINUES during break (break_id=%s)", current_break_id)
                    logger.info("[Break] Intervals will be created in real-time with is_break=%s", current_break_id)
                    logger.info("=" * 80)
                    self._break_monitoring_logged = True
            else:
                # Reset the break monitoring logged flag when not on break
                self._break_monitoring_logged = False
            
            try:
                logger.debug("[Timing][Python] Inside main activity loop")
                logger.info("Current Interval Status: %s Screenshot Interval frequency: %s App and URL Status: %s Blur Intensity: %s", self.interval, self.screenshot_interval, self.appandurlstatus, self.blur_intensity)


                if getattr(sys, 'frozen', False):  # Running as a PyInstaller EXE
                    script_path = Path(sys.executable).resolve()
                    target_directory = script_path.parents[0]
                    target_directory_stealth = script_path.parents[1]
                else:  # Running as a normal Python script
                    script_path = Path(__file__).resolve()
                    target_directory = f"{script_path.parents[3]}/workstatus"
                
                logger.info("Running device activity force status %s", self.force_stop)
                if self.status and self.current_session_start_time is not None:
                    await self._try_finalize_post_switch_slot_interval(user_entity)

                if self.Flag==True: # This is not the first time slot, so we need to get the current slot
                
                    start_date= await self.getCurrentSlot()
                    end_date = start_date + datetime.timedelta(minutes=int(self.interval/60))
                    # If the last saved interval or break stop time falls inside this slot (e.g. break stopped at 17:17, slot is 17:15-17:20),
                    # start this interval at that time so we don't double-count 17:15-17:17 as work time.
                    current_break_id_slot = getattr(self, 'break_id', 0) or 0
                    if current_break_id_slot == 0:
                        trim_to = start_date
                        last_end = await self.activity_service.get_last_activity_interval()
                        if last_end is not None and start_date < last_end < end_date:
                            trim_to = max(trim_to, last_end)
                        session_start = self.current_session_start_time
                        if (
                            session_start is not None
                            and start_date < session_start < end_date
                        ):
                            trim_to = max(
                                trim_to, session_start.replace(microsecond=0)
                            )
                        break_stop_time_attr = getattr(self, '_break_stop_time', None)
                        if break_stop_time_attr is not None and start_date < break_stop_time_attr < end_date:
                            trim_to = max(trim_to, break_stop_time_attr)
                            self._break_stop_time = None  # Use once so next slot is not trimmed
                        if trim_to > start_date:
                            start_date = trim_to
                            logger.info("Trimmed interval start after break stop / last interval end: %s to %s", start_date, end_date)
                    self.main_start_date = start_date
                    self.main_end_date = end_date
                
                else:
                    self.Flag=True # This is the first time slot, so we need to get the starting slot
                    start_date,end_date=await self.getStartingSlot()
                    self.main_start_date = start_date
                    self.main_end_date = end_date
                logger.info(">>"*100)
                logger.info("appVersion: %s", self.appVersion)
                logger.info("current Project id: %s, current task id : %s", self.current_project_id, self.current_task_id)
                logger.info("current break id: %s, current is break: %s", self.break_id, self.is_break)
                logger.info(">>"*100)
                # time/timezone check (skipped when timezone_preference=1)
                try:
                    from ws_package.utils.desktop_key import apply_desktop_key
                    url = ""
                    headers = {}
                    if self.org_api_timezone is None:
                        await self._load_org_settings_from_db()
                    timecheck_failed = False
                    if user_entity is not None:
                        if not self._timezone_checks_enabled(user_entity):
                            logger.info(
                                "Skipping timecheck — timezone_preference=%s (checks OFF)",
                                getattr(self, "timezone_preference", getattr(user_entity, "timezone_preference", None)),
                            )
                        elif self.org_api_timezone is None:
                            logger.warning(
                                "Skipping timecheck — org timezone not loaded yet"
                            )
                        else:
                            logger.info(
                                "Running timecheck — timezone_preference=%s (checks ON)",
                                getattr(self, "timezone_preference", getattr(user_entity, "timezone_preference", None)),
                            )
                            timecheck_failed = not await self.timecheck_with_recheck(
                                user_entity, stop_event
                            )
                    if user_entity is not None and timecheck_failed:
                        logger.info("Time mismatch under activity")
                        active_app_url = [{
                            "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                            "module": "Wrong Timestamp",
                            "active_app": "Wrong Timestamp",
                            "url": "Wrong Timestamp"
                        }]

                        await self.activity_service.save_app_monitors(active_app_url, self.last_count+1, True),

                        logger.info("save last active app as true because of wrong timezone deteceted so next last min save can't be wrong from activity")
                        url = config('timezone_mismatch', 'Application')
                        headers = {
                            "Content-Type": "application/json",
                            "Authorization": f"Bearer {user_entity.sd_token}",
                            "OrgID": f"{user_entity.org_id}",
                            "SDToken": f"{user_entity.sd_token}",
                            "UserID": f"{user_entity.user_id}"
                        }
                        headers = apply_desktop_key(headers)
                        payload = {
                            "device_type": self.get_os_type_caps(),
                            "os_name": self.get_os_type_caps(),
                            "os_version": get_os_version(),
                            "device_id": self.system_unique_id,
                            "status": 1
                        }
                        try:
                            log_testing_request_headers(logger, "POST", url, headers, payload)
                            response = requests.post(url, headers=headers, json=payload, timeout=10)
                            log_api_response_summary(
                                logger,
                                api_name_from_url(url),
                                response.status_code,
                                response.text,
                            )
                        except requests.exceptions.Timeout:
                            logger.error("[API] %s | timeout", api_name_from_url(url))
                        except requests.exceptions.RequestException as e:
                            logger.error("[API] %s | request failed: %s", api_name_from_url(url), e)

                        # Send timezone error message to Electron before stopping
                        try:
                            error_data = {
                                'title': 'Timezone Mismatch Detected',
                                'message': "Your system timezone does not match the organization's configured timezone.\nPlease update your system time settings to continue tracking",
                                'timestamp': datetime.datetime.now().isoformat()
                            }
                            if self._send_to_electron_if_connected('timezone_error', error_data):
                                logger.info("Sent timezone mismatch error to Electron from activity loop: %s", error_data)
                                await precise_sleep(5, stop_event)
                                # CRITICAL: Don't call os._exit(1) - keep Python process alive for error recovery
                                # Electron will handle stopping the timer, but Python should remain running
                                logger.info("Timezone error sent to Electron - Python process will remain alive (not exiting)")
                        except Exception as e:
                            logger.error("Failed to send timezone error to Electron from activity loop: %s", e)

                        # Mark mismatch so the push loop can react and stop safely
                        logger.error("Timezone mismatch detected in activity loop - stopping tracker")
                        self._timezone_mismatch = True
                        self._time_error_type = "timezone_error"
                        self.force_stop = True
                        # CRITICAL: Don't call os._exit(1) - keep Python process alive for error recovery
                        logger.info("Python process will remain alive - monitoring stopped but process continues running")
                    elif user_entity is None:
                        logger.warning("Skipping timecheck: user_entity is None")
                        payload = {
                            "device_type": self.get_os_type_caps(),
                            "os_name": self.get_os_type_caps(),
                            "os_version": get_os_version(),
                            "device_id": self.system_unique_id,
                            "status": 1
                        }
                        try:
                            headers = apply_desktop_key(headers)
                            log_testing_request_headers(logger, "POST", url, headers, payload)
                            response = requests.post(url, headers=headers, json=payload, timeout=10)
                            log_api_response_summary(
                                logger,
                                api_name_from_url(url),
                                response.status_code,
                                response.text,
                            )
                        except requests.exceptions.Timeout:
                            logger.error("[API] %s | timeout", api_name_from_url(url))
                        except requests.exceptions.RequestException as e:
                            logger.error("[API] %s | request failed: %s", api_name_from_url(url), e)
                        
                        logger.error("User entity is None - stopping tracker")
                        self._datetime_mismatch = True
                        self._time_error_type = "datetime_error"
                        self.force_stop = True
                        # Don't call os._exit(1) - let the monitoring loop handle graceful shutdown
                    else:
                        logger.info("Timezone and Time matched successfully")
                except Exception as e:
                    logger.error("No Connection for timestamp and update interval %s", e)

                
                # logger.info(f"Adjusted timestamps - start: {start_date}, end: {end_date}")
                activity_percentage_start = await self.getCurrentSlot()
                activity_percentage_end = activity_percentage_start + datetime.timedelta(minutes=int(self.interval/60))
                
                # Round timestamps to seconds for consistent duplicate detection
                activity_percentage_start_seconds = activity_percentage_start.replace(microsecond=0)
                activity_percentage_end_seconds = activity_percentage_end.replace(microsecond=0)
                activity_percentage_key = (activity_percentage_start_seconds, activity_percentage_end_seconds)
                
                # Check in-memory set first (fast check, prevents race conditions)
                if activity_percentage_key in self._creating_activity_percentages:
                    logger.info("Activity percentage record already being created for slot %s to %s. Skipping duplicate creation.", activity_percentage_start_seconds, activity_percentage_end_seconds)
                else:
                    # Mark as creating to prevent concurrent duplicates
                    self._creating_activity_percentages.add(activity_percentage_key)
                    
                    try:
                        # Flush queue first to ensure any pending writes are visible in database
                        await db_writer_service.process_immediately()
                        await asyncio.sleep(0.2)  # Small delay to ensure commit is visible
                        
                        # Check if a record for this 10-minute slot already exists to prevent duplicates
                        existing_record = await activity_percentage_manager.find_overlapping_activity_percentage(
                            activity_percentage_start, 
                            activity_percentage_end
                        )
                        
                        if existing_record:
                            logger.info("Activity percentage record already exists for slot %s to %s. Skipping creation to prevent duplicates.", activity_percentage_start, activity_percentage_end)
                            # Remove from set since we're not creating
                            self._creating_activity_percentages.discard(activity_percentage_key)
                        else:
                            # Check if this is the very first activity percentage record after app start
                            # This is the first entry if:
                            # 1. No records exist in database, OR
                            # 2. The latest record starts BEFORE self.start_date (actual timer start time when user clicked Start)
                            #    meaning this is the first entry for this session
                            latest_record = await activity_percentage_manager.get_latest_activity_percentage()
                            
                            is_first_entry = False
                            if latest_record is None:
                                # No records exist - this is definitely the first entry
                                is_first_entry = True
                                logger.info("First entry detected: No existing records in database")
                            elif hasattr(self, 'start_date') and self.start_date is not None:
                                # Check if latest record is before actual timer start time - if so, this is first entry for this session
                                # self.start_date is set to datetime.datetime.now() when user clicks Start button
                                if latest_record.start < self.start_date:
                                    is_first_entry = True
                                    logger.info("First entry detected: latest_record.start (%s) < start_date (%s) - first entry after app start", latest_record.start, self.start_date)
                            else:
                                logger.warning("start_date not available - cannot determine if this is first entry. Using default mouse=0")
                            
                            # For the very first entry after app start, set mouse activity = 1 by default
                            # For subsequent entries, use 0 (will be updated by minute timer)
                            initial_mouse = 1.0 if is_first_entry else 0.0
                            initial_average = 1.0 if is_first_entry else 0.0  # Average = mouse + keyboard, so 1.0 if mouse=1
                            
                            # Create new activity percentage entity only if it doesn't exist
                            activity_percentage_entity = ActivityPercentageEntity(
                                start=activity_percentage_start,
                                stop=activity_percentage_end,
                                keyboard=0,
                                mouse=initial_mouse,
                                average=initial_average,
                                minute=0  # Start at 0 - minute timer will increment to 1 on first update: ((0*0) + activity) / 1 = activity
                            )
                            
                            # Use universal queue instead of separate activity_percentage_queue
                            # Note: enqueue is synchronous (thread-safe), not async
                            enqueued = db_write_queue.enqueue(
                                operation_type=WriteOperationType.ACTIVITY_PERCENTAGE,
                                table_name="activity_percentage",
                                data=activity_percentage_entity.model_dump()
                            )
                            
                            if enqueued:
                                first_entry_note = " (FIRST ENTRY - mouse=1 by default)" if is_first_entry else ""
                                logger.info(
                                    "Enqueued initial activity percentage%s: "
                                    "Slot: %s to %s, Keyboard: %s%%, Mouse: %s%%, "
                                    "Average: %s%%, Minute: %s. "
                                    "Minute timer will update with actual activity data when it runs.",
                                    first_entry_note,
                                    activity_percentage_start, activity_percentage_end,
                                    activity_percentage_entity.keyboard,
                                    activity_percentage_entity.mouse,
                                    activity_percentage_entity.average,
                                    activity_percentage_entity.minute)
                                # Remove from set after a delay to allow database write to complete
                                async def remove_after_delay():
                                    await asyncio.sleep(2)  # Wait 2 seconds for DB write
                                    self._creating_activity_percentages.discard(activity_percentage_key)
                                asyncio.create_task(remove_after_delay())
                            else:
                                logger.error("Failed to save activity percentage record")
                                # Remove from set immediately on failure
                                self._creating_activity_percentages.discard(activity_percentage_key)
                    except Exception as e:
                        logger.error("Error checking/creating activity percentage: %s", e, exc_info=True)
                        # Remove from set on error
                        self._creating_activity_percentages.discard(activity_percentage_key)
                
                # Check if we should stop BEFORE creating interval (prevent creating interval after stop)
                if stop_event.is_set():
                    logger.info("Stop event detected - breaking loop before creating interval")
                    break
                # Check if timer was stopped due to limit and date hasn't changed
                today = datetime.datetime.now().date()
                if self.limit_stopped_date is not None and self.limit_stopped_date == today:
                    # Timer was stopped due to limit today - keep it stopped
                    self.status = False
                    logger.debug("Timer remains stopped due to limit exceeded (stopped on %s)", self.limit_stopped_date)
                
                if self.status == True:
                    await precise_sleep(3)  # Avoid boundary_time()==0 immediate re-loop at exact slot edges
                    # Sleep until the end of the slot we are tracking (main_end_date / end_date),
                    # not the next calendar boundary — slow loop iterations must not skip a full slot.
                    slot_end = getattr(self, "main_end_date", None) or end_date
                    sleep_seconds = self._seconds_until_datetime(slot_end)
                    logger.debug(
                        "Sleeping %.1fs until interval end %s (slot %s to %s)",
                        sleep_seconds,
                        slot_end,
                        start_date,
                        end_date,
                    )
                    if sleep_seconds > 0:
                        await precise_sleep(sleep_seconds, stop_event)
                else:
                    # Break 4-hour sleep into 10-minute chunks for better stop_event responsiveness
                    # This prevents threads from appearing dead during long idle periods
                    total_sleep = 14400  # 4 hours
                    chunk_duration = 600  # 10 minutes per chunk
                    chunks = total_sleep // chunk_duration
                    for _ in range(chunks):
                        if stop_event.is_set():
                            break
                        await precise_sleep(chunk_duration, stop_event)
                
                # Check again after sleep - if stop was requested, don't create interval
                if stop_event.is_set():
                    logger.info("Stop event detected after sleep - breaking loop before creating interval")
                    break
                # Reset limit_stopped_date if date changed (new day started)
                if self.limit_stopped_date is not None and self.limit_stopped_date != today:
                    logger.info("Date changed from %s to %s - resetting limit stop status", self.limit_stopped_date, today)
                    self.limit_stopped_date = None
                    # Don't automatically restart - let status_controller handle it
                
                # Initialize defaults so interval saving can proceed even if activity fetch fails
                activityLevel = {'mouse': '0.0', 'average': '0.0', 'keyboard': '0.0'}
                # Save uploadable intervals only inside the fixed-mode window (in_range).
                # Out-of-range slots use save_first_activity_log (is_uploaded=True) and are not sent.
                # CRITICAL: Read break_id at the time of saving to ensure current value is used
                # break_id will be saved in is_break field (0 if break_id is null/0)
                current_break_id = getattr(self, 'break_id', 0) or 0  # Default to 0 if not available
                
                # DEBUG: Log break state when saving intervals
                logger.info("[DEBUG][Break State] Saving interval - break_id=%s, is_break=%s, status=%s, start_date=%s, end_date=%s", current_break_id, getattr(self, 'is_break', 0), self.status, start_date, end_date)
                
                in_range = await self.inrange(start_date, end_date)
                has_any_activity = False

                # Gather activity data; errors here should not block interval creation
                try:
                    await self.midnight(user_entity, self.last_count)

                    logger.info("Device Thread")
                    # self.log_system_resources()
                    
                    # Get actual activity level from database instead of hardcoded 0%
                    activityLevel = await self.get_activity_level_from_db(start_date, end_date)
                    
                    # Check if there was ANY activity during the interval
                    # If user worked even a few seconds, mark interval as active (not idle)
                    keyboard_activity = float(activityLevel.get('keyboard', 0) or 0)
                    mouse_activity = float(activityLevel.get('mouse', 0) or 0)
                    average_activity = float(activityLevel.get('average', 0) or 0)
                    
                    # Check if there was ANY activity (even minimal)
                    has_any_activity = keyboard_activity > 0 or mouse_activity > 0 or average_activity > 0
                    
                    if not has_any_activity:
                        # Double-check: look for app monitor activity during this interval
                        has_any_activity = await self._has_any_activity_in_interval(
                            start_date, end_date, self.last_count + 1
                        )
                        
                        if has_any_activity:
                            logger.info("App monitor activity found for interval %s to %s - marking as active", start_date, end_date)
                    
                except Exception as activity_error:
                    # Log and continue to saving with defaults/fallback
                    logger.error("Error retrieving/processing activity level for interval %s to %s: %s", start_date, end_date, activity_error, exc_info=True)
                    logger.warning("Proceeding to save interval with default activity level due to error")
                
                # CRITICAL: Only mark interval as break interval if it starts AFTER break start time
                # If slot overlaps break (start_date < break_start_time < end_date), save only from break_start_time to end_date as break interval.
                break_start_time = getattr(self, 'break_start_time', None)
                is_break_interval = False
                effective_start = start_date
                effective_end = end_date

                if current_break_id != 0 and break_start_time is not None:
                    if start_date >= break_start_time:
                        is_break_interval = True
                        logger.info("Break is active (break_id=%s) and interval starts on/after break start (start=%s, break_start=%s) - marking as break interval", current_break_id, start_date, break_start_time)
                    elif end_date > break_start_time:
                        # Slot overlaps break: save only the part from break_start_time to end_date as break interval (e.g. 16:06-16:10, not 16:05-16:10 as regular)
                        is_break_interval = True
                        effective_start = break_start_time
                        effective_end = end_date
                        try:
                            activityLevel = await self.get_activity_level_from_db(effective_start, effective_end)
                        except Exception as e:
                            logger.warning("Error getting activity for break-overlap interval: %s", e)
                            activityLevel = {'mouse': '0.0', 'average': '0.0', 'keyboard': '0.0'}
                        logger.info("Break is active (break_id=%s) and slot overlaps break (slot %s to %s, break_start=%s) - saving only break part as break interval: %s to %s", current_break_id, start_date, end_date, break_start_time, effective_start, effective_end)
                    else:
                        logger.info("Break is active (break_id=%s) but interval ends before break start (start=%s, end=%s, break_start=%s) - marking as regular interval", current_break_id, start_date, end_date, break_start_time)
                        current_break_id = 0
                elif current_break_id != 0:
                    logger.info("Break is active (break_id=%s) but interval starts before break start (start=%s, break_start=%s) - marking as regular interval", current_break_id, start_date, break_start_time)
                    current_break_id = 0

                if is_break_interval and current_break_id != 0:
                    start_date = effective_start
                    end_date = effective_end

                # CRITICAL: Break intervals must always use save_activity_log (is_uploaded=False) so they can be uploaded
                # Only use save_first_activity_log (is_uploaded=True) for non-break intervals that are out of range
                if is_break_interval and current_break_id != 0:
                    # Round timestamps to seconds for consistent duplicate detection
                    # This prevents microsecond differences from creating duplicate intervals
                    start_date_seconds = start_date.replace(microsecond=0)
                    end_date_seconds = end_date.replace(microsecond=0)
                    interval_key = (self.last_count + 1, start_date_seconds, end_date_seconds)
                    
                    # Check in-memory set first (fast check, prevents DB queries)
                    if interval_key in self._creating_intervals:
                        logger.info("Skipping duplicate break interval - already creating %s to %s (counter=%s)", start_date_seconds, end_date_seconds, self.last_count + 1)
                        continue
                    
                    # Mark as creating to prevent concurrent duplicates
                    self._creating_intervals.add(interval_key)
                    
                    try:
                        # CRITICAL FIX: Check if break interval already exists in DB before saving
                        # This prevents duplicates when Python restarts during break initiation.
                        # Use exact=True so the pre-break interval (whose end_time may have
                        # microseconds just above start_date) is not falsely matched.
                        existing_interval_in_db = await self.activity_service.check_existing_interval(start_date, end_date, exact=True)
                        if existing_interval_in_db:
                            logger.info("Skipping duplicate break interval - interval already exists in DB for %s to %s", start_date, end_date)
                            self._creating_intervals.discard(interval_key)
                            continue

                        # This is a break interval - must use save_activity_log to set is_uploaded=False
                        # Set is_idle: True if popup is active AND interval has zero activity (both activityLevel and activity_percentage)
                        popup_is_active = self._idle_popup_active and self._idle_popup_triggered_at is not None
                        has_zero_activity = await self._has_zero_activity_for_interval(start_date, end_date)
                        is_idle = popup_is_active and has_zero_activity
                        logger.info("[DEBUG][Break State] Break interval - break_id=%s, is_break=%s, status=%s, will save as is_break=%s", current_break_id, getattr(self, 'is_break', 0), self.status, current_break_id)
                        logger.info("Break interval (break_id=%s) - saving with is_uploaded=False (will be uploaded later) >> status %s, is_idle=%s (popup_is_active=%s, has_zero_activity=%s)", current_break_id, self.status, is_idle, popup_is_active, has_zero_activity)
                        # CRITICAL: Verify break_id is not 0 before saving break interval
                        if current_break_id == 0:
                            logger.warning("[WARNING] Break interval being saved with break_id=0! This should not happen during a break. self.break_id=%s, self.is_break=%s", getattr(self, 'break_id', 'NOT_SET'), getattr(self, 'is_break', 'NOT_SET'))
                        await self.activity_service.save_activity_log(activityLevel, start_date, end_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id, is_idle=is_idle, has_on_demand_screenshot=self.has_on_demand)
                        self.has_on_demand = False
                        # CRITICAL: Immediately flush the queue to ensure break interval is written to database
                        # This ensures the interval is visible in database queries right away
                        try:
                            logger.debug("Flushing queue immediately after creating break interval (break_id=%s)", current_break_id)
                            await db_writer_service.process_immediately()
                        except Exception as flush_error:
                            logger.error("Error flushing queue after break interval creation: %s", flush_error, exc_info=True)
                        
                        # Check for idle detection after break interval is saved
                        # NEW REQUIREMENT: Check for 2 consecutive idle intervals and trigger popup
                        async def check_idle_after_delay():
                            await asyncio.sleep(1)  # Wait for DB write to complete
                            logger.info("[IdleDetection] Checking for idle after break interval save")
                            if await self.check_idle_detection():
                                logger.info("[IdleDetection] Idle detected - triggering popup")
                                await self.arise_idle_popup()
                            else:
                                logger.debug("[IdleDetection] No idle detected after break interval save")
                        asyncio.create_task(check_idle_after_delay())
                    finally:
                        # Remove from set after a delay to allow database write to complete
                        async def remove_after_delay():
                            await asyncio.sleep(2)  # Wait 2 seconds for DB write
                            self._creating_intervals.discard(interval_key)
                        asyncio.create_task(remove_after_delay())
                elif in_range and self.status == True:
                    # CRITICAL FIX: Check if interval already exists before saving to prevent duplicates
                    # This prevents the same interval from being created multiple times when the loop runs
                    # before the database write completes (race condition)
                    # Also checks for overlapping break intervals to prevent creating regular intervals over breaks

                    # Round timestamps to seconds for consistent duplicate detection
                    # This prevents microsecond differences from creating duplicate intervals
                    start_date_seconds = start_date.replace(microsecond=0)
                    end_date_seconds = end_date.replace(microsecond=0)
                    interval_key = (self.last_count + 1, start_date_seconds, end_date_seconds)
                    
                    # Check in-memory set first (fast check, prevents DB queries and race conditions)
                    if interval_key in self._creating_intervals:
                        logger.info("Skipping duplicate interval - already creating %s to %s (counter=%s)", start_date_seconds, end_date_seconds, self.last_count + 1)
                        continue
                    
                    # Mark as creating to prevent concurrent duplicates
                    self._creating_intervals.add(interval_key)
                    
                    interval_exists = False
                    break_interval_overlaps = False
                    regular_interval_overlaps = False
                    try:
                        # Check pending write queue for break intervals that overlap this slot.
                        # This is non-blocking (no DB flush needed) and prevents event-loop stalls.
                        from ws_package.utils.db_write_queue import db_write_queue as _dwq, WriteOperationType as _WOT
                        for _pi in _dwq.get_pending_items(_WOT.ACTIVITY_INTERVAL):
                            _d = _pi.data
                            if (_d.get('is_break', 0) != 0 and
                                    _d.get('counter') == self.last_count + 1):
                                _ps = _d.get('start_time')
                                _pe = _d.get('end_time')
                                if _ps is not None and _pe is not None and _ps < end_date and _pe > start_date:
                                    break_interval_overlaps = True
                                    logger.info(
                                        "Pending break interval in write queue overlaps %s to %s — skipping regular interval",
                                        start_date, end_date
                                    )
                                    break

                        if not break_interval_overlaps:
                            with db_lock_manager.read_lock():
                                async with async_session() as session:
                                    # Check if interval with same start_time, end_time, and counter already exists
                                    check_query = select(ActivityInterval).where(
                                        and_(
                                            ActivityInterval.start_time == start_date,
                                            ActivityInterval.end_time == end_date,
                                            ActivityInterval.counter == self.last_count + 1
                                        )
                                    )
                                    result = await session.execute(check_query)
                                    existing = result.scalars().first()
                                    if existing:
                                        interval_exists = True
                                        logger.info("Interval already exists for %s to %s (counter=%s) - skipping duplicate creation", start_date, end_date, self.last_count + 1)

                                    # Check for ANY overlapping interval (break or regular) with this time range
                                    overlap_query = select(ActivityInterval).where(
                                        and_(
                                            ActivityInterval.start_time < end_date,
                                            ActivityInterval.end_time > start_date,
                                            ActivityInterval.counter == self.last_count + 1
                                        )
                                    )
                                    overlap_result = await session.execute(overlap_query)
                                    overlapping_intervals = overlap_result.scalars().all()

                                    if overlapping_intervals:
                                        break_intervals = [iv for iv in overlapping_intervals if iv.is_break != 0]
                                        regular_intervals = [iv for iv in overlapping_intervals if iv.is_break == 0]

                                        if break_intervals:
                                            break_interval_overlaps = True
                                            logger.info("Break interval overlaps with time range %s to %s (break intervals: %s) - skipping regular interval creation", start_date, end_date, [(iv.start_time, iv.end_time, iv.is_break) for iv in break_intervals])
                                        elif regular_intervals:
                                            regular_interval_overlaps = True
                                            overlapping_details = [(iv.start_time, iv.end_time, iv.is_break) for iv in regular_intervals]
                                            logger.info("Regular interval overlaps with time range %s to %s (overlapping intervals: %s) - skipping duplicate creation to prevent overlap", start_date, end_date, overlapping_details)
                    except Exception as e:
                        logger.warning("Error checking for existing interval: %s - will proceed with creation (non-critical)", e)
                        # If check fails, proceed with creation (better to have duplicate than miss interval)
                    
                    if not interval_exists and not break_interval_overlaps and not regular_interval_overlaps:
                        try:
                            # Trim interval start to break stop / last interval end right before save when not on break.
                            # This handles the case where the slot was set at loop top while still on break (e.g. 17:45-17:50)
                            # and the user stopped the break at 17:47 during the sleep - we now trim to 17:47-17:50.
                            if current_break_id == 0:
                                trim_to = start_date
                                last_end = await self.activity_service.get_last_activity_interval()
                                if last_end is not None and start_date < last_end < end_date:
                                    trim_to = max(trim_to, last_end)
                                session_start = self.current_session_start_time
                                if (
                                    session_start is not None
                                    and start_date < session_start < end_date
                                ):
                                    trim_to = max(
                                        trim_to, session_start.replace(microsecond=0)
                                    )
                                break_stop_attr = getattr(self, '_break_stop_time', None)
                                if break_stop_attr is not None and start_date < break_stop_attr < end_date:
                                    trim_to = max(trim_to, break_stop_attr)
                                    self._break_stop_time = None
                                if trim_to > start_date:
                                    self._creating_intervals.discard(interval_key)
                                    start_date = trim_to
                                    start_date_seconds = start_date.replace(microsecond=0)
                                    end_date_seconds = end_date.replace(microsecond=0)
                                    interval_key = (self.last_count + 1, start_date_seconds, end_date_seconds)
                                    self._creating_intervals.add(interval_key)
                                    try:
                                        activityLevel = await self.get_activity_level_from_db(start_date, end_date)
                                    except Exception as e:
                                        logger.warning("Error getting activity for trimmed interval: %s", e)
                                        activityLevel = {'mouse': '0.0', 'average': '0.0', 'keyboard': '0.0'}
                                    logger.info("Trimmed interval start before save (e.g. break stopped during slot): %s to %s", start_date, end_date)
                                    
                                    # Re-check for overlaps after trimming (non-blocking: queue check + async lock)
                                    try:
                                        from ws_package.utils.db_write_queue import db_write_queue as _dwq2, WriteOperationType as _WOT2
                                        for _pi2 in _dwq2.get_pending_items(_WOT2.ACTIVITY_INTERVAL):
                                            _d2 = _pi2.data
                                            if (_d2.get('is_break', 0) != 0 and
                                                    _d2.get('counter') == self.last_count + 1):
                                                _ps2 = _d2.get('start_time')
                                                _pe2 = _d2.get('end_time')
                                                if _ps2 is not None and _pe2 is not None and _ps2 < end_date and _pe2 > start_date:
                                                    break_interval_overlaps = True
                                                    logger.info(
                                                        "After trim: pending break interval overlaps %s to %s — skipping",
                                                        start_date, end_date
                                                    )
                                                    break

                                        if not break_interval_overlaps:
                                            with db_lock_manager.read_lock():
                                                async with async_session() as session:
                                                    overlap_query_after_trim = select(ActivityInterval).where(
                                                        and_(
                                                            ActivityInterval.start_time < end_date,
                                                            ActivityInterval.end_time > start_date,
                                                            ActivityInterval.counter == self.last_count + 1
                                                        )
                                                    )
                                                    overlap_result_after_trim = await session.execute(overlap_query_after_trim)
                                                    overlapping_intervals_after_trim = overlap_result_after_trim.scalars().all()

                                                    if overlapping_intervals_after_trim:
                                                        break_intervals_after_trim = [iv for iv in overlapping_intervals_after_trim if iv.is_break != 0]
                                                        regular_intervals_after_trim = [iv for iv in overlapping_intervals_after_trim if iv.is_break == 0]

                                                        if break_intervals_after_trim:
                                                            break_interval_overlaps = True
                                                            logger.info("After trimming: Break interval overlaps with trimmed time range %s to %s (break intervals: %s) - skipping regular interval creation", start_date, end_date, [(iv.start_time, iv.end_time, iv.is_break) for iv in break_intervals_after_trim])
                                                        elif regular_intervals_after_trim:
                                                            regular_interval_overlaps = True
                                                            overlapping_details_after_trim = [(iv.start_time, iv.end_time, iv.is_break) for iv in regular_intervals_after_trim]
                                                            logger.info("After trimming: Regular interval overlaps with trimmed time range %s to %s (overlapping intervals: %s) - skipping duplicate creation to prevent overlap", start_date, end_date, overlapping_details_after_trim)
                                    except Exception as e:
                                        logger.warning("Error checking for overlaps after trimming: %s - will proceed with creation (non-critical)", e)
                                    
                                    # If overlap found after trimming, skip creation
                                    if break_interval_overlaps or regular_interval_overlaps:
                                        self._creating_intervals.discard(interval_key)
                                        if break_interval_overlaps:
                                            logger.info("Skipping interval creation after trim - break interval overlaps with trimmed time range %s to %s", start_date, end_date)
                                        if regular_interval_overlaps:
                                            logger.info("Skipping interval creation after trim - regular interval overlaps with trimmed time range %s to %s", start_date, end_date)
                                        continue
                            resolving_policy = self._apply_resolving_slot_save_policy(
                                start_date, end_date, activityLevel
                            )
                            if resolving_policy is not None:
                                activityLevel, is_idle = resolving_policy
                            else:
                                popup_is_active = self._idle_popup_active and self._idle_popup_triggered_at is not None
                                has_zero_activity = await self._has_zero_activity_for_interval(start_date, end_date)
                                is_idle = popup_is_active and has_zero_activity
                            logger.info("[IdleInterval] Regular interval (in range) - break_id=%s, is_break=%s, status=%s, will save as is_break=%s", current_break_id, getattr(self, 'is_break', 0), self.status, current_break_id if current_break_id else 0)
                            logger.info("[IdleInterval] Saving: break_id=%s, is_idle=%s", current_break_id, is_idle)
                            logger.info("[IdleInterval] ActivityLevel: Keyboard=%s%%, Mouse=%s%%, Average=%s%%", activityLevel.get('keyboard', '0.0'), activityLevel.get('mouse', '0.0'), activityLevel.get('average', '0.0'))
                            if is_idle:
                                logger.info("[IdleInterval] ⚠ Interval marked as IDLE - will NOT be sent to server until popup handled")
                            await self.activity_service.save_activity_log(activityLevel, start_date, end_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id, is_idle=is_idle, has_on_demand_screenshot=self.has_on_demand)
                            self.has_on_demand = False
                        finally:
                            # Remove from set after a delay to allow database write to complete
                            async def remove_after_delay():
                                await asyncio.sleep(2)  # Wait 2 seconds for DB write
                                self._creating_intervals.discard(interval_key)
                                
                                # Check for idle detection after interval is saved and DB write completed
                                # NEW REQUIREMENT: Check for 2 consecutive idle intervals and trigger popup
                                logger.info("[IdleDetection] Checking for idle after interval save (in range)")
                                if await self.check_idle_detection():
                                    logger.info("[IdleDetection] Idle detected - triggering popup")
                                    await self.arise_idle_popup()
                                else:
                                    logger.debug("[IdleDetection] No idle detected after interval save")
                            asyncio.create_task(remove_after_delay())
                    else:
                        # Remove from set immediately if we're not creating the interval
                        self._creating_intervals.discard(interval_key)
                        if interval_exists:
                            if await self._refresh_resolving_slot_if_exists(start_date, end_date):
                                logger.info(
                                    "Resolving slot %s to %s already exists — refreshed activity (not idle)",
                                    start_date,
                                    end_date,
                                )
                            else:
                                logger.info("Skipping duplicate interval creation for %s to %s (counter=%s)", start_date, end_date, self.last_count + 1)
                        if break_interval_overlaps:
                            logger.info("Skipping regular interval creation - break interval exists for time range %s to %s", start_date, end_date)
                        if regular_interval_overlaps:
                            logger.info("Skipping regular interval creation - overlapping regular interval exists for time range %s to %s", start_date, end_date)
                else:
                    resolving_policy = self._apply_resolving_slot_save_policy(
                        start_date, end_date, activityLevel
                    )
                    if resolving_policy is not None:
                        activityLevel, is_idle = resolving_policy
                    else:
                        popup_is_active = self._idle_popup_active and self._idle_popup_triggered_at is not None
                        has_zero_activity = await self._has_zero_activity_for_interval(start_date, end_date)
                        is_idle = popup_is_active and has_zero_activity
                    logger.info("[IdleInterval] Regular interval (out of range) - break_id=%s, is_break=%s, status=%s, will save as is_break=%s", current_break_id, getattr(self, 'is_break', 0), self.status, current_break_id if current_break_id else 0)
                    logger.info("[IdleInterval] Saving: is_uploaded=True, break_id=%s, is_idle=%s", current_break_id, is_idle)
                    logger.info("[IdleInterval] ActivityLevel: Keyboard=%s%%, Mouse=%s%%, Average=%s%%", activityLevel.get('keyboard', '0.0'), activityLevel.get('mouse', '0.0'), activityLevel.get('average', '0.0'))
                    if is_idle:
                        logger.info("[IdleInterval] ⚠ Interval marked as IDLE - will NOT be sent to server until popup handled")
                    await self.activity_service.save_first_activity_log(activityLevel, start_date, end_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id, is_idle=is_idle, has_on_demand_screenshot=self.has_on_demand)
                    self.has_on_demand = False
                    # Check for idle detection after out-of-range interval is saved
                    # NEW REQUIREMENT: Check for 2 consecutive idle intervals and trigger popup
                    async def check_idle_after_delay():
                        await asyncio.sleep(1)  # Wait for DB write to complete
                        logger.info("[IdleDetection] Checking for idle after interval save (out of range)")
                        if await self.check_idle_detection():
                            logger.info("[IdleDetection] Idle detected - triggering popup")
                            await self.arise_idle_popup()
                        else:
                            logger.debug("[IdleDetection] No idle detected after interval save")
                    asyncio.create_task(check_idle_after_delay())
                
                # CRITICAL: Periodically flush queue when on break to ensure intervals are saved to DB
                # This ensures break intervals are written without blocking the main loop
                # Check if break_id is set (not 0) to determine if we're on a break
                if current_break_id != 0:
                    now = datetime.datetime.now()
                    # Flush queue every 30 seconds when on break (non-blocking, doesn't disturb functionality)
                    if (self._last_break_queue_flush is None or 
                        (now - self._last_break_queue_flush).total_seconds() >= 30):
                        try:
                            queue_size = db_write_queue.get_queue_size(WriteOperationType.ACTIVITY_INTERVAL)
                            if queue_size > 0:
                                logger.debug("On break (break_id=%s) - flushing %s queued intervals to DB (periodic flush)", current_break_id, queue_size)
                                # CRITICAL: Await the flush to ensure it completes before continuing
                                # This ensures intervals are actually written to database, not just queued
                                await db_writer_service.process_immediately()
                                self._last_break_queue_flush = now
                        except Exception as e:
                            logger.debug("Error in periodic break queue flush: %s", e)
                            # Don't log as error - this is non-critical background operation
                
                # Final check if we should stop
                if stop_event.is_set():
                    break
                    
            except Exception as e:
                logger.exception("Error in device activity loop", exc_info=True)

                # Attempt to persist the interval even after unexpected errors (only if not already saved)
                try:
                    if 'start_date' in locals() and 'end_date' in locals():
                        start_sec = start_date.replace(microsecond=0)
                        end_sec = end_date.replace(microsecond=0)
                        interval_key = (self.last_count + 1, start_sec, end_sec)
                        # Skip if we were already creating this interval (save was likely enqueued before the exception)
                        if interval_key in self._creating_intervals:
                            logger.info("Skipping exception recovery save - interval %s to %s already in progress (avoid duplicate)", start_date, end_date)
                            await asyncio.sleep(5)
                            continue
                        # Flush queue so any pending write for this interval is committed, then check DB
                        try:
                            await db_writer_service.process_immediately()
                        except Exception:
                            pass
                        if await self.activity_service.check_existing_interval(start_date, end_date, exact=True):
                            logger.info("Skipping exception recovery save - interval %s to %s already exists in DB (avoid duplicate)", start_date, end_date)
                            await asyncio.sleep(5)
                            continue
                        logger.warning("Attempting to save interval %s to %s after exception", start_date, end_date)
                        minimal_activity = {'mouse': '0.0', 'average': '0.0', 'keyboard': '0.0'}
                        if await self.inrange(start_date, end_date) and self.status == True:
                            await self.activity_service.save_activity_log(minimal_activity, start_date, end_date, self.last_count+1, self.get_today_weekday(), has_on_demand_screenshot=self.has_on_demand)
                        else:
                            await self.activity_service.save_first_activity_log(minimal_activity, start_date, end_date, self.last_count+1, self.get_today_weekday(), has_on_demand_screenshot=self.has_on_demand)
                        logger.info("Successfully saved interval after exception recovery")
                        self.has_on_demand = False
                except Exception as recovery_error:
                    logger.error("Failed to save interval during exception recovery: %s", recovery_error, exc_info=True)

                # Continue the loop even if there's an error
                await asyncio.sleep(5)  # Brief pause before retrying
                continue

    async def empty_interval(self, user_entity: UserEntity):
        logger.info("Running push empty intervals")
        current = self.last_count + 1
        missing_intervals = await self.find_missing_timestamps(current)
        missing_intervals = self._filter_gaps_before_session(missing_intervals)

        if len(missing_intervals) == 0:
            logger.info("No missing intervals")
            return
        logger.info("System is in sleep: %s", missing_intervals)

        for gap_start, gap_end in missing_intervals:
            overlapping = await self.get_intervals_overlapping_range(gap_start, gap_end)
            sub_segments = self._subtract_overlapping_intervals(gap_start, gap_end, overlapping)
            if not sub_segments:
                logger.info(
                    f"Skip empty interval for {gap_start} - {gap_end}: fully covered by existing intervals (e.g. break)"
                )
                continue
            if overlapping:
                logger.info(
                    f"Sleep gap {gap_start} - {gap_end} overlaps existing intervals; filling only non-overlapping parts: {sub_segments}"
                )

            gap_seconds = (gap_end - gap_start).total_seconds()
            # Silent mode: any gap (including >1 hour) is filled as normal idle/empty
            # slot intervals and uploaded like regular activity — no session stop/open.
            if gap_seconds > 3600 and not is_silent_build():
                total_fill_seconds = sum((e - s).total_seconds() for s, e in sub_segments)
                logger.info(
                    "Sleep gap > 1 hour (gap_seconds=%.0f, fill_seconds=%.0f) - "
                    "saving empty interval(s) for non-overlapping parts only (visible mode)",
                    gap_seconds,
                    total_fill_seconds,
                )
                for seg_start, seg_end in sub_segments:
                    break_id = self._empty_interval_break_id(seg_start)
                    gap_level = await self._activity_level_for_gap_fill(seg_start, seg_end)
                    await self.activity_service.save_first_activity_log(
                        gap_level,
                        seg_start,
                        seg_end,
                        self.last_count + 1,
                        self.get_today_weekday(),
                        break_id=break_id,
                    )
                if total_fill_seconds > 3600:
                    try:
                        sleep_minutes = int(total_fill_seconds / 60)
                        sent = self._send_to_electron_if_connected("sleep_auto_stop", {
                            "message": f"Your system was in sleep for more than 1 hour. Timer has been stopped automatically & time log for the sleep period has been saved. Kindly restart the timer to continue working!!",
                            "sleep_minutes": sleep_minutes,
                        })
                        if sent:
                            logger.info("Sleep auto-stop signal sent to Electron (sleep gap: %s minutes)", sleep_minutes)
                        else:
                            logger.warning("Failed to send sleep_auto_stop to Electron (not connected?)")
                    except Exception as e:
                        logger.error("Error sending sleep_auto_stop to Electron: %s", e, exc_info=True)
                    return
            else:
                if gap_seconds > 3600:
                    logger.info(
                        "[Silent] Sleep gap > 1 hour (%.0fs) — filling idle intervals normally",
                        gap_seconds,
                    )
                else:
                    logger.info("Sleep is less than 1 hour adding up intervals")
                for seg_start, seg_end in sub_segments:
                    start_time = seg_start
                    while start_time < seg_end:
                        interval_end_time = min(
                            start_time + datetime.timedelta(minutes=self.interval / 60), seg_end
                        )
                        break_id = self._empty_interval_break_id(start_time)
                        if await self.activity_service.check_existing_interval(
                            start_time, interval_end_time, exact=True
                        ):
                            logger.info(
                                "Skip sleep gap fill — interval already exists for %s to %s",
                                start_time,
                                interval_end_time,
                            )
                            start_time = interval_end_time
                            continue
                        gap_level = await self._activity_level_for_gap_fill(
                            start_time, interval_end_time
                        )
                        if await self.inrange(start_time, interval_end_time) and self.status is True:
                            logger.info(
                                f"Add Empty Interval in range of tracking mode: {start_time} - {interval_end_time}"
                            )
                            logger.info("user main status activity saved  >>  %s", self.status)
                            await self.activity_service.save_activity_log(
                                gap_level,
                                start_time,
                                interval_end_time,
                                self.last_count + 1,
                                self.get_today_weekday(),
                                break_id=break_id,
                            )
                        else:
                            logger.info(
                                f"Exclude Empty Interval becasue it is out of range of tracking mode: {start_time} - {interval_end_time}"
                            )
                            logger.info("user main status activity saved  >>  %s", self.status)
                            await self.activity_service.save_first_activity_log(
                                gap_level,
                                start_time,
                                interval_end_time,
                                self.last_count + 1,
                                self.get_today_weekday(),
                                break_id=break_id,
                            )
                        start_time = interval_end_time


    async def _push_activity_log(self, user_entity: UserEntity, stop_event=None):
        while not stop_event.is_set():
            try:
                current_break_id = getattr(self, 'break_id', 0) or 0
                try:
                    await asyncio.wait_for(
                        self.update_tracking_duration(user_entity),
                        timeout=10.0,
                    )
                except asyncio.TimeoutError:
                    logger.warning(
                        "[push] update_tracking_duration timed out after 10s — "
                        "continuing with cached stealth-mode data"
                    )
                logger.info(
                    "Running push loop force_stop=%s, timezone_mismatch=%s, datetime_mismatch=%s, time_error_type=%s",
                    self.force_stop,
                    self._timezone_mismatch,
                    self._datetime_mismatch,
                    self._time_error_type,
                )
                if (
                    self._timezone_mismatch is True
                    and self._timezone_checks_enabled(user_entity)
                ):
                    # Real timezone mismatch from timecheck or monitor — stop sync.
                    logger.info("Timezone mismatch under Push loop")
                    active_app_url = [{
                        "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                        "module": "Wrong Timestamp",
                        "active_app": "Wrong Timestamp",
                        "url": "Wrong Timestamp"
                    }]  
                    
                    await self.activity_service.save_app_monitors(active_app_url, self.last_count+1, True),
                    
                    logger.info("save last active app as true because of wrong timezone deteceted so next last min save can't be wrong from activity")
                    url = config('timezone_mismatch', 'Application')
                    headers = {
                        "Content-Type": "application/json",
                        "Authorization": f"Bearer {user_entity.sd_token}",
                        "OrgID": f"{user_entity.org_id}",
                        "SDToken": f"{user_entity.sd_token}",
                        "UserID": f"{user_entity.user_id}"
                    }
                    from ws_package.utils.desktop_key import apply_desktop_key
                    headers = apply_desktop_key(headers)
                    payload = {
                        "device_type": self.get_os_type_caps(),
                        "os_name": self.get_os_type_caps(),
                        "os_version": get_os_version(),
                        "device_id":self.system_unique_id,
                        "status": 1
                    }
                    try:
                        log_testing_request_headers(logger, "POST", url, headers, payload)
                        response = requests.post(url, headers=headers, json=payload, timeout=10)
                        log_api_response_summary(
                            logger,
                            api_name_from_url(url),
                            response.status_code,
                            response.text,
                        )
                    except requests.exceptions.Timeout:
                        logger.error("[API] %s | timeout", api_name_from_url(url))
                    except requests.exceptions.RequestException as e:
                        logger.error("[API] %s | request failed: %s", api_name_from_url(url), e)
                    
                    # Send timezone popup only for true timezone mismatch. datetime_error is emitted by time monitor.
                    try:
                        if self._timezone_mismatch:
                            error_data = {
                                'title': 'Timezone Error',
                                'message': 'Your system timezone must match with Workstatus account timezone to start the tracker',
                                'timestamp': datetime.datetime.now().isoformat()
                            }
                            if self._send_to_electron_if_connected('timezone_error', error_data):
                                logger.info("Sent timezone mismatch error to Electron: %s", error_data)
                                await precise_sleep(5, stop_event)
                                # CRITICAL: Don't call os._exit(1) - keep Python process alive for error recovery
                                # Electron will handle stopping the timer, but Python should remain running
                                logger.info("Timezone error sent to Electron - Python process will remain alive (not exiting)")
                    except Exception as e:
                        logger.error("Failed to send time integrity error to Electron: %s", e)
                    
                    logger.error(
                        "Time integrity error detected (%s) - stopping tracker IMMEDIATELY - NO DATA SYNC",
                        self._time_error_type,
                    )
                    self.force_stop = True
                    # CRITICAL: Skip all data sync operations when timezone error is detected
                    # Don't sync intervals, don't sync unsent data, just stop
                    logger.error("Skipping all data sync operations (empty_interval, unsent intervals, current intervals)")
                    # CRITICAL: Don't call os._exit(1) - keep Python process alive for error recovery
                    # The monitoring loop will stop gracefully due to force_stop=True, but process remains alive
                    logger.info("Python process will remain alive - monitoring stopped but process continues running")
                    # Wait a bit to allow error message to be sent; skip empty_interval and interval send (no last 10-15 sec to DB/API)
                    await precise_sleep(5, stop_event)
                    continue
                elif self.force_stop == True and self.status == False:
                    # This is a WebSocket stop command, not a timezone mismatch
                    # Don't exit - just skip the push activity and wait
                    logger.info("[Tracker] Tracking stopped via WebSocket - skipping push activity")
                    await precise_sleep(self.interval, stop_event)
                    continue
                else:
                    logger.info("Force Stop Status False under Push")
                    
                
                await self.empty_interval(user_entity)
                
                logger.info("Today is %s and tracking days are %s", self.get_today_weekday(), self.no_of_days)
                if self.no_of_days is not None and self.get_today_weekday() in self.no_of_days:
                    
                    self.activity_service.application_activity_log_url = config('application_activity_log_url', 'Application')
                    prev_day_unsent = await self.getting_unsent_interval(user_entity)
                    logger.info("Previous day unsent intervals %s", prev_day_unsent)
                    if prev_day_unsent:
                        start = prev_day_unsent[0].start_time
                        await self.sending_unsent_intervals(start, prev_day_unsent, user_entity)

                    # Headers for the activity log api
                    sd_token = user_entity.sd_token
                    org_id = user_entity.org_id
                    user_id = user_entity.user_id
                    appVersion =  self.appVersion

                    # CRITICAL: Filter by current session counter to prevent picking intervals from previous sessions
                    current_session_counter = self.last_count + 1
                    intervals = await self.activity_service.get_activity_intervals(self.main_start_date, self.main_end_date, counter=current_session_counter)
                    
                    # Safety valve: auto-reset stale break if Electron crashed mid-break
                    await self.check_stale_break()

                    if not is_silent_build():
                        # NEW REQUIREMENT: Check 60-minute timeout before checking API pause
                        timeout_reached = await self.check_idle_popup_timeout()
                        if timeout_reached:
                            logger.warning(
                                "[IdleTimeout] Auto-stop triggered — waiting for Electron stop "
                                "(do not exit push alone; MonitorThread would restart it)"
                            )
                            self.status = False
                            # Wait for stop_event from Electron-driven stop path
                            await precise_sleep(self.interval, stop_event)
                            if stop_event.is_set():
                                break
                            continue

                        # Check if API calls should be paused (idle popup active or all-zero activity).
                        # _api_pause_cycles tracks consecutive pauses so we can enforce a hard cap:
                        # after _MAX_API_PAUSE_CYCLES pauses the upload is forced through regardless,
                        # preventing indefinite stall — EXCEPT while the idle popup is active (60+ min idle
                        # must never sync until the user responds or auto-stop marks intervals uploaded).
                        should_pause_api = await self._should_pause_api_calls(intervals)
                        idle_popup_blocking = (
                            self._idle_popup_active and self._idle_popup_triggered_at is not None
                        )
                        force_upload_after_pause_cap = (
                            should_pause_api
                            and self._api_pause_cycles >= self._MAX_API_PAUSE_CYCLES
                            and not idle_popup_blocking
                        )
                        if should_pause_api and not force_upload_after_pause_cap:
                            self._api_pause_cycles += 1
                            logger.info(
                                "[APIBlock] API calls paused — cycle %d/%d "
                                "(zero activity or idle popup active); intervals saved to DB",
                                self._api_pause_cycles, self._MAX_API_PAUSE_CYCLES,
                            )
                            self._api_pause_active = True
                            await precise_sleep(self.interval, stop_event)
                            continue
                        else:
                            if force_upload_after_pause_cap:
                                logger.warning(
                                    "[APIBlock] Max pause cycles (%d) reached — forcing upload "
                                    "to prevent indefinite data hold (idle popup not active)",
                                    self._MAX_API_PAUSE_CYCLES,
                                )
                            self._api_pause_active = False
                            self._api_pause_cycles = 0  # reset on any successful push path
                    
                    # NEW: If we have held idle intervals and non-idle interval found, send held intervals first
                    # CRITICAL: Only send held intervals from current counter (filter out intervals from previous counters)
                    current_counter = self.last_count + 1
                    if self._held_idle_intervals:
                        # Filter held intervals by current counter
                        current_counter_held = [iv for iv in self._held_idle_intervals if getattr(iv, 'counter', None) == current_counter]
                        if current_counter_held:
                            logger.info("[APIBlock] Sending %s held idle interval(s) from current counter (%s) before non-idle interval", len(current_counter_held), current_counter)
                            held_intervals = current_counter_held.copy()
                            # Remove sent intervals from held list
                            self._held_idle_intervals = [iv for iv in self._held_idle_intervals if getattr(iv, 'counter', None) != current_counter]
                            
                            # Process and send held idle intervals
                            sending_held_intervals = self.process_intervals_bulk(held_intervals, org_id, user_entity)
                            self.activity_service.headers = {
                                'SDToken': str(sd_token),
                                'OrgID': str(org_id),  
                                'UserID': str(user_id), 
                                'appVersion': str(appVersion),
                                'Authorization': f'Bearer {sd_token}'
                            }
                            
                            held_sent_count = 0
                            async for response in sending_held_intervals:
                                if not response or not isinstance(response, (list, tuple)):
                                    logger.info("No interval found in held intervals %s", response)
                                    continue
                                
                                _response = self.activity_service.push_activity_logs(response[0])
                                logger.info("Response for held idle intervals: %s", _response)
                                held_sent_count += 1
                            
                            logger.info("[APIBlock] Successfully sent %s held idle interval(s)", held_sent_count)
                        else:
                            # All held intervals are from previous counter - clear them
                            logger.info("[APIBlock] Clearing %s held idle interval(s) from previous counter (current counter=%s)", len(self._held_idle_intervals), current_counter)
                            self._held_idle_intervals = []
                    
                    # CRITICAL: Filter out held idle intervals and unsent is_idle rows before sending
                    held_interval_ids = {getattr(held, 'id', None) for held in self._held_idle_intervals}
                    filtered_intervals = [
                        iv for iv in intervals
                        if getattr(iv, 'id', None) not in held_interval_ids
                        and not (
                            getattr(iv, 'is_idle', False)
                            and not getattr(iv, 'is_uploaded', False)
                        )
                    ]
                    
                    excluded = len(intervals) - len(filtered_intervals)
                    if excluded:
                        logger.info(
                            "[APIBlock] Excluded %s interval(s) from upload (held or is_idle pending popup)",
                            excluded,
                        )
                    
                    # Only send non-idle intervals (held idle intervals are sent separately above)
                    sending_intervals = self.process_intervals_bulk(filtered_intervals, org_id, user_entity)

                    self.activity_service.headers = {
                        'SDToken': str(sd_token),
                        'OrgID': str(org_id),  
                        'UserID': str(user_id), 
                        'appVersion': str(appVersion),
                        'Authorization': f'Bearer {sd_token}'
                    }
                    
                    async for response in sending_intervals:
                        if not response or not isinstance(response, (list, tuple)):
                            logger.info("No interval found %s", response)
                            continue
    
                        _response = self.activity_service.push_activity_logs(response[0])
                        logger.info("Response: %s", _response)
                        
                        # Check if sync failed after sleep event
                        sync_failed = False
                        if _response is None:
                            sync_failed = True
                        elif isinstance(_response, dict):
                            # Check for error responses (not success)
                            if _response.get("message") == "Token is Expired":
                                sync_failed = True
                                self._send_subscription_expired_popup(_response)
                            elif _response.get("planStatus") != "active" or _response.get("isdeleted") == 1 or _response.get("isdeactivated") == 1:
                                sync_failed = True
                                self._send_subscription_expired_popup(_response)
                        
                        # If sync failed, check if there was a recent sleep event
                        if sync_failed:
                            try:
                                wake_detector = get_system_wake_detector()
                                wake_stats = wake_detector.get_stats()
                                last_wake_time = wake_stats.get('last_wake_time')
                                
                                if last_wake_time and isinstance(last_wake_time, datetime.datetime):
                                    time_since_wake = (datetime.datetime.now() - last_wake_time).total_seconds()
                                    # If wake was recent (within last 30 minutes) and sync failed
                                    if time_since_wake < 1800:  # 30 minutes
                                        os_type = platform.system()
                                        logger.error("*** ⚠️  DATA SYNC FAILED AFTER SYSTEM SLEEP - PLATFORM: %s - WAKE TIME: %s - TIME SINCE WAKE: %s MINUTES - DATA NOT SYNCED TO WEB - THIS MAY CAUSE DATA LOSS ⚠️  ***", os_type.upper(), last_wake_time.strftime('%Y-%m-%d %H:%M:%S'), time_since_wake/60)
                            except Exception as e:
                                logger.debug("Could not check wake time for sync failure: %s", e)
                        if _response and _response.get("message") == "Token is Expired":
                            logger.info("Token is Expired")
                            self.status = False
                            await precise_sleep(14400, stop_event) # main sleep for thread 3

                        elif _response and (_response.get("planStatus") != "active" or _response.get("isdeleted") == 1 or _response.get("isdeactivated") == 1):
                            logger.error("Either plan status Deactivated use deleted or user deactivated")
                            self.status = False
                            await precise_sleep(14400, stop_event)  # main sleep for thread 3
                            
                        elif _response and _response["planStatus"] == "active":
                            for interval in response[1]:
                                await self.activity_service.update_activity_interval(interval)  # Pass individual interval
                            for interval_payload in response[0]:
                                await precise_sleep(2, stop_event)#For updating properly is_uploaded = 1 in db
                                for interval in interval_payload.get('intervals', []):  # Ensure 'intervals' exists
                                    screen_url = interval.get("screenUrl")  # Access the screenUrl from each interval payload
                                    await self.screenshot_controller.update_screenshots(screen_url, self.last_count+1)
                else:
                    logger.info("Not in Tracking days")
                logger.info("Push thread")
                # self.log_system_resources()
                await precise_sleep(self.interval, stop_event)
                
                # Check if we should stop
                if stop_event.is_set():
                    break
                    
            except Exception as e:
                from ws_package.utils.error_codes import ErrorCode
                logger.error("%s: %s", ErrorCode.PUSH_LOOP_FAILED, e, exc_info=True)
                # Continue the loop even if there's an error
                await asyncio.sleep(5)  # Brief pause before retrying
                continue
                    
    def is_link(self, url):
        """Check if the given string is a valid HTTP/HTTPS URL."""
        return self.screenshot_manager.is_link(url)
        
    def forward_ss(self, ss):
        return self.screenshot_manager.forward_ss(ss)
    
    async def ss(self, interval_start_time, interval_end_time, user_entity: UserEntity): # This is a validator chck if screenshot is link or not everytime and renaming before uploading 
        ss_array = await self.activity_service.get_screenshots(interval_start_time, interval_end_time)
        ans_array = []
        for ss in ss_array:
            if self.is_link(ss):
                ans_array.append(ss)

            else:
                try:
                    # Validate screenshot file exists and is accessible
                    if not os.path.exists(ss):
                        logger.warning("Screenshot file not found: %s", ss)
                        continue
                        
                    if not os.access(ss, os.R_OK):
                        logger.warning("Screenshot file not readable: %s", ss)
                        continue
                    
                    try:
                        task = asyncio.create_task(
                            self.screenshot_controller.screenshot_service.sync(
                                ss,
                                f"{user_entity.org_id}/{user_entity.user_id}/"
                                f"{self.s3_screenshot_path}{self.screenshot_controller.format_ss_name(self.forward_ss(ss))}/"
                                f"{self.get_os_type()}/{self.screenshot_controller.unix_format_ss_name(self.forward_ss(ss))}",
                                user_entity
                            ),
                        )
                        # Track task to prevent silent failures
                        self._background_tasks.add(task)
                        task.add_done_callback(self._background_tasks.discard)
                        
                        response = await task
                        s3_upload_link = response
                        ans_array.append(s3_upload_link)
                        # Delete local file once we have AWS link - no need to wait for next interval
                        if s3_upload_link and ss:
                            try:
                                screenshot_dir = os.path.dirname(ss)
                                screenshot_name = os.path.basename(ss)
                                self.screenshot_controller.delete_screenshot(screenshot_name, screenshot_dir)
                            except Exception as del_e:
                                logger.warning("Failed to delete screenshot file after upload: %s", del_e)
                    except ValidationError as e:
                        logger.error("Screenshot sync validation error for screenshot: %s", e)
                        logger.error("Context: file=%s, dest=%s%s", ss, self.s3_screenshot_path, self.screenshot_controller.format_ss_name(self.forward_ss(ss)))
                        # Skip this screenshot and continue processing
                        continue
                    except ScreenshotError as e:
                        error_message = str(e)
                        # Check for S3 time tampering error (S3_TIME_TAMPERING or legacy message)
                        if "S3_TIME_TAMPERING" in error_message or "Time tampering detected" in error_message or "RequestTimeTooSkewed" in error_message:
                            logger.error("S3 TIME TAMPERING DETECTED during screenshot upload: %s", error_message)
                            self.handle_s3_time_error(error_message)
                            # Continue processing other screenshots
                            continue
                        logger.error("Screenshot sync error for screenshot: %s", e)
                        logger.error("Context: file=%s, dest=%s%s", ss, self.s3_screenshot_path, self.screenshot_controller.format_ss_name(self.forward_ss(ss)))
                        # Skip this screenshot and continue processing
                        continue
                    except NetworkError as e:
                        logger.error("Network error during screenshot sync for screenshot: %s", e)
                        logger.error("Context: file=%s, dest=%s%s", ss, self.s3_screenshot_path, self.screenshot_controller.format_ss_name(self.forward_ss(ss)))
                        # Skip this screenshot and continue processing
                        continue
                    except Exception as e:
                        error_message = str(e)
                        if "S3_TIME_TAMPERING" in error_message or "RequestTimeTooSkewed" in error_message:
                            logger.error("S3 TIME TAMPERING DETECTED during screenshot upload: %s", error_message)
                            self.handle_s3_time_error(error_message)
                        else:
                            logger.error("Unexpected error during screenshot sync for screenshot: %s", e)
                            logger.error("Context: file=%s, dest=%s%s", ss, self.s3_screenshot_path, self.screenshot_controller.format_ss_name(self.forward_ss(ss)))
                        # Skip this screenshot and continue processing
                        continue
                    
                    # Extract timestamp with error handling
                    try:
                        particular_timestamp = self.screenshot_controller.extract_datetime_from_filename(ss)
                    except Exception as e:
                        logger.warning("Failed to extract timestamp from filename %s: %s", ss, e)
                        particular_timestamp = datetime.datetime.now()
                    
                    screenshot_entity = ScreenshotEntity(
                        file_name=ss,
                        blur=True,
                        s3_upload_link=s3_upload_link,
                        file_size=str(0000),
                        is_uploaded=False,
                        file_location=ss,
                        timestamp=particular_timestamp,
                        is_s3_uploaded=True, 
                        counter=self.last_count+1
                    )                   
                    
                except FileNotFoundError as e:
                    logger.warning("Screenshot file not found: %s - %s", ss, e)
                    continue
                except PermissionError as e:
                    logger.warning("Permission denied accessing screenshot: %s - %s", ss, e)
                    continue
                except OSError as e:
                    logger.warning("OS error accessing screenshot: %s - %s", ss, e)
                    continue
                except APIError as e:
                    logger.error("API error uploading screenshot %s: %s", ss, e)
                    continue
                except NetworkError as e:
                    logger.error("Network error uploading screenshot %s: %s", ss, e)
                    continue
                except ValidationError as e:
                    logger.error("Validation error with screenshot %s: %s", ss, e)
                    continue
                except Exception as e:
                    logger.error("Unexpected error uploading screenshot %s: %s", ss, e)
                    continue
        
        return ans_array

    def group_intervals_by_day(self, intervals):
        """Group intervals by their start date."""
        grouped_intervals = defaultdict(list)
        
        for interval in intervals:
            # Extract the date from the start_time
            interval_date = interval.start_time.date()
            grouped_intervals[interval_date].append(interval)
            
        return grouped_intervals
    
    def _calculate_gap_seconds(self, prev_interval, current_interval) -> float:
        """Return the time gap (in seconds) between two consecutive intervals."""
        if not prev_interval or not current_interval:
            return 0.0
        try:
            return (current_interval.start_time - prev_interval.end_time).total_seconds()
        except Exception as e:
            logger.debug("Failed to calculate gap between intervals: %s", e)
            return 0.0

    def _determine_sleep_reason(self, gap_seconds: float) -> str:
        """
        Provide a descriptive reason for large gaps.
        Uses idle_time_limit_in_seconds from API instead of hardcoded values.
        """
        # Use API value, fallback to default if not set
        idle_threshold = getattr(self, 'idle_time_limit_in_seconds', 3600)
        
        system_name = platform.system()
        if system_name == "Windows":
            return "windows_sleep_or_restart" if gap_seconds >= idle_threshold else "windows_idle_gap"
        if system_name == "Darwin":
            return "macos_sleep_or_shutdown" if gap_seconds >= idle_threshold else "macos_idle_gap"
        return "linux_sleep_or_shutdown" if gap_seconds >= idle_threshold else "linux_idle_gap"

    def _log_session_split_reason(self, reason: str, prev_interval, current_interval, gap_seconds: float) -> None:
        """Log session split metadata for diagnostics."""
        gap_minutes = gap_seconds / 60 if gap_seconds else 0.0
        logger.info(
            "Session split detected | reason=%s | platform=%s | gap_minutes=%.2f | prev_end=%s | next_start=%s | prev_counter=%s | next_counter=%s",
            reason,
            platform.system(),
            gap_minutes,
            getattr(prev_interval, "end_time", None),
            getattr(current_interval, "start_time", None),
            getattr(prev_interval, "counter", None),
            getattr(current_interval, "counter", None),
        )

    def _should_start_new_session(self, prev_interval, current_interval) -> bool:
        """Determine whether a new session should begin based on counters and gaps."""
        if not prev_interval:
            return False
        
        # Counter increments indicate application restart or forced stop
        if current_interval.counter != prev_interval.counter:
            self._log_session_split_reason(
                "application_restart",
                prev_interval,
                current_interval,
                self._calculate_gap_seconds(prev_interval, current_interval),
            )
            return True

        gap_seconds = self._calculate_gap_seconds(prev_interval, current_interval)
        if gap_seconds > self.session_gap_threshold_seconds:
            reason = self._determine_sleep_reason(gap_seconds)
            self._log_session_split_reason(reason, prev_interval, current_interval, gap_seconds)
            return True

        return False

    async def _verify_user_activity_during_period(self, start_time: datetime.datetime, end_time: datetime.datetime) -> bool:
        """
        Verify if there was actual user activity (keyboard/mouse) during a time period.
        This helps prevent creating intervals for background wake events.
        
        Returns:
            True if user activity detected, False otherwise
        """
        try:
            # On macOS, check HID idle time to verify user activity
            if platform.system() == "Darwin":
                try:
                    # Get current HID idle time
                    ioreg_output = subprocess.check_output(
                        ['ioreg', '-c', 'IOHIDSystem'],
                        stderr=subprocess.DEVNULL,
                        timeout=2
                    ).decode('ascii', errors='ignore')
                    
                    # Extract HIDIdleTime (time since last keyboard/mouse input)
                    for line in ioreg_output.splitlines():
                        if 'HIDIdleTime' in line:
                            try:
                                idle_time_ns = int(line.split('=')[-1].strip().rstrip(';'))
                                idle_time_seconds = idle_time_ns / 1000000000.0
                                
                                # If idle time is very recent (< 5 minutes), user was likely active
                                # If idle time is long (> 10 minutes), likely no user activity
                                current_time = datetime.datetime.now()
                                period_duration = (end_time - start_time).total_seconds()
                                
                                # If the period is in the past and idle time is much longer than period duration,
                                # it's likely there was no user activity during that period
                                if end_time < current_time:
                                    # Period is in the past - check if idle time suggests no activity
                                    if idle_time_seconds > period_duration + 300:  # 5 min buffer
                                        logger.debug(
                                            f"No user activity during period {start_time} - {end_time}: "
                                            f"Current idle time ({idle_time_seconds:.0f}s) >> period duration ({period_duration:.0f}s)"
                                        )
                                        return False
                                
                                # If idle time is short (< 5 min), user is currently active
                                # This suggests the period might have had activity
                                if idle_time_seconds < 300:
                                    return True
                                    
                            except (ValueError, IndexError):
                                pass
                    
                    # If we can't determine, be conservative and return False
                    # Better to skip an interval than create a false one
                    return False
                    
                except Exception as e:
                    logger.debug("Could not verify macOS user activity: %s", e)
                    # On error, be conservative - don't create interval
                    return False
            
            # For Windows/Linux, use a simpler check
            # Check if there are any app monitor records during this period
            try:
                with db_lock_manager.read_lock():
                    async with async_session() as session:
                        query = select(Appmonitor).where(
                            Appmonitor.timestamp >= start_time,
                            Appmonitor.timestamp <= end_time
                        ).limit(1)
                        
                        result = await session.execute(query)
                        app_data = result.scalars().first()
                        
                        # If there are app monitor records, there was likely user activity
                        return app_data is not None
                        
            except Exception as e:
                logger.debug("Could not verify user activity via app monitor: %s", e)
                # On error, be conservative
                return False
                
        except Exception as e:
            logger.debug("Error verifying user activity during period: %s", e)
            # Fail-safe: return False to prevent false intervals
            return False

    def is_idle_interval(self, interval) -> bool:
        """
        Our idle definition: keyboard AND mouse activity are zero for the whole interval.
        """
        if not interval:
            return False
        
        try:
            activity_level = interval.activityLevel
            if isinstance(activity_level, str):
                activity_level = json.loads(activity_level)
            activity_level = activity_level or {}
            mouse = float(activity_level.get("mouse", 0) or 0)
            keyboard = float(activity_level.get("keyboard", 0) or 0)
            average = float(activity_level.get("average", 0) or 0)
            return mouse == 0.0 and keyboard == 0.0 and average == 0.0
        except Exception as e:
            logger.debug("Failed to evaluate idle interval: %s", e)
            return False

    def group_intervals_by_session(self, intervals):
        """
        Group intervals into logical sessions.
        
        Splits occur when:
        1. The counter changes (indicates application restart or forced stop).
        2. The chronological gap between intervals exceeds the configured threshold.
        
        Idle intervals remain within the current session.
        """
        if not intervals:
            return []
        
        sorted_intervals = sorted(intervals, key=lambda x: x.start_time)
        sessions = []
        current_session = [sorted_intervals[0]]
        
        for i in range(1, len(sorted_intervals)):
            prev_interval = sorted_intervals[i - 1]
            current_interval = sorted_intervals[i]
            
            if self._should_start_new_session(prev_interval, current_interval):
                sessions.append(current_session)
                current_session = [current_interval]
            else:
                current_session.append(current_interval)
        
        if current_session:
            sessions.append(current_session)
        
        return sessions

    def get_day_start_time(self, target_date, intervals_for_day):
        """
        Returns the appropriate start time for a given day's intervals.
        For the current day: uses current_session_start_time if set (after break end, new session),
        otherwise the timer start time (self.start_date). For other days, uses 00:00:00 of that day.
        """
        if not intervals_for_day:
            return datetime.datetime.combine(target_date, datetime.time.min)

        pinned = self._get_pinned_activity_log_session_start(target_date)
        if pinned is not None:
            return pinned

        if target_date == self.start_date.date():
            # After break end, counter is incremented and current_session_start_time is set:
            # use it so payload "start" is the new session start time, not the original timer start
            session_start = getattr(self, "current_session_start_time", None)
            if session_start is not None and session_start.date() == target_date:
                return self._normalize_activity_log_start(session_start)
            return self._normalize_activity_log_start(self.start_date)
        else:
            return datetime.datetime.combine(target_date, datetime.time.min)
    
    def _get_activity_level_from_interval(self, interval) -> dict:
        return ActivityIntervalMixin._get_activity_level_from_interval(self, interval)

    async def get_activity_level_from_db(self, start_time, end_time):
        return await ActivityIntervalMixin.get_activity_level_from_db(self, start_time, end_time)

    async def _is_app_monitor_data_stale(self, app_monitor_time: datetime.datetime, wake_time: Optional[datetime.datetime]) -> bool:
        """
        Check if app monitor data is stale (from before system wake).
        
        Args:
            app_monitor_time: Timestamp of app monitor entry
            wake_time: When system woke up (if known)
            
        Returns:
            bool: True if data is likely stale (from before wake or within 30 seconds after wake)
        """
        if wake_time is None:
            return False  # Can't determine if stale without wake time
        
        # If app monitor data is from before wake or within 30 seconds after wake, it's likely stale
        time_diff = (app_monitor_time - wake_time).total_seconds()
        return time_diff < 30  # Within 30 seconds of wake = likely stale

    async def _has_any_activity_in_interval(self, start_time: datetime.datetime, end_time: datetime.datetime, counter: int) -> bool:
        """
        Check if there was ANY activity (keyboard, mouse, or app monitor) during an interval.
        
        Args:
            start_time: Start of the interval
            end_time: End of the interval
            counter: Counter value to filter by
            
        Returns:
            bool: True if any activity exists during the interval
        """
        try:
            # First check activity_percentage table for keyboard/mouse activity
            result = await activity_percentage_manager.find_overlapping_activity_percentage(start_time, end_time)
            if result is not None:
                keyboard_activity = float(result.keyboard or 0)
                mouse_activity = float(result.mouse or 0)
                average_activity = float(result.average or 0)
                
                if keyboard_activity > 0 or mouse_activity > 0 or average_activity > 0:
                    logger.debug("Activity found in activity_percentage for %s to %s: keyboard=%s, mouse=%s, average=%s",
                               start_time, end_time, keyboard_activity, mouse_activity, average_activity)
                    return True
            
            # Also check app monitor activity as fallback
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    query = select(Appmonitor).where(
                        Appmonitor.counter == counter,
                        Appmonitor.timestamp >= start_time,
                        Appmonitor.timestamp <= end_time
                    ).limit(1)
                    
                    result = await session.execute(query)
                    app_data = result.scalars().first()
                    
                    if app_data:
                        logger.debug("App monitor activity found for %s to %s", start_time, end_time)
                        return True
            
            return False
        except Exception as e:
            logger.debug("Error checking activity in interval: %s", e)
            return False  # Fail-safe: if we can't check, assume no activity

    async def process_intervals_bulk(
        self,
        intervals,
        org_id,
        user_entity: UserEntity,
        last_interval=None,
        *,
        payload_project_id: Optional[str] = None,
        payload_task_id: Optional[str] = None,
        force_stop_time: Optional[datetime.datetime] = None,
    ):
        if intervals is None or len(intervals) == 0:
            logger.info("No intervals found for the given date range.")
            yield None
        
        # Group intervals by day
        grouped_intervals = self.group_intervals_by_day(intervals)
        
        # Process each day separately
        for target_date, day_intervals in grouped_intervals.items():
            # Edge case: Ensure the intervals array contains at most 100 objects per day
            if len(day_intervals) > 100:
                day_intervals = day_intervals[:100]
            logger.info("Target date of intervals %s", target_date)
            # Get the appropriate start time for this day
            day_start_time = self.get_day_start_time(target_date, day_intervals)
            # When project/task was changed on running timer, use current segment start (now) for today's payload
            effective_start_time = day_start_time
            pinned_start = self._get_pinned_activity_log_session_start(target_date)
            if pinned_start is not None:
                effective_start_time = pinned_start
                logger.info("Using pinned activity/log session start: %s", effective_start_time)
            elif target_date == datetime.datetime.now().date() and self.current_segment_start_time is not None:
                effective_start_time = self.current_segment_start_time
                logger.info("Using segment start (project/task changed on running timer): %s", effective_start_time)
            effective_start_time = self._normalize_activity_log_start(effective_start_time)

            # CRITICAL: Filter out intervals that completely end before effective_start_time.
            # General rule: Only filter intervals that completely end before the payload start time
            # This applies to all cases: normal sessions, idle intervals, and break intervals.
            # This prevents including intervals from previous sessions/breaks when a new session starts.
            # FIX: Changed from filtering by start_time < effective_start_time to end_time < effective_start_time
            # This ensures intervals that start at or overlap with effective_start_time are included
            # (e.g., break ends at 10:02, interval created 10:02-10:10 should be included)
            if day_intervals:
                filtered_intervals = []
                filtered_count = 0
                for interval in day_intervals:
                    # Only filter if interval completely ends before effective_start_time
                    if hasattr(interval, 'start_time') and hasattr(interval, 'end_time') and interval.end_time < effective_start_time:
                        filtered_count += 1
                        logger.debug("Filtering out interval (start=%s, end=%s) - completely ends before payload start time (%s)", interval.start_time, interval.end_time, effective_start_time)
                    else:
                        filtered_intervals.append(interval)
                
                if filtered_count > 0:
                    logger.info("Filtered out %s interval(s) that completely end before effective_start_time (%s)", filtered_count, effective_start_time)
                    day_intervals = filtered_intervals
                
                if not day_intervals:
                    logger.info("No intervals remain after filtering - skipping this day")
                    continue

            # Prepare the list of interval payloads for this day
            interval_payloads = []
            
            # Fetch notes for all intervals in this day from Notes table
            interval_ids = [interval.id for interval in day_intervals if hasattr(interval, 'id')]
            interval_note_map = {}
            interval_file_map = {}
            
            # Query Notes table for entries that contain any of these interval IDs
            # Notes.activity_interval_id is a JSON array, so we need to check if any interval_id is in the array
            try:
                with db_lock_manager.read_lock():
                    async with async_session() as session:
                        notes_query = select(Notes)
                        notes_result = await session.execute(notes_query)
                        notes_entries = notes_result.scalars().all()
                        
                        # Create a mapping of interval_id -> note and file_path
                        for notes_entry in notes_entries:
                            if notes_entry.activity_interval_id:  # Check if array is not None
                                for interval_id in notes_entry.activity_interval_id:
                                    if interval_id in interval_ids:
                                        interval_note_map[interval_id] = notes_entry.note
                                        if hasattr(notes_entry, 'file_path') and notes_entry.file_path:
                                            interval_file_map[interval_id] = notes_entry.file_path
                        
                        if interval_note_map:
                            logger.info("Found notes for %s intervals", len(interval_note_map))
            except Exception as e:
                logger.error("Error fetching notes from Notes table: %s", e, exc_info=True)
                # Continue without notes if there's an error
            
            for interval in day_intervals:
                # Format idle_note_url
                interval_id = getattr(interval, 'id', None)
                file_path = interval_file_map.get(interval_id)
                idle_note_url = {"image_url": file_path} if file_path else None

                # For the last interval (stop), extend end_time to now when fetching screenshots so the
                # stop screenshot (captured at stop button press) is included in the payload
                ss_end_time = interval.end_time
                if last_interval is not None:
                    def _dt_match(a, b, tol=1.0):
                        if a is None or b is None:
                            return a == b
                        try:
                            return abs((a - b).total_seconds()) <= tol
                        except Exception:
                            return False
                    last_match = (
                        _dt_match(getattr(interval, 'start_time', None), getattr(last_interval, 'start_time', None))
                        and _dt_match(getattr(interval, 'end_time', None), getattr(last_interval, 'end_time', None))
                        and getattr(interval, 'counter', None) == getattr(last_interval, 'counter', None)
                    )
                    if last_match:
                        ss_end_time = max(interval.end_time, datetime.datetime.now())
                        logger.info("Last interval (stop): extending screenshot range to %s to include stop screenshot", ss_end_time)

                payload_from, payload_to = self._payload_interval_bounds(interval, effective_start_time)
                align_stop = (
                    force_stop_time
                    or getattr(self, "_last_work_stop_time", None)
                )
                if last_interval is not None and last_match and align_stop is not None:
                    payload_to = align_stop.replace(microsecond=0)
                    logger.info(
                        "Last interval (stop): aligning payload 'to' with activity/log stop %s",
                        payload_to,
                    )
                if payload_to is not None and payload_from is not None and payload_to <= payload_from:
                    logger.info(
                        "Skipping interval (start=%s, end=%s) — no time left after clipping to payload start %s",
                        interval.start_time,
                        interval.end_time,
                        effective_start_time,
                    )
                    continue

                interval_payload = {
                    "Entry": interval_id or 0,  # Use 0 as default if id doesn't exist (e.g., ActivityIntervalEntity)
                    "from": payload_from.strftime("%Y-%m-%d %H:%M:%S"),
                    "to": payload_to.strftime("%Y-%m-%d %H:%M:%S"),
                    "screenUrl": await self.ss(payload_from, ss_end_time, user_entity),
                    "activityLevel": await self.get_activity_level_from_db(payload_from, payload_to),
                    "appAndUrls": await self.activity_service.get_apps_urls(payload_from, payload_to, self.appandurlstatus),
                    "deviceType": self.get_os_type_caps(),
                    "os_type": self.get_os_type_caps(),
                    "note": interval_note_map.get(interval_id),  # Get note from Notes table mapping
                    "idle_note_url": idle_note_url,
                    "idleInterval": None,
                    "location": LocationEntity(
                        long="null",
                        lat="null"
                    ).model_dump(),
                    "interval_time_db": int(self.interval/60),
                    "has_on_demand_screenshot": getattr(interval, "has_on_demand_screenshot", False),
                }
                interval_payloads.append(interval_payload)

            # Build the final payload for this day
            project_id = (
                payload_project_id
                if payload_project_id is not None
                else (self.current_project_id if self.current_project_id else "1001")
            )
            task_id = (
                payload_task_id
                if payload_task_id is not None
                else (self.current_task_id if self.current_task_id else "0")
            )
            if payload_project_id is None and not self.current_project_id:
                logger.warning(
                    "No project selected in Python — using fallback project_id=%s. "
                    "Ensure Electron sends set_project_task before start_activity_monitoring.",
                    project_id,
                )
            logger.info(
                "Using project_id=%s, task_id=%s for activity log payload (override=%s)",
                project_id,
                task_id,
                payload_project_id is not None,
            )
            logger.info(
                "Building activity/log stop fields (last_interval=%s, force_stop_time=%s, intervals=%s)",
                last_interval is not None,
                force_stop_time,
                len(interval_payloads),
            )
            
            # When sending last interval (stop click), set "stop" to datetime.now()
            # BUT: Don't set "stop" for sleep intervals (long gaps with zero activity)
            stop_value = None
            stop_dt_for_intervals = None
            if last_interval is not None:
                def _dt_match(a, b, tol=1.0):
                    if a is None or b is None:
                        return a == b
                    try:
                        return abs((a - b).total_seconds()) <= tol
                    except Exception:
                        return False
                
                # NEW: Check if this is a sleep interval (long gap with zero activity)
                # Sleep intervals should NOT get a "stop" timestamp
                is_sleep_interval = False
                if hasattr(last_interval, 'start_time') and hasattr(last_interval, 'end_time'):
                    gap_seconds = (last_interval.end_time - last_interval.start_time).total_seconds()
                    # Check if it's a long interval (>= 1 hour) with zero activity (typical sleep pattern)
                    if gap_seconds >= 3600:  # 1 hour
                        # Check if activity level is zero (sleep intervals have zero activity)
                        try:
                            activity_level = await self.get_activity_level_from_db(last_interval.start_time, last_interval.end_time)
                            if activity_level:
                                keyboard = float(activity_level.get('keyboard', 0) or 0)
                                mouse = float(activity_level.get('mouse', 0) or 0)
                                average = float(activity_level.get('average', 0) or 0)
                                if keyboard == 0.0 and mouse == 0.0 and average == 0.0:
                                    is_sleep_interval = True
                                    logger.info("[SleepInterval] Detected sleep interval (%s hours) - will NOT set 'stop' timestamp", gap_seconds/3600)
                        except Exception as e:
                            logger.debug("Could not check activity level for sleep detection: %s", e)
                            # If we can't check, also check if the interval has activityLevel attribute directly
                            if hasattr(last_interval, 'activityLevel') and last_interval.activityLevel:
                                try:
                                    activity_level = last_interval.activityLevel
                                    keyboard = float(activity_level.get('keyboard', 0) if isinstance(activity_level, dict) else 0)
                                    mouse = float(activity_level.get('mouse', 0) if isinstance(activity_level, dict) else 0)
                                    average = float(activity_level.get('average', 0) if isinstance(activity_level, dict) else 0)
                                    if keyboard == 0.0 and mouse == 0.0 and average == 0.0:
                                        is_sleep_interval = True
                                        logger.info("[SleepInterval] Detected sleep interval from activityLevel attribute (%s hours) - will NOT set 'stop' timestamp", gap_seconds/3600)
                                except Exception as e2:
                                    logger.debug("Could not check activityLevel attribute: %s", e2)
                
                if not is_sleep_interval:
                    last_in_this_day = any(
                        _dt_match(getattr(iv, 'start_time', None), getattr(last_interval, 'start_time', None))
                        and _dt_match(getattr(iv, 'end_time', None), getattr(last_interval, 'end_time', None))
                        and getattr(iv, 'counter', None) == getattr(last_interval, 'counter', None)
                        for iv in day_intervals
                    )
                    if last_in_this_day:
                        stop_dt = (
                            force_stop_time
                            or getattr(self, '_last_work_stop_time', None)
                            or datetime.datetime.now()
                        )
                        stop_dt_for_intervals = stop_dt.replace(microsecond=0)
                        stop_value = stop_dt_for_intervals.strftime("%Y-%m-%d %H:%M:%S")
                        logger.info("Last interval in this batch (stop click): setting payload 'stop' to %s", stop_value)
                else:
                    logger.info("[SleepInterval] Skipping 'stop' timestamp for sleep interval (start=%s, end=%s)", last_interval.start_time, last_interval.end_time)

            if (
                stop_value is None
                and force_stop_time is not None
                and interval_payloads
            ):
                stop_dt_for_intervals = force_stop_time.replace(microsecond=0)
                stop_value = stop_dt_for_intervals.strftime("%Y-%m-%d %H:%M:%S")
                interval_payloads[-1]["to"] = stop_value
                logger.info(
                    "Forced session close: setting payload stop and last interval 'to' to %s",
                    stop_value,
                )
            
            payload = [{
                "projectId": str(project_id),
                "start": effective_start_time.strftime("%Y-%m-%d %H:%M:%S"),
                "stop": stop_value,
                "selfi_id": None,
               "timezone": self._normalize_timezone(str(self.timezone_manager.local_timezone) if self.timezone_manager.local_timezone else str(self.get_system_timezone())),
                "break_id": None,
                "deviceType": self.get_os_type_caps(),
                "os_type": self.get_os_type_caps(),
                "selfiVerification": True,
                "intervals": interval_payloads,
                "time_type": "1",
                "interval_time": int(self.interval/60),
                "source_type": "4",
                "org_id": str(org_id),
                "todo_id": str(task_id) if task_id is not None else "0",
                "deviceId": self.system_unique_id,
                "ip_address": str(get_ip()),
                "os_version": get_os_version()
            }]
            
            logger.info("Generated payload for date %s with start time %s and %s intervals", target_date, effective_start_time.strftime('%Y-%m-%d %H:%M:%S'), len(day_intervals))
            # Clear segment start after using it so next payload uses normal day start
            if effective_start_time == self.current_segment_start_time:
                self.current_segment_start_time = None
            yield payload, day_intervals

    async def delete_old_data_and_screenshots(self):
        """
        Delete old data and screenshots older than 7 days.
        
        FIXED: Separated file operations from database operations to prevent long lock holds.
        Pattern: Read from DB (quick) -> Release lock -> Delete files (slow) -> Re-acquire lock -> Delete from DB (quick)
        """
        # Database operation timeout constant
        DB_OPERATION_TIMEOUT = 5.0
        
        if getattr(sys, 'frozen', False) and platform.system() == "Linux":  # Running as a PyInstaller EXE in linux
            script_path = Path(sys.executable).resolve()
            target_directory = script_path.parents[0] # Just one level up from the executable
            target_directory_stealth = script_path.parents[1] # Two levels up from the executable
        elif getattr(sys, 'frozen', False) and platform.system() == "Windows":  # Running as a PyInstaller EXE in Windows
            script_path = Path(sys.executable).resolve()
            target_directoryw = script_path.parents[0]
        elif sys.platform == 'darwin':
            target_directory_mac = Path.home() / "Library/Application Support/Workstatus"
        else:  # DEV MODE
            script_path = Path(__file__).resolve()
            target_directory = f"{script_path.parents[3]}/workstatus"
            target_directory_stealth = f"{target_directory}/workstatus/" 
        logger.info("Starting deletion of old data and screenshots.")
        
        try:
            seven_days_ago = datetime.datetime.now() - datetime.timedelta(days=7)
            
            # STEP 1: Get list of screenshots to delete (QUICK, inside lock)
            screenshots_to_delete = []
            with db_lock_manager.write_lock():
                async with async_session() as session:
                    screenshot_query = select(Screenshots).where(Screenshots.timestamp < seven_days_ago)
                    try:
                        result = await asyncio.wait_for(
                            session.execute(screenshot_query),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        old_screenshots = result.scalars().all()
                        
                        # Extract screenshot data while session is still open
                        for screenshot in old_screenshots:
                            screenshots_to_delete.append({
                                'id': screenshot.id,
                                'file_location': screenshot.file_location,
                                'file_name': screenshot.file_name
                            })
                    except asyncio.TimeoutError:
                        logger.error("Database query timed out after %ss while getting old screenshots", DB_OPERATION_TIMEOUT)
                        return
                    except Exception as e:
                        logger.error("Error getting old screenshots: %s", e)
                        return
            
            # STEP 2: Delete files OUTSIDE lock (SLOW I/O operations - don't block database)
            if screenshots_to_delete:
                logger.info("Deleting %s screenshot files...", len(screenshots_to_delete))
                for idx, screenshot_data in enumerate(screenshots_to_delete):
                    file_location = screenshot_data['file_location']
                    file_name = screenshot_data['file_name']
                    
                    try:
                        if platform.system() == "Linux":
                            try:
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("/")[1], 
                                    f"{target_directory_stealth}/screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s/screenshots", e, target_directory_stealth)
                            try:
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("/")[1], 
                                    f"{target_directory}/screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s/screenshots", e, target_directory)
                        elif platform.system() == "Windows":
                            try:
                                logger.info("Cleaning function Build for windows")
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("\\")[1],
                                    f"{target_directoryw}\\screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s\\screenshots", e, target_directoryw)
                            # Log cleanup (only once, not per screenshot)
                            if idx == 0:  # Only cleanup logs once (first iteration)
                                try:
                                    logs.cleanup_old_files(f"{target_directoryw}\\logsy")
                                except Exception as e:
                                    logger.error("Error in deleting logs: %s in %s\\logsy", e, target_directoryw)
                        elif sys.platform == 'darwin':
                            try:
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("/")[1], 
                                    f"{target_directory_mac}/screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s/screenshots", e, target_directory_mac)
                    except Exception as e:
                        logger.error("Error deleting screenshot file %s: %s", file_name, e)
                        # Continue with next screenshot even if one fails
            else:
                logger.info("No screenshots older than 7 days found for deletion.")
            
            # STEP 3: Delete database entries (QUICK, inside lock)
            with db_lock_manager.write_lock():
                async with async_session() as session:
                    try:
                        # Delete screenshots from database (bulk delete is faster)
                        if screenshots_to_delete:
                            screenshot_ids = [s['id'] for s in screenshots_to_delete]
                            await asyncio.wait_for(
                                session.execute(
                                    delete(Screenshots).where(Screenshots.id.in_(screenshot_ids))
                                ),
                                timeout=DB_OPERATION_TIMEOUT
                            )
                            logger.info("Deleted %s screenshot database entries.", len(screenshot_ids))

                        # Delete ActivityInterval entries older than 7 days
                        await asyncio.wait_for(
                            session.execute(delete(ActivityInterval).where(ActivityInterval.start_time < seven_days_ago)),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Deleted old ActivityInterval entries.")

                        # Delete OrgConfigurations entries older than 7 days
                        await asyncio.wait_for(
                            session.execute(delete(OrgConfigurations).where(OrgConfigurations.timestamp < seven_days_ago)),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Deleted old OrgConfigurations entries.")

                        # Delete Appmonitor entries older than 7 days
                        await asyncio.wait_for(
                            session.execute(delete(Appmonitor).where(Appmonitor.timestamp < seven_days_ago)),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Deleted old Appmonitor entries.")
                        
                        # Commit with timeout
                        await asyncio.wait_for(
                            session.commit(),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Successfully deleted old data and screenshots from specified tables.")
                        
                    except asyncio.TimeoutError:
                        logger.error("Database operation timed out after %ss in delete_old_data_and_screenshots", DB_OPERATION_TIMEOUT)
                        await session.rollback()
                    except Exception as e:
                        logger.error("Error deleting database entries: %s", e, exc_info=True)
                        await session.rollback()

        except Exception as e:
            logger.error("Error in deleting old data and screenshots: %s", e, exc_info=True)

    async def start_status_activity(self, user_entity: UserEntity, stop_event):
        if user_entity is None:
            logger.error("start_status_activity called with None user_entity, cannot proceed")
            return
        
        await self.status_controller.set_cred(user_entity)
        # Set org_id from user_entity
        self.org_id = user_entity.org_id
        # Update org_id in ScreenshotController and its ScreenshotService
        self.screenshot_controller.org_id = self.org_id
        self.screenshot_controller.screenshot_service.org_id = self.org_id
        logger.info("org_id set to: %s for ScreenshotController and ScreenshotService", self.org_id)
        logger.info("Setting S3 details for db to server")
        
        # Status check is now only called on app restart and system wake, not in a loop
        # This thread just keeps running for db_to_server operations
        while not stop_event.is_set():
            try:
                try:
                    await self.db_to_server(user_entity)
                except Exception as e:
                    logger.error("Error in db_to_server: %s", e)
                
                # Sleep for a long time (only wake up for stop_event)
                # Status check will be triggered by wake callback or app restart
                total_sleep = 3600  # 1 hour
                chunk_duration = 600  # 10 minutes per chunk
                chunks = total_sleep // chunk_duration
                for _ in range(chunks):
                    if stop_event.is_set():
                        break
                    await precise_sleep(chunk_duration, stop_event)
                
                if stop_event.is_set():
                    break
                
                try:
                    await self.delete_old_data_and_screenshots()
                    
                    # Perform periodic log cleanup (works offline, independent of server connectivity)
                    try:
                        from ws_package.utils.logs import perform_comprehensive_log_cleanup
                        cleanup_enabled = config('log_cleanup_enabled', 'System', dtype='bool')
                        if cleanup_enabled is None or cleanup_enabled:
                            perform_comprehensive_log_cleanup()
                    except Exception as e:
                        logger.debug("Error during periodic log cleanup (non-fatal): %s", e)
                    
                    if sys.platform == "darwin":
                        logger.info("Sending mac logs to server")
                        mac_log = Path.home() / "Library/Application Support/Workstatus/logs/workstatus_error.log"
                        if mac_log.exists():
                            timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
                            s3_key = f"{self.s3_logs_path}{user_entity.org_id}/{user_entity.user_id}/{timestamp}-logs.txt"

                            try:
                                task = asyncio.create_task(
                                    self.screenshot_controller.screenshot_service.sync(
                                        str(mac_log),
                                        s3_key,
                                        user_entity
                                    )
                                )
                                # Track task to prevent silent failures
                                self._background_tasks.add(task)
                                task.add_done_callback(self._background_tasks.discard)
                                
                                logs_path = await task
                                logger.info("Running push logs")
                                logger.info("Uploaded from: %s", mac_log) 
                                logger.info("Uploaded to: %s", logs_path)
                            except ValidationError as e:
                                logger.error("Screenshot sync validation error for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation
                            except ScreenshotError as e:
                                logger.error("Screenshot sync error for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation
                            except NetworkError as e:
                                logger.error("Network error during screenshot sync for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation
                            except Exception as e:
                                logger.error("Unexpected error during screenshot sync for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation

                            # Clear the log file after successful upload
                            mac_log.write_text("")
                            logger.info("Cleared log file: %s", mac_log)
                        else:
                            logger.info("Log file %s does not exist, skipping upload.", mac_log)
                    else:
                            logger.debug("MAC ErrorLog file does not exist, skipping upload.")
                except Exception as e:
                    logger.exception("Error in uploading MAC logs: %s", e)
                    
            except Exception as e:
                from ws_package.utils.error_codes import ErrorCode
                logger.error("%s: %s", ErrorCode.STATUS_LOOP_FAILED, e, exc_info=True)
                # Continue the loop even if there's an error
                await asyncio.sleep(5)  # Brief pause before retrying
                continue

    def set_local_timezone(self, user_entity):
        """Set local_timezone from user_entity, fallback to system timezone if not available."""
        self.timezone_manager.set_local_timezone(user_entity)

    async def db_to_server(self, user_entity: UserEntity):
        logger.info("Running db to server")
        if getattr(sys, 'frozen', False) and platform.system() == "Linux":
            script_path = Path(sys.executable).resolve()
            target_directory = script_path.parents[0]
            database_dir = target_directory / "ws_monitor.db"
        elif getattr(sys, 'frozen', False) and platform.system() == "Windows":
            script_path = Path(sys.executable).resolve()
            target_directory = script_path.parents[0]
            database_dir = target_directory / "ws_monitor.db"
        elif getattr(sys, 'frozen', False) and sys.platform == "darwin":
            database_dir = Path.home() / "Library/Application Support/Workstatus/ws_monitor.db"
        else:
            return
        timestamp_db = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        dt_obj = datetime.datetime.strptime(timestamp_db, "%Y-%m-%d_%H-%M-%S")
        unix_timestamp = int(time.mktime(dt_obj.timetuple()))
        database_server = f"database/{user_entity.org_id}/{user_entity.user_id}/{unix_timestamp}.db"

        if not database_dir.exists():
            logger.error("Database file not found for upload: %s", database_dir)
            return

        try:
            # Use safe file copy that coordinates with database locks
            temp_file = None

            # Create copy under database write lock to ensure consistency
            with db_lock_manager.write_lock(timeout=60.0):
                # Wait for any pending transactions
                await asyncio.sleep(0.2)  # Allow any pending operations to complete
                
                # Create temporary copy safely
                temp_file = None
                temp_file_path = None
                try:
                    temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.db')
                    temp_file_path = temp_file.name
                    temp_file.close()
                    
                    logger.info("Creating safe database copy for upload: %s", temp_file_path)
                    shutil.copy2(str(database_dir), temp_file_path)
                    
                    # Verify copy integrity
                    original_size = database_dir.stat().st_size
                    copy_size = Path(temp_file_path).stat().st_size
                    
                    if original_size != copy_size:
                        logger.error("Database copy size mismatch: %s vs %s", original_size, copy_size)
                        if temp_file_path and os.path.exists(temp_file_path):
                            try:
                                os.unlink(temp_file_path)
                            except Exception:
                                pass
                        return
                    
                except Exception as copy_error:
                    logger.error("Error creating database copy: %s", copy_error)
                    if temp_file_path and os.path.exists(temp_file_path):
                        try:
                            os.unlink(temp_file_path)
                        except Exception:
                            pass
                    return
            
            # Upload the copy (outside the lock to avoid blocking database operations)
            if temp_file_path and os.path.exists(temp_file_path):
                try:
                    result = await self.screenshot_controller.screenshot_service.sync(
                        temp_file_path,
                        database_server,
                        user_entity
                    )
                    
                    # Clean up temp file after upload
                    try:
                        os.unlink(temp_file_path)
                        logger.debug("Cleaned up temporary database copy: %s", temp_file_path)
                    except Exception as e:
                        logger.warning("Failed to clean up temp file: %s", e)
                    
                    if self.screenshot_controller.is_link(result):
                        logger.info("Database uploaded successfully: %s", result)
                    else:
                        logger.error("Database upload failed: %s", result)
                except Exception as upload_error:
                    logger.error("Error uploading database copy: %s", upload_error)
                    # Clean up temp file on upload error
                    try:
                        if os.path.exists(temp_file.name):
                            os.unlink(temp_file.name)
                    except Exception:
                        pass
            else:
                logger.error("Temporary database file not available for upload")
                    
        except ValidationError as e:
            logger.error("Screenshot sync validation error for database: %s", e)
            logger.error("Context: file=%s, dest=%s", database_dir, database_server)
            # Continue processing - don't fail the entire operation
        except ScreenshotError as e:
            logger.error("Screenshot sync error for database: %s", e)
            logger.error("Context: file=%s, dest=%s", database_dir, database_server)
            # Continue processing - don't fail the entire operation
        except NetworkError as e:
            logger.error("Network error during screenshot sync for database: %s", e)
            logger.error("Context: file=%s, dest=%s", database_dir, database_server)
            # Continue processing - don't fail the entire operation
        except Exception as e:
            logger.error("Error in database upload: %s", e, exc_info=True)
            # Continue processing - don't fail the entire operation
    
    async def getting_unsent_interval(self, user_entity: UserEntity):
        """Return unsent (``is_uploaded=False``) non-break activity intervals eligible for upload.

        Eligible intervals are those that belong to a session OTHER than the
        currently active one.  Two cases are covered:

        1. **Previous-day intervals** — any date before today.
        2. **Same-day previous-session intervals** — today's date but a counter
           value strictly less than the current session counter.  These are
           intervals from a tracking session that ended earlier today (e.g.,
           user tracked 9–10 am, stopped, restarted at 11 am).  The original
           query excluded today entirely, permanently orphaning these records.

        Intervals from the current session (``counter == current_session_counter``)
        are intentionally excluded — the active push loop handles those.
        Break intervals (``is_break != 0``) are excluded — they are handled by
        ``process_break_interval``.

        Results are grouped by date + counter so we never mix sessions in a
        single payload.  We return the oldest eligible group first.
        """
        DB_OPERATION_TIMEOUT = 5.0

        logger.info("Running unsent intervals check (excluding current session and break intervals)")
        today = date.today()
        current_counter = self.last_count + 1

        with db_lock_manager.read_lock():
            async with async_session() as session:
                try:
                    # Find the oldest eligible interval.
                    # Eligible = (any previous day) OR (today but earlier session counter).
                    first_entry = await asyncio.wait_for(
                        session.scalar(
                            select(ActivityInterval)
                            .where(ActivityInterval.is_uploaded.is_(False))
                            .where(ActivityInterval.is_break == 0)
                            .where(
                                or_(
                                    func.date(ActivityInterval.start_time) != today,
                                    ActivityInterval.counter < current_counter,
                                )
                            )
                            .order_by(ActivityInterval.start_time.asc())
                            .limit(1)
                        ),
                        timeout=DB_OPERATION_TIMEOUT,
                    )

                    if not first_entry:
                        logger.info("No unsent non-break intervals found from previous sessions")
                        return []

                    first_date = first_entry.start_time.date()
                    first_entry_counter = getattr(first_entry, 'counter', None)
                    logger.info(
                        "Found unsent non-break intervals from %s (counter=%s)",
                        first_date, first_entry_counter,
                    )

                    # Fetch all intervals for that date+counter group.
                    query = (
                        select(ActivityInterval)
                        .where(ActivityInterval.is_uploaded.is_(False))
                        .where(ActivityInterval.is_break == 0)
                        .where(func.date(ActivityInterval.start_time) == first_date)
                        .order_by(ActivityInterval.start_time.asc())
                    )
                    if first_entry_counter is not None:
                        query = query.where(ActivityInterval.counter == first_entry_counter)
                        logger.info(
                            "Filtering by counter=%s to keep session intervals together",
                            first_entry_counter,
                        )

                    result = await asyncio.wait_for(
                        session.scalars(query),
                        timeout=DB_OPERATION_TIMEOUT,
                    )
                    intervals = result.all()
                    logger.info(
                        "Retrieved %d unsent non-break interval(s) from %s (counter=%s)",
                        len(intervals), first_date, first_entry_counter,
                    )
                    return intervals

                except asyncio.TimeoutError:
                    logger.error(
                        "Database query timed out after %ss in getting_unsent_interval",
                        DB_OPERATION_TIMEOUT,
                    )
                    return []
                except Exception as e:
                    logger.error("Error getting unsent intervals: %s", e)
                    return []

    async def sending_unsent_intervals(self, start, prev_day_unsent, user_entity: UserEntity):
        logger.info("Sending previous day unsent intervals")
        try:
            # Handle both single entity and list/iterable
            if not prev_day_unsent:
                logger.info("No unsent intervals to send")
                return
            
            # Convert single entity to list if needed
            if not isinstance(prev_day_unsent, (list, tuple)):
                prev_day_unsent = [prev_day_unsent]
            elif len(prev_day_unsent) == 0:
                logger.info("No unsent intervals to send")
                return
            
            payload = []
            for interval in prev_day_unsent:            
                # convert into dict payload
                # Handle both database model (has id) and entity (might not have id)
                interval_id = getattr(interval, 'id', None)
                if interval_id is None:
                    logger.warning("Interval missing id attribute: %s, skipping...", interval)
                    continue
                
                interval_payload = {
                    "Entry": getattr(interval, 'id', None) or 0,
                    "from": interval.start_time.strftime("%Y-%m-%d %H:%M:%S"),
                    "to": interval.end_time.strftime("%Y-%m-%d %H:%M:%S"),
                    "screenUrl": await self.ss(interval.start_time, interval.end_time, user_entity),
                    "activityLevel": self._get_activity_level_from_interval(interval),
                    "appAndUrls": await self.activity_service.get_apps_urls(
                        interval.start_time,
                        interval.end_time,
                        self.appandurlstatus
                    ),
                    "deviceType": self.get_os_type_caps(),
                    "os_type": self.get_os_type_caps(),
                    "note": None,
                    "idle_note_url": None,
                    "idleInterval": None,
                    "location": LocationEntity(long="null", lat="null").model_dump(),
                    "interval_time_db": int(self.interval / 60),
                    "has_on_demand_screenshot": getattr(interval, "has_on_demand_screenshot", False),
                }
                payload.append(interval_payload)

            # Use current project/task IDs if set, otherwise fallback to defaults
            project_id = self.current_project_id if self.current_project_id else "1001"
            task_id = self.current_task_id if self.current_task_id else "0"
            if not self.current_project_id:
                logger.warning(
                    "No project selected in Python — using fallback project_id=%s. "
                    "Ensure Electron sends set_project_task before start_activity_monitoring.",
                    project_id,
                )
            
            final_payload = [{
                "projectId": project_id,
                "start": start.strftime("%Y-%m-%d %H:%M:%S"),
                "stop": None,
                "selfi_id": None,
                "timezone": self._normalize_timezone(str(self.timezone_manager.local_timezone) if self.timezone_manager.local_timezone else str(self.get_system_timezone())),
                "break_id": None,
                "deviceType": self.get_os_type_caps(),
                "os_type": self.get_os_type_caps(),
                "selfiVerification": True,
                "intervals": payload,
                "time_type": "1",
                "interval_time": int(self.interval / 60),
                "source_type": "4",
                "org_id": str(user_entity.org_id),
                "todo_id": task_id,
                "deviceId": self.system_unique_id,
                "ip_address": str(get_ip()),
                "os_version": get_os_version()
            }]

            logger.info("Generated payload for previous day with %s intervals", len(prev_day_unsent))
            
            # Headers for the activity log api - matching the working curl command
            sd_token = user_entity.sd_token
            org_id = user_entity.org_id
            user_id = user_entity.user_id
            self.activity_service.headers = {
                'SDToken': str(sd_token),
                'OrgID': str(org_id), 
                'UserID': str(user_id),
                'DeviceID': self.system_unique_id,
                'Authorization': f'Bearer {sd_token}',
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'deviceType': self.get_os_type_caps(),
            }
            
            for response in final_payload:
                if not response or not isinstance(response, dict):
                    logger.info("No interval found %s", [response])
                    continue
             
                _response = self.activity_service.push_activity_logs([response])
                logger.info("Response: %s", _response)

                if _response is None:
                    logger.error("No response received from server for unsent intervals")
                    continue

                if _response and _response.get("message") == "Token is Expired":
                    logger.info("Token is Expired")
                    self._send_subscription_expired_popup(_response)
                    self.status = False

                elif _response and (
                    _response.get("planStatus") != "active"
                    or _response.get("isdeleted") == 1
                    or _response.get("isdeactivated") == 1
                ):
                    logger.error("Either plan status Deactivated, user deleted or user deactivated")
                    self._send_subscription_expired_popup(_response)
                    self.status = False

                elif _response and _response.get("planStatus") == "active":
                    # update intervals - use the original intervals from prev_day_unsent
                    for interval in prev_day_unsent:
                        await self.activity_service.update_activity_interval(interval)

                    # update screenshots - use the intervals from the payload
                    for interval_payload in payload:
                        screen_url = interval_payload.get("screenUrl")
                        if screen_url:
                            await self.screenshot_controller.update_screenshots(screen_url, self.last_count + 1)

        except Exception as e:
            logger.error("Cannot send previous day unsent intervals: %s", e)
            logger.error("Exception type: %s", type(e).__name__)

            logger.error("Traceback: %s", traceback.format_exc())

    def log_system_resources(self):
        """Log CPU and memory usage at every interval"""
        try:
            # Get memory usage
            memory_info = self.process.memory_info()
            memory_mb = memory_info.rss / 1024 / 1024
            memory_percent = self.process.memory_percent()
            
            # Get CPU usage
            cpu_percent = self.process.cpu_percent()
            
            # Get system-wide CPU usage (non-blocking to prevent memory leaks)
            system_cpu_percent = psutil.cpu_percent()  # Non-blocking call
            
            # Get thread count
            thread_count = self.process.num_threads()
            
            # Get file descriptors (Unix-like systems)
            try:
                fd_count = self.process.num_fds()
            except (AttributeError, psutil.AccessDenied):
                fd_count = "N/A"
            
            logger.info(
                "[SYSTEM_RESOURCES] Memory: %.2fMB (%.1f%%), CPU: %.1f%% (System: %.1f%%), Threads: %s, FDs: %s",
                memory_mb, memory_percent, cpu_percent, system_cpu_percent, thread_count, fd_count)
            
            self._memory_check_counter += 1

            return {
                'memory_mb': memory_mb,
                'memory_percent': memory_percent,
                'cpu_percent': cpu_percent,
                'system_cpu_percent': system_cpu_percent,
                'thread_count': thread_count,
                'fd_count': fd_count
            }
        except Exception as e:
            logger.error("Error logging system resources: %s", e)
            return None

    def check_memory_usage(self):
        """Check if memory usage is approaching limits"""
        try:
            memory_info = self.process.memory_info()
            memory_mb = memory_info.rss / 1024 / 1024
            
            if memory_mb > self.memory_limit_mb:
                logger.warning("[MEMORY_WARNING] Memory usage high: %sMB (limit: %sMB)", memory_mb, self.memory_limit_mb)
                return True  # Memory warning triggered
            
            return False
        except Exception as e:
            logger.error("Error checking memory usage: %s", e)
            return False

    def _finalize_work_stop_time(
        self,
        last_interval=None,
        intervals=None,
        fallback_end: Optional[datetime.datetime] = None,
    ) -> datetime.datetime:
        """Set work-segment stop from last regular interval end (not processing clock)."""
        candidate: Optional[datetime.datetime] = None
        if last_interval is not None and getattr(last_interval, 'end_time', None):
            candidate = last_interval.end_time
        if intervals:
            for iv in intervals:
                is_break = getattr(iv, 'is_break', 0) or 0
                if is_break not in (0, None, False):
                    continue
                et = getattr(iv, 'end_time', None)
                if et is not None and (candidate is None or et > candidate):
                    candidate = et
        if candidate is None and fallback_end is not None:
            candidate = fallback_end
        if candidate is None:
            candidate = datetime.datetime.now()
        self._last_work_stop_time = candidate.replace(microsecond=0)
        return self._last_work_stop_time

    def _break_start_after_work_stop(self) -> datetime.datetime:
        """Break session must start strictly after work stop (work_stop + 1 second)."""
        work_stop = self._last_work_stop_time or datetime.datetime.now()
        return work_stop.replace(microsecond=0) + datetime.timedelta(seconds=1)

    async def last_api_call(
        self,
        user_entity: UserEntity,
        keep_idle_time: bool = True,
        *,
        close_time: Optional[datetime.datetime] = None,
        payload_project_id: Optional[str] = None,
        payload_task_id: Optional[str] = None,
    ):
        """Save last activity interval to DB (serialized with project switch)."""
        with self._session_close_lock:
            return await self._last_api_call_impl(
                user_entity,
                keep_idle_time=keep_idle_time,
                close_time=close_time,
                payload_project_id=payload_project_id,
                payload_task_id=payload_task_id,
            )

    async def _last_api_call_impl(
        self,
        user_entity: UserEntity,
        keep_idle_time: bool = True,
        *,
        close_time: Optional[datetime.datetime] = None,
        payload_project_id: Optional[str] = None,
        payload_task_id: Optional[str] = None,
    ):
        """Internal implementation of last_api_call (caller must hold _session_close_lock)."""
        try:
            logger.info(
                "=== last_api_call STARTED (keep_idle_time=%s, close_time=%s, project_override=%s) ===",
                keep_idle_time,
                close_time,
                payload_project_id,
            )
            logger.info("User entity: %s", user_entity.user_id if user_entity else 'None')
            logger.info("Getting last activity interval")

            trimmed_partial = None
            if close_time is not None:
                trimmed_partial = await self._trim_open_partial_at_project_switch(close_time)

            # Scope to current session counter so post-gap intervals (reassigned or not)
            # do not become the "last" end when closing after a long sleep gap.
            current_session_counter_for_last = self.last_count + 1
            last_interval_end = await self.activity_service.get_last_activity_interval(
                counter=current_session_counter_for_last
            )
            
            # NEW: Check if the last interval is a sleep interval (long gap with zero activity)
            # If so, don't create a new interval from sleep end to wake-up time
            is_sleep_interval = False
            if last_interval_end:
                try:
                    # Get the actual last interval to check its duration and activity
                    with db_lock_manager.read_lock():
                        async with async_session() as session:

                            query = (
                                select(ActivityInterval)
                                .where(ActivityInterval.counter == current_session_counter_for_last)
                                .order_by(desc(ActivityInterval.id))
                                .limit(1)
                            )
                            result = await session.execute(query)
                            last_interval_obj = result.scalar_one_or_none()
                            
                            if last_interval_obj and last_interval_obj.end_time == last_interval_end:
                                gap_seconds = (last_interval_obj.end_time - last_interval_obj.start_time).total_seconds()
                                # Check if it's a long interval (>= 1 hour) with zero activity (sleep pattern)
                                if gap_seconds >= 3600:  # 1 hour
                                    activity_level = last_interval_obj.activityLevel
                                    if activity_level:
                                        keyboard = float(activity_level.get('keyboard', 0) or 0)
                                        mouse = float(activity_level.get('mouse', 0) or 0)
                                        average = float(activity_level.get('average', 0) or 0)
                                        if keyboard == 0.0 and mouse == 0.0 and average == 0.0:
                                            is_sleep_interval = True
                                            logger.info("[SleepInterval] Last interval is a sleep interval (%s hours) - skipping last interval creation to prevent sending wake-up time as 'stop'", gap_seconds/3600)
                except Exception as e:
                    logger.debug("Could not check if last interval is sleep interval: %s", e)
                    # Continue with normal flow if check fails (fail-safe)
            
            end_date = (close_time or datetime.datetime.now()).replace(microsecond=0)

            # Ensure start_date always exists (prevents UnboundLocalError on sleep-gap skip)
            start_date: datetime.datetime | None = None
            last_interval = None
            last_interval_id = None
            trimmed_partial_used = False

            if trimmed_partial is not None:
                last_interval = trimmed_partial
                last_interval_id = getattr(trimmed_partial, "id", None)
                time_diff_seconds = 0
                trimmed_partial_used = True
                logger.info(
                    "Project switch close: using trimmed partial %s to %s (id=%s) — "
                    "not creating a duplicate closing interval",
                    trimmed_partial.start_time,
                    trimmed_partial.end_time,
                    last_interval_id,
                )

            # If last interval is a sleep interval, skip creating a new interval
            elif is_sleep_interval:
                logger.info("[SleepInterval] Skipping last interval creation - last interval was a sleep interval ending at %s", last_interval_end)
                last_interval = None
                last_interval_id = None
                # Skip the interval creation logic and go directly to sending unsent intervals
                # Set time_diff_seconds to 0 to skip the interval creation block
                time_diff_seconds = 0
            else:
                # Determine start_date for the last interval
                if last_interval_end is None:
                    # No previous interval exists - use session start_date
                    if self.start_date:
                        start_date = self.start_date
                        logger.info("No previous interval found, using session start_date: %s", start_date)
                    else:
                        # Fallback: use current time minus 1 minute (shouldn't happen, but safety check)
                        start_date = end_date - datetime.timedelta(minutes=1)
                        logger.warning("No previous interval and no session start_date - using fallback: %s", start_date)
                else:
                    # Use last interval's end_time as start_date for final interval
                    start_date = last_interval_end
                    logger.info("Using last interval end_time as start_date: %s", start_date)
                
                # Ensure start_date is valid and before end_date
                if not start_date:
                    logger.error("start_date is None - cannot create last interval. last_interval_end=%s, self.start_date=%s", last_interval_end, self.start_date)
                    return
                
                # If start_date is equal to or after end_date, adjust start_date to be 1 second before end_date
                if start_date >= end_date:
                    logger.warning("start_date (%s) >= end_date (%s) - adjusting start_date to be 1 second before end_date", start_date, end_date)
                    start_date = end_date - datetime.timedelta(seconds=1)
                
                time_diff_seconds = (end_date - start_date).total_seconds()
                logger.info("Creating last interval: start_date=%s, end_date=%s, status=%s, time_diff=%s seconds", start_date, end_date, self.status, time_diff_seconds)
            
            # Skip creating interval for very small gaps (<= 5 seconds) to avoid unnecessary small intervals
            # These gaps are typically just processing time between last interval save and stop_event call
            if trimmed_partial_used:
                pass
            elif time_diff_seconds > 0 and time_diff_seconds <= 10:
                logger.info("Skipping last interval creation for small gap (%ss) - this is normal processing time, not actual activity", time_diff_seconds)
                last_interval = None
                last_interval_id = None
            # Always create the last interval if we have valid dates and gap is significant
            elif not trimmed_partial_used and time_diff_seconds > 10:
                logger.info("Save last activity interval to DB (from %s to %s - actual stop time, duration: %ss)", start_date, end_date, time_diff_seconds)
                # CRITICAL FIX FOR WINDOWS: Check if break has started (break_id is set)
                # If break has started and interval starts before break start, save the regular part with is_uploaded=False
                # so it can be uploaded by break_start_event
                current_break_id = getattr(self, 'break_id', 0) or 0
                break_start_time = getattr(self, 'break_start_time', None)
                
                # Determine if this interval should be marked as uploaded or not
                should_upload = False
                interval_end_date = end_date  # Default to original end_date
                
                if current_break_id != 0 and break_start_time:
                    # Break has started - check if this interval overlaps with pre-break time
                    if start_date < break_start_time:
                        # Interval starts before break - save only the part before break start as regular interval
                        # This ensures the regular part is uploaded, and break part is handled separately
                        interval_end_date = break_start_time
                        should_upload = True
                        logger.info("[Windows Break Fix] Break has started (break_id=%s, break_start_time=%s)", current_break_id, break_start_time)
                        logger.info("[Windows Break Fix] Interval starts at %s (before break start) - saving regular part from %s to %s with is_uploaded=False", start_date, start_date, interval_end_date)
                        logger.info("[Windows Break Fix] Part from %s to %s will be handled as break interval", break_start_time, end_date)
                    elif end_date <= break_start_time:
                        # Interval ends before or at break start - it's a regular interval that should be uploaded
                        should_upload = True
                        logger.info("[Windows Break Fix] Break has started (break_id=%s, break_start_time=%s)", current_break_id, break_start_time)
                        logger.info("[Windows Break Fix] Interval ends at %s (before break start) - saving with is_uploaded=False so it can be uploaded", end_date)
                    else:
                        # Interval starts at or after break start - it's a break interval (shouldn't happen in last_api_call, but handle it)
                        logger.warning("[Windows Break Fix] Interval starts at %s >= break_start_time %s - this shouldn't happen in last_api_call", start_date, break_start_time)
                
                # Get actual activity level/percentage for the interval being saved (start_date to interval_end_date)
                # This retrieves activity percentage from activity_percentage table or cache
                # Must be done AFTER break handling to ensure we get activity for the correct time range
                try:
                    activityLevel = await self.get_activity_level_from_db(start_date, interval_end_date)
                    logger.info("Retrieved activity level for last interval (%s to %s): mouse=%s, keyboard=%s, average=%s", start_date, interval_end_date, activityLevel.get('mouse', '0.0'), activityLevel.get('keyboard', '0.0'), activityLevel.get('average', '0.0'))
                except Exception as e:
                    logger.error("Error retrieving activity level for last interval: %s", e, exc_info=True)
                    # Fallback to zeros if retrieval fails
                    activityLevel = {'mouse': '0.0', 'average': '0.0', 'keyboard': '0.0'}
                    logger.warning("Using fallback activity level (zeros) for last interval due to error")
                
                # Last API call is never a break, so break_id=0 (will be saved as is_break=0)
                # Set is_idle: True if popup is active AND interval has zero activity (both activityLevel and activity_percentage)
                popup_is_active = self._idle_popup_active and self._idle_popup_triggered_at is not None
                has_zero_activity = await self._has_zero_activity_for_interval(start_date, interval_end_date)
                is_idle = popup_is_active and has_zero_activity
                
                # Ensure interval_end_date is valid (after start_date)
                if interval_end_date <= start_date:
                    logger.warning("[Windows Break Fix] interval_end_date (%s) <= start_date (%s) - skipping interval creation", interval_end_date, start_date)
                    last_interval = None
                    last_interval_id = None
                elif should_upload:
                    # Save with is_uploaded=False so it can be uploaded by break_start_event
                    last_interval = await self.activity_service.save_activity_log(activityLevel, start_date, interval_end_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id, is_idle=is_idle, has_on_demand_screenshot=self.has_on_demand)
                    logger.info("[Windows Break Fix] Last interval saved with is_uploaded=False (will be uploaded by break_start_event): start_time=%s, end_time=%s, is_break=0", last_interval.start_time if last_interval else 'None', last_interval.end_time if last_interval else 'None')
                elif not keep_idle_time:
                    # CRITICAL FIX: If keep_idle_time is False, we must discard this interval (not upload it)
                    # We save it with is_uploaded=True so it's marked as handled/uploaded locally, but won't be sent to server
                    # We also ensure it's NOT added to the manual send list later
                    last_interval = await self.activity_service.save_first_activity_log(activityLevel, start_date, interval_end_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id, is_idle=is_idle, has_on_demand_screenshot=self.has_on_demand)
                    logger.info("Last interval saved with is_uploaded=True (keep_idle_time=False): start_time=%s, end_time=%s, is_break=0", last_interval.start_time if last_interval else 'None', last_interval.end_time if last_interval else 'None')
                else:
                    # Normal case (stop button click): save with is_uploaded=False so the last partial interval
                    # (e.g. 10:50-10:54) is included in the unsent query and synced to the server.
                    # Do NOT use save_first_activity_log here - that marks is_uploaded=True and the interval would never be sent.
                    last_interval = await self.activity_service.save_activity_log(activityLevel, start_date, interval_end_date, self.last_count+1, self.get_today_weekday(), break_id=current_break_id, is_idle=is_idle, has_on_demand_screenshot=self.has_on_demand)
                    logger.info("Last interval saved with is_uploaded=False (stop button - will be synced): start_time=%s, end_time=%s, is_break=0", last_interval.start_time if last_interval else 'None', last_interval.end_time if last_interval else 'None')
                logger.info("Last interval saved: start_time=%s, end_time=%s, is_uploaded=%s, counter=%s", last_interval.start_time if last_interval else 'None', last_interval.end_time if last_interval else 'None', last_interval.is_uploaded if last_interval else 'None', last_interval.counter if last_interval else 'None')
                self.has_on_demand = False
                # Flush immediately to ensure last interval is committed before sending to server
                try:
                    # Check if there are items in the queue before processing
                    activity_queue_size = db_write_queue.get_queue_size(WriteOperationType.ACTIVITY_INTERVAL)
                    total_queue_size = db_write_queue.get_total_queue_size()
                    logger.info("Queue status before flush: ACTIVITY_INTERVAL=%s, Total=%s", activity_queue_size, total_queue_size)
                    
                    if total_queue_size > 0:
                        logger.info("Processing %s pending queue items immediately...", total_queue_size)
                        processed_count = await db_writer_service.process_immediately()
                        logger.info("Flushed %s queued database writes - last interval committed to database", processed_count)
                    else:
                        logger.info("No items in queue - last interval already processed")
                    
                    # Give database time to ensure commit is fully visible (1 second delay)
                    logger.info("Waiting 1 second for database commit to be fully visible...")
                    await asyncio.sleep(1.0)
                    
                    # CRITICAL: Verify the last interval is actually in the database with an ID
                    # This ensures the interval exists before we try to query for it
                    if last_interval:
                        max_verification_retries = 5
                        verification_delay = 0.5
                        interval_verified = False
                        
                        for verify_attempt in range(max_verification_retries):
                            try:
                                with db_lock_manager.read_lock():
                                    async with async_session() as session:
                                        # Query for the interval by matching start_time, end_time, and counter
                                        query = select(ActivityInterval).where(
                                            ActivityInterval.start_time == last_interval.start_time,
                                            ActivityInterval.end_time == last_interval.end_time,
                                            ActivityInterval.counter == last_interval.counter
                                        ).order_by(ActivityInterval.id.desc()).limit(1)
                                        
                                        result = await session.execute(query)
                                        db_interval = result.scalar_one_or_none()
                                        
                                        if db_interval and db_interval.id:
                                            # Found the interval in database with an ID - store it separately
                                            last_interval_id = db_interval.id
                                            interval_verified = True
                                            logger.info("✓ Verified last interval in database: id=%s, start_time=%s, end_time=%s, counter=%s", db_interval.id, db_interval.start_time, db_interval.end_time, db_interval.counter)
                                            break
                            except Exception as verify_error:
                                logger.warning("Error verifying last interval in database (attempt %s/%s): %s", verify_attempt + 1, max_verification_retries, verify_error)
                            
                            if verify_attempt < max_verification_retries - 1:
                                await asyncio.sleep(verification_delay)
                        
                        if not interval_verified:
                            logger.error("⚠ CRITICAL: Last interval not found in database after %s attempts. Interval may not be committed!", max_verification_retries)
                            logger.error("   Last interval details: start_time=%s, end_time=%s, counter=%s", last_interval.start_time, last_interval.end_time, last_interval.counter)
                        else:
                            logger.info("✓ Last interval verified and has database ID: %s", last_interval_id)
                            
                except Exception as e:
                    logger.error("Error flushing database write queue after last interval: %s", e, exc_info=True)
            else:
                # time_diff <= 0 or was skipped for small gap
                if time_diff_seconds <= 0:
                    logger.error("Cannot create last interval: start_date=%s, end_date=%s, time_diff=%ss (time_diff <= 0)", start_date, end_date, time_diff_seconds)
                # else: already logged skip message above            
            # Additional flush before querying to ensure last interval is definitely committed
            # Check queue again and process if needed
            logger.info("Checking queue status before querying intervals...")
            try:
                # Check queue status
                activity_queue_size = db_write_queue.get_queue_size(WriteOperationType.ACTIVITY_INTERVAL)
                total_queue_size = db_write_queue.get_total_queue_size()
                logger.info("Queue status before final flush: ACTIVITY_INTERVAL=%s, Total=%s", activity_queue_size, total_queue_size)
                
                if total_queue_size > 0:
                    logger.info("Processing %s remaining queue items immediately before querying...", total_queue_size)
                    processed_count = await db_writer_service.process_immediately()
                    logger.info("Flushed %s queued database writes, last interval should now be committed", processed_count)
                else:
                    logger.info("Queue is empty - all items already processed")
                
                # Give database more time to ensure commit is visible (1.5 seconds)
                logger.info("Waiting 1.5 seconds for database commit to be fully visible before querying...")
                await asyncio.sleep(1.5)
            except Exception as e:
                logger.error("Error flushing database write queue before fetch: %s", e, exc_info=True)
            
            logger.info("Collecting and sending unsend activity intervals to server in last API call")
            # CRITICAL: Filter by current session counter to prevent picking intervals from previous sessions
            current_session_counter = self.last_count + 1
            logger.info("Filtering intervals by current session counter=%s", current_session_counter)
            
            # Query all unsent intervals from current session only (counter filter applied at database level)
            intervals = await self.activity_service.get_activity_intervals(None, None, counter=current_session_counter)
            logger.info("Found %s unsent intervals from current session (counter=%s)", len(intervals) if intervals else 0, current_session_counter)
            
            # If we just saved a last interval, ensure it's included in the results with retry logic
            # Retry up to 3 times with increasing delays if the interval isn't found
            # CRITICAL: Only include last_interval if keep_idle_time is True (or if it was a break interval that needed upload)
            if last_interval and (keep_idle_time or should_upload):
                max_retries = 3
                retry_delays = [1.0, 1.5, 2.0]  # Increasing delays: 1s, 1.5s, 2s
                last_interval_found = False
                
                # Helper function to compare datetimes with tolerance (1 second) to handle precision differences
                def datetime_matches(dt1, dt2, tolerance_seconds=1.0):
                    """Check if two datetimes match within tolerance"""
                    if dt1 is None or dt2 is None:
                        return False
                    try:
                        time_diff = abs((dt1 - dt2).total_seconds())
                        return time_diff <= tolerance_seconds
                    except Exception:
                        return False
                
                for attempt in range(max_retries):
                    if intervals:
                        # Check if last_interval is in the results using tolerance-based comparison
                        # This handles datetime precision differences between saved and queried intervals
                        last_interval_found = any(
                            datetime_matches(getattr(interval, 'start_time', None), last_interval.start_time) and 
                            datetime_matches(getattr(interval, 'end_time', None), last_interval.end_time) and
                            getattr(interval, 'counter', None) == getattr(last_interval, 'counter', None)
                            for interval in intervals
                        )
                        if last_interval_found:
                            logger.info("Last interval found in query results (attempt %s)", attempt + 1)
                            break
                    
                    if attempt < max_retries - 1:
                        logger.warning("Last interval not found in query results (attempt %s/%s) - waiting %ss and re-querying...", attempt + 1, max_retries, retry_delays[attempt])
                        logger.warning("Last interval details: start_time=%s, end_time=%s, counter=%s", last_interval.start_time, last_interval.end_time, last_interval.counter if hasattr(last_interval, 'counter') else 'N/A')
                        await asyncio.sleep(retry_delays[attempt])
                        # Re-query with counter filter applied at database level
                        intervals = await self.activity_service.get_activity_intervals(None, None, counter=current_session_counter)
                        logger.info("Re-queried after delay: Found %s intervals from current session (counter=%s)", len(intervals) if intervals else 0, current_session_counter)
                    else:
                        logger.warning("Last interval still not found after %s attempts - will manually include it in results", max_retries)
                        logger.warning("Last interval details: start_time=%s, end_time=%s, counter=%s", last_interval.start_time, last_interval.end_time, last_interval.counter if hasattr(last_interval, 'counter') else 'N/A')
                        # Log all intervals found for debugging
                        if intervals:
                            logger.info("Intervals found in database (first 5):")
                            for idx, interval in enumerate(intervals[:5]):
                                logger.info("  Interval %s: start_time=%s, end_time=%s, is_uploaded=%s, counter=%s", idx+1, getattr(interval, 'start_time', 'N/A'), getattr(interval, 'end_time', 'N/A'), getattr(interval, 'is_uploaded', 'N/A'), getattr(interval, 'counter', 'N/A'))
                        else:
                            logger.warning("No intervals found in database at all - this is unexpected")
                
                
                # FALLBACK: If last interval still not found after retries, manually include it
                # This ensures the last interval is sent to server even if query doesn't find it due to transaction isolation
                # CRITICAL: Only do this if keep_idle_time=True or should_upload=True (same condition as line 4777)
                # When keep_idle_time=False and should_upload=False, the interval was saved with is_uploaded=True
                # and should NOT be sent to the server
                # ALSO: Do NOT include sleep intervals (long gaps with zero activity) even if they meet the above conditions
                # ALSO: Do NOT include intervals that were explicitly saved with is_uploaded=True
                if not last_interval_found and (keep_idle_time or should_upload):
                    # NEW: First check if interval was saved with is_uploaded=True - if so, don't include it
                    interval_is_uploaded = False
                    if last_interval:
                        try:
                            if hasattr(last_interval, 'is_uploaded'):
                                interval_is_uploaded = bool(last_interval.is_uploaded)
                            elif hasattr(last_interval, 'isUploaded'):
                                interval_is_uploaded = bool(last_interval.isUploaded)
                        except Exception as e:
                            logger.debug("Could not check is_uploaded status for manual inclusion: %s", e)
                    
                    if interval_is_uploaded:
                        logger.info("[SleepInterval] Last interval was saved with is_uploaded=True - will NOT manually include it (interval should not be sent to server)")
                    else:
                        # NEW: Check if this is a sleep interval before manually including it
                        is_sleep_interval_for_manual_check = False
                        if last_interval:
                            try:
                                if hasattr(last_interval, 'start_time') and hasattr(last_interval, 'end_time'):
                                    gap_seconds = (last_interval.end_time - last_interval.start_time).total_seconds()
                                    # Check if it's a long interval (>= 1 hour) with zero activity (sleep pattern)
                                    if gap_seconds >= 3600:  # 1 hour
                                        # Check activity level
                                        activity_level = None
                                        if hasattr(last_interval, 'activityLevel'):
                                            activity_level = last_interval.activityLevel
                                        elif hasattr(last_interval, 'activity_level'):
                                            activity_level = last_interval.activity_level
                                        
                                        if activity_level:
                                            keyboard = float(activity_level.get('keyboard', 0) if isinstance(activity_level, dict) else 0)
                                            mouse = float(activity_level.get('mouse', 0) if isinstance(activity_level, dict) else 0)
                                            average = float(activity_level.get('average', 0) if isinstance(activity_level, dict) else 0)
                                            if keyboard == 0.0 and mouse == 0.0 and average == 0.0:
                                                is_sleep_interval_for_manual_check = True
                                                logger.info("[SleepInterval] Last interval is a sleep interval (%s hours) - will NOT manually include it to prevent sending wake-up time as 'stop'", gap_seconds/3600)
                            except Exception as e:
                                logger.debug("Could not check if last interval is sleep interval for manual inclusion: %s", e)
                                # Continue with normal flow if check fails (fail-safe)
                        
                        if not is_sleep_interval_for_manual_check:
                            logger.warning("Last interval not found in query results - manually including it to ensure it's sent to server")
                            try:
                                # Ensure intervals is a list
                                if intervals is None:
                                    intervals = []
                                
                                # Check if last_interval is already in the list (might have been added by another process)
                                already_in_list = any(
                                    datetime_matches(getattr(interval, 'start_time', None), last_interval.start_time) and 
                                    datetime_matches(getattr(interval, 'end_time', None), last_interval.end_time) and
                                    getattr(interval, 'counter', None) == getattr(last_interval, 'counter', None)
                                    for interval in intervals
                                )
                                
                                if not already_in_list:
                                    # Manually add the last interval to the results
                                    # Convert entity to database model format if needed
                                    if isinstance(last_interval, ActivityInterval):
                                        intervals.append(last_interval)
                                    else:
                                        # If it's an entity, create a mock database model object
                                        # The process_intervals_bulk will handle it correctly
                                        intervals.append(last_interval)
                                    logger.info("✓ Manually added last interval to results - total intervals: %s", len(intervals))
                                else:
                                    logger.info("Last interval already in list (found after manual check)")
                            except Exception as e:
                                logger.error("Error manually including last interval: %s", e, exc_info=True)
                        else:
                            logger.info("[SleepInterval] Skipping manual inclusion of sleep interval - it was saved with is_uploaded=True and should not be sent to server")
                elif not last_interval_found:
                    logger.info("Last interval not found in query results, but keep_idle_time=%s and should_upload=%s - NOT manually including it (interval was saved with is_uploaded=True and should not be sent)", keep_idle_time, should_upload)
            
            
            # Log final status
            if last_interval:
                logger.info("Last saved interval details: start_time=%s, end_time=%s, is_uploaded=%s, counter=%s", last_interval.start_time, last_interval.end_time, last_interval.is_uploaded if hasattr(last_interval, 'is_uploaded') else 'N/A', last_interval.counter if hasattr(last_interval, 'counter') else 'N/A')
                if intervals:
                    # Final check if last interval is in results (using tolerance-based comparison)
                    def datetime_matches_final(dt1, dt2, tolerance_seconds=1.0):
                        if dt1 is None or dt2 is None:
                            return False
                        try:
                            time_diff = abs((dt1 - dt2).total_seconds())
                            return time_diff <= tolerance_seconds
                        except Exception:
                            return False
                    
                    last_interval_in_list = any(
                        datetime_matches_final(getattr(interval, 'start_time', None), last_interval.start_time) and 
                        datetime_matches_final(getattr(interval, 'end_time', None), last_interval.end_time) and
                        getattr(interval, 'counter', None) == getattr(last_interval, 'counter', None)
                        for interval in intervals
                    )
                    if last_interval_in_list:
                        logger.info("✓ Last interval confirmed in query results - will be sent to server")
                    else:
                        logger.warning("⚠ Last interval may not be in query results - but was manually included if needed")
                else:
                    logger.warning("⚠ Query returned 0 intervals - last interval was manually included if needed")

            work_stop = self._finalize_work_stop_time(
                last_interval=last_interval,
                intervals=intervals,
                fallback_end=end_date,
            )
            logger.info("Work segment stop for payload: %s (break will start at %s)", work_stop, work_stop + datetime.timedelta(seconds=1))

            sending_intervals = self.process_intervals_bulk(
                intervals,
                user_entity.org_id,
                user_entity,
                last_interval=last_interval,
                payload_project_id=payload_project_id,
                payload_task_id=payload_task_id,
                force_stop_time=close_time or work_stop,
            )

            self.activity_service.headers = {
                'SDToken': str(user_entity.sd_token),
                'OrgID': str(user_entity.org_id),  
                'UserID': str(user_entity.user_id), 
                'appVersion': str(self.appVersion),
                'Authorization': f'Bearer {user_entity.sd_token}'
            }
            
            # Track if last_interval was updated during batch processing
            last_interval_updated = False
            at_least_one_batch_successful = False
            # Track all interval IDs that were updated during batch processing
            updated_interval_ids = set()
            
            async for response in sending_intervals:
                if not response or not isinstance(response, (list, tuple)):
                    logger.info("No interval found %s", response)
                    continue

                _response = self.activity_service.push_activity_logs(response[0])
                logger.info("Response: %s", _response)
                if _response and _response.get("message") == "Token is Expired":
                    logger.info("Token is Expired")
                    self._send_subscription_expired_popup(_response)
                    self.status = False

                elif _response and (
                    _response.get("planStatus") != "active"
                    or _response.get("isdeleted") == 1
                    or _response.get("isdeactivated") == 1
                ):
                    logger.error("Either plan status Deactivated use deleted or user deactivated")
                    self._send_subscription_expired_popup(_response)
                    self.status = False
                    
                elif _response and _response.get("planStatus") == "active":
                    at_least_one_batch_successful = True
                    
                    intervals_to_update = []
                    for interval in response[1]:
                        # If interval already has an ID, use it directly
                        if hasattr(interval, 'id') and interval.id:
                            intervals_to_update.append(interval)
                            updated_interval_ids.add(interval.id)
                        else:
                            # Query database to find the actual record with ID
                            try:
                                with db_lock_manager.read_lock():
                                    async with async_session() as session:
                                        # Match by start_time, end_time, and counter
                                        query = select(ActivityInterval).where(
                                            ActivityInterval.start_time == interval.start_time,
                                            ActivityInterval.end_time == interval.end_time,
                                            ActivityInterval.counter == getattr(interval, 'counter', current_session_counter)
                                        ).order_by(ActivityInterval.id.desc()).limit(1)
                                        
                                        result = await session.execute(query)
                                        db_interval = result.scalar_one_or_none()
                                        
                                        if db_interval and db_interval.id:
                                            intervals_to_update.append(db_interval)
                                            updated_interval_ids.add(db_interval.id)
                                            logger.debug("Found database record for interval: id=%s, start_time=%s, end_time=%s", db_interval.id, db_interval.start_time, db_interval.end_time)
                                        else:
                                            logger.warning("Could not find database record for interval: start_time=%s, end_time=%s, counter=%s", interval.start_time, interval.end_time, getattr(interval, 'counter', 'N/A'))
                            except Exception as e:
                                logger.error("Error querying database for interval: %s", e, exc_info=True)
                                # Fallback: try to update anyway if it has an ID
                                if hasattr(interval, 'id') and interval.id:
                                    intervals_to_update.append(interval)
                    
                    # Update all intervals that have IDs
                    for interval in intervals_to_update:
                        await self.activity_service.update_activity_interval(interval)
                        # Check if this interval is the last_interval
                        if last_interval:
                            # Match by start_time, end_time, and counter
                            if (hasattr(interval, 'start_time') and hasattr(last_interval, 'start_time') and
                                hasattr(interval, 'end_time') and hasattr(last_interval, 'end_time') and
                                hasattr(interval, 'counter') and hasattr(last_interval, 'counter')):
                                if (interval.start_time == last_interval.start_time and
                                    interval.end_time == last_interval.end_time and
                                    interval.counter == last_interval.counter):
                                    last_interval_updated = True
                                    # Update last_interval_id with the actual database ID
                                    if hasattr(interval, 'id') and interval.id:
                                        last_interval_id = interval.id
                                    logger.info("Last interval (id=%s) was updated in this batch", getattr(interval, 'id', 'N/A'))
                    
                    # Flush the database write queue to ensure is_uploaded=1 is committed before shutdown
                    logger.info("Flushing database write queue to commit interval updates...")
                    try:
                        processed_count = await db_writer_service.process_immediately()
                        logger.info("Successfully processed %s queued database updates", processed_count)
                        
                        # Verify updates are committed and visible (prevents duplicate sends in next session)
                        if processed_count > 0 and response[1]:
                            # Small delay to ensure SQLite transaction is visible
                            await asyncio.sleep(0.3)
                            logger.debug("Verified interval updates are committed to database")
                    except Exception as e:
                        logger.error("Error flushing database write queue: %s", e, exc_info=True)
                    
                    # CRITICAL: Ensure last interval is marked is_uploaded before update_screenshots, so its
                    # screenshots can be deleted (validation requires is_uploaded=True). Use tolerance query
                    # in case DB datetime precision differs from in-memory entity.
                    if last_interval and not last_interval_updated:
                        try:
                            st = getattr(last_interval, 'start_time', None)
                            et = getattr(last_interval, 'end_time', None)
                            if st is not None and et is not None:
                                tol = datetime.timedelta(seconds=2)
                                with db_lock_manager.read_lock():
                                    async with async_session() as session:
                                        q = select(ActivityInterval).where(
                                            ActivityInterval.counter == getattr(last_interval, 'counter', current_session_counter),
                                            ActivityInterval.start_time >= st - tol,
                                            ActivityInterval.start_time <= st + tol,
                                            ActivityInterval.end_time >= et - tol,
                                            ActivityInterval.end_time <= et + tol,
                                        ).order_by(ActivityInterval.id.desc()).limit(1)
                                        res = await session.execute(q)
                                        db_last = res.scalar_one_or_none()
                                        if db_last and db_last.id:
                                            await self.activity_service.update_activity_interval(db_last)
                                            last_interval_updated = True
                                            logger.info("Last interval (id=%s) found with tolerance and marked uploaded before screenshot delete", db_last.id)
                                if last_interval_updated:
                                    await db_writer_service.process_immediately()
                                    await asyncio.sleep(0.3)
                        except Exception as e:
                            logger.warning("Could not pre-update last interval for screenshot delete: %s", e)
                    
                    for interval_payload in response[0]:
                        for interval in interval_payload.get('intervals', []):  # Ensure 'intervals' exists
                            screen_url = interval.get("screenUrl")  # Access the screenUrl from each interval payload
                            await self.screenshot_controller.update_screenshots(screen_url, self.last_count+1)
            
            # After all batches are processed, explicitly update last_interval if it wasn't updated yet
            # This ensures the last saved interval gets is_uploaded=True even if it wasn't in any response[1]
            if last_interval and at_least_one_batch_successful and not last_interval_updated:
                logger.info("Last interval was not updated in any batch - explicitly updating it now")
                # Verify last_interval was actually sent by checking if it's in the original intervals list
                last_interval_was_sent = any(
                    (hasattr(interval, 'start_time') and hasattr(last_interval, 'start_time') and
                     hasattr(interval, 'end_time') and hasattr(last_interval, 'end_time') and
                     hasattr(interval, 'counter') and hasattr(last_interval, 'counter') and
                     interval.start_time == last_interval.start_time and
                     interval.end_time == last_interval.end_time and
                     interval.counter == last_interval.counter)
                    for interval in intervals
                ) if intervals else False
                
                if last_interval_was_sent:
                    # Query database to get the actual record with ID (use tolerance for datetime precision)
                    try:
                        st = getattr(last_interval, 'start_time', None)
                        et = getattr(last_interval, 'end_time', None)
                        if st is None or et is None:
                            db_interval = None
                        else:
                            tol = datetime.timedelta(seconds=2)
                            with db_lock_manager.read_lock():
                                async with async_session() as session:
                                    query = select(ActivityInterval).where(
                                        ActivityInterval.counter == getattr(last_interval, 'counter', current_session_counter),
                                        ActivityInterval.start_time >= st - tol,
                                        ActivityInterval.start_time <= st + tol,
                                        ActivityInterval.end_time >= et - tol,
                                        ActivityInterval.end_time <= et + tol,
                                    ).order_by(ActivityInterval.id.desc()).limit(1)
                                    result = await session.execute(query)
                                    db_interval = result.scalar_one_or_none()
                        if db_interval and db_interval.id:
                            await self.activity_service.update_activity_interval(db_interval)
                            logger.info("Last interval (id=%s) explicitly updated after successful send", db_interval.id)
                            last_interval_id = db_interval.id
                        elif st is not None and et is not None:
                            logger.warning("Could not find database record for last interval to update")
                    except Exception as e:
                        logger.error("Error querying database for last interval: %s", e, exc_info=True)
                        # Fallback: try to update if we have an ID
                        if last_interval_id:
                            # Query database to get the interval object with ID
                            try:
                                async with async_session() as session:
                                    query = select(ActivityInterval).where(ActivityInterval.id == last_interval_id)
                                    result = await session.execute(query)
                                    db_interval = result.scalar_one_or_none()
                                    if db_interval:
                                        await self.activity_service.update_activity_interval(db_interval)
                                        logger.info("Last interval (id=%s) explicitly updated after successful send (fallback)", last_interval_id)
                            except Exception as fallback_error:
                                logger.error("Error in fallback update: %s", fallback_error, exc_info=True)
                    # Flush again to ensure this update is committed
                    try:
                        processed_count = await db_writer_service.process_immediately()
                        logger.info("Flushed %s queued database updates after last interval update", processed_count)
                        # Small delay to ensure SQLite transaction is visible
                        if processed_count > 0:
                            await asyncio.sleep(0.3)
                            logger.debug("Verified last interval update is committed to database")
                    except Exception as e:
                        logger.error("Error flushing database write queue after last interval update: %s", e, exc_info=True)
                    # Delete last interval screenshots now that is_uploaded is committed (they were not deleted in the loop)
                    try:
                        st = getattr(last_interval, 'start_time', None)
                        et = getattr(last_interval, 'end_time', None)
                        if st is not None and et is not None:
                            last_ss_urls = await self.activity_service.get_screenshots(st, et)
                            if last_ss_urls:
                                await self.screenshot_controller.update_screenshots(last_ss_urls, self.last_count + 1)
                                logger.info("Last interval screenshots updated/deleted after explicit interval update (count=%s)", len(last_ss_urls))
                    except Exception as e:
                        logger.warning("Could not delete last interval screenshots after explicit update: %s", e)
                else:
                    logger.warning("Last interval (id=%s) was not in the intervals list - may not have been sent", last_interval_id or 'N/A')
            
            # CRITICAL: Ensure ALL intervals from current session that were successfully sent are marked as uploaded
            # Mark any intervals from current session that weren't updated in the batch processing loop
            if at_least_one_batch_successful and intervals:
                intervals_updated_count = 0
                for interval in intervals:
                    # If interval doesn't have an ID, query database to get it
                    interval_to_update = None
                    if hasattr(interval, 'id') and interval.id:
                        if interval.id not in updated_interval_ids:
                            interval_to_update = interval
                    else:
                        # Query database to get the actual record with ID
                        try:
                            with db_lock_manager.read_lock():
                                async with async_session() as session:
                                    query = select(ActivityInterval).where(
                                        ActivityInterval.start_time == interval.start_time,
                                        ActivityInterval.end_time == interval.end_time,
                                        ActivityInterval.counter == getattr(interval, 'counter', current_session_counter)
                                    ).order_by(ActivityInterval.id.desc()).limit(1)
                                    
                                    result = await session.execute(query)
                                    db_interval = result.scalar_one_or_none()
                                    
                                    if db_interval and db_interval.id:
                                        if db_interval.id not in updated_interval_ids:
                                            interval_to_update = db_interval
                        except Exception as e:
                            logger.error("Error querying database for interval: %s", e, exc_info=True)
                    
                    if interval_to_update:
                        # This interval was sent but not updated yet - mark it as uploaded
                        try:
                            await self.activity_service.update_activity_interval(interval_to_update)
                            intervals_updated_count += 1
                            logger.debug("Marked interval (id=%s, counter=%s) from current session as uploaded", interval_to_update.id, getattr(interval_to_update, 'counter', 'N/A'))
                        except Exception as e:
                            logger.error("Error updating interval (id=%s): %s", getattr(interval_to_update, 'id', 'N/A'), e, exc_info=True)
                
                if intervals_updated_count > 0:
                    logger.info("Marked %s additional intervals from current session (counter=%s) as uploaded", intervals_updated_count, current_session_counter)
                    # Flush the queue to ensure all updates are committed
                    try:
                        processed_count = await db_writer_service.process_immediately()
                        logger.info("Flushed %s queued database updates after marking all current session intervals as uploaded", processed_count)
                        if processed_count > 0:
                            await asyncio.sleep(0.3)
                            logger.debug("Verified all current session interval updates are committed to database")
                    except Exception as e:
                        logger.error("Error flushing database write queue after marking all intervals: %s", e, exc_info=True)
                else:
                    logger.info("All %s intervals from current session (counter=%s) were already marked as uploaded", len(intervals), current_session_counter)
            
            # CRITICAL FINAL STEP: Verify all intervals from current session are marked as uploaded in database
            # This prevents them from being sent again when timer restarts
            if at_least_one_batch_successful and intervals:
                logger.info("Performing final verification: checking all %s intervals from current session are marked as uploaded in database...", len(intervals))
                try:
                    # Wait a bit more to ensure all updates are committed
                    await asyncio.sleep(0.5)
                    
                    with db_lock_manager.read_lock():
                        async with async_session() as session:
                            # Query for all intervals from current session
                            query = select(ActivityInterval).where(
                                ActivityInterval.counter == current_session_counter,
                                ActivityInterval.is_uploaded == False
                            )
                            result = await session.execute(query)
                            unsent_intervals = result.scalars().all()
                            
                            if unsent_intervals:
                                logger.warning("⚠ Found %s intervals from current session (counter=%s) still marked as unsent after upload!", len(unsent_intervals), current_session_counter)
                                for interval in unsent_intervals:
                                    logger.warning("   Unsent interval: id=%s, start_time=%s, end_time=%s", interval.id, interval.start_time, interval.end_time)
                                # Force update them
                                for interval in unsent_intervals:
                                    try:
                                        await self.activity_service.update_activity_interval(interval)
                                        logger.info("Force-marked interval (id=%s) as uploaded", interval.id)
                                    except Exception as e:
                                        logger.error("Error force-updating interval (id=%s): %s", interval.id, e)
                                
                                # Flush one more time
                                processed_count = await db_writer_service.process_immediately()
                                logger.info("Flushed %s queued database updates after force-marking intervals", processed_count)
                                await asyncio.sleep(0.5)
                            else:
                                logger.info("✓ Final verification passed: All intervals from current session (counter=%s) are marked as uploaded", current_session_counter)
                except Exception as e:
                    logger.error("Error in final verification step: %s", e, exc_info=True)
            
            logger.info("=== last_api_call COMPLETED SUCCESSFULLY ===")
        except Exception as e:
            logger.error("=== last_api_call FAILED: %s ===", e, exc_info=True)
            raise  # Re-raise to let caller know it failed

    async def stop_event(self, user_entity: UserEntity, keep_idle_time: bool = True):
        """Stop all activity monitoring and cleanup resources"""
        try:
            logger.info("=== stop_event() called (keep_idle_time=%s) ===", keep_idle_time)

            # On timezone/datetime integrity error: do NOT send last interval to DB/API.
            if (
                getattr(self, '_timezone_mismatch', False)
                and self._timezone_checks_enabled(user_entity)
            ):
                logger.error(
                    "Timezone mismatch detected - SKIPPING last_api_call (no last interval sent to DB/API)"
                )
                return True

            logger.info("Starting last_api_call (normal stop)")

            # Stop screenshot disabled: no screenshot captured on stop button press
            # await self._capture_stop_screenshot(user_entity)

            # CRITICAL: Before stopping, check for and send any remaining break intervals
            # This ensures break intervals are uploaded even if break_stop_event wasn't called explicitly
            try:
                logger.info("Checking for remaining break intervals before stop...")
                
                # CRITICAL: Skip processing break intervals if break_stop_event was called recently
                # This prevents duplicate API calls when both BreakStopEventThread and StopEventThread run
                should_skip_break_processing = False
                if self._last_break_stop_event_time is not None:
                    time_since_break_stop = (datetime.datetime.now() - self._last_break_stop_event_time).total_seconds()
                    if time_since_break_stop < 5:  # If break_stop_event was called within last 5 seconds
                        logger.info("break_stop_event was called %ss ago - skipping duplicate break interval processing to prevent duplicate API calls", time_since_break_stop)
                        should_skip_break_processing = True
                
                if not should_skip_break_processing:
                    # Check if there are any break intervals with is_uploaded=0
                    # Flush queue first to ensure all pending writes are committed
                    await db_writer_service.process_immediately()
                    await asyncio.sleep(0.3)
                    
                    with db_lock_manager.read_lock():
                        async with async_session() as session:
                            query = select(ActivityInterval).where(
                                ActivityInterval.is_uploaded.is_(False),
                                ActivityInterval.is_break != 0
                            )
                            result = await session.execute(query)
                            remaining_break_intervals = result.scalars().all()
                            
                            if remaining_break_intervals and len(remaining_break_intervals) > 0:
                                logger.info("Found %s remaining break interval(s) with is_uploaded=0 - calling break_stop_event to send them", len(remaining_break_intervals))
                                # Set user_entity if not already set
                                if not hasattr(self, 'user_entity') or self.user_entity is None:
                                    self.user_entity = user_entity
                                # Call break_stop_event to send remaining break intervals
                                await self.break_stop_event()
                                logger.info("✓ Remaining break intervals sent via break_stop_event")
                            else:
                                logger.info("No remaining break intervals found - nothing to send")
            except Exception as break_error:
                logger.error("Error checking/sending remaining break intervals: %s", break_error, exc_info=True)
                # Continue with stop_event even if break interval check fails

            # hitting last api - this saves and sends final activity data
            await self.last_api_call(user_entity, keep_idle_time=keep_idle_time)

            logger.info("=== stop_event() completed - last_api_call finished ===")
            return True
        except Exception as e:
            logger.error("Error in stop_event: %s", e, exc_info=True)
            # Don't fail completely - threads are already stopped
            return False

    async def _send_active_app_update_to_electron(self, app_name: str, app_icon: Optional[str] = None):
        """
        Send active app update to Electron for UI display.
        
        Args:
            app_name: Current active application name
            app_icon: Base64 encoded icon (optional)
        """
        try:
            electron_comm = self._get_electron_comm()

            if not is_ipc_ready():
                logger.debug("IPC server not ready - skipping active app update")
                return
            
            if not electron_comm.is_connected():
                logger.debug("IPC not connected - skipping active app update")
                return
            
            # Send active app change message
            message_data = {
                'app_name': app_name,
                'app_icon': app_icon,
                'timestamp': datetime.datetime.now().isoformat()
            }
            
            success = electron_comm.send_to_electron('active_app_change', message_data)
            
            if success:
                logger.debug("Sent active app update to Electron: %s", app_name)
            else:
                logger.debug("Failed to send active app update to Electron: %s", app_name)
                
        except Exception as e:
            logger.error("Error sending active app update to Electron: %s", e, exc_info=True)
    
    async def _send_file_upload_success_to_electron(self, s3_url: str):
        """Sends file upload success message to Electron for UI notification."""
        try:
            electron_comm = self._get_electron_comm()

            if not is_ipc_ready():
                logger.debug("IPC server not ready - skipping file upload success notification")
                return False
            
            if not electron_comm.is_connected():
                logger.debug("IPC not connected - skipping file upload success notification")
                return False
            
            # Send file upload success message
            message_data = {
                'message': 'File uploaded successfully',
                's3_url': s3_url,
                'timestamp': datetime.datetime.now().isoformat()
            }
            
            success = electron_comm.send_to_electron('idle_file_upload_success', message_data)
            
            if success:
                logger.info("[SUCCESS] Sent file upload success message to Electron (URL: %s...)", s3_url[:120])
            else:
                logger.warning("[FAILED] Failed to send file upload success message to Electron")
            return success
        except Exception as e:
            logger.error("Error sending file upload success message to Electron: %s", e, exc_info=True)
            return False
    
    async def _send_file_upload_failure_to_electron(self):
        """Sends file upload failure message to Electron for UI notification."""
        try:
            electron_comm = self._get_electron_comm()

            if not is_ipc_ready():
                logger.debug("IPC server not ready - skipping file upload failure notification")
                return False
            
            if not electron_comm.is_connected():
                logger.debug("IPC not connected - skipping file upload failure notification")
                return False
            
            # Send file upload failure message
            message_data = {
                'message': 'File upload failed',
                'timestamp': datetime.datetime.now().isoformat()
            }
            
            success = electron_comm.send_to_electron('idle_file_upload_failure', message_data)
            
            if success:
                logger.info("[SUCCESS] Sent file upload failure message to Electron")
            else:
                logger.warning("[FAILED] Failed to send file upload failure message to Electron")
            return success
        except Exception as e:
            logger.error("Error sending file upload failure message to Electron: %s", e, exc_info=True)
            return False
    
    async def check_idle_popup_timeout(self) -> bool:
        """
        Check if 60 minutes have passed since idle popup was triggered.
        If timeout reached, auto-stop timer and mark all idle intervals as unkeep.
        
        Returns:
            bool: True if timeout reached and auto-stop triggered, False otherwise
        """
        if is_silent_build():
            return False
        try:
            if not self._idle_popup_active or self._idle_popup_triggered_at is None:
                return False
            
            current_time = datetime.datetime.now()
            elapsed_seconds = (current_time - self._idle_popup_triggered_at).total_seconds()
            
            logger.info("[IdleTimeout] Checking timeout: elapsed=%ss, limit=%ss", int(elapsed_seconds), self._idle_popup_timeout_seconds)
            
            if elapsed_seconds >= self._idle_popup_timeout_seconds:
                logger.warning("[IdleTimeout] ⚠ TIMEOUT REACHED: %ss >= %ss - Auto-stopping timer", int(elapsed_seconds), self._idle_popup_timeout_seconds)
                
                # CRITICAL FIX: Mark ALL intervals created during idle popup period (from _idle_popup_triggered_at to now)
                # as unkeep (is_uploaded=True). This includes both idle intervals AND intervals with activity
                # that occurred while popup was active but not handled
                with db_lock_manager.read_lock():
                    async with async_session() as session:
                        # Query ALL intervals from popup trigger time onwards (not just idle ones)
                        query = select(ActivityInterval).where(
                            and_(
                                ActivityInterval.counter == self.last_count + 1,
                                ActivityInterval.start_time >= self._idle_popup_triggered_at,
                                ActivityInterval.is_uploaded == False
                            )
                        )
                        result = await session.execute(query)
                        all_intervals = result.scalars().all()

                        if all_intervals:
                            interval_ids = []
                            idle_count = 0
                            activity_count = 0
                            for interval in all_intervals:
                                interval_ids.append(interval.id)
                                if interval.is_idle:
                                    idle_count += 1
                                else:
                                    activity_count += 1
                                db_write_queue.enqueue(
                                    WriteOperationType.UPDATE_ACTIVITY_INTERVAL,
                                    interval.id,
                                    {'is_uploaded': True}
                                )
                            
                            logger.warning("[IdleTimeout] Marking %s intervals from popup period as unkeep (is_uploaded=True): %s idle + %s with activity, interval_ids=%s%s", len(interval_ids), idle_count, activity_count, interval_ids, '...' if len(interval_ids) > 10 else '')
                            
                            # Flush immediately
                            await db_writer_service.process_immediately()
                
                # Clear popup state
                self._idle_popup_active = False
                self._idle_popup_triggered_at = None
                self._api_pause_active = False
                # Stop saving new intervals; Electron must send a real stop so
                # monitoring_started / threads are cleared (do NOT set force_stop —
                # that sticks across restart and blocks the next Start).
                self.status = False

                logger.warning("[IdleTimeout] Timer stopped due to unhandled idle popup (60 minutes timeout)")
                
                # Ask Electron to drive the normal stop path (idle_popup_response /
                # stop_activity_monitoring). Breaking the push loop alone is not enough:
                # MonitorThread would restart push within ~60s and leave a half-stopped
                # session that fails the next Start with MONITORING_NOT_STARTED.
                try:
                    electron_comm = self._get_electron_comm()
                    if electron_comm and electron_comm.is_connected():
                        electron_comm.send_to_electron('idle_timeout_auto_stop', {
                            'message': 'Your system has been idle for 60 minutes thus timer has stopped automatically...',
                            'timestamp': current_time.isoformat(),
                        })
                        logger.info("[IdleTimeout] Sent auto-stop signal to Electron")
                except Exception as e:
                    logger.error("[IdleTimeout] Error sending auto-stop signal to Electron: %s", e, exc_info=True)
                
                return True
            
            return False
            
        except Exception as e:
            logger.error("[IdleTimeout] Error checking idle popup timeout: %s", e, exc_info=True)
            return False
    
    async def arise_idle_popup(self):
        """
        Trigger idle popup in Electron app.
        This method sends a message to Electron to display the idle popup.
        Includes debounce mechanism to prevent duplicate triggers and retry logic for TCP disconnections.
        
        NEW REQUIREMENT: 
        - If 2 consecutive idle intervals found, show popup
        - While popup active, block server uploads
        - Check 60-minute timeout for auto-stop
        """
        if is_silent_build():
            return
        try:
            logger.info("[IdlePopup] arise_idle_popup() called - checking conditions before sending trigger to Electron")
            
            # Check 60-minute timeout first
            timeout_reached = await self.check_idle_popup_timeout()
            if timeout_reached:
                logger.warning("[IdlePopup] Timeout reached - auto-stop triggered, skipping popup")
                return

            if self._idle_popup_active and self._idle_popup_triggered_at is not None:
                logger.info(
                    "[IdlePopup] Popup already active (triggered_at=%s) — not sending another trigger",
                    self._idle_popup_triggered_at,
                )
                return
            
            # CRITICAL: Never trigger idle popup when break session is active (away time)
            current_break_id = getattr(self, 'break_id', 0)
            if current_break_id != 0 or self.is_break != 0:
                logger.info("[IdlePopup] Break session is active (break_id=%s, is_break=%s) - skipping idle popup trigger", current_break_id, self.is_break)
                return
            
            # No cooldown: Python sends trigger every time 2+ consecutive idle intervals detected.
            # Frontend suppresses duplicate popups (if popup already visible, ignores new triggers).
            # First popup's response (keep/unkeep/stop/continue) handles ALL consecutive idle intervals.
            
            current_time = datetime.datetime.now()
            electron_comm = self._get_electron_comm()
            
            # Check if IPC server is running
            if not is_ipc_ready():
                logger.error("[IdlePopup] IPC server is not running - cannot send idle popup. IPC server may not have started properly.")
                return
            
            # Retry logic: Try sending with exponential backoff if IPC is not connected
            max_retries = 10  # Increased retries to give more time for connection
            retry_delays = [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0]  # Longer delays, more attempts
            success = False
            
            for attempt in range(max_retries):
                # Check if connected before attempting to send
                is_connected = electron_comm.is_connected()
                
                if not is_connected:
                    if attempt < max_retries - 1:
                        delay = retry_delays[attempt] if attempt < len(retry_delays) else 10.0
                        logger.info("IPC not connected for idle popup (attempt %s/%s), IPC server ready: %s, retrying in %ss...",
                                  attempt + 1, max_retries, is_ipc_ready(), delay)
                        await asyncio.sleep(delay)
                        continue
                    else:
                        logger.warning("IPC not connected after %s attempts - idle popup not sent. IPC server ready: %s",
                                     max_retries, is_ipc_ready())
                        break
                
                # Calculate actual consecutive idle intervals
                consecutive_idle_count = await self._get_consecutive_idle_count()
                logger.info("[IdlePopup] Calculated consecutive idle count: %s", consecutive_idle_count)
                
                # Strictly after 2 idle intervals: only show popup when count >= 2
                if consecutive_idle_count < 2:
                    logger.info("[IdlePopup] Not triggered: consecutive_idle_count=%s (need >= 2)", consecutive_idle_count)
                    return
                
                # Send idle popup trigger message to Electron
                logger.info("[IdlePopup] Triggering popup: consecutive_idle_count=%s, timestamp=%s", consecutive_idle_count, current_time.isoformat())
                success = electron_comm.send_to_electron('idle_popup_trigger', {
                    'remaining_intervals': consecutive_idle_count,
                    'timestamp': current_time.isoformat()
                })
                
                if success:
                    self._idle_popup_active = True  # Mark idle popup as active (for interval is_idle marking)
                    self._idle_popup_triggered_at = current_time  # Track when popup was triggered
                    self._api_pause_active = True  # Block server uploads while popup active
                    
                    logger.info("[IdlePopup] ✓ Trigger sent to Electron (2 consecutive zero-activity intervals) [attempt %s]", attempt + 1)
                    logger.info("[IdlePopup] State: active=True, triggered_at=%s, timeout_limit=%ss", current_time.isoformat(), self._idle_popup_timeout_seconds)
                    logger.info("[IdlePopup] Server uploads BLOCKED until popup handled")
                    
                    # Mark all previous consecutive zero-activity intervals as is_idle=1
                    await self._mark_previous_zero_activity_intervals_as_idle()
                    
                    break
                else:
                    if attempt < max_retries - 1:
                        delay = retry_delays[attempt] if attempt < len(retry_delays) else 10.0
                        logger.info("Failed to send idle popup (attempt %s/%s), IPC connected: %s, retrying in %ss...",
                                  attempt + 1, max_retries, electron_comm.is_connected(), delay)
                        await asyncio.sleep(delay)
                    else:
                        logger.warning(
                            "Failed to send idle popup trigger after %s attempts. "
                            "Final IPC status - Connected: %s, IPC server ready: %s",
                            max_retries, electron_comm.is_connected(), is_ipc_ready())
            
            if not success:
                logger.warning("Idle popup could not be sent - IPC connection unavailable. Will retry on next idle detection if cooldown allows.")
            
        except Exception as e:
            logger.error("Error triggering idle popup: %s", e, exc_info=True)
    
    async def _has_zero_activity_for_interval(self, start_date: datetime.datetime, end_date: datetime.datetime) -> bool:
        """
        Check if an interval has zero activity in both activityLevel and activity_percentage.
        
        Args:
            start_date: Interval start time
            end_date: Interval end time
            
        Returns:
            bool: True if both activityLevel and activity_percentage have zero activity
        """
        try:
            # Get activityLevel
            activityLevel = await self.get_activity_level_from_db(start_date, end_date)
            keyboard = float(activityLevel.get('keyboard', 0) or 0)
            mouse = float(activityLevel.get('mouse', 0) or 0)
            average = float(activityLevel.get('average', 0) or 0)
            has_activity_level_activity = keyboard > 0 or mouse > 0 or average > 0
            
            # Get activity_percentage
            has_activity_percentage_activity = False
            try:
                activity_percentage = await activity_percentage_manager.find_overlapping_activity_percentage(
                    start_date, end_date
                )
                if activity_percentage:
                    has_activity_percentage_activity = (
                        activity_percentage.keyboard > 0 or 
                        activity_percentage.mouse > 0 or 
                        activity_percentage.average > 0
                    )
            except Exception as e:
                logger.debug("Error checking activity_percentage for interval %s to %s: %s", start_date, end_date, e)
            
            # Both must be zero
            return not has_activity_level_activity and not has_activity_percentage_activity
            
        except Exception as e:
            logger.error("Error checking zero activity for interval: %s", e, exc_info=True)
            return False
    
    async def handle_idle_popup_response(self, keep_idle_time: bool, action: str, project_id: Optional[str] = None, task_id: Optional[str] = None, note: Optional[str] = None, file_path: Optional[str] = None):
        """
        Handle idle popup response from Electron.

        CRITICAL: The first popup response (keep/unkeep/stop/continue) applies to ALL consecutive
        idle intervals in the current session. Python sends idle triggers every interval (no cooldown),
        frontend shows only one popup and suppresses duplicates; this single response handles all.
        
        Args:
            keep_idle_time: True if "keep" checkbox is checked, False if unchecked
            action: Action taken ('continue', 'stop', 'reassign')
            project_id: Project ID for reassign action (required if action is 'reassign')
            task_id: Task ID for reassign action (optional, defaults to "0" if not provided)
            note: Optional note provided by user when keeping idle time (max 120 characters)
            file_path: Optional local file path to upload to S3
        """
        try:
            logger.info("[IdleResponse] Handling idle popup response: keep_idle_time=%s, action=%s, project_id=%s, task_id=%s, file_path=%s", keep_idle_time, action, project_id, task_id, file_path)

            await self._persist_idle_popup_intervals_minimum_activity(minimum_percent=1.0)
            
            # Upload file to S3 if file_path is provided
            s3_file_url = None
            file_upload_success = False
            if file_path:
                try:
                    s3_file_url = await self._upload_file_to_s3(file_path)
                    if s3_file_url:
                        logger.info("File uploaded to S3 successfully: %s", s3_file_url)
                        file_upload_success = True
                    else:
                        logger.warning("File upload to S3 failed or returned no URL for file: %s", file_path)
                        # Send failure message to Electron immediately
                        await self._send_file_upload_failure_to_electron()
                except Exception as e:
                    logger.error("Error uploading file to S3: %s", e, exc_info=True)
                    # Send failure message to Electron immediately
                    await self._send_file_upload_failure_to_electron()
                    # Continue without file - note will be saved without file_path
            
            # Save note and/or file URL to the idle intervals
            # We save if there's a note OR if a file was successfully uploaded
            if (note is not None and note.strip()) or s3_file_url:
                # Use empty string for note if only file is provided
                note_text = note if (note is not None and note.strip()) else ""
                
                # Truncate note to maximum 120 characters
                MAX_NOTE_LENGTH = 120
                if len(note_text) > MAX_NOTE_LENGTH:
                    original_length = len(note_text)
                    note_text = note_text[:MAX_NOTE_LENGTH]
                    logger.warning("Note truncated from %s to %s characters", original_length, MAX_NOTE_LENGTH)
                
                await self._save_note_to_idle_intervals(note_text, s3_file_url)
                logger.info("Note/File saved to idle intervals: note_length=%s, has_file=%s", len(note_text), s3_file_url is not None)

            # Send success message to Electron ONLY after database queuing
            if file_upload_success and s3_file_url:
                await self._send_file_upload_success_to_electron(s3_file_url)
                logger.info("File upload success message sent to Electron after database queuing")
            
            # Handle reassign action
            if action == 'reassign':
                if not project_id:
                    logger.error("Reassign action requires project_id - ignoring reassign request")
                    return
                
                # Set current project and task IDs so new intervals after reassign will use these values
                self.set_project_task_ids(project_id, task_id)
                logger.info("Set current project_id=%s, task_id=%s after reassign", self.current_project_id, self.current_task_id)
                
                # Reassign idle intervals to the new project/task
                await self._idle_reassign(project_id, task_id)
                
                # Clear idle popup active flag after handling reassign
                self._idle_popup_active = False
                self._idle_popup_triggered_at = None
                self._api_pause_active = False  # Resume API calls after handling response
                logger.info("Cleared idle popup active flag after handling reassign - API calls will resume")
                return
            
            # NEW: Handle held idle intervals based on keep_idle_time
            # CRITICAL: Only handle held intervals from current counter
            current_counter = self.last_count + 1
            if self._held_idle_intervals:
                # Filter held intervals by current counter
                current_counter_held = [iv for iv in self._held_idle_intervals if getattr(iv, 'counter', None) == current_counter]
                if current_counter_held:
                    if keep_idle_time:
                        # User wants to keep idle time - held intervals will be sent (is_uploaded=False)
                        logger.info("[IdleResponse] User chose to keep idle time - %s held idle interval(s) from current counter (%s) will be sent", len(current_counter_held), current_counter)
                        # Remove current counter intervals from held list (they will be sent in next sync/upload cycle)
                        self._held_idle_intervals = [iv for iv in self._held_idle_intervals if getattr(iv, 'counter', None) != current_counter]
                    else:
                        # User doesn't want to keep idle time - mark held intervals as uploaded (won't be sent)
                        logger.info("[IdleResponse] User chose not to keep idle time - marking %s held idle interval(s) from current counter (%s) as uploaded", len(current_counter_held), current_counter)
                        for interval in current_counter_held:
                            try:
                                interval.is_uploaded = True
                                # Save to database
                                db_writer_service.queue_write(interval)
                            except Exception as e:
                                logger.error("Error marking held idle interval %s as uploaded: %s", interval.id, e, exc_info=True)
                        # Remove current counter intervals from held list
                        self._held_idle_intervals = [iv for iv in self._held_idle_intervals if getattr(iv, 'counter', None) != current_counter]
                
                # Clear any remaining held intervals from previous counters
                if self._held_idle_intervals:
                    logger.info("[IdleResponse] Clearing %s held idle interval(s) from previous counter(s)", len(self._held_idle_intervals))
                    self._held_idle_intervals = []
            
            # Handle continue/stop actions
            # If keep_idle_time is True: keep is_uploaded=false (do nothing, current state is correct)
            # If keep_idle_time is False: set is_uploaded=1 for all consecutive idle intervals

            if keep_idle_time:
                # User checked "keep" box - keep is_uploaded=false (no action needed)
                logger.info("User checked 'keep' box - idle intervals will remain with is_uploaded=False")
                
                # CRITICAL: If user chose to keep idle time and continue, sync immediately
                if action == 'continue':
                    logger.info("Action is 'continue' with keep_idle_time=True - triggering immediate sync")
                    await self.sync_activity_immediately()
            else:
                # User unchecked "keep" box - mark idle/popup-period intervals as uploaded (is_uploaded=1)
                await self._mark_idle_intervals_as_uploaded()
            
            # Clear idle popup active flag after handling response
            logger.info("[IdleResponse] Clearing popup state and resuming normal operation")
            self._idle_popup_active = False
            self._last_idle_resolution_time = datetime.datetime.now()
            self._idle_popup_triggered_at = None
            self._api_pause_active = False  # Resume API calls after handling response
            logger.info("[IdleResponse] State cleared - unsent idle intervals suppressed when keep was unchecked")
            
        except Exception as e:
            logger.error("Error handling idle popup response: %s", e, exc_info=True)

    def handle_idle_popup_shown(self):
        """
        Called when Electron confirms the idle popup was shown to the user.
        No-op since cooldown was removed; frontend suppresses duplicate popups.
        """
        logger.debug("Idle popup shown confirmed by Electron (cooldown removed)")

    async def sync_activity_immediately(self):
        """
        Immediately sync all unuploaded activity intervals to the server.
        Called when user chooses to keep idle time in the popup.
        """
        try:
            logger.info("=== sync_activity_immediately() called ===")
            
            # 1. Flush DB queue to ensure all pending intervals are committed
            await db_writer_service.process_immediately()
            await asyncio.sleep(0.5) # Wait for commit visibility
            
            # 2. Get unuploaded intervals from current session only
            # CRITICAL: Filter by current session counter to prevent picking intervals from previous sessions
            current_session_counter = self.last_count + 1
            logger.info("Filtering intervals by current session counter=%s for immediate sync", current_session_counter)
            intervals = await self.activity_service.get_activity_intervals(self.main_start_date, self.main_end_date, counter=current_session_counter)
            
            if not intervals:
                logger.info("No intervals found to sync immediately")
                return

            intervals = self._filter_intervals_excluding_in_progress_resolving_slot(intervals)

            if not intervals:
                logger.info("No completed intervals to sync immediately")
                return
            
            logger.info("Found %s intervals to sync immediately", len(intervals))
            
            # 3. Process and upload using process_intervals_bulk
            org_id = self.user_entity.org_id
            user_entity = self.user_entity
            
            # process_intervals_bulk is an async generator
            async for response in self.process_intervals_bulk(intervals, org_id, user_entity):
                if not response or not isinstance(response, (list, tuple)) or len(response) < 2:
                    continue
                
                payload = response[0]
                batch_intervals = response[1]
                
                logger.info("Sending immediate sync payload for %s intervals", len(batch_intervals))
                
                # Send to API
                api_response = self.activity_service.push_activity_logs(payload)
                logger.info("Immediate sync API response: %s", api_response)
                
                if api_response and api_response.get("planStatus") == "active":
                    # Mark intervals as uploaded in DB
                    for interval in batch_intervals:
                        try:
                            await self.activity_service.update_activity_interval(interval)
                        except Exception as e:
                            logger.error("Error marking interval %s as uploaded: %s", interval.id, e)
                    
                    # Update screenshot statuses
                    for day_payload in payload:
                        for interval_data in day_payload.get('intervals', []):
                            screen_url = interval_data.get("screenUrl")
                            if screen_url:
                                try:
                                    await self.screenshot_controller.update_screenshots(screen_url, self.last_count + 1)
                                except Exception as e:
                                    logger.error("Error updating screenshot status for %s: %s", screen_url, e)
                else:
                    logger.warning("Immediate sync API call failed or returned inactive status: %s", api_response)
            
            logger.info("=== sync_activity_immediately() completed ===")
            
        except Exception as e:
            logger.error("Error in sync_activity_immediately: %s", e, exc_info=True)
    
    async def _mark_idle_intervals_as_uploaded(self):
        """
        Mark idle intervals as is_uploaded=True so they are never sent to the server.
        Called when the user unchecks "Keep idle time" (Continue/Stop) or on auto-stop.

        Covers:
        - All unsent intervals from the current idle-popup period (start_time >= triggered_at)
        - All is_idle=True rows (race where is_idle was not committed yet)
        - Consecutive zero-activity streak from newest (fallback when flags lag)
        """
        try:
            await db_writer_service.process_immediately()
            await asyncio.sleep(0.5)

            current_counter = self.last_count + 1
            interval_ids = set()
            popup_triggered_at = self._idle_popup_triggered_at

            with db_lock_manager.read_lock():
                async with async_session() as session:
                    # 1) Entire idle-popup window (10/5/15 min rows + any with activity during popup)
                    if popup_triggered_at is not None:
                        popup_query = select(ActivityInterval).where(
                            and_(
                                ActivityInterval.counter == current_counter,
                                ActivityInterval.is_break == 0,
                                ActivityInterval.start_time >= popup_triggered_at,
                                ActivityInterval.is_uploaded == False,
                            )
                        )
                        popup_result = await session.execute(popup_query)
                        for interval in popup_result.scalars().all():
                            interval_ids.add(interval.id)
                        logger.info(
                            "[IdleUnkeep] Popup-period scan (from %s): %s unsent interval(s)",
                            popup_triggered_at,
                            len(interval_ids),
                        )

                    # 2) is_idle=True (may overlap popup scan)
                    idle_flag_query = select(ActivityInterval).where(
                        ActivityInterval.counter == current_counter,
                        ActivityInterval.is_break == 0,
                        ActivityInterval.is_idle == True,
                        ActivityInterval.is_uploaded == False,
                    )
                    idle_flag_result = await session.execute(idle_flag_query)
                    for interval in idle_flag_result.scalars().all():
                        interval_ids.add(interval.id)

                    # 3) Consecutive zero-activity from newest (intervals not yet flagged is_idle)
                    all_query = select(ActivityInterval).where(
                        ActivityInterval.counter == current_counter,
                        ActivityInterval.is_break == 0,
                        ActivityInterval.is_uploaded == False,
                    ).order_by(desc(ActivityInterval.start_time))
                    all_result = await session.execute(all_query)
                    for interval in all_result.scalars().all():
                        if interval.id in interval_ids:
                            continue
                        if self._last_idle_resolution_time and interval.end_time <= self._last_idle_resolution_time:
                            continue
                        has_zero = await self._has_zero_activity_for_interval(
                            interval.start_time, interval.end_time
                        )
                        if has_zero:
                            interval_ids.add(interval.id)
                        else:
                            break

            if not interval_ids:
                logger.warning(
                    "[IdleUnkeep] No intervals to mark as uploaded (counter=%s, popup_at=%s)",
                    current_counter,
                    popup_triggered_at,
                )
                return

            interval_id_list = sorted(interval_ids)
            logger.info(
                "[IdleUnkeep] Marking %s interval(s) as is_uploaded=True: ids=%s%s",
                len(interval_id_list),
                interval_id_list[:15],
                '...' if len(interval_id_list) > 15 else '',
            )

            updates = [{"id": iid, "is_uploaded": True} for iid in interval_id_list]
            db_write_queue.enqueue(
                WriteOperationType.BULK_UPDATE,
                "activity_interval",
                {"updates": updates},
            )

            await db_writer_service.process_immediately()
            await asyncio.sleep(0.5)
            logger.info("[IdleUnkeep] Committed is_uploaded=True for %s interval(s)", len(updates))

        except Exception as e:
            logger.error("Error marking idle intervals as uploaded: %s", e, exc_info=True)

    async def _upload_file_to_s3(self, local_file_path: str) -> Optional[str]:
        """
        Upload a file to S3 and return the S3 URL.
        
        Args:
            local_file_path: Local file path to upload
            
        Returns:
            S3 URL if successful, None otherwise
        """
        try:
            # Get user_entity if not available
            user_entity = None
            if hasattr(self, 'user_entity') and self.user_entity is not None:
                user_entity = self.user_entity

            if user_entity is None:
                logger.error("Cannot upload file to S3: user_entity is not available and cannot be retrieved from database")
                return None
            
            # Get org_id from user_entity
            org_id = user_entity.org_id
            if org_id is None:
                logger.error("Cannot upload file to S3: org_id is not set in user_entity")
                return None
            
            logger.info("Using user_entity with org_id: %s, user_id: %s", org_id, user_entity.user_id)
            
            # Check if file exists
            if not os.path.exists(local_file_path):
                logger.error("Cannot upload file to S3: file does not exist: %s", local_file_path)
                return None
            
            # Get S3 configuration (refresh STS credentials when expire_time passed)
            s3_config = await s3_detail_for_upload(org_id, user_entity)
            if not s3_config:
                logger.error("Cannot upload file to S3: S3 configuration not found for org_id: %s", org_id)
                return None
            
            # Create S3sync instance
            s3_sync = S3sync(s3_config)
            
            # Generate S3 destination path: notes/{org_id}/{user_id}/{timestamp}-{filename}
            file_name = os.path.basename(local_file_path)
            timestamp = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
            s3_dest_path = f"notes/{org_id}/{user_entity.user_id}/{timestamp}-{file_name}"
            
            logger.info("Uploading file to S3: %s -> %s", local_file_path, s3_dest_path)
            
            # Upload based on bucket type
            if user_entity.bucket == "hetzner":
                logger.info("Uploading to Hetzner bucket")
                s3_url = await s3_sync.upload_file_to_hetzner(local_file_path, s3_dest_path)
            else:
                logger.info("Uploading to AWS bucket")
                s3_url = await s3_sync.upload_file(local_file_path, s3_dest_path)
            
            if s3_url:
                logger.info("File uploaded successfully to S3: %s", s3_url)
                return s3_url
            else:
                logger.error("S3 upload returned no URL for file: %s", local_file_path)
                return None
        except Exception as e:
            logger.error("Error uploading file to S3: %s", e, exc_info=True)
            return None

    async def _save_note_to_idle_intervals(self, note: str, file_path: Optional[str] = None):
        """
        Save note to Notes table with array of idle interval IDs.
        This is called when user provides a note for idle time.
        
        Args:
            note: Note text to save
            file_path: Optional S3 URL of uploaded file
        """
        try:
            # Flush queue first
            await db_writer_service.process_immediately()
            await asyncio.sleep(0.5)  # Increased delay to ensure is_idle updates are committed
            
            current_counter = self.last_count + 1
            
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    # First, try to find intervals by is_idle flag (if they've been marked)
                    query_by_flag = select(ActivityInterval).where(
                        ActivityInterval.counter == current_counter,
                        ActivityInterval.is_break == 0,
                        ActivityInterval.is_idle == True,
                        ActivityInterval.is_uploaded == False
                    ).order_by(desc(ActivityInterval.start_time))
                    
                    result_by_flag = await session.execute(query_by_flag)
                    idle_intervals_by_flag = result_by_flag.scalars().all()
                    
                    # Also find intervals with zero activity that might not be marked as is_idle yet
                    # Get all intervals for current counter (excluding breaks)
                    query_all = select(ActivityInterval).where(
                        ActivityInterval.counter == current_counter,
                        ActivityInterval.is_break == 0,
                        ActivityInterval.is_uploaded == False
                    ).order_by(desc(ActivityInterval.start_time))
                    
                    result_all = await session.execute(query_all)
                    all_intervals = result_all.scalars().all()
                    
                    # Find consecutive zero-activity intervals from newest to oldest
                    intervals_to_mark = []
                    for interval in all_intervals:
                        # Check if interval has zero activity
                        has_zero_activity = await self._has_zero_activity_for_interval(interval.start_time, interval.end_time)
                        
                        if has_zero_activity:
                            # Include if not already in the list
                            if interval not in intervals_to_mark:
                                intervals_to_mark.append(interval)
                        else:
                            # Found activity - stop checking further intervals
                            break
                    
                    # Combine both sets (remove duplicates)
                    idle_intervals = list(set(idle_intervals_by_flag + intervals_to_mark))
                    
                    # Debug logging
                    logger.info("[DEBUG] _save_note_to_idle_intervals: Found %s intervals with is_idle=True, %s intervals with zero activity, %s total unique idle intervals", len(idle_intervals_by_flag), len(intervals_to_mark), len(idle_intervals))
                    
                    if not idle_intervals:
                        logger.info("No idle intervals found to save note")
                        return
                    
                    logger.info("Found %s idle intervals to save note: %s, file_path: %s", len(idle_intervals), note, file_path)
                    
                    # Collect all interval IDs
                    interval_ids = [interval.id for interval in idle_intervals]
                    
                    # Prepare note data with optional file_path
                    note_data = {
                        "activity_interval_id": interval_ids,
                        "note": note
                    }
                    if file_path:
                        note_data["file_path"] = file_path
                    
                    # Queue the insert operation
                    db_write_queue.enqueue(
                        WriteOperationType.NOTES,
                        "notes",
                        note_data
                    )
                    
                    logger.info("Queued Notes entry with %s interval IDs: %s, file_path: %s", len(interval_ids), interval_ids, file_path)
                    
        except Exception as e:
            logger.error("Error saving note to Notes table: %s", e, exc_info=True)

    async def _save_note_to_current_interval(self, note: str, file_path: Optional[str] = None):
        """
        Save note to Notes table with current running interval ID.
        This is called when user adds a note while timer is running.
        
        Args:
            note: Note text to save
            file_path: Optional S3 URL of uploaded file
        """
        try:
            logger.info("[AddNote] Starting to save note to current interval: note_length=%s", len(note))
            
            # Flush queue first to ensure latest intervals are in DB
            logger.info("[AddNote] Flushing write queue to ensure intervals are committed...")
            await db_writer_service.process_immediately()
            await asyncio.sleep(0.5)  # Delay to ensure intervals are committed
            
            current_counter = self.last_count + 1
            logger.info("[AddNote] Using counter: %s", current_counter)
            
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    # Find the latest interval with current_counter (regardless of uploaded status)
                    # The upcoming interval will have ID = latest_interval.id + 1
                    query = select(ActivityInterval).where(
                        ActivityInterval.counter == current_counter
                    ).order_by(desc(ActivityInterval.id)).limit(1)
                    
                    result = await session.execute(query)
                    latest_interval = result.scalar_one_or_none()
                    
                    if not latest_interval:
                        logger.error("[AddNote] No interval found with counter %s", current_counter)
                        return
                    
                    # Use latest interval ID + 1 (upcoming interval that will be created)
                    target_interval_id = latest_interval.id + 1
                    logger.info("[AddNote] Latest interval found: ID=%s, counter=%s, is_uploaded=%s", latest_interval.id, latest_interval.counter, latest_interval.is_uploaded)
                    logger.info("[AddNote] Using target interval ID: %s (latest_id + 1) for upcoming interval", target_interval_id)
                    
                    # Prepare note data with target interval ID
                    note_data = {
                        "activity_interval_id": [target_interval_id],
                        "note": note
                    }
                    if file_path:
                        note_data["file_path"] = file_path
                    
                    # Queue the insert operation
                    enqueue_success = db_write_queue.enqueue(
                        WriteOperationType.NOTES,
                        "notes",
                        note_data
                    )
                    
                    if not enqueue_success:
                        logger.error("[AddNote] Failed to enqueue Notes entry - queue may be full")
                        return
                    
                    logger.info("[AddNote] Successfully queued Notes entry with interval ID: %s", target_interval_id)
                    
                    # Force immediate processing of the queue to ensure note is written
                    logger.info("[AddNote] Processing write queue immediately to save note...")
                    await db_writer_service.process_immediately()
                    logger.info("[AddNote] Write queue processed - note should now be in database")
                    
        except Exception as e:
            logger.error("[AddNote] Error saving note to current interval: %s", e, exc_info=True)

    
    async def _should_pause_api_calls(self, intervals: list) -> bool:
        """
        Check if API calls should be paused.
        
        NEW REQUIREMENT: 
        - While idle popup is active, ALL intervals are considered idle and NOT sent to server.
        - When 1 idle interval is found, hold it until next non-idle interval found or idle popup handled.
        
        Returns True if:
        1. Idle popup is active (triggered but not yet handled) - BLOCKS ALL UPLOADS
        2. All intervals have zero activity (single or multiple) - HOLDS until non-idle or popup handled
        
        Args:
            intervals: List of ActivityInterval objects to check
            
        Returns:
            bool: True if API calls should be paused, False otherwise
        """
        if is_silent_build():
            return False
        try:
            # Case 1: Check if idle popup is active (triggered but not yet handled)
            # CRITICAL: While popup active, block ALL server uploads
            if self._idle_popup_active and self._idle_popup_triggered_at is not None:
                logger.info("[APIBlock] Pausing uploads: Idle popup is active (triggered at %s)", self._idle_popup_triggered_at.isoformat())
                return True
            
            # Case 2: Check intervals for idle activity
            # NEW: Hold idle intervals (single or multiple) even if there are non-idle intervals in the list
            # This ensures idle intervals are held until non-idle interval found or popup handled
            if not intervals:
                # No intervals - don't pause (nothing to send anyway)
                return False
            
            # CRITICAL: Only process intervals from current counter (filter out intervals from previous counters)
            current_counter = self.last_count + 1
            current_counter_intervals = [iv for iv in intervals if getattr(iv, 'counter', None) == current_counter]
            
            if not current_counter_intervals:
                # All intervals are from previous counter - don't hold them
                logger.debug("[APIBlock] All intervals are from previous counter (not %s) - not holding", current_counter)
                return False
            
            # CRITICAL FIX: Also check database for any unsent idle intervals that might not be in the intervals list
            # This catches newly created idle intervals (e.g., interval 75) that were just saved but not yet in the query results
            try:
                # Flush queue to ensure latest intervals are in DB
                await db_writer_service.process_immediately()
                await asyncio.sleep(0.1)  # Small delay for DB write to complete
                
                with db_lock_manager.read_lock():
                    async with async_session() as session:
                        # Query for unsent idle intervals from current counter that might not be in intervals list
                        query = select(ActivityInterval).where(
                            ActivityInterval.counter == current_counter,
                            ActivityInterval.is_break == 0,
                            ActivityInterval.is_uploaded == 0
                        )
                        result = await session.execute(query)
                        db_intervals = result.scalars().all()
                        
                        # Check each DB interval for zero activity and add to current_counter_intervals if not already there
                        interval_ids_in_list = {getattr(iv, 'id', None) for iv in current_counter_intervals}
                        for db_interval in db_intervals:
                            # Skip if already in the list
                            if getattr(db_interval, 'id', None) in interval_ids_in_list:
                                continue
                            
                            # Check if it has zero activity
                            activity_level = db_interval.activityLevel if hasattr(db_interval, 'activityLevel') else {}
                            keyboard = float(activity_level.get('keyboard', '0.0') or '0.0')
                            mouse = float(activity_level.get('mouse', '0.0') or '0.0')
                            average = float(activity_level.get('average', '0.0') or '0.0')
                            
                            if keyboard == 0 and mouse == 0 and average == 0:
                                # Found an idle interval in DB that's not in the list - add it
                                current_counter_intervals.append(db_interval)
                                logger.debug("[APIBlock] Found unsent idle interval in DB (id=%s) that wasn't in intervals list - adding to check", db_interval.id)
            except Exception as e:
                logger.warning("[APIBlock] Error checking database for idle intervals: %s - continuing with intervals list only", e)
            
            # Separate idle and non-idle intervals
            idle_intervals = []
            non_idle_intervals = []
            
            for interval in current_counter_intervals:
                activity_level = interval.activityLevel if hasattr(interval, 'activityLevel') else {}
                keyboard = float(activity_level.get('keyboard', '0.0') or '0.0')
                mouse = float(activity_level.get('mouse', '0.0') or '0.0')
                average = float(activity_level.get('average', '0.0') or '0.0')
                
                # Check if interval has zero activity
                if keyboard == 0 and mouse == 0 and average == 0:
                    idle_intervals.append(interval)
                else:
                    non_idle_intervals.append(interval)
            
            # Hold idle intervals (single or multiple) until non-idle found or popup handled
            if idle_intervals:
                for interval in idle_intervals:
                    # Check if already held (by ID to avoid duplicates)
                    if not any(getattr(held, 'id', None) == getattr(interval, 'id', None) for held in self._held_idle_intervals):
                        self._held_idle_intervals.append(interval)
                        logger.info("[APIBlock] Holding idle interval (id=%s, counter=%s, start=%s, end=%s) - will send when non-idle interval found or popup handled", interval.id, getattr(interval, 'counter', 'N/A'), interval.start_time, interval.end_time)
                
                if len(idle_intervals) == 1:
                    logger.info("[APIBlock] Holding 1 idle interval (counter=%s) - will send when non-idle interval found or popup handled", current_counter)
                else:
                    logger.info("[APIBlock] Holding %s idle intervals (counter=%s) - will send when non-idle interval found or popup handled", len(idle_intervals), current_counter)
            
            # If ALL intervals are idle, pause API calls (don't send anything)
            if not non_idle_intervals:
                return True
            
            # If there are non-idle intervals, don't pause (they will be sent, and held idle intervals will be sent before them in _push_activity_log)
            logger.debug("[APIBlock] Non-idle interval(s) found (%s) - held idle intervals will be sent before non-idle intervals", len(non_idle_intervals))
            return False
            
        except Exception as e:
            logger.error("[APIBlock] Error checking if API calls should be paused: %s", e, exc_info=True)
            return False  # Don't pause on error


    async def _idle_reassign(self, project_id: str, task_id: Optional[str] = None):
        """
        Reassign idle intervals to a new project and task by sending them to the API with the new project/task IDs.
        
        Args:
            project_id: Project ID to assign idle intervals to (mandatory)
            task_id: Task ID to assign idle intervals to (optional, defaults to "0" if not provided)
        """
        try:
            # Convert task_id to string, default to "0" if None
            task_id_str = str(task_id) if task_id is not None and task_id != "" else "0"
            project_id_str = str(project_id)
            
            logger.info("=== Starting idle interval reassign: project_id=%s, task_id=%s ===", project_id_str, task_id_str)
            
            # Flush queue first to ensure all pending writes are committed
            await db_writer_service.process_immediately()
            await asyncio.sleep(0.3)

            # Get all idle intervals that are not uploaded
            idle_intervals = []
            with db_lock_manager.read_lock():
                async with async_session() as session:
                    # Get ALL idle intervals (is_idle=1) that are not uploaded (is_uploaded=0)
                    query = select(ActivityInterval).where(
                        ActivityInterval.is_idle == True,
                        ActivityInterval.is_uploaded == False
                    ).order_by(ActivityInterval.start_time)
                    
                    result = await session.execute(query)
                    idle_intervals = result.scalars().all()
            
            if not idle_intervals:
                logger.info("No idle intervals found to reassign")
                return
            
            logger.info("Found %s idle intervals to reassign to project %s, task %s", len(idle_intervals), project_id_str, task_id_str)
            
            # Use process_intervals_bulk to build payloads, but override project/task IDs
            # Group intervals by day and process each day separately
            grouped_intervals = self.group_intervals_by_day(idle_intervals)
            
            success_count = 0
            failed_count = 0
            
            for target_date, day_intervals in grouped_intervals.items():
                try:
                    logger.info("Processing %s idle intervals for date %s", len(day_intervals), target_date)
                    
                    # Edge case: Ensure at most 100 intervals per day
                    if len(day_intervals) > 100:
                        day_intervals = day_intervals[:100]
                        logger.warning("Limited to 100 intervals for date %s", target_date)
                    
                    # Get day start time
                    day_start_time = self.get_day_start_time(target_date, day_intervals)
                    
                    # Build interval payloads
                    effective_start_time = day_start_time
                    interval_payloads = []
                    # Fetch notes for all intervals in this day from Notes table
                    interval_ids = [interval.id for interval in day_intervals if hasattr(interval, 'id')]
                    interval_note_map = {}
                    
                    # Query Notes table for entries that contain any of these interval IDs
                    # Notes.activity_interval_id is a JSON array, so we need to check if any interval_id is in the array
                    try:
                        with db_lock_manager.read_lock():
                            async with async_session() as session:
                                notes_query = select(Notes)
                                notes_result = await session.execute(notes_query)
                                notes_entries = notes_result.scalars().all()
                                
                                # Create a mapping of interval_id -> note
                                for notes_entry in notes_entries:
                                    if notes_entry.activity_interval_id:  # Check if array is not None
                                        for interval_id in notes_entry.activity_interval_id:
                                            if interval_id in interval_ids:
                                                interval_note_map[interval_id] = notes_entry.note
                                
                                if interval_note_map:
                                    logger.info("Found notes for %s intervals: %s", len(interval_note_map), list(interval_note_map.keys()))
                    except Exception as e:
                        logger.error("Error fetching notes from Notes table: %s", e, exc_info=True)

                    
                    for interval in day_intervals:
                        try:
                            payload_from, payload_to = self._payload_interval_bounds(interval, effective_start_time)
                            if payload_to is not None and payload_from is not None and payload_to <= payload_from:
                                logger.info(
                                    "Skipping idle reassignment interval (id=%s, start=%s, end=%s) — no time left after clipping to payload start %s",
                                    interval.id,
                                    interval.start_time,
                                    interval.end_time,
                                    effective_start_time,
                                )
                                continue
                            screen_url = await self.ss(payload_from, payload_to, self.user_entity)
                            activity_level = await self.get_activity_level_from_db(payload_from, payload_to)
                            app_and_urls = await self.activity_service.get_apps_urls(payload_from, payload_to, self.appandurlstatus)
                            
                            interval_payload = {
                                "Entry": interval.id,
                                "from": payload_from.strftime("%Y-%m-%d %H:%M:%S"),
                                "to": payload_to.strftime("%Y-%m-%d %H:%M:%S"),
                                "screenUrl": screen_url,
                                "activityLevel": activity_level,
                                "appAndUrls": app_and_urls,
                                "deviceType": self.get_os_type_caps(),
                                "os_type": self.get_os_type_caps(),
                                "note": interval_note_map.get(getattr(interval, 'id', None)),
                                "idle_note_url": None,
                                "idleInterval": None,
                                "location": LocationEntity(
                                    long="null",
                                    lat="null"
                                ).model_dump(),
                                "interval_time_db": int(self.interval/60),
                                "has_on_demand_screenshot": getattr(interval, "has_on_demand_screenshot", False),
                            }
                            interval_payloads.append(interval_payload)
                        except Exception as e:
                            logger.error("Error building payload for interval %s: %s", interval.id, e, exc_info=True)
                            failed_count += 1
                            continue
                    
                    if not interval_payloads:
                        logger.warning("No interval payloads created for date %s", target_date)
                        continue
                    
                    # Build the final payload using the provided project_id and task_id
                    payload = [{
                        "projectId": project_id_str,
                        "start": effective_start_time.strftime("%Y-%m-%d %H:%M:%S"),
                        "stop": None,
                        "selfi_id": None,
                        "timezone": self._normalize_timezone(str(self.timezone_manager.local_timezone) if self.timezone_manager.local_timezone else str(self.get_system_timezone())),
                        "break_id": None,
                        "deviceType": self.get_os_type_caps(),
                        "os_type": self.get_os_type_caps(),
                        "selfiVerification": True,
                        "intervals": interval_payloads,
                        "time_type": "1",
                        "interval_time": int(self.interval/60),
                        "source_type": "4",
                        "org_id": str(self.user_entity.org_id),
                        "todo_id": task_id_str,
                        "deviceId": self.system_unique_id,
                        "ip_address": str(get_ip()),
                        "os_version": get_os_version()
                    }]
                    
                    logger.info("Sending %s idle intervals to API with project_id=%s, task_id=%s", len(interval_payloads), project_id_str, task_id_str)
                    
                    # Send to API
                    try:
                        response = self.activity_service.push_activity_logs(payload)
                        logger.info("Successfully sent idle intervals for date %s to API", target_date)
                        
                        # Mark intervals as uploaded
                        from ws_package.repositories.activity_repo import activity_manager
                        for interval in day_intervals:
                            try:
                                await activity_manager.update_activity_interval(interval)
                                success_count += 1
                            except Exception as e:
                                logger.error("Error marking interval %s as uploaded: %s", interval.id, e, exc_info=True)
                                failed_count += 1
                        
                    except Exception as e:
                        logger.error("Error sending idle intervals to API for date %s: %s", target_date, e, exc_info=True)
                        failed_count += len(day_intervals)
                        
                except Exception as e:
                    logger.error("Error processing day %s for reassign: %s", target_date, e, exc_info=True)
                    failed_count += len(day_intervals)
            
            # Flush queue to ensure all updates are processed
            if success_count > 0:
                logger.info("Flushing database write queue to process %s is_uploaded updates...", success_count)
                await db_writer_service.process_immediately()
                await asyncio.sleep(0.5)
            
            logger.info("=== Idle reassign completed: %s intervals reassigned successfully, %s failed ===", success_count, failed_count)
            
        except Exception as e:
            logger.error("Error in _idle_reassign: %s", e, exc_info=True)
            raise
