#!/usr/bin/env python3
"""
爬虫管理页面模块 - 注入到 search_app.py
"""
import json, sqlite3, os, subprocess, re

CONFIG_PATH = "/root/gov_crawler/daily_crawl_config.json"
SEARCH_DB = "/root/search.db"
GOV_CRAWLER_DIR = "/root/gov_crawler"

INDUSTRIES = [
    {"id": "power", "cn": "电力", "en": "Power"},
    {"id": "terminals", "cn": "终端/接收站", "en": "Terminals"},
    {"id": "transmission", "cn": "管道输送", "en": "Transmission"},
    {"id": "production", "cn": "生产/开采", "en": "Production"},
    {"id": "altfuel", "cn": "替代燃料", "en": "Alternative Fuel"},
    {"id": "hpi", "cn": "石油炼制", "en": "HPI (Hydrocarbon Processing Industry)"},
    {"id": "cpi", "cn": "化工加工", "en": "CPI (Chemical Processing Industry)"},
    {"id": "metals", "cn": "金属与矿物", "en": "Metals & Minerals"},
    {"id": "pulp", "cn": "制浆/造纸/木材", "en": "Pulp, Paper & Wood"},
    {"id": "food", "cn": "食品与饮料", "en": "Food & Beverage"},
    {"id": "manufacturing", "cn": "工业制造", "en": "Industrial Manufacturing"},
    {"id": "pharma", "cn": "制药与生物技术", "en": "Pharmaceutical & Biotech"},
]

def get_industries_json():
    import json as _j
    return _j.dumps(INDUSTRIES, ensure_ascii=False)

INDUSTRY_IDS = [ind["id"] for ind in INDUSTRIES]



_SCRIPT_URL_CACHE = {}
_SCRIPT_SITE_CACHE = {}

def extract_script_sitename(script_name):
    """Extract SITE_NAME constant from script source to match DB"""
    if not script_name:
        return ""
    for prefix in ["node ", "bash ", "python3 "]:
        if script_name.startswith(prefix):
            script_name = script_name[len(prefix):]
            break
    if script_name in _SCRIPT_SITE_CACHE:
        return _SCRIPT_SITE_CACHE.get(script_name, "")
    script_path = os.path.join(GOV_CRAWLER_DIR, script_name)
    if not os.path.isfile(script_path):
        _SCRIPT_SITE_CACHE[script_name] = ""
        return ""
    try:
        with open(script_path, "r", errors="replace") as f:
            head = f.read(5000)
    except:
        _SCRIPT_SITE_CACHE[script_name] = ""
        return ""
    m = re.search("^SITE_NAME\s*=\s*[\\\"']([^\\\"']+)[\\\"']", head, re.MULTILINE)
    if not m:
        m = re.search("^NAME\s*=\s*[\\\"']([^\\\"']+)[\\\"']", head, re.MULTILINE)
    if not m:
        m = re.search("^SITE\s*=\s*[\\\"']([^\\\"']+)[\\\"']", head, re.MULTILINE)
    sn = m.group(1) if m else ""
    _SCRIPT_SITE_CACHE[script_name] = sn
    return sn

def extract_script_url(script_name):
    if not script_name:
        return ""
    for prefix in ["node ", "bash ", "python3 "]:
        if script_name.startswith(prefix):
            script_name = script_name[len(prefix):]
            break
    if script_name in _SCRIPT_URL_CACHE:
        return _SCRIPT_URL_CACHE.get(script_name, "")
    script_path = os.path.join(GOV_CRAWLER_DIR, script_name)
    if not os.path.isfile(script_path):
        _SCRIPT_URL_CACHE[script_name] = ""
        return ""
    try:
        with open(script_path, "r", errors="replace") as f:
            head = f.read(5000)
    except:
        _SCRIPT_URL_CACHE[script_name] = ""
        return ""
    # Priority 1: LIST_URL (most precise - list page URL)
    m = re.search("(?m)^(?:LIST_URL|list_url|listUrl)\\s*=\\s*[\"\'](https?://[^\"\']+)[\"\']", head)
    # Priority 2: API_URL
    if not m:
        m = re.search("(?m)^(?:API_URL|api_url|apiUrl)\\s*=\\s*[\"\'](https?://[^\"\']+)[\"\']", head)
    # Priority 3: PAGE_URL / SEARCH_URL / BASE_URL
    if not m:
        m = re.search("(?m)^(?:PAGE_URL|SEARCH_URL|search_url|BASE_URL)\\s*=\\s*[\"\'](https?://[^\"\']+)[\"\']", head)
    # Priority 4: BASE / base_url / DOMAIN
    if not m:
        m = re.search("(?m)^(?:BASE|base_url|DOMAIN)\\s*=\\s*[\"\'](https?://[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}[^\"\']*)[\"\']", head)
    # Priority 5: domain from docstring
    if not m:
        m = re.search(r"(https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})", head)
    url = m.group(1).rstrip("/") if m else ""
    _SCRIPT_URL_CACHE[script_name] = url
    return url

def set_crawler_industry(name, industry_id):
    """Set industry for a crawler in config"""
    cfg = json.load(open(CONFIG_PATH))
    if isinstance(cfg, list):
        cfg = {"crawlers": cfg}
    if industry_id and industry_id not in INDUSTRY_IDS:
        return {"success": False, "error": f"Unknown industry: {industry_id}"}
    for c in cfg["crawlers"]:
        if c["name"] == name:
            if industry_id:
                c["industry"] = industry_id
            else:
                c.pop("industry", None)
            json.dump(cfg, open(CONFIG_PATH, "w"), ensure_ascii=False, indent=2)
            return {"success": True, "name": name, "industry": industry_id or ""}
    return {"success": False, "error": f"Crawler \"{name}\" not found"}

def get_crawler_stats():
    """获取所有爬虫的状态数据"""
    cfg = json.load(open(CONFIG_PATH))
    if isinstance(cfg, list):
        cfg = {"crawlers": cfg}
    crawlers = cfg['crawlers']
    db = sqlite3.connect(SEARCH_DB)
    db.row_factory = sqlite3.Row
    rows = db.execute("""
        SELECT site_name,
               COUNT(*) as total,
               MAX(SUBSTR(publish_date,1,10)) as latest_date,
               MIN(SUBSTR(publish_date,1,10)) as oldest_date,
               SUM(CASE WHEN SUBSTR(publish_date,1,10) >= date('now', '-7 days') THEN 1 ELSE 0 END) as week_new,
               SUM(CASE WHEN SUBSTR(publish_date,1,10) >= date('now', '-1 days') THEN 1 ELSE 0 END) as day_new
        FROM gov_raw
        GROUP BY site_name
    """).fetchall()
    db_stats = {r['site_name']: dict(r) for r in rows}
    # Build fuzzy lookup: normalized name -> actual site_name
    def normalize(s):
        return s.replace(' ', '').replace('—', '-').replace('–', '-').replace('（', '(').replace('）', ')').replace('　', '').replace(' ', '')
    db_stats_norm = {normalize(k): v for k, v in db_stats.items()}
    db.close()
    result = []
    for c in crawlers:
        name = c.get('name', c.get('site_name', c.get('script', '')))
        script = c.get('script', c.get('command', ''))
        # 去除 node / bash 前缀后检查文件是否存在
        script_file = script
        for prefix in ['node ', 'bash ', 'python3 ']:
            if script.startswith(prefix):
                script_file = script[len(prefix):]
                break
        script_path = os.path.join(GOV_CRAWLER_DIR, script_file) if script_file else ''
        script_exists = os.path.isfile(script_path) if script_path else False
        stats = db_stats.get(name, {})
        # Strategy 0: aggregate by site_names array (for unified crawlers covering multiple DB site_names)
        if not stats and 'site_names' in c and isinstance(c['site_names'], list):
            site_names_list = c['site_names']
            matched = [v for k, v in db_stats.items() if k in site_names_list]
            if matched:
                stats = {
                    'total': sum(m.get('total', 0) for m in matched),
                    'latest_date': max((m.get('latest_date', '-') for m in matched if m.get('latest_date') and m['latest_date'] != '-'), default='-'),
                    'oldest_date': min((m.get('oldest_date', '-') for m in matched if m.get('oldest_date') and m['oldest_date'] != '-'), default='-'),
                    'week_new': sum(m.get('week_new', 0) for m in matched),
                    'day_new': sum(m.get('day_new', 0) for m in matched),
                }
        if not stats:
            # Try fuzzy match by normalized name
            stats = db_stats_norm.get(normalize(name), {})
            if not stats:
                # Strategy 1: config name contains site_name
                name_norm = normalize(name)
                for sn, v in db_stats_norm.items():
                    if sn in name_norm:
                        stats = v
                        break
            if not stats:
                # Strategy 2: site_name contains config name
                for sn, v in db_stats_norm.items():
                    if name_norm in sn:
                        stats = v
                        break
            if not stats:
                # Strategy 3: shared prefix >= 6 chars
                for sn, v in db_stats_norm.items():
                    min_len = min(len(name_norm), len(sn))
                    if min_len >= 6:
                        common = 0
                        for i in range(min_len):
                            if name_norm[i] == sn[i]:
                                common += 1
                            else:
                                break
                        if common >= 3:
                            stats = v
                            break
            if not stats:
                # Strategy 4: base name (before -/—/·)
                base = name_norm.split('-')[0].split('—')[0].split('\u00b7')[0]
                if len(base) >= 2:
                    for sn, v in db_stats_norm.items():
                        if base in sn or sn in base:
                            stats = v
                            break
            if not stats:
                # Strategy 7: strip common suffixes to find place name
                common_suffixes = ['通知公告', '公示公告', '环境保护', '环评信息', '环评公告',
                                   '环境信息', '政策文件', '新闻专区', '环评审批', '环境影响评价',
                                   '高新区动态', '公告公示', '生态环境保护']
                base = name_norm
                for sfx in common_suffixes:
                    if base.endswith(sfx):
                        base = base[:-len(sfx)]
                        break
                if len(base) >= 2 and base != name_norm:
                    for sn, v in db_stats_norm.items():
                        if base in sn:
                            stats = v
                            break
            if not stats:
                # Strategy 8: match by script SITE_NAME constant
                site_name_from_script = extract_script_sitename(script)
                if site_name_from_script and site_name_from_script in db_stats:
                    stats = db_stats[site_name_from_script]
            if not stats:
                # Strategy 9: match by script filename slug
                if script:
                    script_base = os.path.basename(script).replace('.py', '')
                    if script_base.startswith('crawl_'):
                        slug = script_base[6:]
                        for sn, v in db_stats.items():
                            if slug and (slug in sn or (len(slug) >= 3 and any(slug[:i] in sn for i in [4,6,8]))):
                                stats = v
                                break
        total = stats.get('total', 0)
        latest = stats.get('latest_date', '-')
        oldest = stats.get('oldest_date', '-')
        week_new = stats.get('week_new', 0)
        day_new = stats.get('day_new', 0)
        active_3d = active_7d = active_30d = active_180d = False
        if latest and latest != '-':
            try:
                from datetime import datetime, timedelta
                ld = datetime.strptime(latest[:10], '%Y-%m-%d')
                delta = datetime.now() - ld
                active_3d = delta <= timedelta(days=3)
                active_7d = delta <= timedelta(days=7)
                active_30d = delta <= timedelta(days=30)
                active_180d = delta <= timedelta(days=180)
            except:
                pass
        # 读取日跑结果
        last_run_status = '待运行'
        last_run_ts = 0
        try:
            with open('/tmp/crawl_results.json') as _f:
                _results = __import__('json').load(_f)
            for _r in reversed(_results):
                if _r.get('script') == script:
                    last_run_ts = _r.get('ts', 0)
                    if _r.get('timeout'):
                        last_run_status = '⏰ 超时'
                    elif _r.get('has_error'):
                        last_run_status = '❌ 失败'
                    elif _r.get('rc', -1) == 0:
                        new_c = _r.get('new_count', 0)
                        last_run_status = f'✅ +{new_c}条'
                    else:
                        last_run_status = f'⚠ rc={_r.get("rc")}'
                    break
        except:
            pass
        result.append({
            'name': name, 'script': script, 'group': c.get('group', '未分组'),
            'enabled': c.get('enabled', False), 'note': c.get('note', ''),
            'total': total, 'latest': latest, 'oldest': oldest,
            'week_new': week_new, 'day_new': day_new,
            'active': active_180d, 'active_3d': active_3d, 'active_7d': active_7d,
            'active_30d': active_30d, 'active_180d': active_180d,
            'script_exists': script_exists,
            'industry': c.get('industry', ''),
            'url': c.get('url', ''),
            'site_names': c.get('site_names', []),
            'source_url': extract_script_url(script),
            'last_run_status': last_run_status,
            'last_run_ts': last_run_ts,
        })
    return result, cfg


def get_analysis_data(period='day', mode='incremental', metric='records', days=90):
    """
    获取分析数据
    period: day/week/month
    mode: incremental / cumulative
    metric: records (数据量) / 脚本数 (脚本数) / both (双指标)
    days: 回溯天数 (0=全部)

    脚本数按唯一脚本文件(.py)统计，脚本的新增日期取该脚本数据
    在 search.db 中最早的 publish_date。
    """
    import json as _json
    import os as _os
    import datetime as _dt
    from collections import defaultdict as _dd

    db = sqlite3.connect(SEARCH_DB)
    db.row_factory = sqlite3.Row
    records_data = []
    spiders_data = []
    labels = []
    cfg_path = "/root/gov_crawler/daily_crawl_config.json"

    # ====== 1. Build script-to-first-date mapping ======
    # Get all unique site_names from DB with their first date
    db_site_dates = {}
    rows = db.execute("""
        SELECT site_name, MIN(date(publish_date)) as d
        FROM gov_raw WHERE publish_date IS NOT NULL AND SUBSTR(publish_date,1,10) != ''
          AND date(publish_date) IS NOT NULL AND date(publish_date) != '1970-01-01'
        GROUP BY site_name
    """).fetchall()
    for r in rows:
        if r['site_name'] and r['d']:
            db_site_dates[r['site_name']] = r['d']
    
    # Read daily_crawl_config.json, match each config entry to DB site_names
    scripts_map = {}  # script_name -> earliest_date (YYYY-MM-DD or None)
    if _os.path.exists(cfg_path):
        try:
            cfg = _json.load(open(cfg_path))
            if isinstance(cfg, list):
                cfg = {crawlers: cfg}
            crawlers = cfg.get('crawlers', [])
            
            def normalize(s):
                return s.replace(' ', '').replace('—', '-').replace('–', '-') \
                    .replace('（', '(').replace('）', ')').replace('　', '').replace('\xa0', '')
            
            # Index normalized DB names
            db_norm = {normalize(k): k for k in db_site_dates}
            
            for c in crawlers:
                script = c.get('script', '')
                name = c.get('name', '')
                if not script or not name:
                    continue
                
                earliest = None
                name_norm = normalize(name)
                
                # Strategy 0: site_names array
                if 'site_names' in c and isinstance(c['site_names'], list):
                    for sn in c['site_names']:
                        if sn in db_site_dates:
                            d = db_site_dates[sn]
                            if earliest is None or d < earliest:
                                earliest = d
                
                # Strategy 1: exact match
                if earliest is None and name in db_site_dates:
                    earliest = db_site_dates[name]
                
                # Strategy 2: normalized exact match
                if earliest is None and name_norm in db_norm:
                    earliest = db_site_dates[db_norm[name_norm]]
                
                # Strategy 3: strip (#NNN) suffix
                if earliest is None:
                    import re as _re
                    clean = _re.sub(r'\(#\d+\)', '', name).strip()
                    clean_norm = normalize(clean)
                    if clean_norm in db_norm:
                        earliest = db_site_dates[db_norm[clean_norm]]
                
                # Strategy 4: fuzzy - config name in DB site_name
                if earliest is None:
                    for sn_norm, sn in db_norm.items():
                        if name_norm in sn_norm:
                            d = db_site_dates[sn]
                            if earliest is None or d < earliest:
                                earliest = d
                
                # Strategy 5: DB site_name in config name  
                if earliest is None:
                    for sn_norm, sn in db_norm.items():
                        if sn_norm in name_norm:
                            d = db_site_dates[sn]
                            if earliest is None or d < earliest:
                                earliest = d
                
                # Strategy 6: shared prefix >= 3 chars
                if earliest is None:
                    for sn_norm, sn in db_norm.items():
                        min_len = min(len(name_norm), len(sn_norm))
                        if min_len >= 6:
                            common = 0
                            for i in range(min_len):
                                if name_norm[i] == sn_norm[i]:
                                    common += 1
                                else:
                                    break
                            if common >= 3:
                                d = db_site_dates[sn]
                                if earliest is None or d < earliest:
                                    earliest = d
                
                # Strategy 7: base name before -
                if earliest is None:
                    base = name_norm.split('-')[0].split('—')[0]
                    if len(base) >= 2:
                        for sn_norm, sn in db_norm.items():
                            if base in sn_norm:
                                d = db_site_dates[sn]
                                if earliest is None or d < earliest:
                                    earliest = d
                
                # Strategy 8: strip common suffixes
                if earliest is None:
                    common_suffixes = ['通知公告', '公示公告', '环境保护', '环评信息', '环评公告',
                                       '环境信息', '政策文件', '环评审批', '环境影响评价', '公告公示', '行政许可公示']
                    base = name_norm
                    for sfx in common_suffixes:
                        if base.endswith(sfx):
                            base = base[:-len(sfx)]
                            break
                    if len(base) >= 2 and base != name_norm:
                        for sn_norm, sn in db_norm.items():
                            if base in sn_norm or sn_norm in base:
                                d = db_site_dates[sn]
                                if earliest is None or d < earliest:
                                    earliest = d
                
                scripts_map[script] = earliest
                
                if earliest is None:
                    # Fallback: use file mtime
                    try:
                        mtime = _os.path.getmtime('/root/gov_crawler/' + script)
                        scripts_map[script] = _dt.datetime.fromtimestamp(mtime).strftime('%Y-%m-%d')
                    except:
                        pass
        except Exception as e:
            print(f"get_analysis_data config error: {e}")
        # ====== 2. Compute records data from gov_raw ======
    rec_where = "WHERE publish_date IS NOT NULL AND SUBSTR(publish_date,1,10) != '' AND date(publish_date) IS NOT NULL"
    if days > 0:
        rec_where += f" AND date(publish_date) >= date('now', '-{days} days')"

    if period == 'day':
        rec_date_expr = "date(publish_date)"
        rec_label_expr = "date(publish_date)"
    elif period == 'week':
        rec_date_expr = "date(publish_date, 'weekday 1', '-7 days')"
        rec_label_expr = "strftime('%Y-第%W周', publish_date)"
    else:
        rec_date_expr = "strftime('%Y-%m-01', publish_date)"
        rec_label_expr = "strftime('%Y年%m月', publish_date)"

    q_rec = f"""
        SELECT {rec_date_expr} as period_date,
               {rec_label_expr} as period_label,
               COUNT(*) as cnt
        FROM gov_raw
        {rec_where}
        GROUP BY period_date
        ORDER BY period_date ASC
    """
    rec_rows = db.execute(q_rec).fetchall()
    rec_labels = [r['period_label'] for r in rec_rows]
    rec_inc = [r['cnt'] for r in rec_rows]

    # ====== 3. Compute spider data from scripts_map ======
    # Filter scripts by first_date within range
    filtered_scripts = {}
    for s, d in scripts_map.items():
        if d is None:
            # No data match - use file timestamp as proxy for "added date"
            try:
                mtime = _os.path.getmtime(f'/root/gov_crawler/{s}')
                d = _dt.datetime.fromtimestamp(mtime).strftime('%Y-%m-%d')
            except:
                continue
        if days > 0:
            cutoff = (_dt.datetime.now() - _dt.timedelta(days=days)).strftime('%Y-%m-%d')
            if d < cutoff:
                continue
        # Determine period label for this script's date
        if period == 'day':
            plabel = d
        elif period == 'week':
            dd = _dt.datetime.strptime(d, '%Y-%m-%d').date()
            monday = dd - _dt.timedelta(days=dd.weekday())
            plabel = monday.strftime('%Y-第%W周')
        else:
            plabel = d[:7].replace('-', '年') + '月'
        filtered_scripts[s] = (d, plabel)

    # Count scripts per period
    spi_period_cnt = _dd(int)
    for s, (d, plabel) in sorted(filtered_scripts.items(), key=lambda x: x[1][0]):
        spi_period_cnt[plabel] += 1
    spi_labels = sorted(spi_period_cnt.keys())
    spi_inc = [spi_period_cnt[l] for l in spi_labels]

    # ====== 4. Merge records + 脚本数 ======
    all_labels = sorted(set(rec_labels + spi_labels))
    rec_inc_dict = dict(zip(rec_labels, rec_inc))
    spi_inc_dict = dict(zip(spi_labels, spi_inc))

    if mode == 'incremental':
        labels = all_labels
        records_data = [rec_inc_dict.get(l, 0) for l in all_labels]
        spiders_data = [spi_inc_dict.get(l, 0) for l in all_labels]
    else:
        labels = all_labels
        r_carry, s_carry = 0, 0
        for l in all_labels:
            r_carry += rec_inc_dict.get(l, 0)
            s_carry += spi_inc_dict.get(l, 0)
            records_data.append(r_carry)
            spiders_data.append(s_carry)

    db.close()

    result = {
        'labels': labels,
        'record_counts': records_data,
        'spider_counts': spiders_data if spiders_data else None,
        'period': period,
        'mode': mode,
        'metric': metric,
        'days': days,
    }
    return result
def render_admin_page():
    """渲染爬虫管理HTML页面"""
    crawlers, cfg = get_crawler_stats()
    total = len(crawlers)
    enabled = sum(1 for c in crawlers if c['enabled'])
    disabled = total - enabled
    import sqlite3; total_records = sqlite3.connect(SEARCH_DB).execute("SELECT COUNT(*) FROM gov_raw").fetchone()[0]; sqlite3.connect(SEARCH_DB).close()
    active_3d = sum(1 for c in crawlers if c['active_3d'] and c['enabled'])
    active_7d = sum(1 for c in crawlers if c['active_7d'] and c['enabled'])
    active_30d = sum(1 for c in crawlers if c['active_30d'] and c['enabled'])
    active_180d = sum(1 for c in crawlers if c['active_180d'] and c['enabled'])

    groups = {}
    for c in crawlers:
        g = c['group']
        if g not in groups:
            groups[g] = {'total': 0, 'enabled': 0, 'records': 0, 'active': 0}
        groups[g]['total'] += 1
        if c['enabled']:
            groups[g]['enabled'] += 1
        groups[g]['records'] += c['total']
        if c['active']:
            groups[g]['active'] += 1

    from datetime import datetime
    now_str = datetime.now().strftime('%Y-%m-%d %H:%M')
    week_total = sum(c['week_new'] for c in crawlers)
    day_total = sum(c['day_new'] for c in crawlers)

    parts = []
    parts.append(f'''<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>爬虫管理面板</title>
<script src="/static/chart.umd.min.js"></script>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family: -apple-system, sans-serif; background:#f5f6fa; color:#333; padding:20px; }}
h1 {{ font-size:22px; margin-bottom:20px; }}
.tabs {{ display:flex; gap:0; margin-bottom:20px; }}
.tab-btn {{ padding:8px 24px; border:1px solid #ccc; background:#fff; cursor:pointer; font-size:14px; font-weight:500; }}
.tab-btn:first-child {{ border-radius:6px 0 0 6px; }}
.tab-btn:last-child {{ border-radius:0 6px 6px 0; }}
.tab-btn.active {{ background:#3498db; color:#fff; border-color:#3498db; }}
.tab-btn:hover:not(.active) {{ background:#eef; }}
.tab-content {{ display:none; }}
.tab-content.active {{ display:block; }}
.stats-row {{ display:flex; gap:12px; flex-wrap:wrap; margin-bottom:20px; }}
.stat-card {{ background:#fff; border-radius:8px; padding:14px 18px; flex:1; min-width:120px; box-shadow:0 1px 3px rgba(0,0,0,0.08); }}
.stat-card .num {{ font-size:28px; font-weight:700; }}
.stat-card .label {{ font-size:13px; color:#888; margin-top:4px; }}
.stat-card.green .num {{ color:#27ae60; }}
.stat-card.blue .num {{ color:#2980b9; }}
.stat-card.red .num {{ color:#e74c3c; }}
.active-card {{ flex:2; min-width:220px; }}
.active-rows {{ display:flex; flex-direction:column; gap:6px; }}
.active-row {{ display:flex; align-items:center; gap:8px; font-size:13px; }}
.active-row .period {{ width:48px; color:#666; text-align:right; flex-shrink:0; }}
.active-row .bar {{ flex:1; height:6px; background:#eee; border-radius:3px; overflow:hidden; }}
.active-row .bar .fill {{ height:100%; border-radius:3px; transition:width 0.5s; }}
.active-row .count {{ width:32px; font-weight:700; text-align:right; flex-shrink:0; }}
.active-row:nth-child(1) .bar .fill {{ background:#e74c3c; }}
.active-row:nth-child(2) .bar .fill {{ background:#e67e22; }}
.active-row:nth-child(3) .bar .fill {{ background:#2980b9; }}
.active-row:nth-child(4) .bar .fill {{ background:#27ae60; }}
.filters {{ margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap; }}
.filters button {{ padding:5px 14px; border:1px solid #ccc; border-radius:15px; background:#fff; cursor:pointer; font-size:13px; }}
.filters button.active {{ background:#3498db; color:#fff; border-color:#3498db; }}
.filters button:hover {{ background:#eef; }}
table {{ width:100%; border-collapse:collapse; font-size:13px; background:#fff; border-radius:6px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,0.08); }}
th {{ background:#2c3e50; color:#fff; padding:8px 10px; text-align:left; font-weight:500; white-space:nowrap; }}
td {{ padding:7px 10px; border-bottom:1px solid #eee; }}
tr:hover {{ background:#f0f7ff; }}
tr.disabled {{ opacity:0.55; }}
.badge {{ display:inline-block; padding:1px 8px; border-radius:10px; font-size:11px; font-weight:500; }}
.badge-green {{ background:#d4edda; color:#155724; }}
.badge-red {{ background:#f8d7da; color:#721c24; }}
.badge-gray {{ background:#e2e3e5; color:#383d41; }}
.toggle-btn {{ display:inline-block; padding:2px 10px; border-radius:4px; cursor:pointer; font-size:12px; text-decoration:none; }}
.toggle-on {{ background:#27ae60; color:#fff; }}
.toggle-off {{ background:#e74c3c; color:#fff; }}
.missing-script {{ color:#e74c3c; font-weight:bold; }}
.nowrap {{ white-space:nowrap; }}
.analysis-controls {{ display:flex; gap:12px; flex-wrap:wrap; margin-bottom:16px; align-items:center; }}
.control-group {{ display:flex; gap:4px; align-items:center; }}
.control-group label {{ font-size:13px; color:#666; margin-right:4px; }}
.control-group select, .control-group button {{
  padding:5px 12px; border:1px solid #ccc; border-radius:4px; background:#fff; font-size:13px; cursor:pointer;
}}
.control-group button.active-ctrl {{
  background:#3498db; color:#fff; border-color:#3498db;
}}
.chart-container {{ background:#fff; border-radius:8px; padding:20px; box-shadow:0 1px 3px rgba(0,0,0,0.08); margin-bottom:20px; min-height:350px; }}
.chart-summary {{ display:flex; gap:16px; flex-wrap:wrap; margin-top:12px; }}
.chart-stat {{ font-size:14px; }}
.chart-stat span {{ font-weight:700; color:#2980b9; }}
th {{ cursor:pointer; user-select:none; }}
th.sort-asc::after {{ content:" ▲"; font-size:11px; }}
th.sort-desc::after {{ content:" ▼"; font-size:11px; }}
@media(max-width:700px) {{ table {{ font-size:12px; }} th,td {{ padding:5px 6px; }} }}
</style>
</head>
<body>
<h1>🔍 爬虫管理面板</h1>
<div class="tabs">
  <button class="tab-btn active" onclick="switchTab(event,'tab-manage')">📋 管理视图</button>
  <button class="tab-btn" onclick="switchTab(event,'tab-analysis')">📊 分析视图</button>
  <button class="tab-btn" onclick="window.location.href='/'">🔙 返回搜索</button>
</div>

<!-- TAB 1: 管理视图 -->
<div id="tab-manage" class="tab-content active">
<div class="stats-row">
  <div class="stat-card green"><div class="num">{total_records}</div><div class="label">总数据条数</div></div>
  <div class="stat-card blue"><div class="num">{enabled}/{total}</div><div class="label">爬虫 (启用/总数)</div></div>
  <div class="stat-card active-card">
    <div class="label">活跃站点（启用数）</div>
    <div class="active-rows">
      <div class="active-row"><span class="period">近3日</span><div class="bar"><div class="fill" style="width:{active_3d/total*100 if total else 0:.0f}%"></div></div><span class="count" style="color:#e74c3c">{active_3d}</span></div>
      <div class="active-row"><span class="period">近7日</span><div class="bar"><div class="fill" style="width:{active_7d/total*100 if total else 0:.0f}%"></div></div><span class="count" style="color:#e67e22">{active_7d}</span></div>
      <div class="active-row"><span class="period">近1月</span><div class="bar"><div class="fill" style="width:{active_30d/total*100 if total else 0:.0f}%"></div></div><span class="count" style="color:#2980b9">{active_30d}</span></div>
      <div class="active-row"><span class="period">近半年</span><div class="bar"><div class="fill" style="width:{active_180d/total*100 if total else 0:.0f}%"></div></div><span class="count" style="color:#27ae60">{active_180d}</span></div>
    </div>
  </div>
  <div class="stat-card green"><div class="num">+{day_total}</div><div class="label">今日新增</div></div>
  <div class="stat-card blue"><div class="num">+{week_total}</div><div class="label">近7日新增</div></div>
  <div class="stat-card red"><div class="num">{disabled}</div><div class="label">已禁用</div></div>
  <div class="stat-card" id="daily-run-card" style="cursor:pointer; background:#f0fdf4; border:2px dashed #27ae60;" onclick="runDaily()">
    <div class="num" style="font-size:22px;" id="daily-run-icon">▶️</div>
    <div class="label" id="daily-run-label">手动日跑</div>
  </div>
</div>
<div id="daily-run-log" style="display:none; background:#1e1e1e; color:#0f0; padding:12px; border-radius:6px; margin-top:10px; max-height:300px; overflow-y:auto; font-family:monospace; font-size:11px; white-space:pre-wrap;"></div>
<div style="font-size:12px; color:#999; margin-bottom:15px;">更新时间: {now_str}</div>
<div class="filters">
  <button class="active" onclick="filterGroup('all')">全部 ({total})</button>
''')
    for gname, gdata in sorted(groups.items()):
        parts.append(f'  <button onclick="filterGroup(\'{gname}\')">{gname} ({gdata["total"]})</button>\n')
    parts.append('''</div>
<table id="crawler-table">
<thead><tr>
  <th onclick="sortTable(0)" style="cursor:pointer">状态</th><th onclick="sortTable(1)" style="cursor:pointer">名称</th><th onclick="sortTable(2)" style="width:60px;cursor:pointer">条数</th><th onclick="sortTable(3)" style="width:90px;cursor:pointer">最新</th>
  <th onclick="sortTable(4)" style="width:70px;cursor:pointer">7日新增</th><th onclick="sortTable(5)" style="width:60px;cursor:pointer">活跃</th><th onclick="sortTable(6)" style="cursor:pointer">分组</th><th onclick="sortTable(7)" style="cursor:pointer">脚本</th><th style="width:200px">来源URL</th><th style="width:80px">日跑状态</th><th style="width:70px">操作</th>
</tr></thead>
<tbody>
''')
    for c in sorted(crawlers, key=lambda x: (not x['enabled'], (x.get('latest') or '')[::-1], -x.get('total',0))):
        tr_class = 'disabled' if not c['enabled'] else ''
        active_marker = '🟢' if c['enabled'] and c['active'] else ('🟡' if c['enabled'] and c['week_new'] > 0 else ('⚪' if c['enabled'] else '⚫'))
        script_display = c['script']
        if not c['script_exists']:
            script_display = f'<span class="missing-script">⚠ {c["script"]}</span>'
        toggle_url = f'/crawler/admin/toggle?name={c["name"]}'
        toggle_btn = f'<a href="{toggle_url}" class="toggle-btn toggle-on" onclick="return confirm(\'禁用 {c["name"]}？\')">禁用</a>' if c['enabled'] else f'<a href="{toggle_url}" class="toggle-btn toggle-off" onclick="return confirm(\'启用 {c["name"]}？\')">启用</a>'
        # 从脚本源码提取真实请求URL（config url字段优先覆盖）
        _display_url = c.get('url', '') or c.get('source_url', '')
        if _display_url:
            url_cell = '<a href="%s" target="_blank" title="%s" style="color:#2980b9;font-size:11px;text-decoration:none;">%s</a>' % (_display_url, _display_url, _display_url[:55])
        else:
            url_cell = '<span style="color:#ccc; font-size:11px;">-</span>'

        run_status = c['last_run_status']
        run_color = '#27ae60' if '✅' in run_status else ('#e67e22' if '⏰' in run_status else ('#e74c3c' if '❌' in run_status else '#999'))
        parts.append(f'''<tr class="{tr_class}">
  <td>{active_marker}</td>
  <td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="{c['name']}">{c['name']}</td>
  <td style="text-align:right">{c['total']}</td>
  <td class="nowrap">{(c['latest'] or '')[:10] if (c['latest'] or '') != '-' else '-'}</td>
  <td style="text-align:right">{c['week_new']}</td>
  <td class="active-col" style="text-align:center">{'✅' if c['active'] else '❌'}</td>
  <td>{c['group']}</td>
  <td style="font-size:11px; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{script_display}</td>
  <td style="font-size:11px; max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{url_cell}</td>
  <td style="font-size:12px; text-align:center; color:{run_color};">{run_status}</td>
  <td>{toggle_btn}</td>
</tr>
''')
    parts.append('''</tbody></table>
<div id="tab-analysis" class="tab-content">
  <div class="analysis-controls">
    <div class="control-group">
      <label>时间轴</label>
      <select id="period-select" onchange="loadChart()">
        <option value="day">按日</option>
        <option value="week">按周</option>
        <option value="month">按月</option>
      </select>
    </div>
    <div class="control-group">
      <label>纵轴</label>
      <button id="mode-inc" class="active-ctrl" onclick="setMode('incremental')">单日增量</button>
      <button id="mode-cum" onclick="setMode('cumulative')">累计总量</button>
    </div>
    <div class="control-group">
      <label>指标</label>
      <button id="metric-both" class="active-ctrl" onclick="setMetric('both')">双指标</button>
      <button id="metric-records" onclick="setMetric('records')">数据量</button>
      <button id="metric-spiders" onclick="setMetric('spiders')">脚本数</button>
    </div>
    <div class="control-group">
      <label>时间范围</label>
      <select id="range-select" onchange="loadChart()">
        <option value="30">近30天</option>
        <option value="90" selected>近90天</option>
        <option value="180">近半年</option>
        <option value="365">近一年</option>
        <option value="0">全部</option>
      </select>
    </div>
  </div>
  <div style="font-size:12px; color:#888; margin-bottom:8px;">
    <span style="display:inline-block; width:12px; height:3px; background:#3498db; margin-right:4px; vertical-align:middle;"></span> 数据量
    <span style="display:inline-block; width:12px; height:3px; background:#27ae60; margin-left:12px; margin-right:4px; vertical-align:middle;"></span> 脚本数
  </div>
  <div class="chart-container">
    <canvas id="analysis-chart" height="80"></canvas>
  </div>
  <div id="chart-summary" class="chart-summary"></div>
</div>  <!-- end tab-analysis -->

<script>
var currentMode = 'incremental';
var currentMetric = 'both';
var chartInstance = null;

function switchTab(event, tabId) {
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
  document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
  document.getElementById(tabId).classList.add('active');
  event.target.classList.add('active');
  if (tabId === 'tab-analysis') loadChart();
}

function setMode(mode) {
  currentMode = mode;
  document.getElementById('mode-inc').classList.toggle('active-ctrl', mode === 'incremental');
  document.getElementById('mode-cum').classList.toggle('active-ctrl', mode === 'cumulative');
  loadChart();
}

function setMetric(metric) {
  currentMetric = metric;
  document.getElementById('metric-both').classList.toggle('active-ctrl', metric === 'both');
  document.getElementById('metric-records').classList.toggle('active-ctrl', metric === 'records');
  document.getElementById('metric-spiders').classList.toggle('active-ctrl', metric === 'spiders');
  loadChart();
}

function filterGroup(group) {
  const rows = document.querySelectorAll('#crawler-table tbody tr');
  const btns = document.querySelectorAll('.filters button');
  btns.forEach(b => b.classList.remove('active'));
  event.target.classList.add('active');
  rows.forEach(r => {
    const groupCell = r.querySelector('td:nth-child(7)').textContent;
    r.style.display = (group === 'all' || groupCell === group) ? '' : 'none';
  });
}

function loadChart() {
  var period = document.getElementById('period-select').value;
  var days = document.getElementById('range-select').value;
  var mode = currentMode;
  var metric = currentMetric;
  var summaryDiv = document.getElementById('chart-summary');
  summaryDiv.innerHTML = '<div class="chart-stat">⏳ 加载中...</div>';
  fetch('/crawler/admin/analysis?period=' + period + '&mode=' + mode + '&metric=' + metric + '&days=' + days)
    .then(r => r.json())
    .then(data => {
      if (!data.labels || data.labels.length === 0) {
        summaryDiv.innerHTML = '<div class="chart-stat">暂无数据</div>';
        return;
      }
      renderChart(data);
      var unit = period === 'month' ? '月' : (period === 'week' ? '周' : '日');
      var rc = data.record_counts || [];
      var sc = data.spider_counts || [];
      // Records stats
      var r_total = rc.length > 0 ? rc[rc.length - 1] : 0;
      var r_sum = rc.reduce(function(a,b){return a+b}, 0);
      var r_avg = rc.length > 0 ? (r_sum / rc.length).toFixed(1) : '0';
      var r_max = rc.length > 0 ? Math.max.apply(null, rc) : 0;
      // Spider stats
      var s_total = sc.length > 0 ? sc[sc.length - 1] : 0;
      var s_sum = sc.reduce(function(a,b){return a+b}, 0);
      var s_avg = sc.length > 0 ? (s_sum / sc.length).toFixed(1) : '0';
      var s_max = sc.length > 0 ? Math.max.apply(null, sc) : 0;
      summaryDiv.innerHTML =
        '<div class="chart-stat" style="border-right:1px solid #ddd; padding-right:16px;">' +
          '<b style="color:#3498db">📊 数据量</b> 总计: <span>' + r_total + '</span> | ' +
          '均值: <span>' + r_avg + '</span>/' + unit + ' | 峰值: <span>' + r_max + '</span>/' + unit +
        '</div>' +
        '<div class="chart-stat">' +
          '<b style="color:#27ae60">📜 脚本数</b> 总计: <span>' + s_total + '</span> | ' +
          '均值: <span>' + s_avg + '</span>/' + unit + ' | 峰值: <span>' + s_max + '</span>/' + unit +
        '</div>' +
        '<div class="chart-stat" style="color:#888; border-left:1px solid #ddd; padding-left:16px;">🗓 <span>' + data.labels.length + '</span> 个时间区间</div>';
    })
    .catch(function(err) {
      summaryDiv.innerHTML = '<div class="chart-stat" style="color:red">加载失败: ' + err.message + '</div>';
    });
}

function renderChart(data) {
  var ctx = document.getElementById('analysis-chart').getContext('2d');
  if (chartInstance) chartInstance.destroy();

  var isCum = currentMode === 'cumulative';
  var labelSuffix = isCum ? '累计' : '增量';

  var labels = data.labels;
  var rc = data.record_counts || [];
  var sc = data.spider_counts || [];

  if (labels.length > 120) {
    var step = Math.ceil(labels.length / 120);
    var nl = [], nrc = [], nsc = [];
    for (var i = 0; i < labels.length; i += step) {
      nl.push(labels[i]);
      nrc.push(rc[i] || 0);
      nsc.push(sc[i] || 0);
    }
    if (nl[nl.length-1] !== labels[labels.length-1]) {
      nl.push(labels[labels.length-1]);
      nrc.push(rc[rc.length-1] || 0);
      nsc.push(sc[sc.length-1] || 0);
    }
    labels = nl; rc = nrc; sc = nsc;
  }

  var datasets = [];
  var activeMetric = currentMetric;

  if (activeMetric === 'records' || activeMetric === 'both') {
    datasets.push({
      label: '数据量 (' + labelSuffix + ')', data: rc, fill: true,
      backgroundColor: 'rgba(52,152,219,0.15)',
      borderColor: 'rgba(52,152,219,0.85)',
      pointBackgroundColor: 'rgba(52,152,219,1)',
      pointRadius: 2, pointHoverRadius: 5,
      borderWidth: 2, tension: 0.35,
    });
  }

  if (activeMetric === 'spiders' || activeMetric === 'both') {
    datasets.push({
      label: '脚本数 (' + labelSuffix + ')', data: sc, fill: true,
      backgroundColor: 'rgba(39,174,96,0.12)',
      borderColor: 'rgba(39,174,96,0.85)',
      pointBackgroundColor: 'rgba(39,174,96,1)',
      pointRadius: 2, pointHoverRadius: 5,
      borderWidth: 2, tension: 0.35,
      yAxisID: 'y',
    });
  }

  chartInstance = new Chart(ctx, {
    type: 'line',
    data: { labels: labels, datasets: datasets },
    options: {
      responsive: true, maintainAspectRatio: false,
      interaction: { mode: 'index', intersect: false },
      plugins: {
        legend: { display: datasets.length > 1, position: 'top' },
        tooltip: { callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y; } } }
      },
      scales: {
        x: { grid: { display: false }, ticks: { font: { size: 11 }, maxRotation: 45 } },
        y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.06)' }, ticks: { font: { size: 11 } } }
      }
    }
  });
}

// Daily run
var dailyCheckTimer = null;
function runDaily() {
  var card = document.getElementById('daily-run-card');
  var icon = document.getElementById('daily-run-icon');
  var label = document.getElementById('daily-run-label');
  var logDiv = document.getElementById('daily-run-log');
  icon.textContent = '⏳'; label.textContent = '启动中...';
  card.style.cursor = 'default'; card.onclick = null;
  fetch('/crawler/admin/run-daily').then(function(r){return r.json()}).then(function(data){
    if (data.running) {
      icon.textContent = '🔄'; label.textContent = '运行中...';
      logDiv.style.display = 'block';
      logDiv.textContent = data.message + '\\n\\n日志即将刷新...';
      dailyCheckTimer = setInterval(checkStatus, 5000);
      checkStatus();
    } else {
      icon.textContent = '❌'; label.textContent = data.message;
      setTimeout(resetButton, 3000);
    }
  }).catch(function(e){ icon.textContent = '❌'; label.textContent = '网络错误'; setTimeout(resetButton, 3000); });
}
function checkStatus() {
  fetch('/crawler/admin/run-status').then(function(r){return r.json()}).then(function(data){
    var icon = document.getElementById('daily-run-icon');
    var label = document.getElementById('daily-run-label');
    var logDiv = document.getElementById('daily-run-log');
    if (data.running && data.log_tail) logDiv.textContent = data.log_tail;
    if (!data.running) {
      icon.textContent = '✅'; label.textContent = '日跑完成';
      if (dailyCheckTimer) { clearInterval(dailyCheckTimer); dailyCheckTimer = null; }
      setTimeout(function(){ location.reload(); }, 3000);
    }
  });
}
function editUrl(el, name) {
  var currentUrl = el.textContent === '+添加URL' ? '' : el.textContent;
  var newUrl = prompt('请输入 ' + name + ' 的来源URL:', currentUrl);
  if (newUrl === null) return;
  if (newUrl === currentUrl) return;
  fetch('/crawler/admin/set-url?name=' + encodeURIComponent(name) + '&url=' + encodeURIComponent(newUrl))
    .then(function(r){return r.json()})
    .then(function(data){
      if (data.success) location.reload();
      else alert('设置失败: ' + (data.error || '未知错误'));
    })
    .catch(function(e){ alert('网络错误: ' + e.message); });
}
function resetButton() {
  var card = document.getElementById('daily-run-card');
  var icon = document.getElementById('daily-run-icon');
  var label = document.getElementById('daily-run-label');
  icon.textContent = '▶️'; label.textContent = '手动日跑';
  card.style.cursor = 'pointer'; card.onclick = runDaily;
}

// ── 列排序 ──
var sortColumn = null;
var sortAsc = true;
function sortTable(colIdx) {
  var table = document.getElementById('crawler-table');
  var tbody = table.querySelector('tbody');
  var rows = Array.from(tbody.querySelectorAll('tr'));
  var headers = table.querySelectorAll('th');
  headers.forEach(function(h, i){ h.classList.remove('sort-asc', 'sort-desc'); });
  if (sortColumn === colIdx) { sortAsc = !sortAsc; } else { sortColumn = colIdx; sortAsc = true; }
  headers[colIdx].classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
  rows.sort(function(a, b) {
    var va = a.cells[colIdx] ? a.cells[colIdx].textContent.trim() : '';
    var vb = b.cells[colIdx] ? b.cells[colIdx].textContent.trim() : '';
    var na = parseFloat(va), nb = parseFloat(vb);
    if (!isNaN(na) && !isNaN(nb)) { return sortAsc ? na - nb : nb - na; }
    return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
  });
  rows.forEach(function(r){ tbody.appendChild(r); });
}
</script>
</body></html>''')
    return ''.join(parts)


def set_crawler_url(name, url):
    """设置爬虫的来源URL"""
    cfg = json.load(open(CONFIG_PATH))
    if isinstance(cfg, list):
        cfg = {"crawlers": cfg}
    for c in cfg['crawlers']:
        cname = c.get('name', c.get('site_name', ''))
        if cname == name:
            if url:
                c['url'] = url
            else:
                c.pop('url', None)
            json.dump(cfg, open(CONFIG_PATH, 'w'), ensure_ascii=False, indent=2)
            return {'success': True, 'name': name, 'url': url}
    return {'success': False, 'error': f'Crawler "{name}" not found'}

def toggle_crawler(name):
    """切换爬虫启用/禁用状态"""
    cfg = json.load(open(CONFIG_PATH))
    if isinstance(cfg, list):
        cfg = {crawlers: cfg}
    for c in cfg['crawlers']:
        if c['name'] == name:
            c['enabled'] = not c['enabled']
            json.dump(cfg, open(CONFIG_PATH, 'w'), ensure_ascii=False, indent=2)
            return {'success': True, 'name': name, 'new_status': 'enabled' if c['enabled'] else 'disabled'}
    return {'success': False, 'error': f'Crawler "{name}" not found'}


# ─── 手动日跑 ───

RUN_PID_FILE = "/tmp/daily_crawl_run.pid"
RUN_LOG_TAIL = 20

def run_daily_crawl():
    import os, subprocess
    status = get_daily_run_status()
    if status.get('running'):
        return {'success': False, 'message': '日跑已在运行中', 'running': True}
    try:
        pid = os.fork()
        if pid == 0:
            os.setsid()
            subprocess.run(['bash', '/root/daily_incremental.sh'],
                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            try: os.remove(RUN_PID_FILE)
            except: pass
            os._exit(0)
        else:
            with open(RUN_PID_FILE, 'w') as f: f.write(str(pid))
            return {'success': True, 'message': '日跑已启动', 'running': True, 'pid': pid}
    except Exception as e:
        return {'success': False, 'message': f'启动失败: {e}', 'running': False}

def get_daily_run_status():
    import os, subprocess
    if os.path.exists(RUN_PID_FILE):
        try:
            with open(RUN_PID_FILE) as f: pid = int(f.read().strip())
            os.kill(pid, 0)
            log_tail = ''
            try:
                log_tail = subprocess.check_output(
                    ['tail', f'-{RUN_LOG_TAIL}', '/var/log/crawler_daily.log'],
                    timeout=3, stderr=subprocess.DEVNULL).decode('utf-8', errors='replace')
            except: pass
            return {'running': True, 'pid': pid, 'log_tail': log_tail}
        except (OSError, ValueError):
            try: os.remove(RUN_PID_FILE)
            except: pass
    return {'running': False}
