"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASK v2.0 Project                                                          ║
║  Task Monitor - HTTP-Based Web Monitoring System                             ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.0                                                             ║
║  Date    : 2026-01-30                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    - MonitorHandler : HTTP request handler for REST API and web UI           ║
║    - TaskMonitor    : Threaded HTTP server wrapper for task monitoring       ║
║                                                                              ║
║  API Endpoints:                                                              ║
║    - GET  /api/status : Task status and RMI statistics                       ║
║    - GET  /api/health : Health check endpoint                                ║
║    - GET  /api/clear  : Clear all statistics                                 ║
║    - GET  /api/cpu    : CPU usage (requires psutil)                          ║
║    - GET  /api/config : Get task configuration                               ║
║    - POST /api/config : Update task configuration                            ║
║    - GET  /api/performance/system : System-wide performance stats            ║
║    - GET  /api/performance/tasks  : All tasks performance stats              ║
║    - GET  /api/performance/tasks/{id} : Specific task performance stats      ║
║    - GET  /api/performance/tasks/{id}/methods : Task methods performance     ║
║    - GET  /api/sysinfo : System information (OS, CPU, GPU, Memory, Network)  ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import TYPE_CHECKING
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import json
import time
import queue
import traceback
import os
from datetime import datetime
from .task_monitor_css import MONITOR_CSS

try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

if TYPE_CHECKING:
    from .task_manager import TaskManager


class MonitorHandler(BaseHTTPRequestHandler):
    manager: TaskManager = None
    start_time: float = 0
    gconfig = None
    # Memory history for leak detection graph (72 hours × 60 points = 4320 points, 1 point per minute)
    memory_history: list = []
    memory_history_max: int = 4320  # 72 hours * 60 points/hour
    memory_history_interval: int = 60  # seconds between recordings
    memory_history_last_record: float = 0

    def log_message(self, format, *args): pass

    def do_GET(self):
        routes = {
            '/api/status': self._json,
            '/api/health': self._health,
            '/api/clear': self._clear,
            '/api/cpu': self._cpu,
            '/api/config': self._config,
            '/api/gconfig': self._gconfig,
            '/api/logo': self._logo,
            '/api/rmi_methods': self._rmi_methods,
            '/api/measurement': self._get_measurement,
            '/api/smblock': self._smblock,
            '/api/subscriptions': self._subscriptions,
            '/api/performance/system': self._perf_system,
            '/api/performance/tasks': self._perf_tasks,
            '/api/sysinfo': self._sysinfo,
            '/api/sysinfo/memory_history': self._memory_history,
            '/api/external': self._external_get,
            '/api/logs': self._logs,
        }
        # Extract path without query parameters
        path_only = self.path.split('?')[0]
        # Check static routes first
        if path_only in routes:
            routes[path_only]()
            return
        # Dynamic routes for /api/performance/tasks/{id} and /api/performance/tasks/{id}/methods
        if self.path.startswith('/api/performance/tasks/'):
            parts = self.path.split('/')
            if len(parts) == 5:  # /api/performance/tasks/{id}
                self._perf_task_by_id(parts[4])
            elif len(parts) == 6 and parts[5] == 'methods':  # /api/performance/tasks/{id}/methods
                self._perf_task_methods(parts[4])
            else:
                self._send_json({'error': 'Invalid path'})
            return
        self._html()

    def do_POST(self):
        if self.path == '/api/config':
            self._update_config()
        elif self.path == '/api/gconfig':
            self._update_gconfig()
        elif self.path == '/api/stop':
            self._stop_all()
        elif self.path == '/api/measurement':
            self._set_measurement()
        elif self.path == '/api/subscriptions/toggle':
            self._subscriptions_toggle()
        elif self.path == '/api/subscriptions/clear':
            self._subscriptions_clear()
        elif self.path == '/api/external':
            self._external_set()
        elif self.path == '/api/external/test/slack':
            self._external_test('slack')
        elif self.path == '/api/external/test/email':
            self._external_test('email')
        elif self.path == '/api/external/test/webhook':
            self._external_test('webhook')
        elif self.path == '/api/external/report':
            self._external_report()

    def _config(self):
        data = []
        for tid, ti in self.manager._tasks.items():
            vars = {}
            if ti.job_class:
                keys = [k for k, v in ti.injection.items() if not (isinstance(v, str) and (v.startswith("task:") or v.startswith("smblock:")))]
                if keys:
                    try:
                        resp_q = self.manager._mgr.Queue()
                        ti.request_q.put({'m': '__get_config__', 'a': (keys,), 'kw': {}, 'rq': resp_q})
                        resp = resp_q.get(timeout=0.2)  # 200ms timeout for faster loading
                        vars = resp.get('r') or {k: ti.injection[k] for k in keys}
                    except queue.Empty:
                        vars = {k: ti.injection[k] for k in keys}  # Timeout fallback (use injection)
                    except Exception as e:
                        print(f"[Monitor] Config read error for '{tid}': {e}")
                        vars = {k: ti.injection[k] for k in keys}
                    # Filter out non-JSON-serializable values (e.g., SmBlock proxy objects)
                    vars = {k: v for k, v in vars.items() if isinstance(v, (str, int, float, bool, type(None), list, dict))}
            data.append({'id': tid, 'class': ti.task_class_name, 'vars': vars})
        self._send_json(data)

    def _rmi_methods(self):
        """RMI 메서드별 호출 통계 및 시간 측정 반환"""
        data = []
        for tid, ti in self.manager._tasks.items():
            methods = dict(ti.rmi_methods) if ti.rmi_methods else {}
            timing = dict(ti.rmi_timing) if ti.rmi_timing else {}
            total = sum(methods.values()) if methods else 0
            # Get alive status from worker
            worker = self.manager._workers.get(tid)
            alive = worker.is_alive() if worker else False

            # Build methods_timing with min/avg/max for each method
            methods_timing = {}
            for m, count in methods.items():
                t = timing.get(m)
                if t:
                    ipc_list, func_list = t
                    ipc_list = list(ipc_list) if ipc_list else []
                    func_list = list(func_list) if func_list else []
                    if ipc_list and func_list:
                        methods_timing[m] = {
                            'count': count,
                            'ipc': {'min': round(min(ipc_list), 2), 'avg': round(sum(ipc_list)/len(ipc_list), 2), 'max': round(max(ipc_list), 2)},
                            'func': {'min': round(min(func_list), 2), 'avg': round(sum(func_list)/len(func_list), 2), 'max': round(max(func_list), 2)},
                        }
                    else:
                        methods_timing[m] = {'count': count, 'ipc': None, 'func': None}
                else:
                    methods_timing[m] = {'count': count, 'ipc': None, 'func': None}

            data.append({
                'id': tid,
                'class': ti.task_class_name,
                'mode': ti.mode,
                'alive': alive,
                'total': total,
                'methods': methods,
                'methods_timing': methods_timing
            })
        self._send_json(data)

    def _get_measurement(self):
        """RMI 메서드 측정 상태 조회"""
        enabled = self.manager._measurement.value if self.manager._measurement else False
        self._send_json({'enabled': enabled})

    def _set_measurement(self):
        """RMI 메서드 측정 on/off 토글"""
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            enabled = data.get('enabled')
            if enabled is None:
                # Toggle if no value specified
                enabled = not self.manager._measurement.value
            self.manager._measurement.value = bool(enabled)
            self._send_json({'status': 'ok', 'enabled': self.manager._measurement.value})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _sysinfo(self):
        """시스템 정보 조회"""
        from .task_monitor_sysinfo import HwInfoCollector
        try:
            data = HwInfoCollector.collect_all()
            # Record memory history for leak detection (once per minute)
            mem = data.get('memory', {})
            if mem:
                import time
                now = time.time()
                # Only record if interval has passed
                if now - MonitorHandler.memory_history_last_record >= MonitorHandler.memory_history_interval:
                    record = {
                        'ts': now,
                        'used_gb': mem.get('used_gb', 0),
                        'percent': mem.get('usage_percent', 0)
                    }
                    MonitorHandler.memory_history.append(record)
                    MonitorHandler.memory_history_last_record = now
                    # Trim to max size
                    if len(MonitorHandler.memory_history) > MonitorHandler.memory_history_max:
                        MonitorHandler.memory_history = MonitorHandler.memory_history[-MonitorHandler.memory_history_max:]
            self._send_json(data)
        except Exception as e:
            self._send_json({'error': str(e), 'trace': traceback.format_exc()})

    def _memory_history(self):
        """메모리 히스토리 조회 (메모리 릭 그래프용)"""
        self._send_json({
            'history': MonitorHandler.memory_history,
            'max_points': MonitorHandler.memory_history_max
        })

    def _logs(self):
        """로그 조회"""
        from urllib.parse import urlparse, parse_qs
        try:
            # Parse query parameters
            parsed = urlparse(self.path)
            params = parse_qs(parsed.query)
            count = int(params.get('count', [100])[0])
            level = params.get('level', [None])[0]
            task_name = params.get('task', [None])[0]

            # Get log_task client
            log_task_info = self.manager._tasks.get('log_task')
            if not log_task_info:
                # Try to find any task with log_task class
                for tid, ti in self.manager._tasks.items():
                    if ti.task_class_name == 'log_task':
                        log_task_info = ti
                        break

            if log_task_info:
                try:
                    resp_q = self.manager._mgr.Queue()
                    log_task_info.request_q.put({
                        'm': 'get_recent_logs',
                        'a': (),
                        'kw': {'count': count, 'level': level, 'task_name': task_name},
                        'rq': resp_q
                    })
                    resp = resp_q.get(timeout=2.0)
                    if resp.get('e'):
                        self._send_json({'error': resp['e'], 'logs': []})
                    else:
                        self._send_json({'logs': resp.get('r', [])})
                except queue.Empty:
                    self._send_json({'error': 'LogTask timeout', 'logs': []})
            else:
                # Fallback: read directly from log files
                from pathlib import Path
                from .task_log import LEVEL_MAP
                import json as json_module

                # Check platform_config first (new format), then task_config (old format)
                log_dir = "logs"
                if self.gconfig:
                    log_dir = self.gconfig.data_get("platform_config._log/log_task.log_dir") or \
                              self.gconfig.data_get("task_config.log/log_task.log_dir") or "logs"
                log_path = Path(log_dir)

                if not log_path.exists():
                    self._send_json({'logs': [], 'warning': 'Log directory not found'})
                    return

                level_no = LEVEL_MAP.get(level, 0) if level else 0
                results = []
                log_files = sorted(log_path.glob("*.log"), key=lambda f: f.stat().st_mtime, reverse=True)

                for log_file in log_files[:5]:  # 최근 5개 파일만
                    if len(results) >= count:
                        break
                    try:
                        with open(log_file, "r", encoding="utf-8") as f:
                            lines = f.readlines()
                        for line in reversed(lines):
                            if len(results) >= count:
                                break
                            try:
                                record = json_module.loads(line.strip())
                                if level_no > 0:
                                    rec_level_no = LEVEL_MAP.get(record.get("lv", ""), 0)
                                    if rec_level_no < level_no:
                                        continue
                                if task_name and record.get("task") != task_name:
                                    continue
                                results.append(record)
                            except:
                                continue
                    except:
                        continue

                self._send_json({'logs': results})
        except Exception as e:
            self._send_json({'error': str(e), 'logs': []})

    def _external_get(self):
        """외부연동 설정 조회"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            self._send_json(ext_mgr.get_config())
        except Exception as e:
            self._send_json({'error': str(e)})

    def _external_set(self):
        """외부연동 설정 저장"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            success = ext_mgr.set_config(data)
            self._send_json({'status': 'ok' if success else 'error'})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _external_test(self, target: str):
        """외부연동 테스트"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            if target == 'slack':
                result = ext_mgr.test_slack()
            elif target == 'email':
                result = ext_mgr.test_email()
            elif target == 'webhook':
                result = ext_mgr.test_webhook()
            else:
                result = {'success': False, 'error': 'Invalid target'}
            self._send_json(result)
        except Exception as e:
            self._send_json({'success': False, 'error': str(e)})

    def _external_report(self):
        """Health Report 즉시 발송"""
        from .task_monitor_ext_link import ExternalLinkManager
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            targets = data.get('targets')
            ext_mgr = ExternalLinkManager(self.gconfig, self.manager)
            results = ext_mgr.send_report(targets)
            self._send_json({'status': 'ok', 'results': results})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _smblock(self):
        """SmBlock 공유 메모리 풀 통계 조회"""
        from .task_manager import SmBlockHandler
        data = []

        # Find which tasks use each smblock
        smblock_users = {}  # name -> [task_ids]
        for tid, ti in self.manager._tasks.items():
            for k, v in ti.injection.items():
                if isinstance(v, dict) and "_smblock" in v:
                    pool_name = v["_smblock"]
                    if pool_name not in smblock_users:
                        smblock_users[pool_name] = []
                    smblock_users[pool_name].append(f"{tid}.{k}")

        for name, pool in SmBlockHandler._pools.items():
            try:
                free_count = pool.count()
                used_count = pool.maxsize - free_count
                usage_pct = (used_count / pool.maxsize * 100) if pool.maxsize > 0 else 0
                pool_data = {
                    'name': name,
                    'shape': list(pool.shape),
                    'maxsize': pool.maxsize,
                    'used': used_count,
                    'free': free_count,
                    'usage_pct': round(usage_pct, 1),
                    'item_size': pool.item_size,
                    'total_mb': round(pool.item_size * pool.maxsize / 1024 / 1024, 2),
                    'users': smblock_users.get(name, []),
                }
                data.append(pool_data)
            except Exception as e:
                data.append({'name': name, 'error': str(e)})

        self._send_json(data)

    def _subscriptions(self):
        """Signal 구독 현황 및 통계 조회"""
        data = {
            'mode': 'unknown',
            'subscriptions': [],
            'queues': [],
            'stats': {'enabled': False, 'cleared_at': 0, 'signals': {}},
        }
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json(data)
            return
        try:
            data['mode'] = broker.mode
            # 구독자 목록 (signal_name:task_id -> True 형태)
            with broker._lock:
                all_keys = list(broker._subscribers.keys())
            subs_by_signal = {}
            for key in all_keys:
                if key == '__version__':
                    continue
                parts = key.split(':', 1)
                if len(parts) == 2:
                    signal_name, task_id = parts
                    if signal_name not in subs_by_signal:
                        subs_by_signal[signal_name] = []
                    subs_by_signal[signal_name].append(task_id)
            data['subscriptions'] = [{'signal': k, 'subscribers': v} for k, v in subs_by_signal.items()]
            # 등록된 큐 목록
            with broker._lock:
                queue_ids = list(broker._queues.keys())
            data['queues'] = queue_ids
            # 통계 정보
            data['stats'] = broker.get_stats()
        except Exception as e:
            data['error'] = str(e)
        self._send_json(data)

    def _subscriptions_toggle(self):
        """Signal 통계 수집 on/off 토글"""
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json({'status': 'error', 'message': 'No signal broker'})
            return
        try:
            length = int(self.headers.get('Content-Length', 0))
            body = json.loads(self.rfile.read(length).decode()) if length > 0 else {}
            enabled = body.get('enabled')
            if enabled is None:
                # Toggle if no value specified
                enabled = not broker.stats_enabled
            broker.stats_enabled = bool(enabled)
            self._send_json({'status': 'ok', 'enabled': broker.stats_enabled})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _subscriptions_clear(self):
        """Signal 통계 초기화"""
        broker = getattr(self.manager, '_signal_broker', None)
        if not broker:
            self._send_json({'status': 'error', 'message': 'No signal broker'})
            return
        try:
            cleared_at = broker.clear_stats()
            self._send_json({'status': 'ok', 'cleared_at': cleared_at})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    # ─────────────────────────────────────────────────────────────────────────
    # Performance API (PERF-006)
    # ─────────────────────────────────────────────────────────────────────────

    def _perf_system(self):
        """시스템 전체 성능 통계 반환 (/api/performance/system)"""
        perf = self.manager.performance.get_system_performance()
        self._send_json(perf.to_dict())

    def _perf_tasks(self):
        """모든 Task 성능 통계 반환 (/api/performance/tasks)"""
        tasks = self.manager.performance.get_all_tasks_performance()
        self._send_json([t.to_dict() for t in tasks])

    def _perf_task_by_id(self, task_id: str):
        """특정 Task 성능 통계 반환 (/api/performance/tasks/{id})"""
        perf = self.manager.performance.get_task_performance(task_id)
        if perf:
            self._send_json(perf.to_dict())
        else:
            self._send_json({'error': f'Task {task_id} not found'})

    def _perf_task_methods(self, task_id: str):
        """특정 Task의 메서드별 성능 통계 반환 (/api/performance/tasks/{id}/methods)"""
        methods = self.manager.performance.get_task_methods_performance(task_id)
        self._send_json([m.to_dict() for m in methods])

    def _update_config(self):
        length = int(self.headers.get('Content-Length', 0))
        data = json.loads(self.rfile.read(length).decode())
        tid, key, value = data.get('task_id'), data.get('key'), data.get('value')
        result = {'status': 'error', 'message': 'Task not found'}
        print(f"[Monitor:_update_config] tid={tid}, key={key}, value={value}, type={type(value)}")

        if tid in self.manager._tasks:
            ti = self.manager._tasks[tid]
            old_val = ti.injection.get(key)
            print(f"[Monitor:_update_config] old_val={old_val}, type={type(old_val)}")
            if old_val is not None and not (isinstance(old_val, str) and old_val.startswith("task:")):
                try:
                    if isinstance(old_val, bool):
                        value = value.lower() in ('true', '1', 'yes')
                    elif isinstance(old_val, (int, float)):
                        # Try int first, fallback to float
                        try:
                            value = int(value)
                        except ValueError:
                            value = float(value)
                    print(f"[Monitor:_update_config] converted value={value}, type={type(value)}")
                    ti.injection[key] = value
                    print(f"[Monitor:_update_config] injection updated: {ti.injection.get(key)}")
                    resp_q = self.manager._mgr.Queue()
                    print(f"[Monitor:_update_config] sending RMI __set_config__ to {tid}")
                    ti.request_q.put({'m': '__set_config__', 'a': (key, value), 'kw': {}, 'rq': resp_q})
                    try:
                        resp = resp_q.get(timeout=2.0)
                        print(f"[Monitor:_update_config] RMI response: {resp}")
                        result = {'status': 'ok', 'task_id': tid, 'key': key, 'value': value} if resp.get('r') else {'status': 'error', 'message': resp.get('e', 'Unknown')}
                    except queue.Empty:
                        print(f"[Monitor:_update_config] RMI timeout for {tid}")
                        result = {'status': 'error', 'message': f'Timeout waiting for task {tid}'}
                    except Exception as e:
                        print(f"[Monitor:_update_config] RMI exception: {e}")
                        result = {'status': 'error', 'message': f'RMI error: {e}'}
                except Exception as e:
                    print(f"[Monitor:_update_config] exception: {e}")
                    result = {'status': 'error', 'message': str(e)}
        else:
            print(f"[Monitor:_update_config] task {tid} not found in {list(self.manager._tasks.keys())}")
        self._send_json(result)

    def _gconfig(self):
        if not self.gconfig:
            self._send_json({'error': 'GConfig not configured', 'data': None})
            return
        try:
            data = self.gconfig.to_dict() if hasattr(self.gconfig, 'to_dict') else self.gconfig._cache._data
            filepath = getattr(self.gconfig, '_filepath', None)
            home_dir = os.path.dirname(filepath) if filepath else None
            log_dir = os.path.join(home_dir, 'log') if home_dir else None
            self._send_json({
                'data': data,
                'filepath': filepath,
                'home_dir': home_dir,
                'log_dir': log_dir
            })
        except Exception as e:
            self._send_json({'error': str(e), 'data': None})

    def _update_gconfig(self):
        if not self.gconfig:
            self._send_json({'status': 'error', 'message': 'GConfig not configured'})
            return
        try:
            length = int(self.headers.get('Content-Length', 0))
            data = json.loads(self.rfile.read(length).decode())
            path, value, vtype = data.get('path'), data.get('value'), data.get('vtype')
            if not path:
                self._send_json({'status': 'error', 'message': 'Path required'})
                return
            # Convert value type based on original type (vtype)
            if isinstance(value, str):
                if vtype == 'str':
                    pass  # Keep as string
                elif vtype == 'bool':
                    value = value.lower() in ('true', '1', 'yes')
                elif vtype == 'null':
                    value = None if value.lower() in ('null', '') else value
                elif vtype == 'num':
                    try: value = int(value)
                    except ValueError:
                        try: value = float(value)
                        except ValueError: pass
                else:
                    # Fallback: auto-detect type
                    if value.lower() == 'true': value = True
                    elif value.lower() == 'false': value = False
                    elif value.lower() == 'null': value = None
                    else:
                        try: value = int(value)
                        except ValueError:
                            try: value = float(value)
                            except ValueError: pass
            self.gconfig.data_set(path, value)
            self._send_json({'status': 'ok', 'path': path, 'value': value})
        except Exception as e:
            self._send_json({'status': 'error', 'message': str(e)})

    def _stop_all(self):
        print(f"[Monitor:_stop_all] called")
        try:
            # skip_monitor=True to avoid deadlock (HTTP request waiting for server.shutdown)
            print(f"[Monitor:_stop_all] calling manager.stop_all(skip_monitor=True)")
            self.manager.stop_all(skip_monitor=True)
            print(f"[Monitor:_stop_all] manager.stop_all() completed")
            self._send_json({'status': 'ok', 'message': 'All tasks stopped'})
        except Exception as e:
            print(f"[Monitor:_stop_all] exception: {e}")
            self._send_json({'status': 'error', 'message': str(e)})

    def _logo(self):
        logo_path = os.path.join(os.path.dirname(__file__), 'div_logo.png')
        try:
            with open(logo_path, 'rb') as f:
                data = f.read()
            self.send_response(200)
            self.send_header('Content-Type', 'image/png')
            self.send_header('Cache-Control', 'max-age=86400')
            self.end_headers()
            self.wfile.write(data)
        except FileNotFoundError:
            self.send_response(404)
            self.end_headers()

    def _cpu(self):
        if not HAS_PSUTIL:
            self._send_json({'error': 'psutil not installed', 'tasks': []})
            return
        tasks = []
        for tid, ti in self.manager._tasks.items():
            w = self.manager._workers.get(tid)
            alive = w.is_alive() if w else False
            rmi_fail = ti.rmi_fail.value if ti and ti.rmi_fail else 0
            restart = ti.restart_cnt.value if ti and ti.restart_cnt else 0
            mode = ti.mode if ti else ''
            task_data = {'id': tid, 'class': ti.task_class_name if ti else '', 'mode': mode, 'alive': alive, 'rmi_fail': rmi_fail, 'restart': restart, 'pid': None, 'cpu': 0, 'memory': 0, 'threads': 0, 'vars': {}}
            try:
                if hasattr(w, 'pid') and w.pid:
                    p = psutil.Process(w.pid)
                    task_data['pid'] = w.pid
                    task_data['cpu'] = p.cpu_percent(interval=0.1)
                    task_data['memory'] = p.memory_info().rss / 1024 / 1024
                    task_data['threads'] = p.num_threads()
            except psutil.NoSuchProcess:
                pass  # Process ended
            except Exception as e:
                print(f"[Monitor] CPU info error for '{tid}': {e}")
            # Add config vars (filtered)
            if ti and ti.job_class:
                keys = [k for k, v in ti.injection.items() if not (isinstance(v, str) and (v.startswith("task:") or v.startswith("smblock:"))) and not (isinstance(v, dict) and "_smblock" in v)]
                if keys:
                    try:
                        resp_q = self.manager._mgr.Queue()
                        ti.request_q.put({'m': '__get_config__', 'a': (keys,), 'kw': {}, 'rq': resp_q})
                        resp = resp_q.get(timeout=0.2)
                        vars_data = resp.get('r') or {k: ti.injection[k] for k in keys}
                    except Exception:
                        vars_data = {k: ti.injection[k] for k in keys}
                    # Filter non-serializable
                    task_data['vars'] = {k: v for k, v in vars_data.items() if isinstance(v, (str, int, float, bool, type(None), list, dict))}
            tasks.append(task_data)
        self._send_json({'system_cpu': psutil.cpu_percent(interval=0.1),
                         'system_memory': psutil.virtual_memory().percent, 'tasks': tasks})

    def _clear(self):
        self.manager.clear_stats()
        self._send_json({'status': 'cleared'})

    def _json(self):
        self._send_json(self._status())

    def _health(self):
        """Health check with full statistics (기존통계 + RMI 메서드 통계)"""
        status = self.manager.get_status()
        uptime = int(time.time() - self.start_time)

        # Build task stats with rmi_methods
        tasks = []
        for tid, info in status.items():
            tasks.append({
                'id': tid,
                'class': info['class'],
                'mode': info['mode'],
                'alive': info['alive'],
                'rmi_rx': info['rmi_rx'],
                'rmi_tx': info['rmi_tx'],
                'rmi_fail': info['rmi_fail'],
                'rmi_avg': info['rmi_avg'],
                'rmi_methods': info.get('rmi_methods', {}),
            })

        self._send_json({
            'status': 'ok',
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'uptime_seconds': uptime,
            'tasks': tasks
        })

    def _send_json(self, data):
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps(data, indent=2).encode())

    def _html(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(self._page().encode())

    def _status(self):
        uptime = int(time.time() - self.start_time)
        h, r = divmod(uptime, 3600)
        mi, s = divmod(r, 60)
        status = self.manager.get_status()
        tasks = []
        running = 0
        for tid, info in status.items():
            if info['alive']: running += 1
            tasks.append({
                'id': tid, 'class': info['class'], 'mode': info['mode'],
                'status': 'running' if info['alive'] else 'stopped',
                'rmi_rx': info['rmi_rx'], 'rmi_tx': info['rmi_tx'], 'rmi_fail': info['rmi_fail'],
                'rmi_avg': info['rmi_avg'], 'rmi_min': info['rmi_min'], 'rmi_max': info['rmi_max'],
                'rxq_size': info['rxq_size'], 'txq_size': info['txq_size'], 'restart': info['restart'],
            })
        return {'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'uptime': f'{h}시간 {mi}분 {s}초', 'total': len(tasks), 'running': running, 'tasks': tasks}

    def _page(self):
        d = self._status()
        # Get app_info from gconfig
        app_name = self.gconfig.data_get("app_info.name", "") if self.gconfig else ""
        app_ver = self.gconfig.data_get("app_info.version", "") if self.gconfig else ""
        app_id = self.gconfig.data_get("app_info.id", "") if self.gconfig else ""
        app_info_text = f"{app_name}:{app_ver} id={app_id}" if app_name else ""
        app_info_html = f'<span class="app-info">{app_info_text}</span>' if app_name else ""
        title_text = f"ALASKA Task Monitor {app_info_text}".strip()
        title_html = f"ALASKA Task Monitor {app_info_html}".strip()
        return f'''<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>{title_text}</title>
<style>{MONITOR_CSS}</style></head>
<body>
<div class="header">
<h1>{title_html}</h1>
<span class="header-info" id="info-text">Uptime: {d['uptime']} | Tasks: {d['running']}/{d['total']} | {d['timestamp']}</span>
<div class="refresh-ctrl">
<input type="checkbox" id="autoRefresh" checked>
<label for="autoRefresh">Auto</label>
<select id="refreshInterval">
<option value="1">1s</option>
<option value="2">2s</option>
<option value="3" selected>3s</option>
<option value="5">5s</option>
<option value="10">10s</option>
</select>
<button class="btn-refresh" onclick="manualRefresh()">Refresh</button>
<button class="btn-clear" onclick="clearStats()">Clear</button>
</div>
</div>
<div class="main-container">
<div class="sidebar" id="sidebar">
<div class="sidebar-nav">
<div class="nav-category" onclick="toggleCategory(this)"><span>RMI</span><span class="nav-category-icon">▼</span></div>
<div class="nav-group">
<div class="nav-item active" onclick="showPanel('cpu')" data-panel="cpu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg>
<span>Task/Mem</span>
</div>
<div class="nav-item" onclick="showPanel('rmi')" data-panel="rmi">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
<span>Speed</span>
</div>
<div class="nav-item" onclick="showPanel('rmicall')" data-panel="rmicall">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>
<span>Count</span>
</div>
<div class="nav-item" onclick="showPanel('signals')" data-panel="signals">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8.59 13.51L15.42 17.49M15.41 6.51L8.59 10.49M21 5a3 3 0 11-6 0 3 3 0 016 0zM21 19a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
<span>Signals</span>
</div>
</div>
<div class="nav-category" onclick="toggleCategory(this)"><span>FILE</span><span class="nav-category-icon">▼</span></div>
<div class="nav-group">
<div class="nav-item" onclick="showPanel('gconfig')" data-panel="gconfig">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg>
<span>Config</span>
</div>
<div class="nav-item" onclick="showPanel('logs')" data-panel="logs">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>
<span>Logs</span>
</div>
</div>
<div class="nav-category" onclick="toggleCategory(this)"><span>EXTERNAL</span><span class="nav-category-icon">▼</span></div>
<div class="nav-group">
<div class="nav-item" onclick="showPanel('extlink')" data-panel="extlink">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<span>Integration</span>
</div>
<div class="nav-item" onclick="showPanel('sysinfo')" data-panel="sysinfo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
<span>SysInfo</span>
</div>
<div class="nav-item" onclick="showPanel('settings')" data-panel="settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
<span>Settings</span>
</div>
</div>
</div>
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle Sidebar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
</div>
<div class="content-area">
<div id="panel-rmi" class="panel">
<div id="rmi-loading">Loading Latency data...</div>
<div id="rmi-content" style="display:none">
<div style="display:flex;padding:6px 12px;margin-bottom:4px;color:#9e9e9e;font-size:12px;border-bottom:1px solid #333">
<span style="width:28px"></span>
<span style="flex:1">Task ID</span>
<span style="width:100px">Class</span>
<span style="width:70px">Mode</span>
<span style="width:70px">Status</span>
<span style="width:140px;text-align:center">Latency (min/avg/max)</span>
</div>
<div id="rmi-tree" class="tree-container" style="font-size:13px"></div>
</div>
</div>
<div id="panel-rmicall" class="panel active">
<div id="rmicall-loading">Loading RMI method stats...</div>
<div id="rmicall-content" style="display:none">
<div style="margin-bottom:12px;display:flex;align-items:center;gap:16px">
<span style="color:#9e9e9e;font-size:12px" id="rmicall-interval">Interval: -</span>
<button id="measurement-toggle" class="btn-refresh" onclick="toggleMeasurement()" style="padding:4px 12px">
<span id="measurement-status">OFF</span>
</button>
<span style="color:#666;font-size:11px">measurement: config._monitor.measurement</span>
</div>
<div style="display:flex;padding:6px 12px;margin-bottom:4px;color:#9e9e9e;font-size:12px;border-bottom:1px solid #333">
<span style="width:28px"></span>
<span style="flex:1">Task ID</span>
<span style="width:120px">Class</span>
<span style="width:70px">Mode</span>
<span style="width:70px">Status</span>
<span style="width:80px;text-align:right">Total</span>
<span style="width:80px;text-align:right">Rate</span>
</div>
<div id="rmicall-tree" class="tree-container" style="font-size:13px"></div>
</div>
</div>
<div id="panel-cpu" class="panel">
<div id="cpu-loading">Loading CPU data...</div>
<div id="cpu-content" style="display:none">
<div id="application-section" style="margin-bottom:16px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Application</div>
<div id="appinfo-content" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
<div id="smblock-section" style="margin-bottom:16px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">SmBlock Pools</div>
<div id="smblock-tree" class="tree-container" style="font-size:13px"></div>
</div>
<div style="display:flex;gap:16px;margin-bottom:16px">
<div style="flex:2;min-width:0">
<div style="background:#1e3a5f;border-radius:4px;overflow:hidden">
<table id="cpu-table" style="width:100%;border-collapse:collapse;font-size:13px">
<thead>
<tr style="background:#0d2137;color:#9e9e9e;font-size:12px">
<th style="padding:8px 6px;text-align:center;width:40px;color:#ff9800">Status</th>
<th style="padding:8px 10px;text-align:left">Task ID</th>
<th style="padding:8px 10px;text-align:left">Class</th>
<th style="padding:8px 10px;text-align:center;color:#ff9800">Fail/Rst</th>
<th style="padding:8px 10px;text-align:center;width:130px;color:#ff9800">CPU</th>
<th style="padding:8px 10px;text-align:right">Memory</th>
<th style="padding:8px 10px;text-align:right">PID</th>
</tr>
</thead>
<tbody id="cpu-tbody"></tbody>
</table>
</div>
</div>
<div id="task-vars-panel" style="flex:1;min-width:280px;background:#1e3a5f;border-radius:4px;padding:12px">
<div style="color:#9e9e9e;font-size:12px;margin-bottom:12px;font-weight:bold">Task Variables</div>
<div id="task-vars-content" style="color:#666;font-style:italic">Select a task to view variables</div>
</div>
</div>
</div>
</div>
<div id="panel-gconfig" class="panel">
<div id="gconfig-loading">Loading GConfig...</div>
<div id="gconfig-content" style="display:none">
<div class="gconfig-header">
<span id="gconfig-filepath"></span>
<button class="btn-refresh" onclick="loadGConfigData()">Refresh</button>
<button class="btn-expand" onclick="expandAllTree()">Expand All</button>
<button class="btn-collapse" onclick="collapseAllTree()">Collapse All</button>
</div>
<div class="gconfig-body">
<div class="gconfig-tree-wrap">
<div id="gconfig-tree" class="tree-container"></div>
</div>
<div class="value-setter">
<h3>Value Setter</h3>
<div id="setter-content">
<div class="value-setter-empty">트리에서 값을 선택하세요</div>
</div>
</div>
</div>
</div>
</div>
<div id="panel-signals" class="panel">
<div id="signals-loading">Loading Signal subscriptions...</div>
<div id="signals-content" style="display:none">
<div style="margin-bottom:16px;display:flex;align-items:center;gap:16px;flex-wrap:wrap">
<span style="color:#9e9e9e;font-size:12px">Signal Broker Mode: </span>
<span id="signals-mode" style="color:#81d4fa;font-weight:bold">-</span>
<button class="btn-refresh" onclick="loadSignalsData()">Refresh</button>
<span style="border-left:1px solid #444;height:20px"></span>
<span style="color:#9e9e9e;font-size:12px">Stats:</span>
<button id="signals-stats-toggle" class="btn-refresh" onclick="toggleSignalStats()">
<span id="signals-stats-status">ON</span>
</button>
<button class="btn-clear" onclick="clearSignalStats()">Clear Stats</button>
<span id="signals-cleared-at" style="color:#666;font-size:11px"></span>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap">
<div style="flex:1;min-width:280px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Signal Subscriptions</div>
<div id="signals-subs" class="tree-container" style="font-size:13px;min-height:200px"></div>
</div>
<div style="flex:1;min-width:280px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Registered Queues</div>
<div id="signals-queues" class="tree-container" style="font-size:13px;min-height:150px"></div>
</div>
<div style="flex:1;min-width:280px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Signal Statistics</div>
<div id="signals-stats" class="tree-container" style="font-size:13px;min-height:150px"></div>
</div>
</div>
</div>
</div>
<div id="panel-sysinfo" class="panel">
<div id="sysinfo-loading">Loading System Info...</div>
<div id="sysinfo-content" style="display:none">
<div style="margin-bottom:12px;display:flex;align-items:center;gap:16px">
<button class="btn-refresh" onclick="loadSysInfo()">Refresh</button>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap">
<div style="flex:1;min-width:300px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">System</div>
<div id="sysinfo-system" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
<div style="flex:1;min-width:200px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">CPU</div>
<div id="sysinfo-cpu" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
<div style="flex:1;min-width:200px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Memory</div>
<div id="sysinfo-memory" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-top:16px">
<div style="flex:1;min-width:280px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">GPU</div>
<div id="sysinfo-gpu" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
<div style="flex:2;min-width:400px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Storage</div>
<div id="sysinfo-storage" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-top:16px">
<div style="flex:1;min-width:300px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Network Interfaces</div>
<div id="sysinfo-network" class="tree-container" style="font-size:13px;padding:12px;max-height:200px;overflow-y:auto"></div>
</div>
<div style="flex:1;min-width:250px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">ARP Table</div>
<div id="sysinfo-arp" class="tree-container" style="font-size:13px;padding:12px;max-height:200px;overflow-y:auto"></div>
</div>
<div style="flex:1;min-width:300px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Routing Table</div>
<div id="sysinfo-routing" class="tree-container" style="font-size:13px;padding:12px;max-height:200px;overflow-y:auto"></div>
</div>
</div>
</div>
</div>
<div id="panel-extlink" class="panel">
<div id="extlink-loading">Loading External Link settings...</div>
<div id="extlink-content" style="display:none">
<div style="margin-bottom:12px;display:flex;align-items:center;gap:16px">
<button class="btn-refresh" onclick="loadExtLink()">Refresh</button>
<button class="btn-refresh" onclick="saveExtLink()">Save</button>
</div>
<div style="margin-bottom:16px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Health Report</div>
<div class="tree-container" style="padding:12px">
<div style="display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid #444;padding-bottom:8px">
<button class="extlink-tab active" onclick="showExtTab('slack')" data-tab="slack" style="padding:6px 16px;background:#0288d1;border:none;color:#fff;border-radius:4px 4px 0 0;cursor:pointer">Slack</button>
<button class="extlink-tab" onclick="showExtTab('email')" data-tab="email" style="padding:6px 16px;background:#444;border:none;color:#9e9e9e;border-radius:4px 4px 0 0;cursor:pointer">Email</button>
<button class="extlink-tab" onclick="showExtTab('webhook')" data-tab="webhook" style="padding:6px 16px;background:#444;border:none;color:#9e9e9e;border-radius:4px 4px 0 0;cursor:pointer">Webhook</button>
</div>
<div id="extlink-tab-slack" class="extlink-tab-content" style="display:block">
<div id="extlink-slack"></div>
</div>
<div id="extlink-tab-email" class="extlink-tab-content" style="display:none">
<div id="extlink-email"></div>
</div>
<div id="extlink-tab-webhook" class="extlink-tab-content" style="display:none">
<div id="extlink-webhook"></div>
</div>
<div style="margin-top:16px;padding-top:12px;border-top:1px solid #444">
<div style="margin-bottom:8px;color:#ffcc80;font-weight:bold">Schedule Settings</div>
<div id="extlink-report"></div>
</div>
</div>
</div>
<div style="margin-bottom:16px">
<div style="margin-bottom:8px;color:#9e9e9e;font-size:13px;font-weight:bold">Alert Settings</div>
<div id="extlink-alert" class="tree-container" style="font-size:13px;padding:12px"></div>
</div>
</div>
</div>
</div>
<div id="panel-logs" class="panel">
<div class="panel-tabs">
<div class="panel-tab active" onclick="switchLogsTab('logs')">Logs</div>
<div class="panel-tab" onclick="switchLogsTab('memory')">Memory</div>
</div>
<div id="logs-tab-logs" class="tab-content active">
<div style="margin-bottom:12px;display:flex;align-items:center;gap:16px;flex-wrap:wrap">
<button class="btn-refresh" onclick="loadLogs()">Refresh</button>
<select id="log-level" style="background:#2d2d2d;color:#e0e0e0;border:1px solid #444;padding:6px 10px;border-radius:4px">
<option value="">All Levels</option>
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
<select id="log-task" style="background:#2d2d2d;color:#e0e0e0;border:1px solid #444;padding:6px 10px;border-radius:4px">
<option value="">All Tasks</option>
</select>
<select id="log-count" style="background:#2d2d2d;color:#e0e0e0;border:1px solid #444;padding:6px 10px;border-radius:4px">
<option value="50">50 logs</option>
<option value="100" selected>100 logs</option>
<option value="200">200 logs</option>
<option value="500">500 logs</option>
</select>
<label style="display:flex;align-items:center;gap:6px;color:#9e9e9e;font-size:13px">
<input type="checkbox" id="log-auto" checked style="width:14px;height:14px"> Auto Refresh
</label>
</div>
<div id="logs-loading">Loading logs...</div>
<div id="logs-content" style="display:none">
<div id="logs-table-wrap" style="max-height:calc(100vh - 250px);overflow-y:auto;overflow-x:auto">
<table id="logs-table" style="width:100%;table-layout:fixed">
<thead style="position:sticky;top:0">
<tr>
<th style="width:180px">Time</th>
<th style="width:80px">Level</th>
<th style="width:120px">Task</th>
<th style="width:auto">Message</th>
</tr>
</thead>
<tbody id="logs-body"></tbody>
</table>
</div>
</div>
</div>
<div id="logs-tab-memory" class="tab-content">
<div style="margin-bottom:8px;color:var(--text-accent2);font-size:14px;font-weight:bold">Memory Usage History (Leak Detection)</div>
<div class="tree-container" style="padding:12px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:8px;flex-wrap:wrap">
<span id="memory-current" style="color:#81d4fa;font-size:13px">Current: --</span>
<span id="memory-trend" style="font-size:13px">Trend: --</span>
<span id="memory-stats" style="color:#666;font-size:12px">Points: --</span>
<span id="memory-warning" style="color:#f44336;font-size:13px;display:none"></span>
</div>
<canvas id="memory-graph" width="800" height="150" style="width:100%;height:150px;background:var(--bg-main);border-radius:4px"></canvas>
<div style="display:flex;justify-content:space-between;margin-top:4px;font-size:11px;color:#666">
<span id="memory-time-start">--</span>
<span>Memory Trend (72h max, 1 min interval)</span>
<span id="memory-time-end">Now</span>
</div>
</div>
</div>
</div>
<div id="panel-settings" class="panel">
<div style="max-width:500px">
<h2 style="margin:0 0 20px 0;color:var(--text-accent);font-size:18px;border-bottom:1px solid var(--border-color);padding-bottom:10px">Settings</h2>
<div style="background:var(--bg-content);padding:16px;border-radius:8px;margin-bottom:16px">
<div style="margin-bottom:12px;color:var(--text-primary);font-size:14px;font-weight:500">Theme</div>
<div style="display:flex;align-items:center;gap:10px">
<span style="font-size:16px">🌙</span>
<label class="theme-toggle">
<input type="checkbox" id="themeToggle" onchange="toggleThemeSwitch()">
<span class="theme-slider"></span>
</label>
<span style="font-size:16px">☀️</span>
<span style="color:var(--text-secondary);font-size:12px;margin-left:8px">Dark / Light</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variable declarations (must be at top to avoid TDZ errors)
let refreshTimer = null;
let rmiCallTime = 0;        // RMI call timestamp
let selectedPath = null;    // GConfig selected path
let selectedVtype = null;   // GConfig selected value type
let logsTimer = null;       // Logs auto-refresh timer
let logsTaskList = [];      // Logs task list

// Theme initialization
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
// Set toggle state (checked = light mode)
setTimeout(() => {{
    const toggle = document.getElementById('themeToggle');
    if (toggle) toggle.checked = savedTheme === 'light';
}}, 0);

function toggleThemeSwitch() {{
    const toggle = document.getElementById('themeToggle');
    const newTheme = toggle.checked ? 'light' : 'dark';
    document.documentElement.setAttribute('data-theme', newTheme);
    localStorage.setItem('theme', newTheme);
}}

function switchLogsTab(tab) {{
    // Update tab buttons
    document.querySelectorAll('#panel-logs .panel-tab').forEach(t => t.classList.remove('active'));
    document.querySelector(`#panel-logs .panel-tab[onclick*="${{tab}}"]`).classList.add('active');
    // Update tab content
    document.querySelectorAll('#panel-logs .tab-content').forEach(c => c.classList.remove('active'));
    document.getElementById('logs-tab-' + tab).classList.add('active');
}}

const checkbox = document.getElementById('autoRefresh');
const intervalSelect = document.getElementById('refreshInterval');
const savedEnabled = localStorage.getItem('refreshEnabled');
const savedInterval = localStorage.getItem('refreshInterval');
if (savedEnabled !== null) checkbox.checked = savedEnabled === 'true';
if (savedInterval) intervalSelect.value = savedInterval;

function updateStats() {{
    fetch('/api/status').then(r => r.json()).then(data => {{
        document.getElementById('rmi-loading').style.display = 'none';
        document.getElementById('rmi-content').style.display = 'block';

        let html = '';
        data.tasks.forEach((t, idx) => {{
            const treeId = `rmi-tree-${{idx}}`;
            const isExpanded = localStorage.getItem(`rmi-${{t.id}}`) !== 'false';
            const statusColor = t.status === 'running' ? '#4caf50' : t.status === 'stopped' ? '#f44336' : '#ff9800';

            html += `<div class="tree-node">
                <div class="tree-parent" onclick="toggleRmiTree('${{treeId}}','${{t.id}}')" style="display:flex;align-items:center;padding:10px 12px;background:#1e3a5f;border-radius:4px;margin-bottom:2px;cursor:pointer">
                    <span class="tree-arrow" id="arrow-${{treeId}}" style="margin-right:10px;transition:transform 0.2s">${{isExpanded ? '▼' : '▶'}}</span>
                    <span style="flex:1;font-weight:bold;color:#81d4fa">${{t.id}}</span>
                    <span style="width:100px;color:#9e9e9e">${{t.class}}</span>
                    <span style="width:70px;color:#9e9e9e">${{t.mode}}</span>
                    <span style="width:70px;color:${{statusColor}}">${{t.status}}</span>
                    <span style="width:140px;text-align:center;color:#e0e0e0">${{t.rmi_min.toFixed(1)}} / ${{t.rmi_avg.toFixed(1)}} / ${{t.rmi_max.toFixed(1)}}</span>
                </div>
                <div class="tree-children" id="${{treeId}}" style="margin-left:28px;display:${{isExpanded ? 'block' : 'none'}}">
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:120px;color:#ffcc80">RMI Count</span>
                        <span style="color:#e0e0e0">Rx: ${{t.rmi_rx.toLocaleString()}} / Tx: ${{t.rmi_tx.toLocaleString()}}</span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:120px;color:#ffcc80">Failures</span>
                        <span><span style="color:#f44336">${{t.rmi_fail.toLocaleString()}}</span> RMI / <span style="color:#ff9800">${{t.restart}}</span> Restart</span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:120px;color:#ffcc80">Queue Size</span>
                        <span style="color:#e0e0e0">RxQ: ${{t.rxq_size}} / TxQ: ${{t.txq_size}}</span>
                    </div>
                </div>
            </div>`;
        }});
        document.getElementById('rmi-tree').innerHTML = html;
        document.getElementById('info-text').innerHTML =
            `Uptime: ${{data.uptime}} | Tasks: ${{data.running}}/${{data.total}} running | ${{data.timestamp}}`;
    }}).catch(err => {{
        document.getElementById('rmi-loading').textContent = 'Error: ' + err.message;
        document.getElementById('rmi-loading').style.color = '#f44336';
    }});
}}

function toggleRmiTree(treeId, taskId) {{
    const el = document.getElementById(treeId);
    const arrow = document.getElementById('arrow-' + treeId);
    const isHidden = el.style.display === 'none';
    el.style.display = isHidden ? 'block' : 'none';
    arrow.textContent = isHidden ? '▼' : '▶';
    localStorage.setItem(`rmi-${{taskId}}`, isHidden ? 'true' : 'false');
}}

function refreshActivePanel() {{
    const activePanel = localStorage.getItem('activePanel') || 'cpu';
    if (activePanel === 'cpu') loadCpuData();
    else if (activePanel === 'rmi') updateStats();
    else if (activePanel === 'rmicall') loadRmiCallData();
    // cpu, gconfig: no auto-refresh (static data)
}}

function startRefresh() {{
    stopRefresh();
    if (checkbox.checked) {{
        refreshTimer = setInterval(refreshActivePanel, parseInt(intervalSelect.value) * 1000);
    }}
}}

function stopRefresh() {{
    if (refreshTimer) {{ clearInterval(refreshTimer); refreshTimer = null; }}
}}

checkbox.addEventListener('change', () => {{
    localStorage.setItem('refreshEnabled', checkbox.checked);
    startRefresh();
}});

intervalSelect.addEventListener('change', () => {{
    localStorage.setItem('refreshInterval', intervalSelect.value);
    startRefresh();
}});

startRefresh();

function clearStats() {{ fetch('/api/clear').then(() => updateStats()); }}

function manualRefresh() {{
    const activePanel = localStorage.getItem('activePanel') || 'cpu';
    if (activePanel === 'cpu') loadCpuData();
    else if (activePanel === 'rmi') updateStats();
    else if (activePanel === 'rmicall') loadRmiCallData();
    else if (activePanel === 'gconfig') loadGConfigData();
}}

function toggleSidebar() {{
    const sidebar = document.getElementById('sidebar');
    sidebar.classList.toggle('collapsed');
    localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
}}

function toggleCategory(el) {{
    el.classList.toggle('collapsed');
}}

function showPanel(name) {{
    const panel = document.querySelector(`.panel#panel-${{name}}`);
    const navItem = document.querySelector(`.nav-item[data-panel="${{name}}"]`);
    if (!panel || !navItem) {{
        name = 'cpu';  // Fallback to default panel (Task/Mem)
        localStorage.setItem('activePanel', name);
        return showPanel(name);
    }}
    document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
    document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
    panel.classList.add('active');
    navItem.classList.add('active');
    localStorage.setItem('activePanel', name);
    // Load panel data on switch
    if (name === 'rmi') updateStats();
    else if (name === 'rmicall') loadRmiCallData();
    else if (name === 'cpu') loadCpuData();
    else if (name === 'gconfig') loadGConfigData();
    else if (name === 'signals') loadSignalsData();
    else if (name === 'sysinfo') loadSysInfo();
    else if (name === 'extlink') loadExtLink();
    else if (name === 'logs') {{ loadLogs(); startLogsTimer(); }}
    // Restart refresh timer for new panel
    startRefresh();
}}

// Restore sidebar state
if (localStorage.getItem('sidebarCollapsed') === 'true') {{
    document.getElementById('sidebar').classList.add('collapsed');
}}

const savedPanel = localStorage.getItem('activePanel');
if (savedPanel) showPanel(savedPanel);

let cpuTasksData = [];  // Store tasks data for selection
let selectedTaskId = null;
let systemCpuData = {{cpu: 0, mem: 0}};  // Store system CPU/Memory data

function loadCpuData() {{
    fetch('/api/cpu').then(r => r.json()).then(data => {{
        document.getElementById('cpu-loading').style.display = 'none';
        document.getElementById('cpu-content').style.display = 'block';
        if (data.error) {{
            document.getElementById('cpu-tbody').innerHTML = '<tr><td colspan="7" style="color:#f44336;padding:12px">psutil not installed. Run: pip install psutil</td></tr>';
            return;
        }}
        systemCpuData = {{cpu: data.system_cpu, mem: data.system_memory}};

        cpuTasksData = data.tasks;  // Store for selection
        let html = '';
        data.tasks.forEach((t, idx) => {{
            const cpuClass = t.cpu < 30 ? 'cpu-low' : t.cpu < 70 ? 'cpu-mid' : 'cpu-high';
            const statusColor = t.alive ? '#4caf50' : '#f44336';
            const failColor = t.rmi_fail > 0 ? '#f44336' : '#666';
            const restartColor = t.restart > 0 ? '#ff9800' : '#666';
            const isSelected = selectedTaskId === t.id;
            const rowBg = isSelected ? '#2a4a6f' : (idx % 2 === 0 ? '#1e3a5f' : '#1a3050');
            // Mode icon: Process=CPU chip, Thread=branch
            const modeColor = t.mode === 'process' ? '#64b5f6' : '#ce93d8';
            const modeIcon = t.mode === 'process'
                ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="' + modeColor + '" stroke-width="2" title="Process"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg>'
                : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="' + modeColor + '" stroke-width="2" title="Thread"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 009 9"/></svg>';

            html += `<tr onclick="selectTask('${{t.id}}')" style="cursor:pointer;background:${{rowBg}}"
                onmouseover="this.style.background='#2a4a6f'"
                onmouseout="this.style.background='${{isSelected ? '#2a4a6f' : rowBg}}'">
                <td style="padding:8px 6px;text-align:center"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${{statusColor}}"></span></td>
                <td style="padding:8px 10px;color:#81d4fa;font-weight:bold"><span style="display:inline-flex;align-items:center;gap:6px">${{t.id}}${{modeIcon}}</span></td>
                <td style="padding:8px 10px;color:#9e9e9e">${{t.class || '-'}}</td>
                <td style="padding:8px 10px;text-align:center;font-size:11px"><span style="color:${{failColor}}">${{t.rmi_fail}}</span>/<span style="color:${{restartColor}}">${{t.restart}}</span></td>
                <td style="padding:8px 10px">
                    <div style="display:flex;align-items:center;gap:4px">
                        <div class="cpu-bar" style="height:12px;width:70px">
                            <div class="cpu-bar-fill ${{cpuClass}}" style="width:${{Math.min(t.cpu, 100)}}%"></div>
                        </div>
                        <span style="color:#e0e0e0;font-size:11px">${{t.cpu.toFixed(1)}}%</span>
                    </div>
                </td>
                <td style="padding:8px 10px;text-align:right;color:#9e9e9e">${{t.memory.toFixed(1)}} MB</td>
                <td style="padding:8px 10px;text-align:right;color:#9e9e9e">${{t.pid || '-'}}</td>
            </tr>`;
        }});

        document.getElementById('cpu-tbody').innerHTML = html;
        loadSmBlockData();
        loadAppInfoData();

        // Refresh selected task vars if any
        if (selectedTaskId) {{
            const task = cpuTasksData.find(t => t.id === selectedTaskId);
            if (task) showTaskVars(task);
        }}
    }}).catch(err => {{
        document.getElementById('cpu-loading').textContent = 'Error: ' + err.message;
        document.getElementById('cpu-loading').style.color = '#f44336';
    }});
}}

function selectTask(taskId) {{
    selectedTaskId = taskId;
    const task = cpuTasksData.find(t => t.id === taskId);
    if (task) {{
        showTaskVars(task);
        loadCpuData();  // Refresh to show selection highlight
    }}
}}

function showTaskVars(task) {{
    const container = document.getElementById('task-vars-content');
    const vars = task.vars || {{}};
    const varKeys = Object.keys(vars);

    if (varKeys.length === 0) {{
        container.innerHTML = `<div style="color:#81d4fa;margin-bottom:8px;font-weight:bold">${{task.id}}</div>
            <div style="color:#666;font-style:italic">No config variables</div>`;
        return;
    }}

    let html = `<div style="color:#81d4fa;margin-bottom:12px;font-weight:bold">${{task.id}}</div>
        <table style="width:100%;border-collapse:collapse;font-size:12px">
        <thead><tr style="background:#0d2137;color:#9e9e9e">
            <th style="padding:6px 8px;text-align:left">Variable</th>
            <th style="padding:6px 8px;text-align:left">Value</th>
            <th style="padding:6px 8px;text-align:center;width:50px"></th>
        </tr></thead><tbody>`;

    varKeys.forEach((k, idx) => {{
        const val = vars[k];
        const inputId = `cpu-cfg-${{task.id}}-${{k}}`;
        const isBool = typeof val === 'boolean';
        const rowBg = idx % 2 === 0 ? '#1a3050' : '#1e3a5f';

        if (isBool) {{
            html += `<tr style="background:${{rowBg}}">
                <td style="padding:6px 8px;color:#ffcc80">${{k}}</td>
                <td style="padding:6px 8px">
                    <label style="cursor:pointer;margin-right:8px">
                        <input type="radio" name="${{inputId}}" value="true" ${{val ? 'checked' : ''}}> <span style="color:#4caf50">T</span>
                    </label>
                    <label style="cursor:pointer">
                        <input type="radio" name="${{inputId}}" value="false" ${{!val ? 'checked' : ''}}> <span style="color:#f44336">F</span>
                    </label>
                </td>
                <td style="padding:6px 8px;text-align:center">
                    <button class="btn-save" style="padding:2px 6px;font-size:10px" onclick="updateCpuConfigBool('${{task.id}}','${{k}}','${{inputId}}')">Save</button>
                    <span id="msg-${{inputId}}" class="msg" style="font-size:10px"></span>
                </td>
            </tr>`;
        }} else {{
            html += `<tr style="background:${{rowBg}}">
                <td style="padding:6px 8px;color:#ffcc80">${{k}}</td>
                <td style="padding:6px 8px">
                    <input type="text" id="${{inputId}}" class="config-input" style="width:100%;padding:3px 6px;font-size:11px" value="${{val}}">
                </td>
                <td style="padding:6px 8px;text-align:center">
                    <button class="btn-save" style="padding:2px 6px;font-size:10px" onclick="updateCpuConfig('${{task.id}}','${{k}}','${{inputId}}')">Save</button>
                    <span id="msg-${{inputId}}" class="msg" style="font-size:10px"></span>
                </td>
            </tr>`;
        }}
    }});
    html += `</tbody></table>`;
    container.innerHTML = html;
}}

function updateCpuConfig(taskId, key, inputId) {{
    const input = document.getElementById(inputId);
    const msgEl = document.getElementById('msg-' + inputId);
    fetch('/api/config', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{task_id: taskId, key: key, value: input.value}})
    }}).then(r => r.json()).then(res => {{
        msgEl.className = res.status === 'ok' ? 'msg msg-ok' : 'msg msg-err';
        msgEl.textContent = res.status === 'ok' ? 'OK' : (res.message || 'Error');
        setTimeout(() => {{ msgEl.textContent = ''; }}, 2000);
    }});
}}

function updateCpuConfigBool(taskId, key, inputId) {{
    const radios = document.getElementsByName(inputId);
    const msgEl = document.getElementById('msg-' + inputId);
    let newVal = false;
    radios.forEach(r => {{ if (r.checked) newVal = (r.value === 'true'); }});
    fetch('/api/config', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{task_id: taskId, key: key, value: newVal ? 'true' : 'false'}})
    }}).then(r => r.json()).then(res => {{
        msgEl.className = res.status === 'ok' ? 'msg msg-ok' : 'msg msg-err';
        msgEl.textContent = res.status === 'ok' ? 'OK' : (res.message || 'Error');
        setTimeout(() => {{ msgEl.textContent = ''; }}, 2000);
    }});
}}

function loadSmBlockData() {{
    fetch('/api/smblock').then(r => r.json()).then(data => {{
        const treeEl = document.getElementById('smblock-tree');
        if (!treeEl) return;
        if (data.length === 0) {{
            treeEl.innerHTML = '<div style="color:#666;padding:8px 0">No SmBlock pools configured</div>';
            return;
        }}

        let html = '';
        data.forEach((s, idx) => {{
            if (s.error) {{
                html += `<div style="padding:10px 12px;background:#3e2723;border-radius:4px;margin-bottom:2px">
                    <span style="color:#f44336">${{s.name}}: ${{s.error}}</span>
                </div>`;
                return;
            }}

            const treeId = `smblock-tree-${{idx}}`;
            const isExpanded = localStorage.getItem(`smblock-${{s.name}}`) !== 'false';
            const usageClass = s.usage_pct > 80 ? 'fail' : s.usage_pct > 50 ? 'warn' : '';
            const barColor = s.usage_pct > 80 ? '#f44336' : s.usage_pct > 50 ? '#ff9800' : '#4caf50';
            const users = s.users || [];

            // Parent row: SmBlock summary
            html += `<div class="tree-node">
                <div class="tree-parent" onclick="toggleSmBlockTree('${{treeId}}','${{s.name}}')" style="display:flex;align-items:center;padding:10px 12px;background:#1e3a5f;border-radius:4px;margin-bottom:2px;cursor:pointer">
                    <span class="tree-arrow" id="arrow-${{treeId}}" style="margin-right:10px">${{isExpanded ? '▼' : '▶'}}</span>
                    <span style="width:120px;font-weight:bold;color:#81d4fa">${{s.name}}</span>
                    <span style="width:130px;color:#9e9e9e">${{s.shape.join(' x ')}}</span>
                    <span style="width:80px;color:#9e9e9e">max: ${{s.maxsize}}</span>
                    <span style="width:150px">
                        <div style="display:flex;align-items:center;gap:8px">
                            <div class="cpu-bar" style="flex:1;height:14px">
                                <div class="cpu-bar-fill" style="width:${{s.usage_pct}}%;background:${{barColor}}"></div>
                            </div>
                            <span class="num ${{usageClass}}" style="width:45px">${{s.usage_pct}}%</span>
                        </div>
                    </span>
                    <span style="width:80px;text-align:right;color:#9e9e9e">${{s.total_mb}} MB</span>
                </div>
                <div class="tree-children" id="${{treeId}}" style="margin-left:28px;display:${{isExpanded ? 'block' : 'none'}}">
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:100px;color:#ffcc80">Shape</span>
                        <span style="color:#e0e0e0">${{s.shape.join(' x ')}} (H x W x C)</span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:100px;color:#ffcc80">MaxSize</span>
                        <span style="color:#e0e0e0">${{s.maxsize}} blocks</span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:100px;color:#ffcc80">Item Size</span>
                        <span style="color:#e0e0e0">${{s.item_size.toLocaleString()}} bytes (${{(s.item_size / 1024 / 1024).toFixed(2)}} MB)</span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:100px;color:#ffcc80">Total Size</span>
                        <span style="color:#e0e0e0">${{s.total_mb}} MB</span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:100px;color:#ffcc80">Used/Free</span>
                        <span><span style="color:#f44336">${{s.used}}</span> / <span style="color:#4caf50">${{s.free}}</span></span>
                    </div>
                    <div style="display:flex;padding:6px 12px;border-bottom:1px solid #333">
                        <span style="width:100px;color:#ffcc80">Users</span>
                        <span style="color:#64b5f6">${{users.length > 0 ? users.join(', ') : '(none)'}}</span>
                    </div>
                </div>
            </div>`;
        }});

        treeEl.innerHTML = html;
    }}).catch(err => {{
        const treeEl = document.getElementById('smblock-tree');
        if (treeEl) treeEl.innerHTML = `<div style="color:#f44336">Error: ${{err.message}}</div>`;
    }});
}}

function toggleSmBlockTree(treeId, name) {{
    const el = document.getElementById(treeId);
    const arrow = document.getElementById('arrow-' + treeId);
    const isHidden = el.style.display === 'none';
    el.style.display = isHidden ? 'block' : 'none';
    arrow.textContent = isHidden ? '▼' : '▶';
    localStorage.setItem(`smblock-${{name}}`, isHidden ? 'true' : 'false');
}}

function loadAppInfoData() {{
    fetch('/api/gconfig').then(r => r.json()).then(data => {{
        const el = document.getElementById('appinfo-content');
        if (!el) return;
        const info = data.data?.app_info || {{}};
        const name = info.name || '-';
        const version = info.version || '-';
        const id = info.id || '-';
        const filepath = data.filepath || '-';
        const homeDir = data.home_dir || '-';
        const logDir = data.log_dir || '-';

        const cpuVal = systemCpuData.cpu ? systemCpuData.cpu.toFixed(1) : '-';
        const memVal = systemCpuData.mem ? systemCpuData.mem.toFixed(1) : '-';
        el.innerHTML = `
            <div style="display:flex;flex-wrap:wrap;gap:20px;font-size:13px;margin-bottom:8px">
                <span><span style="color:#9e9e9e">Name:</span> <strong style="color:#81d4fa">${{name}}</strong></span>
                <span><span style="color:#9e9e9e">Ver:</span> <span style="color:#e0e0e0">${{version}}</span></span>
                <span><span style="color:#9e9e9e">ID:</span> <span style="color:#e0e0e0">${{id}}</span></span>
                <span><span style="color:#9e9e9e">CPU:</span> <strong style="color:#4fc3f7">${{cpuVal}}</strong>%</span>
                <span><span style="color:#9e9e9e">Memory:</span> <strong style="color:#4fc3f7">${{memVal}}</strong>%</span>
                <button class="btn-refresh" style="padding:2px 8px;font-size:11px" onclick="loadCpuData()">Refresh</button>
            </div>
            <div style="font-size:12px;color:#9e9e9e;line-height:1.6">
                <div><span style="color:#ffcc80">Config:</span> <span style="color:#666">${{filepath}}</span></div>
                <div><span style="color:#ffcc80">Home_Dir:</span> <span style="color:#666">${{homeDir}}</span></div>
                <div><span style="color:#ffcc80">Log_Dir:</span> <span style="color:#666">${{logDir}}</span></div>
            </div>`;
    }}).catch(err => {{
        const el = document.getElementById('appinfo-content');
        if (el) el.innerHTML = `<div style="color:#f44336">Error: ${{err.message}}</div>`;
    }});
}}

let rmiCallPrev = {{}};  // taskId:method -> count
let rmiCallCache = null; // cached data for diff check

function loadRmiCallData() {{
    fetch('/api/rmi_methods').then(r => r.json()).then(data => {{
        document.getElementById('rmicall-loading').style.display = 'none';
        document.getElementById('rmicall-content').style.display = 'block';

        const now = Date.now();
        const elapsed = rmiCallTime > 0 ? (now - rmiCallTime) / 1000 : 0;
        rmiCallTime = now;

        document.getElementById('rmicall-interval').textContent = elapsed > 0 ? `Interval: ${{elapsed.toFixed(1)}}s` : 'Interval: -';

        let html = '';
        let newPrev = {{}};

        data.forEach((task, idx) => {{
            const methods = Object.entries(task.methods).sort((a, b) => b[1] - a[1]);
            let taskPrevTotal = 0, taskCurrTotal = 0;

            // Calculate totals
            methods.forEach(m => {{
                const key = `${{task.id}}:${{m[0]}}`;
                const prev = rmiCallPrev[key] || 0;
                taskPrevTotal += prev;
                taskCurrTotal += m[1];
                newPrev[key] = m[1];
            }});

            const totalDiff = taskCurrTotal - taskPrevTotal;
            const totalPerSec = elapsed > 0 ? (totalDiff / elapsed) : 0;
            const treeId = `tree-${{idx}}`;
            const isExpanded = localStorage.getItem(`rmicall-${{task.id}}`) !== 'false';
            const statusClass = task.alive ? 'running' : 'stopped';
            const statusText = task.alive ? 'running' : 'stopped';

            // Parent node: Task/Class/Mode/Status/Sum
            const hasChildren = methods.length > 0;
            const arrowHtml = hasChildren ? `<span class="tree-arrow" id="arrow-${{treeId}}" style="margin-right:8px;transition:transform 0.2s">${{isExpanded ? '▼' : '▶'}}</span>` : `<span style="width:20px"></span>`;
            const clickAttr = hasChildren ? `onclick="toggleTree('${{treeId}}','${{task.id}}')" style="display:flex;align-items:center;padding:8px 12px;background:#1e3a5f;border-radius:4px;margin-bottom:2px;cursor:pointer"` : `style="display:flex;align-items:center;padding:8px 12px;background:#1e3a5f;border-radius:4px;margin-bottom:2px"`;

            html += `<div class="tree-node">
                <div class="tree-parent" ${{clickAttr}}>
                    ${{arrowHtml}}
                    <span style="flex:1;font-weight:bold">${{task.id}}</span>
                    <span style="width:120px;color:#9e9e9e">${{task.class}}</span>
                    <span style="width:70px;color:#9e9e9e">${{task.mode || '-'}}</span>
                    <span style="width:70px" class="${{statusClass}}">${{statusText}}</span>
                    <span style="width:80px;text-align:right;color:#4fc3f7">${{taskCurrTotal.toLocaleString()}}</span>
                    <span style="width:80px;text-align:right;color:${{totalPerSec > 0 ? '#4caf50' : '#666'}}">${{totalPerSec.toFixed(1)}}/s</span>
                </div>`;

            if (hasChildren) {{
                const timing = task.methods_timing || {{}};
                html += `<div class="tree-children" id="${{treeId}}" style="margin-left:20px;display:${{isExpanded ? 'block' : 'none'}}">`;
                // Header for method details
                html += `<div style="display:flex;align-items:center;padding:4px 12px;border-bottom:1px solid #444;color:#9e9e9e;font-size:11px">
                    <span style="width:28px"></span>
                    <span style="flex:1">Method</span>
                    <span style="width:60px;text-align:right">Prev</span>
                    <span style="width:60px;text-align:right">Curr</span>
                    <span style="width:60px;text-align:right">Rate</span>
                    <span style="width:140px;text-align:center;color:#ff9800">IPC (min/avg/max)</span>
                    <span style="width:140px;text-align:center;color:#ff9800">FUNC (min/avg/max)</span>
                </div>`;
                methods.forEach(m => {{
                    const key = `${{task.id}}:${{m[0]}}`;
                    const methodName = m[0];
                    const curr = m[1];
                    const prev = rmiCallPrev[key] || 0;
                    const diff = curr - prev;
                    const perSec = elapsed > 0 ? (diff / elapsed) : 0;

                    const isOneWay = methodName.startsWith('on_');
                    const wayBadge = isOneWay
                        ? '<span style="background:#ff9800;color:#000;padding:1px 5px;border-radius:3px;font-size:10px;margin-right:6px">1W</span>'
                        : '<span style="background:#2196f3;color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;margin-right:6px">2W</span>';

                    // Get timing data for this method
                    const t = timing[methodName];
                    let ipcStr = '-';
                    let funcStr = '-';
                    if (t && t.ipc && t.func) {{
                        ipcStr = `${{t.ipc.min}}/${{t.ipc.avg}}/${{t.ipc.max}}`;
                        funcStr = `${{t.func.min}}/${{t.func.avg}}/${{t.func.max}}`;
                    }}

                    html += `<div style="display:flex;align-items:center;padding:4px 12px;border-bottom:1px solid #333">
                        ${{wayBadge}}
                        <span style="flex:1">${{methodName}}</span>
                        <span style="width:60px;text-align:right;color:#9e9e9e">${{prev.toLocaleString()}}</span>
                        <span style="width:60px;text-align:right">${{curr.toLocaleString()}}</span>
                        <span style="width:60px;text-align:right;color:${{perSec > 0 ? '#4caf50' : '#666'}}">${{perSec.toFixed(1)}}/s</span>
                        <span style="width:140px;text-align:center;color:#81d4fa">${{ipcStr}}</span>
                        <span style="width:140px;text-align:center;color:#a5d6a7">${{funcStr}}</span>
                    </div>`;
                }});
                html += `</div>`;
            }}
            html += `</div>`;
        }});

        rmiCallPrev = newPrev;
        document.getElementById('rmicall-tree').innerHTML = html;
        updateMeasurementStatus();
    }});
}}

function toggleTree(treeId, taskId) {{
    const el = document.getElementById(treeId);
    const arrow = document.getElementById('arrow-' + treeId);
    const isHidden = el.style.display === 'none';
    el.style.display = isHidden ? 'block' : 'none';
    arrow.textContent = isHidden ? '▼' : '▶';
    localStorage.setItem(`rmicall-${{taskId}}`, isHidden ? 'true' : 'false');
}}

function updateMeasurementStatus() {{
    fetch('/api/measurement').then(r => r.json()).then(data => {{
        const btn = document.getElementById('measurement-toggle');
        const status = document.getElementById('measurement-status');
        if (data.enabled) {{
            btn.style.background = '#4caf50';
            status.textContent = 'ON';
        }} else {{
            btn.style.background = '#f44336';
            status.textContent = 'OFF';
        }}
    }});
}}

function toggleMeasurement() {{
    fetch('/api/measurement', {{method: 'POST', headers: {{'Content-Type': 'application/json'}}, body: '{{}}'}})
        .then(r => r.json())
        .then(data => {{
            if (data.status === 'ok') {{
                updateMeasurementStatus();
            }}
        }});
}}

function loadGConfigData() {{
    fetch('/api/gconfig').then(r => r.json()).then(data => {{
        document.getElementById('gconfig-loading').style.display = 'none';
        document.getElementById('gconfig-content').style.display = 'block';
        if (data.error) {{
            document.getElementById('gconfig-tree').innerHTML = `<p style="color:#f44336">${{data.error}}</p>`;
            return;
        }}
        document.getElementById('gconfig-filepath').textContent = data.filepath || 'No file';
        document.getElementById('gconfig-tree').innerHTML = renderTree(data.data, '');
        // Reset value setter
        selectedPath = null;
        selectedVtype = null;
        document.getElementById('setter-content').innerHTML = '<div class="value-setter-empty">트리에서 값을 선택하세요</div>';
    }});
}}

function loadSignalsData() {{
    fetch('/api/subscriptions').then(r => r.json()).then(data => {{
        document.getElementById('signals-loading').style.display = 'none';
        document.getElementById('signals-content').style.display = 'block';
        document.getElementById('signals-mode').textContent = data.mode || 'unknown';
        if (data.error) {{
            document.getElementById('signals-subs').innerHTML = `<p style="color:#f44336">${{data.error}}</p>`;
            return;
        }}
        // Render subscriptions
        let subsHtml = '';
        if (data.subscriptions && data.subscriptions.length > 0) {{
            data.subscriptions.forEach(sub => {{
                const subscriberList = sub.subscribers.map(s => `<span style="color:#81d4fa;padding:2px 6px;background:#1e3a5f;border-radius:3px;margin:2px;display:inline-block">${{s}}</span>`).join('');
                subsHtml += `<div style="margin-bottom:12px;padding:10px;background:#383838;border-radius:4px;border-left:3px solid #ff9800">
                    <div style="color:#ffcc80;font-weight:bold;margin-bottom:6px">${{sub.signal}}</div>
                    <div>${{subscriberList}}</div>
                </div>`;
            }});
        }} else {{
            subsHtml = '<div style="color:#666;font-style:italic">No active subscriptions</div>';
        }}
        document.getElementById('signals-subs').innerHTML = subsHtml;
        // Render queues
        let queuesHtml = '';
        if (data.queues && data.queues.length > 0) {{
            queuesHtml = data.queues.map(q => `<div style="padding:6px 10px;background:#1e3a5f;border-radius:4px;margin-bottom:4px;color:#81d4fa">${{q}}</div>`).join('');
        }} else {{
            queuesHtml = '<div style="color:#666;font-style:italic">No registered queues</div>';
        }}
        document.getElementById('signals-queues').innerHTML = queuesHtml;
        // Render stats
        const stats = data.stats || {{}};
        const statsStatus = document.getElementById('signals-stats-status');
        const clearedAt = document.getElementById('signals-cleared-at');
        statsStatus.textContent = stats.enabled ? 'ON' : 'OFF';
        statsStatus.parentElement.style.background = stats.enabled ? '#4caf50' : '#666';
        if (stats.cleared_at && stats.cleared_at > 0) {{
            const clearDate = new Date(stats.cleared_at * 1000);
            clearedAt.textContent = `Cleared: ${{clearDate.toLocaleString()}}`;
        }} else {{
            clearedAt.textContent = '';
        }}
        let statsHtml = '';
        const signals = stats.signals || {{}};
        const signalNames = Object.keys(signals);
        if (signalNames.length > 0) {{
            signalNames.forEach(name => {{
                const s = signals[name];
                const lastTime = s.last_time > 0 ? new Date(s.last_time * 1000).toLocaleString() : '-';
                statsHtml += `<div style="padding:8px 10px;background:#383838;border-radius:4px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">
                    <span style="color:#ffcc80;font-weight:bold">${{name}}</span>
                    <span style="display:flex;gap:16px">
                        <span style="color:#81d4fa">Count: <span style="color:#4caf50;font-weight:bold">${{s.count.toLocaleString()}}</span></span>
                        <span style="color:#9e9e9e;font-size:11px">Last: ${{lastTime}}</span>
                    </span>
                </div>`;
            }});
        }} else {{
            statsHtml = '<div style="color:#666;font-style:italic">No signal stats recorded</div>';
        }}
        document.getElementById('signals-stats').innerHTML = statsHtml;
    }}).catch(err => {{
        document.getElementById('signals-loading').textContent = 'Error: ' + err.message;
        document.getElementById('signals-loading').style.color = '#f44336';
    }});
}}

function toggleSignalStats() {{
    fetch('/api/subscriptions/toggle', {{method: 'POST'}}).then(r => r.json()).then(data => {{
        if (data.status === 'ok') {{
            loadSignalsData();
        }}
    }});
}}

function clearSignalStats() {{
    if (confirm('Clear all signal statistics?')) {{
        fetch('/api/subscriptions/clear', {{method: 'POST'}}).then(r => r.json()).then(data => {{
            if (data.status === 'ok') {{
                loadSignalsData();
            }}
        }});
    }}
}}

function loadSysInfo() {{
    fetch('/api/sysinfo').then(r => r.json()).then(data => {{
        document.getElementById('sysinfo-loading').style.display = 'none';
        document.getElementById('sysinfo-content').style.display = 'block';
        if (data.error) {{
            document.getElementById('sysinfo-system').innerHTML = `<p style="color:#f44336">${{data.error}}</p>`;
            return;
        }}
        // System
        const sys = data.system || {{}};
        document.getElementById('sysinfo-system').innerHTML = `
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;width:100px;display:inline-block">OS:</span><span style="color:#81d4fa">${{sys.os || 'N/A'}}</span></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;width:100px;display:inline-block">Hostname:</span><span style="color:#e0e0e0">${{sys.hostname || 'N/A'}}</span></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;width:100px;display:inline-block">Windows ID:</span><span style="color:#e0e0e0">${{sys.windows_id || 'N/A'}}</span></div>
            <div><span style="color:#9e9e9e;width:100px;display:inline-block">Uptime:</span><span style="color:#4caf50">${{sys.uptime_str || 'N/A'}}</span></div>
        `;
        // CPU
        const cpu = data.cpu || {{}};
        document.getElementById('sysinfo-cpu').innerHTML = `
            <div style="margin-bottom:6px"><span style="color:#9e9e9e">Name:</span> <span style="color:#e0e0e0">${{cpu.name || 'N/A'}}</span></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e">Cores:</span> <span style="color:#81d4fa">${{cpu.cores_physical || 0}}C / ${{cpu.cores_logical || 0}}T</span></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e">Freq:</span> <span style="color:#e0e0e0">${{cpu.frequency_mhz || 0}} MHz</span></div>
            <div><span style="color:#9e9e9e">Usage:</span> <span style="color:#ff9800">${{(cpu.usage_percent || 0).toFixed(1)}}%</span></div>
        `;
        // Memory
        const mem = data.memory || {{}};
        const memPct = mem.usage_percent || 0;
        const memColor = memPct > 90 ? '#f44336' : memPct > 70 ? '#ff9800' : '#4caf50';
        document.getElementById('sysinfo-memory').innerHTML = `
            <div style="margin-bottom:6px"><span style="color:#9e9e9e">Total:</span> <span style="color:#e0e0e0">${{mem.total_gb || 0}} GB</span></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e">Used:</span> <span style="color:#81d4fa">${{mem.used_gb || 0}} GB</span></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e">Available:</span> <span style="color:#4caf50">${{mem.available_gb || 0}} GB</span></div>
            <div><span style="color:#9e9e9e">Usage:</span> <span style="color:${{memColor}}">${{memPct.toFixed(1)}}%</span></div>
        `;
        // GPU
        const gpus = data.gpu || [];
        if (gpus.length > 0) {{
            document.getElementById('sysinfo-gpu').innerHTML = gpus.map(g => `
                <div style="margin-bottom:8px;padding:8px;background:#383838;border-radius:4px">
                    <div style="color:#81d4fa;font-weight:bold">${{g.name || 'Unknown'}}</div>
                    <div style="color:#9e9e9e;font-size:12px">VRAM: ${{g.vram_mb || 0}} MB | Driver: ${{g.driver_version || 'N/A'}}</div>
                </div>
            `).join('');
        }} else {{
            document.getElementById('sysinfo-gpu').innerHTML = '<div style="color:#666;font-style:italic">No GPU info</div>';
        }}
        // Storage
        const storage = data.storage || [];
        if (storage.length > 0) {{
            document.getElementById('sysinfo-storage').innerHTML = storage.map(s => {{
                const pct = s.usage_percent || 0;
                const color = pct > 95 ? '#f44336' : pct > 80 ? '#ff9800' : '#4caf50';
                return `<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
                    <span style="color:#81d4fa;width:50px">${{s.device || s.mountpoint}}</span>
                    <div style="flex:1;background:#333;height:16px;border-radius:3px;overflow:hidden">
                        <div style="width:${{pct}}%;height:100%;background:${{color}}"></div>
                    </div>
                    <span style="color:#e0e0e0;width:160px;text-align:right">${{s.used_gb}}/${{s.total_gb}} GB (${{pct.toFixed(1)}}%)</span>
                </div>`;
            }}).join('');
        }} else {{
            document.getElementById('sysinfo-storage').innerHTML = '<div style="color:#666;font-style:italic">No storage info</div>';
        }}
        // Network
        const net = data.network || {{}};
        const ifaces = net.interfaces || [];
        if (ifaces.length > 0) {{
            document.getElementById('sysinfo-network').innerHTML = `<table style="width:100%;font-size:12px">
                <tr style="color:#9e9e9e"><th style="text-align:left">Name</th><th style="text-align:left">IP</th><th style="text-align:left">MAC</th></tr>
                ${{ifaces.map(i => `<tr><td style="color:#81d4fa">${{i.name}}</td><td style="color:#e0e0e0">${{i.ip}}</td><td style="color:#666">${{i.mac}}</td></tr>`).join('')}}
            </table>`;
        }} else {{
            document.getElementById('sysinfo-network').innerHTML = '<div style="color:#666;font-style:italic">No network info</div>';
        }}
        // ARP
        const arp = net.arp_table || [];
        if (arp.length > 0) {{
            document.getElementById('sysinfo-arp').innerHTML = `<table style="width:100%;font-size:12px">
                <tr style="color:#9e9e9e"><th style="text-align:left">IP</th><th style="text-align:left">MAC</th></tr>
                ${{arp.slice(0, 20).map(a => `<tr><td style="color:#e0e0e0">${{a.ip}}</td><td style="color:#666">${{a.mac}}</td></tr>`).join('')}}
            </table>${{arp.length > 20 ? `<div style="color:#666;font-size:11px">... and ${{arp.length - 20}} more</div>` : ''}}`;
        }} else {{
            document.getElementById('sysinfo-arp').innerHTML = '<div style="color:#666;font-style:italic">No ARP entries</div>';
        }}
        // Routing
        const routes = net.routing_table || [];
        if (routes.length > 0) {{
            document.getElementById('sysinfo-routing').innerHTML = `<table style="width:100%;font-size:12px">
                <tr style="color:#9e9e9e"><th style="text-align:left">Dest</th><th style="text-align:left">Gateway</th><th style="text-align:left">Metric</th></tr>
                ${{routes.slice(0, 15).map(r => `<tr><td style="color:#e0e0e0">${{r.destination}}</td><td style="color:#81d4fa">${{r.gateway}}</td><td style="color:#666">${{r.metric || '-'}}</td></tr>`).join('')}}
            </table>${{routes.length > 15 ? `<div style="color:#666;font-size:11px">... and ${{routes.length - 15}} more</div>` : ''}}`;
        }} else {{
            document.getElementById('sysinfo-routing').innerHTML = '<div style="color:#666;font-style:italic">No routing entries</div>';
        }}
    }}).catch(err => {{
        document.getElementById('sysinfo-loading').style.display = 'none';
        document.getElementById('sysinfo-content').style.display = 'block';
        document.getElementById('sysinfo-system').innerHTML = `<p style="color:#f44336">Error: ${{err.message}}</p>`;
    }});
    // Load memory graph after sysinfo
    loadMemoryGraph();
}}

function loadMemoryGraph() {{
    fetch('/api/sysinfo/memory_history').then(r => r.json()).then(data => {{
        const history = data.history || [];
        if (history.length < 2) {{
            document.getElementById('memory-current').textContent = 'Current: Collecting data...';
            document.getElementById('memory-trend').textContent = 'Trend: Need more data';
            return;
        }}

        const canvas = document.getElementById('memory-graph');
        const ctx = canvas.getContext('2d');
        const width = canvas.width;
        const height = canvas.height;

        // Clear canvas
        ctx.fillStyle = '#1a1a1a';
        ctx.fillRect(0, 0, width, height);

        // Calculate min/max for scaling
        const values = history.map(h => h.used_gb);
        const minVal = Math.min(...values);
        const maxVal = Math.max(...values);
        const range = maxVal - minVal || 1;
        const padding = range * 0.1;
        const yMin = Math.max(0, minVal - padding);
        const yMax = maxVal + padding;
        const yRange = yMax - yMin;

        // Draw grid lines
        ctx.strokeStyle = '#333';
        ctx.lineWidth = 1;
        for (let i = 0; i <= 4; i++) {{
            const y = height - (i / 4) * height;
            ctx.beginPath();
            ctx.moveTo(0, y);
            ctx.lineTo(width, y);
            ctx.stroke();
        }}

        // Draw memory usage line
        ctx.strokeStyle = '#2196f3';
        ctx.lineWidth = 2;
        ctx.beginPath();
        history.forEach((point, i) => {{
            const x = (i / (history.length - 1)) * width;
            const y = height - ((point.used_gb - yMin) / yRange) * height;
            if (i === 0) ctx.moveTo(x, y);
            else ctx.lineTo(x, y);
        }});
        ctx.stroke();

        // Draw trend line (linear regression)
        const n = history.length;
        let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
        history.forEach((p, i) => {{
            sumX += i;
            sumY += p.used_gb;
            sumXY += i * p.used_gb;
            sumX2 += i * i;
        }});
        const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
        const intercept = (sumY - slope * sumX) / n;

        // Draw trend line
        ctx.strokeStyle = slope > 0.001 ? '#ff5722' : slope < -0.001 ? '#4caf50' : '#9e9e9e';
        ctx.lineWidth = 1;
        ctx.setLineDash([5, 5]);
        ctx.beginPath();
        const y0 = height - ((intercept - yMin) / yRange) * height;
        const y1 = height - ((slope * (n - 1) + intercept - yMin) / yRange) * height;
        ctx.moveTo(0, y0);
        ctx.lineTo(width, y1);
        ctx.stroke();
        ctx.setLineDash([]);

        // Update status text
        const latest = history[history.length - 1];
        const first = history[0];
        document.getElementById('memory-current').textContent = 'Current: ' + latest.used_gb.toFixed(2) + ' GB (' + latest.percent.toFixed(1) + '%)';

        // slope is GB per sample (1 sample = 1 minute), convert to MB/hour
        const slopePerHour = slope * 60 * 1024;  // MB per hour
        let trendText = 'Trend: ';
        let trendColor = '#9e9e9e';
        if (slopePerHour > 10) {{  // More than 10 MB/hour increase
            trendText += '+' + slopePerHour.toFixed(1) + ' MB/h';
            trendColor = '#ff5722';
        }} else if (slopePerHour < -10) {{
            trendText += slopePerHour.toFixed(1) + ' MB/h';
            trendColor = '#4caf50';
        }} else {{
            trendText += 'Stable (' + (slopePerHour >= 0 ? '+' : '') + slopePerHour.toFixed(1) + ' MB/h)';
            trendColor = '#4caf50';
        }}
        document.getElementById('memory-trend').innerHTML = '<span style="color:' + trendColor + '">' + trendText + '</span>';

        // Warning for potential leak (more than 100 MB/hour sustained increase)
        const warningEl = document.getElementById('memory-warning');
        if (slopePerHour > 100) {{
            warningEl.textContent = '⚠️ Potential memory leak detected! (+' + slopePerHour.toFixed(0) + ' MB/h)';
            warningEl.style.display = 'inline';
        }} else {{
            warningEl.style.display = 'none';
        }}

        // Time labels (show date and time for 72-hour window)
        const startTime = new Date(first.ts * 1000);
        const endTime = new Date(latest.ts * 1000);
        const formatDateTime = (d) => {{
            const mm = String(d.getMonth() + 1).padStart(2, '0');
            const dd = String(d.getDate()).padStart(2, '0');
            const hh = String(d.getHours()).padStart(2, '0');
            const mi = String(d.getMinutes()).padStart(2, '0');
            return mm + '/' + dd + ' ' + hh + ':' + mi;
        }};
        document.getElementById('memory-time-start').textContent = formatDateTime(startTime);
        document.getElementById('memory-time-end').textContent = formatDateTime(endTime);

        // Draw Y-axis labels
        ctx.fillStyle = '#666';
        ctx.font = '10px monospace';
        ctx.textAlign = 'left';
        for (let i = 0; i <= 4; i++) {{
            const val = yMin + (i / 4) * yRange;
            const y = height - (i / 4) * height;
            ctx.fillText(val.toFixed(2) + ' GB', 4, y - 2);
        }}

        // Show stats (duration and data points)
        const durationMin = Math.round((latest.ts - first.ts) / 60);
        const durationHours = (durationMin / 60).toFixed(1);
        const maxPoints = data.max_points || 4320;
        document.getElementById('memory-stats').textContent =
            'Duration: ' + durationHours + 'h | Points: ' + history.length + '/' + maxPoints;
    }}).catch(err => {{
        console.error('Memory graph error:', err);
    }});
}}

let extLinkConfig = {{}};
function loadExtLink() {{
    fetch('/api/external').then(r => r.json()).then(data => {{
        document.getElementById('extlink-loading').style.display = 'none';
        document.getElementById('extlink-content').style.display = 'block';
        extLinkConfig = data;
        if (data.error) {{
            document.getElementById('extlink-slack').innerHTML = `<p style="color:#f44336">${{data.error}}</p>`;
            return;
        }}
        // Slack
        const slack = data.slack || {{}};
        document.getElementById('extlink-slack').innerHTML = `
            <div style="margin-bottom:8px"><label><input type="checkbox" id="slack-enabled" ${{slack.enabled ? 'checked' : ''}}> Enabled</label>
            <button class="btn-refresh" style="margin-left:12px;padding:2px 8px;font-size:11px" onclick="testExtLink('slack')">Test</button></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;display:block;margin-bottom:2px">Webhook URL:</span><input type="text" id="slack-url" value="${{slack.webhook_url || ''}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;display:block;margin-bottom:2px">Channel:</span><input type="text" id="slack-channel" value="${{slack.channel || ''}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            <div><span style="color:#9e9e9e;display:block;margin-bottom:2px">Username:</span><input type="text" id="slack-username" value="${{slack.username || 'Alaska Monitor'}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
        `;
        // Email
        const email = data.email || {{}};
        document.getElementById('extlink-email').innerHTML = `
            <div style="margin-bottom:8px"><label><input type="checkbox" id="email-enabled" ${{email.enabled ? 'checked' : ''}}> Enabled</label>
            <button class="btn-refresh" style="margin-left:12px;padding:2px 8px;font-size:11px" onclick="testExtLink('email')">Test</button></div>
            <div style="display:flex;gap:8px;margin-bottom:6px">
                <div style="flex:2"><span style="color:#9e9e9e;display:block;margin-bottom:2px">SMTP Host:</span><input type="text" id="email-host" value="${{email.smtp_host || ''}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
                <div style="flex:1"><span style="color:#9e9e9e;display:block;margin-bottom:2px">Port:</span><input type="number" id="email-port" value="${{email.smtp_port || 587}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            </div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;display:block;margin-bottom:2px">User:</span><input type="text" id="email-user" value="${{email.smtp_user || ''}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;display:block;margin-bottom:2px">To Addrs:</span><input type="text" id="email-to" value="${{(email.to_addrs || []).join(', ')}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
        `;
        // Webhook
        const wh = data.webhook || {{}};
        document.getElementById('extlink-webhook').innerHTML = `
            <div style="margin-bottom:8px"><label><input type="checkbox" id="wh-enabled" ${{wh.enabled ? 'checked' : ''}}> Enabled</label>
            <button class="btn-refresh" style="margin-left:12px;padding:2px 8px;font-size:11px" onclick="testExtLink('webhook')">Test</button></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;display:block;margin-bottom:2px">URL:</span><input type="text" id="wh-url" value="${{wh.url || ''}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            <div style="display:flex;gap:8px;margin-bottom:6px">
                <div style="flex:1"><span style="color:#9e9e9e;display:block;margin-bottom:2px">Method:</span><select id="wh-method" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"><option ${{wh.method === 'POST' ? 'selected' : ''}}>POST</option><option ${{wh.method === 'PUT' ? 'selected' : ''}}>PUT</option></select></div>
                <div style="flex:1"><span style="color:#9e9e9e;display:block;margin-bottom:2px">Timeout:</span><input type="number" id="wh-timeout" value="${{wh.timeout || 10}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            </div>
        `;
        // Health Report
        const hr = data.health_report || {{}};
        document.getElementById('extlink-report').innerHTML = `
            <div style="margin-bottom:8px"><label><input type="checkbox" id="hr-enabled" ${{hr.enabled ? 'checked' : ''}}> Enabled</label>
            <button class="btn-refresh" style="margin-left:12px;padding:2px 8px;font-size:11px" onclick="sendReport()">Send Now</button></div>
            <div style="margin-bottom:6px"><span style="color:#9e9e9e;display:block;margin-bottom:2px">Schedule (HH:MM, comma separated):</span><input type="text" id="hr-schedule" value="${{(hr.schedule || []).join(', ')}}" style="width:100%;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px"></div>
            <div><span style="color:#9e9e9e;display:block;margin-bottom:2px">Targets:</span>
                <label style="margin-right:12px"><input type="checkbox" id="hr-slack" ${{(hr.targets || []).includes('slack') ? 'checked' : ''}}> Slack</label>
                <label style="margin-right:12px"><input type="checkbox" id="hr-email" ${{(hr.targets || []).includes('email') ? 'checked' : ''}}> Email</label>
                <label><input type="checkbox" id="hr-webhook" ${{(hr.targets || []).includes('webhook') ? 'checked' : ''}}> Webhook</label>
            </div>
        `;
        // Alert Settings
        const alert = data.alert || {{}};
        document.getElementById('extlink-alert').innerHTML = `
            <div style="display:flex;flex-wrap:wrap;gap:16px">
                <label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="alert-task-restart" ${{alert.on_task_restart ? 'checked' : ''}}> On Task Restart</label>
                <label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="alert-signal-error" ${{alert.on_signal_error ? 'checked' : ''}}> On Signal Error</label>
                <div style="display:flex;align-items:center;gap:6px">
                    <label>On RMI Fail &gt;</label>
                    <input type="number" id="alert-rmi-threshold" value="${{alert.on_rmi_fail_threshold || 0}}" style="width:60px;padding:4px;background:#333;border:1px solid #555;color:#e0e0e0;border-radius:3px">
                    <span style="color:#9e9e9e">times</span>
                </div>
            </div>
        `;
    }}).catch(err => {{
        document.getElementById('extlink-loading').style.display = 'none';
        document.getElementById('extlink-content').style.display = 'block';
        document.getElementById('extlink-slack').innerHTML = `<p style="color:#f44336">Error: ${{err.message}}</p>`;
    }});
}}

function saveExtLink() {{
    const config = {{
        slack: {{
            enabled: document.getElementById('slack-enabled').checked,
            webhook_url: document.getElementById('slack-url').value,
            channel: document.getElementById('slack-channel').value,
            username: document.getElementById('slack-username').value
        }},
        email: {{
            enabled: document.getElementById('email-enabled').checked,
            smtp_host: document.getElementById('email-host').value,
            smtp_port: parseInt(document.getElementById('email-port').value) || 587,
            smtp_user: document.getElementById('email-user').value,
            to_addrs: document.getElementById('email-to').value.split(',').map(s => s.trim()).filter(s => s)
        }},
        webhook: {{
            enabled: document.getElementById('wh-enabled').checked,
            url: document.getElementById('wh-url').value,
            method: document.getElementById('wh-method').value,
            timeout: parseInt(document.getElementById('wh-timeout').value) || 10
        }},
        health_report: {{
            enabled: document.getElementById('hr-enabled').checked,
            schedule: document.getElementById('hr-schedule').value.split(',').map(s => s.trim()).filter(s => s),
            targets: ['slack', 'email', 'webhook'].filter(t => document.getElementById('hr-' + t).checked)
        }},
        alert: {{
            on_task_restart: document.getElementById('alert-task-restart').checked,
            on_signal_error: document.getElementById('alert-signal-error').checked,
            on_rmi_fail_threshold: parseInt(document.getElementById('alert-rmi-threshold').value) || 0
        }}
    }};
    fetch('/api/external', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify(config)
    }}).then(r => r.json()).then(data => {{
        alert(data.status === 'ok' ? 'Saved!' : 'Save failed: ' + (data.message || 'Unknown error'));
        if (data.status === 'ok') loadExtLink();
    }});
}}

function testExtLink(target) {{
    fetch('/api/external/test/' + target, {{method: 'POST'}}).then(r => r.json()).then(data => {{
        alert(data.success ? 'Test successful!' : 'Test failed: ' + (data.error || 'Unknown error'));
    }});
}}

function sendReport() {{
    const targets = ['slack', 'email', 'webhook'].filter(t => document.getElementById('hr-' + t).checked);
    fetch('/api/external/report', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{targets: targets}})
    }}).then(r => r.json()).then(data => {{
        if (data.status === 'ok') {{
            const results = data.results || {{}};
            const msg = Object.entries(results).map(([k, v]) => `${{k}}: ${{v ? 'OK' : 'Failed'}}`).join(', ');
            alert('Report sent! ' + msg);
        }} else {{
            alert('Send failed: ' + (data.message || 'Unknown error'));
        }}
    }});
}}

function showExtTab(tab) {{
    document.querySelectorAll('.extlink-tab').forEach(t => {{
        t.style.background = '#444';
        t.style.color = '#9e9e9e';
        t.classList.remove('active');
    }});
    document.querySelectorAll('.extlink-tab-content').forEach(c => c.style.display = 'none');
    const btn = document.querySelector(`.extlink-tab[data-tab="${{tab}}"]`);
    if (btn) {{
        btn.style.background = '#0288d1';
        btn.style.color = '#fff';
        btn.classList.add('active');
    }}
    const content = document.getElementById('extlink-tab-' + tab);
    if (content) content.style.display = 'block';
}}

function renderTree(obj, path, depth=0) {{
    const keyClass = `tree-key tree-key-${{Math.min(depth, 4)}}`;
    if (obj === null) return `<span class="tree-val-null tree-val-edit" onclick="editGConfig('${{path}}', null, 'null')">null</span>`;
    if (typeof obj === 'boolean') return `<span class="tree-val-bool tree-val-edit" onclick="editGConfig('${{path}}', ${{obj}}, 'bool')">${{obj}}</span>`;
    if (typeof obj === 'number') return `<span class="tree-val-num tree-val-edit" onclick="editGConfig('${{path}}', ${{obj}}, 'num')">${{obj}}</span>`;
    if (typeof obj === 'string') return `<span class="tree-val-str tree-val-edit" onclick="editGConfig('${{path}}', '${{escapeHtml(obj).replace(/'/g, "\\\\'")}}', 'str')">"${{escapeHtml(obj)}}"</span>`;
    if (Array.isArray(obj)) {{
        if (obj.length === 0) return '<span class="tree-bracket">[]</span>';
        const id = 'tree-' + Math.random().toString(36).substr(2, 9);
        let html = `<span class="tree-expanded" id="${{id}}"><span class="tree-toggle" onclick="toggleTree('${{id}}')" title="Click to collapse"></span><span class="tree-bracket">[</span>`;
        obj.forEach((v, i) => {{
            const childPath = path ? `${{path}}[${{i}}]` : `[${{i}}]`;
            html += `<div class="tree-node"><span class="${{keyClass}}">${{i}}</span>: ${{renderTree(v, childPath, depth+1)}}${{i < obj.length - 1 ? ',' : ''}}</div>`;
        }});
        html += '<span class="tree-bracket">]</span></span>';
        return html;
    }}
    if (typeof obj === 'object') {{
        const keys = Object.keys(obj);
        if (keys.length === 0) return '<span class="tree-bracket">{{}}</span>';
        const id = 'tree-' + Math.random().toString(36).substr(2, 9);
        let html = `<span class="tree-expanded" id="${{id}}"><span class="tree-toggle" onclick="toggleTree('${{id}}')" title="Click to collapse"></span><span class="tree-bracket">{{</span>`;
        keys.forEach((k, i) => {{
            const childPath = path ? `${{path}}.${{k}}` : k;
            html += `<div class="tree-node"><span class="${{keyClass}}">"${{escapeHtml(k)}}"</span>: ${{renderTree(obj[k], childPath, depth+1)}}${{i < keys.length - 1 ? ',' : ''}}</div>`;
        }});
        html += '<span class="tree-bracket">}}</span></span>';
        return html;
    }}
    return String(obj);
}}

function formatPath(path) {{
    // Convert "a.b.c" to "(a).(b).(c)"
    return path.split('.').map(p => `(${{p}})`).join('.');
}}

function editGConfig(path, value, vtype) {{
    // Remove previous selection highlight
    document.querySelectorAll('.tree-val-edit.selected').forEach(el => el.classList.remove('selected'));
    // Highlight current selection
    event.target.classList.add('selected');

    selectedPath = path;
    selectedVtype = vtype;
    const displayVal = vtype === 'str' ? value : (value === null ? '' : value);
    const typeLabel = {{'str': 'String', 'num': 'Number', 'bool': 'Boolean', 'null': 'Null'}}[vtype] || vtype;
    const displayPath = formatPath(path);

    // Check if this is a task_config path (RMI will be triggered)
    const parts = path.split('.');
    const isTaskConfig = path.startsWith('task_config.') && parts.length >= 3 && parts[1] !== '_monitor';
    const rmiNotice = isTaskConfig ? '<div class="setter-rmi-notice">📡 task_config 변경 시 실행중인 프로세스 변수도 RMI로 함께 변경됩니다</div>' : '';
    const btnText = isTaskConfig ? '파일 + RMI 변경' : '변경';

    document.getElementById('setter-content').innerHTML = `
        ${{rmiNotice}}
        <div class="setter-row">
            <div class="setter-label">Path (data_id)</div>
            <div class="setter-path">${{escapeHtml(displayPath)}}</div>
        </div>
        <div class="setter-row">
            <div class="setter-label">Value</div>
            <input type="text" class="setter-input" id="setter-value" value="${{displayVal}}" onkeydown="if(event.key==='Enter')applySetterValue();">
            <div class="setter-type">Type: ${{typeLabel}}</div>
        </div>
        <button class="btn-change" onclick="applySetterValue()">${{btnText}}</button>
        <div id="setter-msg"></div>
    `;
    document.getElementById('setter-value').focus();
    document.getElementById('setter-value').select();
}}

function applySetterValue() {{
    if (!selectedPath) return;
    const inputEl = document.getElementById('setter-value');
    let value = inputEl.value;
    if (selectedVtype === 'null' && value === '') value = 'null';
    const msgEl = document.getElementById('setter-msg');

    // 1. Save to gconfig file
    fetch('/api/gconfig', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{path: selectedPath, value: value, vtype: selectedVtype}})
    }}).then(r => r.json()).then(res => {{
        if (res.status === 'ok') {{
            // 2. If task_config path, also update running process via RMI
            if (selectedPath.startsWith('task_config.')) {{
                const parts = selectedPath.split('.');
                // task_config.process1/aaa.counter -> taskId=process1, key=counter
                // Note: TaskManager uses "process1/aaa".split('/')[0] as task_id
                if (parts.length >= 3) {{
                    const taskId = parts[1].split('/')[0];
                    const key = parts.slice(2).join('.');
                    // Skip _monitor section
                    if (taskId !== '_monitor') {{
                        fetch('/api/config', {{
                            method: 'POST',
                            headers: {{'Content-Type': 'application/json'}},
                            body: JSON.stringify({{task_id: taskId, key: key, value: value}})
                        }}).then(r => r.json()).then(rmiRes => {{
                            if (rmiRes.status === 'ok') {{
                                msgEl.className = 'setter-msg setter-msg-ok';
                                msgEl.textContent = '파일+프로세스 변경 완료!';
                            }} else {{
                                msgEl.className = 'setter-msg setter-msg-ok';
                                msgEl.textContent = '파일 변경 완료 (RMI: ' + (rmiRes.message || 'error') + ')';
                            }}
                            setTimeout(() => loadGConfigData(), 500);
                        }});
                        return;
                    }}
                }}
            }}
            msgEl.className = 'setter-msg setter-msg-ok';
            msgEl.textContent = '변경 완료!';
            setTimeout(() => loadGConfigData(), 500);
        }} else {{
            msgEl.className = 'setter-msg setter-msg-err';
            msgEl.textContent = res.message || 'Error';
        }}
    }});
}}

function saveGConfig(path, vtype, inputEl) {{
    let value = inputEl.value;
    if (vtype === 'null' && value === '') value = 'null';
    fetch('/api/gconfig', {{
        method: 'POST',
        headers: {{'Content-Type': 'application/json'}},
        body: JSON.stringify({{path: path, value: value}})
    }}).then(r => r.json()).then(res => {{
        if (res.status === 'ok') {{
            loadGConfigData();
        }} else {{
            const msgEl = document.getElementById('msg-' + path.replace(/\\./g, '-'));
            if (msgEl) {{ msgEl.textContent = res.message || 'Error'; msgEl.style.color = '#f44336'; }}
        }}
    }});
}}

function escapeHtml(str) {{
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}}

function toggleTree(id) {{
    const el = document.getElementById(id);
    if (el.classList.contains('tree-expanded')) {{
        el.classList.remove('tree-expanded');
        el.classList.add('tree-collapsed');
    }} else {{
        el.classList.remove('tree-collapsed');
        el.classList.add('tree-expanded');
    }}
}}

function expandAllTree() {{
    document.querySelectorAll('.tree-collapsed').forEach(el => {{
        el.classList.remove('tree-collapsed');
        el.classList.add('tree-expanded');
    }});
}}

function collapseAllTree() {{
    document.querySelectorAll('.tree-expanded').forEach(el => {{
        el.classList.remove('tree-expanded');
        el.classList.add('tree-collapsed');
    }});
}}

// Logs functions
function loadLogs() {{
    const level = document.getElementById('log-level').value;
    const task = document.getElementById('log-task').value;
    const count = document.getElementById('log-count').value;
    let url = `/api/logs?count=${{count}}`;
    if (level) url += `&level=${{level}}`;
    if (task) url += `&task=${{task}}`;

    fetch(url).then(r => r.json()).then(data => {{
        document.getElementById('logs-loading').style.display = 'none';
        document.getElementById('logs-content').style.display = 'block';

        if (data.error) {{
            document.getElementById('logs-body').innerHTML = `<tr><td colspan="4" style="color:#f44336">${{data.error}}</td></tr>`;
            return;
        }}

        const logs = data.logs || [];
        if (logs.length === 0) {{
            document.getElementById('logs-body').innerHTML = `<tr><td colspan="4" style="color:#9e9e9e;text-align:center">No logs found</td></tr>`;
            return;
        }}

        // Update task dropdown
        const tasks = new Set();
        logs.forEach(log => tasks.add(log.task));
        const taskSelect = document.getElementById('log-task');
        const currentTask = taskSelect.value;
        const newTasks = Array.from(tasks).sort();
        if (JSON.stringify(newTasks) !== JSON.stringify(logsTaskList)) {{
            logsTaskList = newTasks;
            const escapeHtml = s => (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
            taskSelect.innerHTML = '<option value="">All Tasks</option>' + newTasks.map(t => '<option value="' + escapeHtml(t) + '"' + (t === currentTask ? ' selected' : '') + '>' + escapeHtml(t) + '</option>').join('');
        }}

        // Level colors
        const levelColors = {{
            'DEBUG': '#9e9e9e',
            'INFO': '#4caf50',
            'WARNING': '#ff9800',
            'ERROR': '#f44336',
            'CRITICAL': '#9c27b0'
        }};

        let html = '';
        logs.forEach(log => {{
            const ts = new Date(log.ts * 1000);
            const timeStr = ts.toLocaleString('sv-SE', {{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'}}).replace(' ', ' ') + '.' + String(ts.getMilliseconds()).padStart(3, '0');
            const levelColor = levelColors[log.lv] || '#e0e0e0';
            const msg = log.msg || '';
            const msgEscaped = msg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
            const msgHtml = msgEscaped.replace(/\\\\n/g, '<br>').replace(/\\n/g, '<br>');
            html += '<tr>' +
                '<td style="font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + timeStr + '</td>' +
                '<td style="color:' + levelColor + ';font-weight:bold;text-align:center">' + log.lv + '</td>' +
                '<td style="color:#81d4fa;overflow:hidden;text-overflow:ellipsis">' + log.task + '</td>' +
                '<td style="font-family:monospace;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word">' + msgHtml + '</td>' +
            '</tr>';
        }});
        document.getElementById('logs-body').innerHTML = html;
    }}).catch(err => {{
        document.getElementById('logs-loading').textContent = 'Error: ' + err.message;
        document.getElementById('logs-loading').style.color = '#f44336';
    }});
}}

function startLogsTimer() {{
    if (logsTimer) clearInterval(logsTimer);
    if (document.getElementById('log-auto').checked) {{
        logsTimer = setInterval(loadLogs, 3000);
    }}
}}

document.getElementById('log-auto').addEventListener('change', startLogsTimer);
document.getElementById('log-level').addEventListener('change', loadLogs);
document.getElementById('log-task').addEventListener('change', loadLogs);
document.getElementById('log-count').addEventListener('change', loadLogs);
</script>
</body></html>'''


class TaskMonitor:
    """웹 기반 TASK 모니터"""
    __slots__ = ('_manager', '_port', '_server', '_thread', '_running', '_gconfig')

    def __init__(self, manager: TaskManager, port: int = 8080, gconfig=None):
        self._manager = manager
        self._port = port
        self._server = None
        self._thread = None
        self._running = False
        self._gconfig = gconfig

    def start(self):
        MonitorHandler.manager = self._manager
        MonitorHandler.start_time = time.time()
        MonitorHandler.gconfig = self._gconfig
        self._server = HTTPServer(('0.0.0.0', self._port), MonitorHandler)
        self._server.timeout = 0.5  # handle_request 타임아웃 설정
        self._running = True
        self._thread = threading.Thread(target=self._serve, daemon=True)
        self._thread.start()
        print(f"[Monitor] http://localhost:{self._port}")

    def _serve(self):
        while self._running:
            self._server.handle_request()

    def stop(self):
        print("[Monitor:stop] called")
        self._running = False
        # handle_request() 타임아웃으로 자연 종료 대기
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=1.0)
        if self._server:
            self._server.server_close()
        print("[Monitor] stopped")

    @property
    def port(self) -> int: return self._port

    @property
    def url(self) -> str: return f"http://localhost:{self._port}"

    def __enter__(self): self.start(); return self
    def __exit__(self, *_): self.stop(); return False
