"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  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     ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

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

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

    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/performance/system': self._perf_system,
            '/api/performance/tasks': self._perf_tasks,
        }
        # Check static routes first
        if self.path in routes:
            routes[self.path]()
            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()

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

    # ─────────────────────────────────────────────────────────────────────────
    # 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, w in self.manager._workers.items():
            ti = self.manager._tasks.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>
*{{box-sizing:border-box}}
body{{font-family:sans-serif;margin:0;background:#1a1a1a;color:#e0e0e0;display:flex;flex-direction:column;height:100vh}}
.header{{display:flex;align-items:center;gap:16px;padding:10px 20px;background:#252525;border-bottom:1px solid #333}}
h1{{color:#4fc3f7;margin:0;font-size:18px;white-space:nowrap}}
.app-info{{color:#ffcc80;font-weight:normal}}
.header-info{{color:#9e9e9e;font-size:12px}}
.refresh-ctrl{{display:flex;align-items:center;gap:8px;margin-left:auto}}
.refresh-ctrl label{{color:#9e9e9e;font-size:12px}}
.refresh-ctrl input[type="checkbox"]{{width:14px;height:14px;cursor:pointer}}
.refresh-ctrl select{{background:#2d2d2d;color:#e0e0e0;border:1px solid #444;padding:3px 6px;border-radius:4px;cursor:pointer;font-size:11px}}
.btn-clear{{background:#d32f2f;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px}}
.btn-clear:hover{{background:#b71c1c}}
.btn-refresh{{background:#0288d1;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px}}
.btn-refresh:hover{{background:#0277bd}}
.main-container{{display:flex;flex:1;overflow:hidden}}
.sidebar{{width:200px;background:#252525;border-right:1px solid #333;display:flex;flex-direction:column;transition:width 0.2s}}
.sidebar.collapsed{{width:48px}}
.sidebar-toggle{{background:#333;border:none;color:#9e9e9e;padding:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-top:1px solid #333;margin-top:auto}}
.sidebar-toggle:hover{{background:#444;color:#fff}}
.sidebar-toggle svg{{width:18px;height:18px}}
.sidebar.collapsed .sidebar-toggle svg{{transform:rotate(180deg)}}
.sidebar-nav{{flex:1;overflow-y:auto}}
.nav-item{{display:flex;align-items:center;gap:12px;padding:12px 16px;color:#9e9e9e;cursor:pointer;border-left:3px solid transparent;transition:all 0.15s}}
.nav-item:hover{{background:#333;color:#e0e0e0}}
.nav-item.active{{background:#0288d1;color:#fff;border-left-color:#4fc3f7}}
.nav-item svg{{width:18px;height:18px;flex-shrink:0}}
.nav-item span{{white-space:nowrap;overflow:hidden;font-size:13px}}
.sidebar.collapsed .nav-item span{{display:none}}
.sidebar.collapsed .nav-item{{justify-content:center;padding:12px}}
.sidebar-logo{{padding:16px 12px;border-bottom:1px solid #333;display:flex;flex-direction:column;align-items:center;gap:10px}}
.sidebar-logo img{{height:40px;opacity:0.9}}
.sidebar.collapsed .sidebar-logo img{{height:28px}}
.sidebar-logo-text{{text-align:center;line-height:1.4}}
.sidebar-logo-title{{color:#4fc3f7;font-size:16px;font-weight:bold}}
.sidebar-logo-sub{{color:#9e9e9e;font-size:12px}}
.sidebar.collapsed .sidebar-logo-text{{display:none}}
.content-area{{flex:1;overflow:auto;padding:16px}}
.panel{{display:none}}
.panel.active{{display:block}}
table{{border-collapse:collapse;width:100%;background:#2d2d2d;box-shadow:0 2px 8px rgba(0,0,0,0.5)}}
th,td{{border:1px solid #444;padding:10px;text-align:left;font-size:13px}}
th{{background:#0288d1;color:#fff}}
tr:nth-child(even){{background:#383838}}
tr:hover{{background:#424242}}
.running{{color:#4caf50;font-weight:bold}}
.stopped{{color:#f44336;font-weight:bold}}
.num{{text-align:right;font-family:monospace;color:#b0bec5}}
.fail{{color:#f44336}}
.warn{{color:#ff9800}}
.cpu-bar{{background:#444;border-radius:4px;height:20px;overflow:hidden}}
.cpu-bar-fill{{height:100%;transition:width 0.3s}}
.cpu-low{{background:#4caf50}}
.cpu-mid{{background:#ff9800}}
.cpu-high{{background:#f44336}}
.config-input{{background:#383838;color:#e0e0e0;border:1px solid #555;padding:6px 10px;border-radius:4px;width:150px}}
.btn-save{{background:#4caf50;color:#fff;border:none;padding:6px 12px;border-radius:4px;cursor:pointer}}
.btn-save:hover{{background:#388e3c}}
.msg{{padding:4px 8px;border-radius:4px;font-size:12px}}
.msg-ok{{background:#4caf50;color:#fff}}
.msg-err{{background:#f44336;color:#fff}}
.gconfig-header{{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding:12px;background:#2d2d2d;border-radius:4px}}
.gconfig-header span{{color:#81d4fa;font-family:monospace}}
.btn-expand,.btn-collapse{{background:#555;color:#fff;border:none;padding:6px 12px;border-radius:4px;cursor:pointer}}
.btn-expand:hover,.btn-collapse:hover{{background:#666}}
.tree-container{{background:#2d2d2d;padding:16px;border-radius:4px;font-family:monospace;font-size:14px;line-height:1.6}}
.tree-node{{margin-left:20px}}
.tree-key{{color:#81d4fa}}
.tree-key-0{{color:#ff7043}}
.tree-key-1{{color:#42a5f5}}
.tree-key-2{{color:#66bb6a}}
.tree-key-3{{color:#ab47bc}}
.tree-key-4{{color:#ffa726}}
.tree-val-str{{color:#a5d6a7}}
.tree-val-num{{color:#ffcc80}}
.tree-val-bool{{color:#ef9a9a}}
.tree-val-null{{color:#9e9e9e}}
.tree-toggle{{cursor:pointer;user-select:none;display:inline-block;width:16px;color:#ff9800}}
.tree-toggle:hover{{color:#ffcc80}}
.tree-bracket{{color:#9e9e9e}}
.tree-collapsed>.tree-node{{display:none}}
.tree-collapsed>.tree-toggle::before{{content:'▶'}}
.tree-expanded>.tree-toggle::before{{content:'▼'}}
.tree-val-edit{{cursor:pointer;padding:2px 4px;border-radius:3px}}
.tree-val-edit:hover{{background:#444}}
.tree-edit-input{{background:#383838;color:#e0e0e0;border:1px solid #4fc3f7;padding:2px 6px;border-radius:3px;font-family:monospace;font-size:14px;min-width:100px}}
.tree-edit-msg{{font-size:11px;margin-left:8px}}
.gconfig-warning{{margin-left:auto;background:#3e2723;color:#ffcc80;padding:6px 12px;border-radius:4px;font-size:12px}}
.gconfig-body{{display:flex;gap:16px;height:calc(100vh - 200px);min-height:400px}}
.gconfig-tree-wrap{{flex:1;min-width:0;overflow-y:auto}}
.value-setter{{flex:1;min-width:0;background:#2d2d2d;padding:16px;border-radius:4px;height:fit-content;align-self:flex-start}}
.value-setter h3{{margin:0 0 16px 0;color:#81d4fa;font-size:14px;border-bottom:1px solid #444;padding-bottom:8px}}
.value-setter-empty{{color:#666;font-size:13px}}
.setter-row{{margin-bottom:12px}}
.setter-label{{color:#9e9e9e;font-size:12px;margin-bottom:4px}}
.setter-path{{color:#81d4fa;font-family:monospace;font-size:13px;word-break:break-all;background:#383838;padding:8px;border-radius:4px}}
.setter-input{{width:100%;background:#383838;color:#e0e0e0;border:1px solid #555;padding:8px;border-radius:4px;font-family:monospace;font-size:13px;box-sizing:border-box}}
.setter-input:focus{{border-color:#4fc3f7;outline:none}}
.setter-type{{color:#9e9e9e;font-size:11px;margin-top:4px}}
.btn-change{{background:#4caf50;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;margin-top:8px;font-size:14px}}
.btn-change:hover{{background:#388e3c}}
.setter-msg{{margin-top:8px;padding:6px;border-radius:4px;font-size:12px;text-align:center}}
.setter-msg-ok{{background:#4caf50;color:#fff}}
.setter-msg-err{{background:#f44336;color:#fff}}
.setter-rmi-notice{{background:#1e3a5f;color:#64b5f6;padding:8px 12px;border-radius:4px;font-size:12px;margin-bottom:12px;border-left:3px solid #2196f3}}
.tree-val-edit.selected{{background:#0288d1;border-radius:3px}}
</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-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/SM</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>Latency</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="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>Method</span>
</div>
<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>File (Permanent)</span>
</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>
</div>
<script>
let refreshTimer = null;
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 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/SM)
        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();
    // 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 rmiCallTime = 0;     // last update timestamp
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 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);
}}

let selectedPath = null;
let selectedVtype = null;

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');
    }});
}}
</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._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
        if self._server:
            print("[Monitor:stop] calling server.shutdown()...")
            self._server.shutdown()
            print("[Monitor:stop] server.shutdown() completed")
        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
