#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
====================================================================
 מערכת ניטור מסכים v2.0 - גרסה משודרגת (מתוקן - ללא קפיצת חלונות CMD)
====================================================================
הידור ל-EXE:
    pip install pyinstaller flask werkzeug pillow mss requests
    pyinstaller --onefile --noconsole --name MonitorSystem monitor_system_v2.py

אין מצב "מרכז" / "לקוח" - כל המחשבים זהים.
כל מחשב צולם, שומר מקומית, ודוחף לכל ה-Peers שהוגדרו.
ארגון תמונות: screenshots/<hostname>__<os_user>/
====================================================================
"""

import os, io, sys, json, re, time, socket, secrets, hashlib, getpass
import platform, threading, functools, subprocess, webbrowser
from datetime import datetime, timedelta

# ─── נתיב בסיס ─────────────────────────────────────────────────
def _base():
    return os.path.dirname(os.path.abspath(
        sys.executable if getattr(sys, "frozen", False) else __file__))

BASE_DIR  = _base()
DB_PATH   = os.path.join(BASE_DIR, "monitoring.db")
SHOTS_DIR = os.path.join(BASE_DIR, "screenshots")
PORT      = 6374
ONLINE_S  = 120
TASK_NAME = "MonitorSystemAutoStart"

os.makedirs(SHOTS_DIR, exist_ok=True)

# ─── לוג ────────────────────────────────────────────────────────
def log(msg):
    line = f"{datetime.now():%Y-%m-%d %H:%M:%S}  {msg}"
    try:
        with open(os.path.join(BASE_DIR, "app.log"), "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        pass
    print(line)

def safe_dir_name(hostname, os_user):
    raw = f"{hostname}__{os_user}"
    return re.sub(r"[^\w.-]", "_", raw)

# ════════════════════════════════════════════════════════════════
#  מסד נתונים
# ════════════════════════════════════════════════════════════════
import sqlite3

def get_db():
    c = sqlite3.connect(DB_PATH, timeout=20)
    c.row_factory = sqlite3.Row
    c.execute("PRAGMA foreign_keys = ON")
    c.execute("PRAGMA journal_mode = WAL")
    return c

DEFAULTS = {
    "api_key":            None,   # per-machine peer-auth key
    "secret_key":         None,   # Flask session key
    "capture_interval":   "60",   # שניות בין צילומים
    "skip_unchanged":     "1",    # דלג אם לא השתנה
    "live_quality":       "65",   # איכות JPEG לצפייה חיה
    "max_shots":          "500",  # מקסימום צילומים לכל משתמש
    "autostart_installed":"0",
}

def init_db():
    from werkzeug.security import generate_password_hash
    db = get_db(); c = db.cursor()

    c.execute("""CREATE TABLE IF NOT EXISTS users (
        id            INTEGER PRIMARY KEY AUTOINCREMENT,
        username      TEXT UNIQUE NOT NULL,
        password_hash TEXT NOT NULL,
        role          TEXT NOT NULL DEFAULT 'viewer',
        can_manage    INTEGER NOT NULL DEFAULT 0,
        can_view_all  INTEGER NOT NULL DEFAULT 0,
        allowed_keys  TEXT NOT NULL DEFAULT '[]',
        last_login    TEXT,
        created_at    TEXT NOT NULL
    )""")

    c.execute("""CREATE TABLE IF NOT EXISTS agents (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        hostname    TEXT NOT NULL,
        os_user     TEXT NOT NULL DEFAULT '',
        ip_address  TEXT,
        first_seen  TEXT NOT NULL,
        last_seen   TEXT NOT NULL,
        is_local    INTEGER NOT NULL DEFAULT 0,
        UNIQUE(hostname, os_user)
    )""")

    c.execute("""CREATE TABLE IF NOT EXISTS screenshots (
        id        INTEGER PRIMARY KEY AUTOINCREMENT,
        agent_id  INTEGER NOT NULL,
        hostname  TEXT NOT NULL,
        os_user   TEXT NOT NULL DEFAULT '',
        filename  TEXT NOT NULL,
        taken_at  TEXT NOT NULL,
        file_hash TEXT,
        FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
    )""")

    c.execute("""CREATE TABLE IF NOT EXISTS peers (
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        ip_address TEXT NOT NULL UNIQUE,
        hostname   TEXT,
        nickname   TEXT,
        api_key    TEXT NOT NULL DEFAULT '',
        added_at   TEXT NOT NULL,
        last_ok    TEXT,
        last_error TEXT
    )""")

    c.execute("""CREATE TABLE IF NOT EXISTS activity (
        id       INTEGER PRIMARY KEY AUTOINCREMENT,
        kind     TEXT NOT NULL,
        msg      TEXT NOT NULL,
        hostname TEXT,
        os_user  TEXT,
        ts       TEXT NOT NULL
    )""")

    c.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)")
    db.commit()

    if db.execute("SELECT COUNT(*) AS n FROM users").fetchone()["n"] == 0:
        db.execute(
            "INSERT INTO users (username,password_hash,role,can_manage,can_view_all,"
            "allowed_keys,created_at) VALUES ('admin',?,'superadmin',1,1,'[]',?)",
            (generate_password_hash("admin123"), datetime.utcnow().isoformat()))

    for k, v in DEFAULTS.items():
        if db.execute("SELECT value FROM settings WHERE key=?", (k,)).fetchone() is None:
            if k == "api_key":   v = secrets.token_hex(24)
            if k == "secret_key": v = secrets.token_hex(32)
            db.execute("INSERT INTO settings VALUES (?,?)", (k, v))

    db.commit(); db.close()

# ── Settings ──
def gs(key, default=None):
    db = get_db()
    r  = db.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
    db.close()
    return r["value"] if r else default

def ss(key, val):
    db = get_db()
    db.execute("INSERT INTO settings VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
               (key, val))
    db.commit(); db.close()

# ── Users ──
def u_get(username=None, uid=None):
    db = get_db()
    r  = (db.execute("SELECT * FROM users WHERE username=?", (username,)).fetchone() if username
          else db.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone())
    db.close(); return r

def u_list():
    db = get_db(); r = db.execute("SELECT * FROM users ORDER BY id").fetchall(); db.close(); return r

def u_create(username, password, role, can_manage, can_view_all, allowed_keys):
    from werkzeug.security import generate_password_hash
    db = get_db()
    db.execute("INSERT INTO users (username,password_hash,role,can_manage,can_view_all,"
               "allowed_keys,created_at) VALUES (?,?,?,?,?,?,?)",
               (username, generate_password_hash(password), role,
                int(can_manage), int(can_view_all),
                json.dumps(allowed_keys), datetime.utcnow().isoformat()))
    db.commit(); db.close()

def u_update(uid, role, can_manage, can_view_all, allowed_keys):
    db = get_db()
    db.execute("UPDATE users SET role=?,can_manage=?,can_view_all=?,allowed_keys=? WHERE id=?",
               (role, int(can_manage), int(can_view_all), json.dumps(allowed_keys), uid))
    db.commit(); db.close()

def u_set_pw(uid, pw):
    from werkzeug.security import generate_password_hash
    db = get_db()
    db.execute("UPDATE users SET password_hash=? WHERE id=?", (generate_password_hash(pw), uid))
    db.commit(); db.close()

def u_delete(uid):
    db = get_db(); db.execute("DELETE FROM users WHERE id=?", (uid,)); db.commit(); db.close()

def u_touch(uid):
    db = get_db()
    db.execute("UPDATE users SET last_login=? WHERE id=?", (datetime.utcnow().isoformat(), uid))
    db.commit(); db.close()

# ── Agents ──
def ag_upsert(hostname, os_user, ip, is_local=False):
    db  = get_db(); now = datetime.utcnow().isoformat()
    r   = db.execute("SELECT id FROM agents WHERE hostname=? AND os_user=?", (hostname, os_user)).fetchone()
    if r:
        db.execute("UPDATE agents SET last_seen=?,ip_address=?,is_local=? WHERE id=?",
                   (now, ip, int(is_local), r["id"]))
        aid = r["id"]
    else:
        cur = db.execute("INSERT INTO agents (hostname,os_user,ip_address,first_seen,last_seen,is_local) "
                         "VALUES (?,?,?,?,?,?)", (hostname, os_user, ip, now, now, int(is_local)))
        aid = cur.lastrowid
    db.commit(); db.close(); return aid

def ag_list():
    db = get_db(); r = db.execute("SELECT * FROM agents ORDER BY hostname,os_user").fetchall(); db.close(); return r

def ag_get(hostname, os_user):
    db = get_db()
    r  = db.execute("SELECT * FROM agents WHERE hostname=? AND os_user=?", (hostname, os_user)).fetchone()
    db.close(); return r

def ag_delete(hostname, os_user):
    db = get_db()
    db.execute("DELETE FROM agents WHERE hostname=? AND os_user=?", (hostname, os_user))
    db.commit(); db.close()

# ── Screenshots ──
def s_add(agent_id, hostname, os_user, filename, taken_at, fhash=None):
    db = get_db()
    db.execute("INSERT INTO screenshots (agent_id,hostname,os_user,filename,taken_at,file_hash) "
               "VALUES (?,?,?,?,?,?)", (agent_id, hostname, os_user, filename, taken_at, fhash))
    db.commit(); db.close()

def s_latest(hostname, os_user=None):
    db = get_db()
    if os_user:
        r = db.execute("SELECT * FROM screenshots WHERE hostname=? AND os_user=? "
                       "ORDER BY taken_at DESC LIMIT 1", (hostname, os_user)).fetchone()
    else:
        r = db.execute("SELECT * FROM screenshots WHERE hostname=? "
                       "ORDER BY taken_at DESC LIMIT 1", (hostname,)).fetchone()
    db.close(); return r

def s_list(hostname=None, os_user=None, limit=300):
    db = get_db()
    if hostname and os_user:
        r = db.execute("SELECT * FROM screenshots WHERE hostname=? AND os_user=? "
                       "ORDER BY taken_at DESC LIMIT ?", (hostname, os_user, limit)).fetchall()
    elif hostname:
        r = db.execute("SELECT * FROM screenshots WHERE hostname=? "
                       "ORDER BY taken_at DESC LIMIT ?", (hostname, limit)).fetchall()
    elif os_user:
        r = db.execute("SELECT * FROM screenshots WHERE os_user=? "
                       "ORDER BY taken_at DESC LIMIT ?", (os_user, limit)).fetchall()
    else:
        r = db.execute("SELECT * FROM screenshots ORDER BY taken_at DESC LIMIT ?", (limit,)).fetchall()
    db.close(); return r

def s_total():
    db = get_db(); n = db.execute("SELECT COUNT(*) AS n FROM screenshots").fetchone()["n"]; db.close(); return n

def s_prune(hostname, os_user, mx):
    db   = get_db()
    rows = db.execute("SELECT id,filename FROM screenshots WHERE hostname=? AND os_user=? "
                      "ORDER BY taken_at DESC", (hostname, os_user)).fetchall()
    if len(rows) > mx:
        dkey = safe_dir_name(hostname, os_user)
        ddir = os.path.join(SHOTS_DIR, dkey)
        for row in rows[mx:]:
            try: os.remove(os.path.join(ddir, row["filename"]))
            except Exception: pass
            db.execute("DELETE FROM screenshots WHERE id=?", (row["id"],))
        db.commit()
    db.close()

def s_delete_agent_shots(hostname, os_user):
    db   = get_db()
    rows = db.execute("SELECT filename FROM screenshots WHERE hostname=? AND os_user=?",
                      (hostname, os_user)).fetchall()
    ddir = os.path.join(SHOTS_DIR, safe_dir_name(hostname, os_user))
    for row in rows:
        try: os.remove(os.path.join(ddir, row["filename"]))
        except Exception: pass
    db.execute("DELETE FROM screenshots WHERE hostname=? AND os_user=?", (hostname, os_user))
    db.commit(); db.close()

# ── Peers ──
def p_list():
    db = get_db(); r = db.execute("SELECT * FROM peers ORDER BY id").fetchall(); db.close(); return r

def p_add(ip, nickname, api_key):
    db = get_db()
    db.execute("INSERT OR REPLACE INTO peers (ip_address,nickname,api_key,added_at) VALUES (?,?,?,?)",
               (ip, nickname, api_key, datetime.utcnow().isoformat()))
    db.commit(); db.close()

def p_remove(pid):
    db = get_db(); db.execute("DELETE FROM peers WHERE id=?", (pid,)); db.commit(); db.close()

def p_ok(pid, hostname):
    db = get_db()
    db.execute("UPDATE peers SET last_ok=?,hostname=?,last_error=NULL WHERE id=?",
               (datetime.utcnow().isoformat(), hostname, pid))
    db.commit(); db.close()

def p_err(pid, err):
    db = get_db()
    db.execute("UPDATE peers SET last_error=? WHERE id=?", (str(err)[:300], pid))
    db.commit(); db.close()

# ── Activity ──
def act(kind, msg, hostname=None, os_user=None):
    db = get_db()
    db.execute("INSERT INTO activity (kind,msg,hostname,os_user,ts) VALUES (?,?,?,?,?)",
               (kind, msg, hostname, os_user, datetime.utcnow().isoformat()))
    db.commit(); db.close()

def act_list(limit=200):
    db = get_db()
    r  = db.execute("SELECT * FROM activity ORDER BY ts DESC LIMIT ?", (limit,)).fetchall()
    db.close(); return r

# ════════════════════════════════════════════════════════════════
#  זיהוי משתמש מערכת ההפעלה (עובד גם בהרצה כ-SYSTEM) - מתוקן
# ════════════════════════════════════════════════════════════════
def get_os_user():
    if platform.system() == "Windows":
        try:
            no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
            out = subprocess.check_output(
                ["query", "session"],
                text=True, 
                timeout=5, 
                stderr=subprocess.DEVNULL,
                creationflags=no_window
            )
            for line in out.splitlines():
                if "Active" in line or "Actif" in line:
                    parts = line.split()
                    for p in parts:
                        if p and not p.isdigit() and p.lower() not in (
                                "console", "services", "rdp-tcp", "active", "disc", "actif"):
                            return p
        except Exception:
            pass
        return os.environ.get("USERNAME", getpass.getuser())
    try:
        out = subprocess.check_output(["who"], text=True, timeout=5)
        first = out.strip().splitlines()[0]
        return first.split()[0]
    except Exception:
        return os.environ.get("USER", getpass.getuser())

# ════════════════════════════════════════════════════════════════
#  מנוע צילום
# ════════════════════════════════════════════════════════════════
_hashes    = {}
_hash_lock = threading.Lock()
_stop      = threading.Event()

def take_png():
    try:
        import mss, mss.tools
        with mss.mss() as sct:
            shot = sct.grab(sct.monitors[0])
            return mss.tools.to_png(shot.rgb, shot.size)
    except Exception:
        pass
    try:
        from PIL import ImageGrab
        img = ImageGrab.grab(all_screens=True)
        buf = io.BytesIO(); img.save(buf, "PNG"); return buf.getvalue()
    except Exception as e:
        log(f"[צילום] שגיאה: {e}"); return None

def png2jpg(png_bytes, quality=70):
    try:
        from PIL import Image
        img = Image.open(io.BytesIO(png_bytes))
        buf = io.BytesIO()
        img.convert("RGB").save(buf, "JPEG", quality=quality)
        return buf.getvalue()
    except Exception:
        return png_bytes

def has_changed(key, png_bytes):
    h = hashlib.md5(png_bytes).hexdigest()
    with _hash_lock:
        old = _hashes.get(key)
        if old == h: return False
        _hashes[key] = h
    return True

def save_local(hostname, os_user, png_bytes, dt, fhash):
    dkey  = safe_dir_name(hostname, os_user)
    ddir  = os.path.join(SHOTS_DIR, dkey)
    os.makedirs(ddir, exist_ok=True)
    fname = f"{dkey}_{dt:%Y%m%d_%H%M%S}.png"
    with open(os.path.join(ddir, fname), "wb") as f:
        f.write(png_bytes)
    aid = ag_upsert(hostname, os_user, "127.0.0.1", is_local=True)
    s_add(aid, hostname, os_user, fname, dt.isoformat(), fhash)
    try:
        mx = int(gs("max_shots", "500") or "500")
        s_prune(hostname, os_user, mx)
    except Exception:
        pass

def push_to_peers(hostname, os_user, png_bytes, dt, fhash):
    import requests
    for peer in p_list():
        try:
            r = requests.post(
                f"http://{peer['ip_address']}:{PORT}/api/upload",
                headers={"X-API-Key": peer["api_key"]},
                data={"hostname": hostname, "os_user": os_user,
                      "taken_at": dt.isoformat(), "file_hash": fhash},
                files={"file": ("s.png", png_bytes, "image/png")},
                timeout=15)
            if r.status_code == 200:
                p_ok(peer["id"], hostname)
            else:
                p_err(peer["id"], f"HTTP {r.status_code}")
        except Exception as e:
            p_err(peer["id"], str(e)); log(f"[peer] {peer['ip_address']}: {e}")

def capture_worker():
    hostname = socket.gethostname()
    log(f"[מנוע] פועל על {hostname}")
    while not _stop.is_set():
        try:
            interval = int(gs("capture_interval", "60") or "60")
            skip     = gs("skip_unchanged", "1") == "1"
            os_user  = get_os_user()
            key      = (hostname, os_user)
            dt       = datetime.now()
            png      = take_png()
            if png is None:
                _stop.wait(60); continue
            fhash = hashlib.md5(png).hexdigest()
            if skip and not has_changed(key, png):
                _stop.wait(max(5, interval)); continue
            save_local(hostname, os_user, png, dt, fhash)
            peers = p_list()
            if peers:
                threading.Thread(target=push_to_peers,
                                 args=(hostname, os_user, png, dt, fhash),
                                 daemon=True).start()
        except Exception as e:
            log(f"[מנוע שגיאה] {e}"); interval = 60
        _stop.wait(max(5, interval))

# ════════════════════════════════════════════════════════════════
#  הפעלה אוטומטית ופריסה מרחוק - מתוקן
# ════════════════════════════════════════════════════════════════
def is_admin():
    if platform.system() == "Windows":
        try:
            import ctypes; return bool(ctypes.windll.shell32.IsUserAnAdmin())
        except: return False
    return os.geteuid() == 0

def self_cmd():
    if getattr(sys, "frozen", False): return f'"{sys.executable}"'
    pw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
    py = pw if os.path.exists(pw) else sys.executable
    return f'"{py}" "{os.path.abspath(__file__)}"'

def install_autostart():
    cmd = self_cmd()
    if platform.system() == "Windows":
        no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
        subprocess.run(["schtasks", "/Delete", "/TN", TASK_NAME, "/F"],
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                       creationflags=no_window)
        res = subprocess.run(
            ["schtasks", "/Create", "/TN", TASK_NAME, "/TR", cmd,
             "/SC", "ONLOGON", "/RU", "INTERACTIVE", "/RL", "HIGHEST", "/F"],
            capture_output=True, text=True, creationflags=no_window)
        if res.returncode != 0:
            res2 = subprocess.run(
                ["schtasks", "/Create", "/TN", TASK_NAME, "/TR", cmd,
                 "/SC", "ONSTART", "/RU", "SYSTEM", "/RL", "HIGHEST", "/F"],
                capture_output=True, text=True, creationflags=no_window)
            if res2.returncode != 0:
                raise RuntimeError(res2.stderr.strip() or "schtasks נכשל")
    else:
        svc = "/etc/systemd/system/monitor-system.service"
        with open(svc, "w") as f:
            f.write(f"[Unit]\nDescription=Monitor System v2\nAfter=network.target\n"
                    f"[Service]\nType=simple\nExecStart={cmd}\nWorkingDirectory={BASE_DIR}\n"
                    f"Restart=always\nRestartSec=10\nUser=root\n"
                    f"[Install]\nWantedBy=multi-user.target\n")
        subprocess.run(["systemctl", "daemon-reload"], check=True)
        subprocess.run(["systemctl", "enable", "monitor-system"], check=True)
        subprocess.run(["systemctl", "restart", "monitor-system"], check=True)

def auto_install():
    if gs("autostart_installed") == "1": return
    if is_admin():
        try:
            install_autostart(); ss("autostart_installed", "1")
            log("[autostart] הותקן אוטומטית")
        except Exception as e:
            log(f"[autostart] כשל: {e}")

def deploy_windows(target_ip, admin_user, admin_pw):
    if not getattr(sys, "frozen", False):
        return False, "נדרש EXE מהודר לפריסה מרחוק"
    exe   = sys.executable
    share = f"\\\\{target_ip}\\c$\\MonitorSystem"
    no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
    try:
        subprocess.run(["net", "use", f"\\\\{target_ip}\\ipc$",
                        f"/user:{admin_user}", admin_pw],
                       check=True, capture_output=True, timeout=15,
                       creationflags=no_window)
        os.makedirs(share, exist_ok=True)
        import shutil; shutil.copy2(exe, f"{share}\\MonitorSystem.exe")
        subprocess.run(
            f'schtasks /Create /S {target_ip} /U {admin_user} /P {admin_pw} '
            f'/TN {TASK_NAME} /TR "C:\\\\MonitorSystem\\\\MonitorSystem.exe" '
            f'/SC ONLOGON /RL HIGHEST /F',
            shell=True, capture_output=True, timeout=30, creationflags=no_window)
        subprocess.run(
            f"schtasks /Run /S {target_ip} /U {admin_user} /P {admin_pw} /TN {TASK_NAME}",
            shell=True, capture_output=True, timeout=15, creationflags=no_window)
        act("deploy", f"פריסה מרחוק ל-{target_ip} הצליחה")
        return True, f"✅ הפריסה ל-{target_ip} הצליחה"
    except Exception as e:
        return False, f"❌ שגיאה: {e}"
    finally:
        subprocess.run(["net", "use", f"\\\\{target_ip}\\ipc$", "/delete"],
                       capture_output=True, creationflags=no_window)

# ════════════════════════════════════════════════════════════════
#  CSS
# ════════════════════════════════════════════════════════════════
BASE_CSS = r"""
:root{--bg:#0d1117;--pnl:#161b22;--p2:#21262d;--p3:#30363d;--acc:#2f81f7;--acc2:#3fb950;--acc3:#d29922;--red:#f85149;--txt:#e6edf3;--dim:#8b949e;--brd:#30363d;--brd2:#21262d}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Segoe UI",system-ui,Arial,sans-serif;background:var(--bg);color:var(--txt);direction:rtl;min-height:100vh}
a{color:var(--acc);text-decoration:none}
a:hover{text-decoration:underline}
/* ─── topbar ─── */
.topbar{display:flex;align-items:center;justify-content:space-between;padding:0 22px;height:54px;background:var(--pnl);border-bottom:1px solid var(--brd);position:sticky;top:0;z-index:200;gap:12px}
.brand{font-size:16px;font-weight:700;display:flex;align-items:center;gap:8px;text-decoration:none;color:var(--txt);white-space:nowrap}
.brand em{color:var(--acc);font-style:normal}
nav{display:flex;align-items:center;gap:2px;flex-wrap:wrap}
nav a{padding:5px 11px;border-radius:6px;color:var(--dim);font-size:13px;font-weight:500;transition:.15s;white-space:nowrap}
nav a:hover,nav a.act{background:var(--p2);color:var(--txt);text-decoration:none}
nav a.act{color:var(--acc)}
.pill{font-size:12px;color:var(--dim);padding:4px 11px;background:var(--p2);border-radius:20px;border:1px solid var(--brd);white-space:nowrap}
/* ─── layout ─── */
.wrap{max-width:1480px;margin:0 auto;padding:26px 22px}
/* ─── flash ─── */
.flash{padding:11px 15px;border-radius:8px;margin-bottom:14px;font-size:13px;display:flex;align-items:center;gap:8px}
.flash.error{background:rgba(248,81,73,.1);border:1px solid rgba(248,81,73,.3);color:#ffa198}
.flash.success{background:rgba(63,185,80,.1);border:1px solid rgba(63,185,80,.3);color:#7ee787}
.flash.info{background:rgba(47,129,247,.1);border:1px solid rgba(47,129,247,.3);color:#79c0ff}
/* ─── stats ─── */
.stats{display:flex;gap:13px;margin-bottom:22px;flex-wrap:wrap}
.stat{background:var(--pnl);border:1px solid var(--brd);border-radius:10px;padding:14px 20px;flex:1;min-width:110px}
.sv{font-size:28px;font-weight:700;color:var(--acc)}
.sl{font-size:11px;color:var(--dim);margin-top:3px}
/* ─── grid & cards ─── */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(272px,1fr));gap:14px}
.card{background:var(--pnl);border:1px solid var(--brd);border-radius:12px;overflow:hidden;transition:.18s}
.card:hover{border-color:var(--acc);box-shadow:0 0 0 1px rgba(47,129,247,.22)}
.thumb{position:relative;aspect-ratio:16/9;overflow:hidden;background:#000;cursor:pointer}
.thumb img{width:100%;height:100%;object-fit:cover;transition:.3s;display:block}
.thumb:hover img{transform:scale(1.04)}
.no-img{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dim);font-size:13px}
.badge-live{position:absolute;top:8px;inset-inline-start:8px;background:var(--red);color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:4px;letter-spacing:.5px}
.cbody{padding:12px 14px}
.ctitle{font-weight:700;font-size:14px;display:flex;align-items:center;gap:7px;margin-bottom:4px}
.cmeta{color:var(--dim);font-size:12px;line-height:1.65}
.cfooter{padding:9px 14px 11px;display:flex;gap:7px;border-top:1px solid var(--brd2)}
/* ─── status dot ─── */
.dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex-shrink:0}
.on-dot{background:var(--acc2);box-shadow:0 0 0 3px rgba(63,185,80,.2)}
.off-dot{background:var(--dim)}
/* ─── buttons ─── */
.btn,button{display:inline-flex;align-items:center;gap:6px;background:var(--acc);color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-weight:600;font-size:13px;transition:opacity .15s;white-space:nowrap;text-decoration:none;line-height:1}
.btn:hover,button:hover{opacity:.82;text-decoration:none}
.sm{padding:5px 9px;font-size:12px}
.sec{background:var(--p2);color:var(--txt);border:1px solid var(--brd)}
.dng{background:var(--red)}
.ok{background:var(--acc2)}
.wrn{background:var(--acc3);color:#0d1117}
/* ─── forms ─── */
.fg{margin-bottom:15px}
label{font-size:13px;color:var(--dim);display:block;margin-bottom:5px;font-weight:500}
input[type=text],input[type=password],input[type=number],input[type=email],select,textarea{width:100%;padding:9px 12px;border-radius:7px;border:1px solid var(--brd);background:var(--p2);color:var(--txt);font-size:14px;transition:.15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--acc)}
.help{color:var(--dim);font-size:12px;margin-top:4px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:560px){.row2{grid-template-columns:1fr}}
/* ─── table ─── */
.tw{overflow-x:auto;border-radius:10px;border:1px solid var(--brd)}
table{width:100%;border-collapse:collapse}
th,td{padding:10px 14px;text-align:right;border-bottom:1px solid var(--brd2);font-size:13px}
th{background:var(--p2);color:var(--dim);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
tr:last-child td{border-bottom:none}
tr:hover td{background:rgba(255,255,255,.02)}
/* ─── section header ─── */
.sh{display:flex;align-items:center;justify-content:space-between;margin:24px 0 14px}
.sh h2{font-size:18px;font-weight:700}
fieldset{border:1px solid var(--brd);border-radius:10px;padding:17px;margin-bottom:17px}
legend{padding:0 10px;color:var(--dim);font-size:13px;font-weight:600}
/* ─── badge ─── */
.b{display:inline-block;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:700}
.bg{background:rgba(63,185,80,.15);color:var(--acc2)}
.br{background:rgba(248,81,73,.15);color:var(--red)}
.bb{background:rgba(47,129,247,.15);color:var(--acc)}
.bo{background:rgba(210,153,34,.15);color:var(--acc3)}
/* ─── misc ─── */
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
.api-box{background:var(--p2);border:1px dashed var(--brd);padding:11px 14px;border-radius:8px;word-break:break-all}
.empty{text-align:center;padding:60px 20px;color:var(--dim)}
.empty .ico{font-size:46px;margin-bottom:14px}
.cl{display:flex;flex-wrap:wrap;gap:7px}
.cl label{display:flex;align-items:center;gap:6px;background:var(--p2);padding:5px 10px;border-radius:8px;font-size:13px;color:var(--txt);cursor:pointer}
.cl input[type=checkbox]{width:auto}
/* ─── tabs ─── */
.tabs{display:flex;border-bottom:1px solid var(--brd);margin-bottom:18px}
.tab{padding:10px 18px;font-size:13px;font-weight:600;color:var(--dim);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:.15s}
.tab.on{color:var(--acc);border-bottom-color:var(--acc)}
.tp{display:none}
.tp.on{display:block}
/* ─── lightbox ─── */
#lb{display:none;position:fixed;inset:0;background:rgba(0,0,0,.93);z-index:3000;align-items:center;justify-content:center;flex-direction:column}
#lb.on{display:flex}
#lb-img{max-width:95vw;max-height:83vh;border-radius:8px;box-shadow:0 8px 40px #000;display:block}
#lb-info{color:rgba(255,255,255,.6);font-size:13px;margin-top:11px;text-align:center}
.lbb{position:fixed;color:#fff;cursor:pointer;background:rgba(255,255,255,.12);width:46px;height:46px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:none;font-size:22px;transition:.15s;z-index:3001}
.lbb:hover{background:rgba(255,255,255,.25)}
#lb-x{top:14px;inset-inline-start:14px}
#lb-p{top:50%;right:14px;transform:translateY(-50%)}
#lb-n{top:50%;left:14px;transform:translateY(-50%)}
/* ─── live view ─── */
.live-wrap{background:#000;border-radius:12px;overflow:hidden;border:1px solid var(--brd)}
#lv-img{width:100%;display:block;min-height:180px}
.live-bar{display:flex;align-items:center;gap:12px;padding:9px 14px;background:var(--p2);border-top:1px solid var(--brd);flex-wrap:wrap}
.ldot{width:9px;height:9px;border-radius:50%;background:var(--red);display:inline-block}
.ldot.pulse{animation:p 1s infinite}
@keyframes p{0%,100%{opacity:1}50%{opacity:.3}}
.lstat{font-size:12px;color:var(--dim)}
/* ─── filter bar ─── */
.fbar{display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap}
.fbar select,.fbar input{width:auto}
/* ─── activity ─── */
.ak-screenshot{color:var(--acc)}
.ak-peer{color:var(--acc2)}
.ak-deploy{color:var(--acc3)}
.ak-login{color:var(--dim)}
.ak-error{color:var(--red)}
"""

# ════════════════════════════════════════════════════════════════
#  תבנית בסיס
# ════════════════════════════════════════════════════════════════
LAYOUT = """<!DOCTYPE html>
<html lang="he" dir="rtl">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{ title }} — ניטור מסכים</title>
<style>""" + BASE_CSS + """</style></head>
<body>
{% if user %}
<div class="topbar">
  <a class="brand" href="{{ url_for('dashboard') }}">🖥️ <em>Monitor</em>System</a>
  <nav>
    <a href="{{ url_for('dashboard') }}" class="{% if request.endpoint=='dashboard' %}act{% endif %}">📊 לוח בקרה</a>
    <a href="{{ url_for('gallery') }}" class="{% if request.endpoint=='gallery' %}act{% endif %}">🖼️ גלריה</a>
    <a href="{{ url_for('activity_page') }}" class="{% if request.endpoint=='activity_page' %}act{% endif %}">📋 פעילות</a>
    {% if user['can_manage'] %}
    <a href="{{ url_for('settings_page') }}" class="{% if request.endpoint=='settings_page' %}act{% endif %}">⚙️ הגדרות</a>
    <a href="{{ url_for('users_page') }}" class="{% if request.endpoint=='users_page' %}act{% endif %}">👥 משתמשים</a>
    {% endif %}
    <a href="{{ url_for('logout') }}">🚪 יציאה</a>
  </nav>
  <span class="pill">{{ user['username'] }} · {{ user['role'] }}</span>
</div>
{% endif %}
<div class="wrap">
  {% with msgs = get_flashed_messages(with_categories=true) %}
    {% for cat, msg in msgs %}<div class="flash {{ cat }}">{{ msg }}</div>{% endfor %}
  {% endwith %}
  {{ content }}
</div>
<!-- Lightbox -->
<div id="lb">
  <button class="lbb" id="lb-x" onclick="lbClose()">✕</button>
  <button class="lbb" id="lb-p" onclick="lbStep(-1)">›</button>
  <button class="lbb" id="lb-n" onclick="lbStep(1)">‹</button>
  <img id="lb-img" src="" alt="">
  <div id="lb-info"></div>
</div>
<script>
var LBI=[],LBIDX=0;
function lbOpen(imgs,idx){LBI=imgs;LBIDX=idx||0;lbShow()}
function lbShow(){
  var it=LBI[LBIDX];
  document.getElementById('lb-img').src=it.src;
  document.getElementById('lb-info').textContent=it.info+(LBI.length>1?' ('+(LBIDX+1)+'/'+LBI.length+')':'');
  document.getElementById('lb').classList.add('on');
}
function lbClose(){document.getElementById('lb').classList.remove('on')}
function lbStep(d){LBIDX=(LBIDX+d+LBI.length)%LBI.length;lbShow()}
document.addEventListener('keydown',function(e){
  if(!document.getElementById('lb').classList.contains('on'))return;
  if(e.key==='Escape')lbClose();
  if(e.key==='ArrowLeft')lbStep(1);
  if(e.key==='ArrowRight')lbStep(-1);
});
document.getElementById('lb').addEventListener('click',function(e){if(e.target===this)lbClose()});
</script>
</body></html>"""

LOGIN_TMPL = """<!DOCTYPE html><html lang="he" dir="rtl"><head><meta charset="UTF-8"><title>התחברות</title>
<style>""" + BASE_CSS + """
.lw{display:flex;align-items:center;justify-content:center;height:100vh}
.lc{background:var(--pnl);border:1px solid var(--brd);border-radius:14px;padding:36px;width:340px}
.lc h1{text-align:center;font-size:19px;margin-bottom:22px}
</style></head><body>
<div class="lw"><div class="lc">
  <h1>🔒 התחברות למערכת</h1>
  {% with msgs=get_flashed_messages(with_categories=true) %}
    {% for cat,msg in msgs %}<div class="flash {{cat}}">{{msg}}</div>{% endfor %}
  {% endwith %}
  <form method="post">
    <div class="fg"><label>שם משתמש</label><input type="text" name="username" required autofocus></div>
    <div class="fg"><label>סיסמה</label><input type="password" name="password" required></div>
    <button type="submit" style="width:100%;justify-content:center">🔑 כניסה</button>
  </form>
  <p class="help" style="margin-top:14px;text-align:center">ברירת מחדל: admin / admin123</p>
</div></div></body></html>"""

# ════════════════════════════════════════════════════════════════
#  Flask App
# ════════════════════════════════════════════════════════════════
def build_app():
    from flask import (Flask, request, redirect, url_for, session,
                       send_from_directory, flash, jsonify, abort,
                       render_template_string, send_file, Response)
    from werkzeug.security import check_password_hash
    from markupsafe import Markup

    app = Flask(__name__)
    app.secret_key = gs("secret_key") or secrets.token_hex(32)
    app.permanent_session_lifetime = timedelta(hours=12)

    # ── helpers ──────────────────────────────────────────────────
    def render(title, inner, **ctx):
        html = render_template_string(inner, **ctx)
        return render_template_string(LAYOUT, title=title,
                                      user=cur_user(), content=Markup(html))

    @app.template_filter("fromjson")
    def fromjson_f(v):
        try: return json.loads(v)
        except: return []

    def cur_user():
        uid = session.get("user_id")
        return u_get(uid=uid) if uid else None

    def login_required(fn):
        @functools.wraps(fn)
        def w(*a, **kw):
            if not cur_user():
                return redirect(url_for("login", next=request.path))
            return fn(*a, **kw)
        return w

    def admin_required(fn):
        @functools.wraps(fn)
        def w(*a, **kw):
            u = cur_user()
            if not u or not u["can_manage"]: abort(403)
            return fn(*a, **kw)
        return w

    def can_see(user, hostname):
        if user["can_view_all"]: return True
        try: allowed = set(json.loads(user["allowed_keys"]))
        except: allowed = set()
        return hostname in allowed

    # ── auth ──────────────────────────────────────────────────────
    @app.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            u = u_get(username=request.form.get("username","").strip())
            if u and check_password_hash(u["password_hash"], request.form.get("password","")):
                session.permanent = True
                session["user_id"] = u["id"]
                u_touch(u["id"])
                act("login", f"כניסה: {u['username']}")
                return redirect(request.args.get("next") or url_for("dashboard"))
            flash("שם משתמש או סיסמה שגויים", "error")
        return render_template_string(LOGIN_TMPL)

    @app.route("/logout")
    def logout():
        session.clear(); return redirect(url_for("login"))

    # ── dashboard ─────────────────────────────────────────────────
    @app.route("/")
    @login_required
    def dashboard():
        user = cur_user()
        now  = datetime.utcnow()
        agents = ag_list()
        rows   = []
        online_n = 0
        for a in agents:
            if not can_see(user, a["hostname"]): continue
            try:
                ls = datetime.fromisoformat(a["last_seen"])
                online = (now - ls).total_seconds() <= ONLINE_S
            except: online = False
            if online: online_n += 1
            latest = s_latest(a["hostname"], a["os_user"])
            dkey   = safe_dir_name(a["hostname"], a["os_user"])
            rows.append({"a": a, "online": online, "latest": latest, "dkey": dkey})

        total_shots = s_total()
        DASH = """
<div class="stats">
  <div class="stat"><div class="sv">{{rows|length}}</div><div class="sl">סוכנים רשומים</div></div>
  <div class="stat"><div class="sv" style="color:var(--acc2)">{{online_n}}</div><div class="sl">מחוברים כעת</div></div>
  <div class="stat"><div class="sv" style="color:var(--dim)">{{rows|length - online_n}}</div><div class="sl">לא מחוברים</div></div>
  <div class="stat"><div class="sv" style="color:var(--acc3)">{{total_shots}}</div><div class="sl">סה"כ צילומים</div></div>
</div>

<div class="sh"><h2>סוכנים פעילים</h2>
  <a class="btn sm sec" href="{{ url_for('gallery') }}">🖼️ גלריה מלאה</a>
</div>

{% if rows %}
<div class="grid">
{% for r in rows %}
  {% set a = r.a %}
  <div class="card">
    <div class="thumb" onclick="{% if r.latest %}lbOpen([{src:'{{ url_for('serve_shot', dkey=r.dkey, filename=r.latest.filename) }}',info:'{{ a.hostname }} / {{ a.os_user }} — {{ r.latest.taken_at[:19] }}'}],0){% endif %}">
      {% if r.latest %}
        <img src="{{ url_for('serve_shot', dkey=r.dkey, filename=r.latest.filename) }}" loading="lazy" alt="">
      {% else %}
        <div class="no-img">📷 אין צילום</div>
      {% endif %}
    </div>
    <div class="cbody">
      <div class="ctitle">
        <span class="dot {{'on-dot' if r.online else 'off-dot'}}"></span>
        <span>{{ a.os_user }}</span>
      </div>
      <div class="cmeta">
        🖥️ {{ a.hostname }}<br>
        🌐 {{ a.ip_address or '—' }}<br>
        🕐 {{ a.last_seen[:19] if a.last_seen else '—' }}<br>
        {% if a.is_local %}<span class="b bb">מקומי</span>{% else %}<span class="b bo">Peer</span>{% endif %}
      </div>
    </div>
    <div class="cfooter">
      <a class="btn sm" href="{{ url_for('agent_view', hostname=a.hostname, os_user=a.os_user) }}">📂 היסטוריה</a>
      <a class="btn sm sec" href="{{ url_for('agent_live', hostname=a.hostname, os_user=a.os_user) }}">📡 חי</a>
    </div>
  </div>
{% endfor %}
</div>
{% else %}
<div class="empty"><div class="ico">🖥️</div><p>אין סוכנים עדיין. התקנת Peer ראשונה תופיע כאן אוטומטית.</p></div>
{% endif %}
<script>setTimeout(()=>location.reload(),30000)</script>"""
        return render("לוח בקרה", DASH, rows=rows, online_n=online_n, total_shots=total_shots)

    # ── agent history ─────────────────────────────────────────────
    @app.route("/agent/<hostname>/<os_user>")
    @login_required
    def agent_view(hostname, os_user):
        user = cur_user()
        if not can_see(user, hostname): abort(403)
        ag = ag_get(hostname, os_user)
        if not ag: abort(404)
        shots  = s_list(hostname=hostname, os_user=os_user, limit=200)
        dkey   = safe_dir_name(hostname, os_user)

        imgs_js = json.dumps([{
            "src":  url_for("serve_shot", dkey=dkey, filename=s["filename"]),
            "info": f"{hostname}/{os_user} — {s['taken_at'][:19]}"
        } for s in shots])

        AGENT = """
<div style="margin-bottom:16px">
  <a href="{{ url_for('dashboard') }}" class="btn sm sec">← לוח בקרה</a>
</div>
<div class="sh"><h2>
  <span class="dot {{'on-dot' if online else 'off-dot'}}"></span>
  {{ os_user }} <span style="color:var(--dim);font-weight:400;font-size:14px">@ {{ hostname }}</span>
</h2>
  <a class="btn sm" href="{{ url_for('agent_live', hostname=hostname, os_user=os_user) }}">📡 צפייה חיה</a>
</div>
<p class="help" style="margin-bottom:18px">IP: {{ ag.ip_address or '—' }} &nbsp;|&nbsp; נראה לאחרונה: {{ ag.last_seen[:19] if ag.last_seen else '—' }} &nbsp;|&nbsp; {{shots|length}} צילומים</p>

{% if shots %}
<script>var IMGS={{imgs_js|safe}};</script>
<div class="grid">
{% for i,s in enum_shots %}
  <div class="card">
    <div class="thumb" onclick="lbOpen(IMGS,{{i}})">
      <img src="{{ url_for('serve_shot', dkey=dkey, filename=s.filename) }}" loading="lazy" alt="">
    </div>
    <div class="cbody"><div class="cmeta">{{ s.taken_at[:19] }}</div></div>
  </div>
{% endfor %}
</div>
{% else %}
<div class="empty"><div class="ico">📭</div><p>אין צילומים עדיין</p></div>
{% endif %}"""

        try:
            ls     = datetime.fromisoformat(ag["last_seen"])
            online = (datetime.utcnow() - ls).total_seconds() <= ONLINE_S
        except: online = False
        return render(f"{os_user}@{hostname}", AGENT,
                      ag=ag, hostname=hostname, os_user=os_user,
                      shots=shots, imgs_js=imgs_js, online=online,
                      dkey=dkey, enum_shots=list(enumerate(shots)))

    # ── live view page ────────────────────────────────────────────
    @app.route("/live/<hostname>/<os_user>")
    @login_required
    def agent_live(hostname, os_user):
        user = cur_user()
        if not can_see(user, hostname): abort(403)
        ag = ag_get(hostname, os_user)
        if not ag: abort(404)

        quality = int(gs("live_quality", "65") or "65")

        LIVE = """
<div style="margin-bottom:16px;display:flex;gap:8px;flex-wrap:wrap">
  <a class="btn sm sec" href="{{ url_for('dashboard') }}">← לוח בקרה</a>
  <a class="btn sm sec" href="{{ url_for('agent_view', hostname=hostname, os_user=os_user) }}">📂 היסטוריה</a>
</div>
<div class="sh"><h2>📡 צפייה חיה — {{ os_user }} @ {{ hostname }}</h2></div>
<div class="live-wrap" style="max-width:1200px">
  <img id="lv-img" src="" alt="מסך חי">
  <div class="live-bar">
    <span class="ldot pulse" id="ldot"></span>
    <span class="lstat" id="lstat">מתחבר...</span>
    <span style="flex:1"></span>
    <label style="color:var(--dim);font-size:12px;margin:0">קצב עדכון:
      <select id="rate-sel" style="width:auto;padding:3px 6px;font-size:12px" onchange="setRate(this.value)">
        <option value="1000">1 שנייה</option>
        <option value="2000" selected>2 שניות</option>
        <option value="5000">5 שניות</option>
        <option value="10000">10 שניות</option>
      </select>
    </label>
    <button class="btn sm sec" onclick="togglePause()" id="pause-btn">⏸ עצור</button>
    <button class="btn sm sec" onclick="fullscreen()">⛶ מסך מלא</button>
  </div>
</div>
<p class="help" style="margin-top:10px">📷 מוצגת תמונה עדכנית מהמסך. איכות JPEG: {{quality}}%</p>
<script>
var H={{hostname|tojson}}, U={{os_user|tojson}};
var timer=null, paused=false, rate=2000;
function refresh(){
  if(paused)return;
  var tmp=new Image();
  tmp.onload=function(){
    document.getElementById('lv-img').src=tmp.src;
    document.getElementById('lstat').textContent='עדכון: '+new Date().toLocaleTimeString('he-IL');
    document.getElementById('ldot').style.background='var(--acc2)';
  };
  tmp.onerror=function(){
    document.getElementById('lstat').textContent='⚠️ שגיאה בטעינת תמונה';
    document.getElementById('ldot').style.background='var(--red)';
  };
  tmp.src='/api/live_frame?h='+encodeURIComponent(H)+'&u='+encodeURIComponent(U)+'&t='+Date.now();
}
function setRate(v){rate=parseInt(v);clearInterval(timer);timer=setInterval(refresh,rate)}
function togglePause(){
  paused=!paused;
  document.getElementById('pause-btn').textContent=paused?'▶ המשך':'⏸ עצור';
  if(!paused)refresh();
}
function fullscreen(){document.getElementById('lv-img').requestFullscreen&&document.getElementById('lv-img').requestFullscreen()}
refresh(); timer=setInterval(refresh,rate);
</script>"""
        return render(f"חי — {os_user}@{hostname}", LIVE,
                      hostname=hostname, os_user=os_user, quality=quality)

    # ── gallery ───────────────────────────────────────────────────
    @app.route("/gallery")
    @login_required
    def gallery():
        user    = cur_user()
        flt_h   = request.args.get("h") or None
        flt_u   = request.args.get("u") or None
        shots   = s_list(hostname=flt_h, os_user=flt_u, limit=400)
        agents  = ag_list()

        # filter by permission
        if not user["can_view_all"]:
            try: allowed = set(json.loads(user["allowed_keys"]))
            except: allowed = set()
            shots  = [s for s in shots  if s["hostname"] in allowed]
            agents = [a for a in agents if a["hostname"] in allowed]

        hostnames = sorted(set(a["hostname"] for a in agents))
        os_users  = sorted(set(a["os_user"]  for a in agents))

        imgs_js = json.dumps([{
            "src":  url_for("serve_shot",
                            dkey=safe_dir_name(s["hostname"], s["os_user"]),
                            filename=s["filename"]),
            "info": f"{s['hostname']}/{s['os_user']} — {s['taken_at'][:19]}"
        } for s in shots])

        GAL = """
<div class="sh"><h2>🖼️ גלריית צילומים ({{ shots|length }})</h2></div>
<form method="get" class="fbar">
  <select name="h" onchange="this.form.submit()">
    <option value="">כל המחשבים</option>
    {% for hn in hostnames %}<option value="{{hn}}" {{'selected' if hn==flt_h else ''}}>{{hn}}</option>{% endfor %}
  </select>
  <select name="u" onchange="this.form.submit()">
    <option value="">כל המשתמשים</option>
    {% for ou in os_users %}<option value="{{ou}}" {{'selected' if ou==flt_u else ''}}>{{ou}}</option>{% endfor %}
  </select>
  {% if flt_h or flt_u %}<a class="btn sm sec" href="{{ url_for('gallery') }}">✕ נקה סינון</a>{% endif %}
</form>

{% if shots %}
<script>var IMGS={{imgs_js|safe}};</script>
<div class="grid">
{% for i,s in enum_shots %}
  {% set dkey = safe_dir(s.hostname, s.os_user) %}
  <div class="card">
    <div class="thumb" onclick="lbOpen(IMGS,{{i}})">
      <img src="{{ url_for('serve_shot', dkey=dkey, filename=s.filename) }}" loading="lazy" alt="">
    </div>
    <div class="cbody">
      <div class="ctitle" style="font-size:13px">{{ s.os_user }}</div>
      <div class="cmeta">{{ s.hostname }}<br>{{ s.taken_at[:19] }}</div>
    </div>
    <div class="cfooter">
      <a class="btn sm sec" href="{{ url_for('agent_view', hostname=s.hostname, os_user=s.os_user) }}">📂 פרטים</a>
      <a class="btn sm sec" href="{{ url_for('agent_live', hostname=s.hostname, os_user=s.os_user) }}">📡 חי</a>
    </div>
  </div>
{% endfor %}
</div>
{% else %}
<div class="empty"><div class="ico">📭</div><p>אין צילומים עם הסינון הנוכחי</p></div>
{% endif %}"""
        return render("גלריה", GAL, shots=shots, hostnames=hostnames,
                      os_users=os_users, flt_h=flt_h, flt_u=flt_u,
                      imgs_js=imgs_js, enum_shots=list(enumerate(shots)),
                      safe_dir=safe_dir_name)

    # ── activity log ──────────────────────────────────────────────
    @app.route("/activity")
    @login_required
    def activity_page():
        events = act_list(300)
        ACT = """
<div class="sh"><h2>📋 יומן פעילות</h2></div>
<div class="tw">
<table>
<thead><tr><th>זמן</th><th>סוג</th><th>תיאור</th><th>מחשב</th><th>משתמש</th></tr></thead>
<tbody>
{% for e in events %}
<tr>
  <td class="mono" style="font-size:12px">{{ e.ts[:19] }}</td>
  <td><span class="b bb">{{ e.kind }}</span></td>
  <td>{{ e.msg }}</td>
  <td>{{ e.hostname or '—' }}</td>
  <td>{{ e.os_user or '—' }}</td>
</tr>
{% else %}
<tr><td colspan="5" style="text-align:center;color:var(--dim)">אין רשומות</td></tr>
{% endfor %}
</tbody>
</table></div>"""
        return render("פעילות", ACT, events=events)

    # ── serve screenshot file ─────────────────────────────────────
    @app.route("/shots/<dkey>/<filename>")
    @login_required
    def serve_shot(dkey, filename):
        directory = os.path.join(SHOTS_DIR, dkey)
        return send_from_directory(directory, filename)

    # ── settings ──────────────────────────────────────────────────
    @app.route("/settings")
    @login_required
    @admin_required
    def settings_page():
        peers    = p_list()
        hostname = socket.gethostname()
        SETT = """
<div class="sh"><h2>⚙️ הגדרות מערכת</h2></div>

<div class="tabs">
  <div class="tab on" onclick="switchTab(this,'t1')">📷 צילום</div>
  <div class="tab" onclick="switchTab(this,'t2')">🔗 Peers</div>
  <div class="tab" onclick="switchTab(this,'t3')">🚀 פריסה</div>
  <div class="tab" onclick="switchTab(this,'t4')">🔑 API ואבטחה</div>
</div>

<!-- טאב 1: צילום -->
<div class="tp on" id="t1">
<form method="post" action="{{ url_for('settings_save') }}">
  <fieldset>
    <legend>תזמון צילום</legend>
    <div class="row2">
      <div class="fg">
        <label>אינטרוול צילום (שניות)</label>
        <input type="number" min="5" name="capture_interval" value="{{ capture_interval }}">
        <p class="help">כל כמה שניות לצלם את המסך.</p>
      </div>
      <div class="fg">
        <label>מקסימום צילומים לכל משתמש</label>
        <input type="number" min="10" name="max_shots" value="{{ max_shots }}">
        <p class="help">ישנים יימחקו אוטומטית.</p>
      </div>
    </div>
    <div class="fg">
      <label>
        <input type="checkbox" name="skip_unchanged" value="1" style="width:auto" {{'checked' if skip_unchanged=='1' else ''}}>
        &nbsp;דלג על צילום אם המסך לא השתנה (חסכון בשטח + I/O)
      </label>
    </div>
  </fieldset>
  <fieldset>
    <legend>איכות תצוגה חיה</legend>
    <div class="fg">
      <label>איכות JPEG לצפייה חיה (1–95)</label>
      <input type="number" min="1" max="95" name="live_quality" value="{{ live_quality }}">
      <p class="help">ערך גבוה = תמונה חדה יותר, תעבורת רשת גדולה יותר.</p>
    </div>
  </fieldset>
  <button type="submit">💾 שמור הגדרות צילום</button>
</form>

<fieldset style="margin-top:22px">
  <legend>הפעלה אוטומטית עם עליית המחשב</legend>
  <p class="help">מתקינה Scheduled Task שמפעיל את התוכנה עם כל כניסת משתמש, ברמת הרשאות Administrator.</p>
  {% if autostart %}
    <p class="flash success" style="display:inline-flex">✅ הפעלה אוטומטית פעילה על מחשב זה</p>
  {% endif %}
  <form method="post" action="{{ url_for('install_auto') }}">
    <button type="submit">🚀 התקן הפעלה אוטומטית</button>
  </form>
</fieldset>
</div><!-- /t1 -->

<!-- טאב 2: Peers -->
<div class="tp" id="t2">
<p class="help" style="margin-bottom:16px">
  כל מחשב ברשת הוא <strong>שוויוני</strong> — אין מרכז ואין לקוח. כל מחשב דוחף את הצילומים שלו לכל ה-Peers שהוגדרו כאן.
  <br>כדי להוסיף Peer: הכנס את ה-IP שלו ואת מפתח ה-API שלו (מוצג בהגדרות של אותו מחשב).
</p>

{% if peers %}
<div class="tw" style="margin-bottom:18px">
<table>
<thead><tr><th>IP</th><th>Hostname</th><th>כינוי</th><th>עדכון אחרון</th><th>שגיאה</th><th></th></tr></thead>
<tbody>
{% for p in peers %}
<tr>
  <td class="mono">{{ p.ip_address }}</td>
  <td>{{ p.hostname or '—' }}</td>
  <td>{{ p.nickname or '—' }}</td>
  <td style="color:var(--dim);font-size:12px">{{ p.last_ok[:16] if p.last_ok else '—' }}</td>
  <td style="color:var(--red);font-size:12px">{{ p.last_error or '' }}</td>
  <td>
    <form method="post" action="{{ url_for('peer_remove', pid=p.id) }}" onsubmit="return confirm('להסיר peer זה?')">
      <button class="btn sm dng" type="submit">🗑️ הסר</button>
    </form>
  </td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% else %}
<p class="help" style="margin-bottom:16px">אין Peers מוגדרים עדיין.</p>
{% endif %}

<form method="post" action="{{ url_for('peer_add') }}" style="max-width:520px">
  <fieldset>
    <legend>הוסף Peer חדש</legend>
    <div class="row2">
      <div class="fg"><label>כתובת IP</label><input type="text" name="ip" placeholder="192.168.1.20" required></div>
      <div class="fg"><label>כינוי (אופציונלי)</label><input type="text" name="nickname" placeholder="מחשב שרה"></div>
    </div>
    <div class="fg"><label>מפתח API של ה-Peer</label><input type="text" name="api_key" placeholder="המפתח מדף הגדרות של המחשב הנוסף" required></div>
    <button type="submit">➕ הוסף Peer</button>
  </fieldset>
</form>
</div><!-- /t2 -->

<!-- טאב 3: פריסה מרחוק -->
<div class="tp" id="t3">
<p class="help" style="margin-bottom:16px">
  פריסה אוטומטית של MonitorSystem.exe למחשב Windows מרוחק ברשת, כולל התקנת Scheduled Task.
  דורש: (א) הרצת EXE מהודר, (ב) הרשאות Domain Admin או Local Admin על המחשב היעד.
</p>
<form method="post" action="{{ url_for('deploy_remote') }}" style="max-width:500px">
  <fieldset>
    <legend>פריסה מרחוק (Windows)</legend>
    <div class="fg"><label>IP/שם מחשב יעד</label><input type="text" name="target_ip" placeholder="192.168.1.50" required></div>
    <div class="row2">
      <div class="fg"><label>שם משתמש מנהל</label><input type="text" name="admin_user" placeholder="Administrator"></div>
      <div class="fg"><label>סיסמה</label><input type="password" name="admin_pw"></div>
    </div>
    <button type="submit">🚀 פרוס עכשיו</button>
  </fieldset>
</form>
</div><!-- /t3 -->

<!-- טאב 4: API -->
<div class="tp" id="t4">
<fieldset>
  <legend>מפתח API של מחשב זה ({{ hostname }})</legend>
  <p class="help" style="margin-bottom:10px">זהו המפתח שיש להזין ב-Peers שרוצים לשלוח אליך תמונות.</p>
  <div class="api-box mono">{{ api_key }}</div>
  <form method="post" action="{{ url_for('regen_key') }}" style="margin-top:10px"
        onsubmit="return confirm('החלפת מפתח תנתק כל Peer שמשתמש במפתח הנוכחי. להמשיך?')">
    <button class="btn sec">🔄 צור מפתח חדש</button>
  </form>
</fieldset>
<fieldset>
  <legend>מחיקת סוכן</legend>
  <p class="help" style="margin-bottom:10px">מחיקת סוכן תסיר אותו ואת כל הצילומים שלו מהמסד המקומי (הקבצים יימחקו).</p>
  <form method="post" action="{{ url_for('agent_delete_route') }}" style="max-width:420px"
        onsubmit="return confirm('למחוק את כל נתוני הסוכן הזה? הפעולה בלתי הפיכה!')">
    <div class="row2">
      <div class="fg"><label>Hostname</label><input type="text" name="hostname" placeholder="DESKTOP-XXXX"></div>
      <div class="fg"><label>OS User</label><input type="text" name="os_user" placeholder="john.smith"></div>
    </div>
    <button class="btn dng" type="submit">🗑️ מחק סוכן</button>
  </form>
</fieldset>
</div><!-- /t4 -->

<script>
function switchTab(el,id){
  document.querySelectorAll('.tab').forEach(t=>t.classList.remove('on'));
  document.querySelectorAll('.tp').forEach(t=>t.classList.remove('on'));
  el.classList.add('on');
  document.getElementById(id).classList.add('on');
}
</script>"""
        return render("הגדרות", SETT,
                      capture_interval=gs("capture_interval","60"),
                      skip_unchanged=gs("skip_unchanged","1"),
                      live_quality=gs("live_quality","65"),
                      max_shots=gs("max_shots","500"),
                      autostart=gs("autostart_installed")=="1",
                      peers=p_list(),
                      api_key=gs("api_key","—"),
                      hostname=hostname)

    @app.route("/settings/save", methods=["POST"])
    @login_required
    @admin_required
    def settings_save():
        def clamp(v, mn, mx, default):
            try: return str(max(mn, min(mx, int(v))))
            except: return str(default)
        ss("capture_interval", clamp(request.form.get("capture_interval","60"), 5, 86400, 60))
        ss("max_shots",        clamp(request.form.get("max_shots","500"), 10, 100000, 500))
        ss("live_quality",     clamp(request.form.get("live_quality","65"), 1, 95, 65))
        ss("skip_unchanged",   "1" if request.form.get("skip_unchanged") else "0")
        flash("ההגדרות נשמרו. יכנסו לתוקף בסבב הצילום הבא.", "success")
        return redirect(url_for("settings_page"))

    @app.route("/settings/autostart", methods=["POST"])
    @login_required
    @admin_required
    def install_auto():
        try:
            install_autostart(); ss("autostart_installed", "1")
            flash("✅ הפעלה אוטומטית הותקנה בהצלחה!", "success")
        except Exception as e:
            flash(f"❌ שגיאה: {e} — יש לוודא הרשאות Administrator.", "error")
        return redirect(url_for("settings_page"))

    @app.route("/settings/regen-key", methods=["POST"])
    @login_required
    @admin_required
    def regen_key():
        ss("api_key", secrets.token_hex(24))
        flash("מפתח API הוחלף. יש לעדכן את כל ה-Peers!", "success")
        return redirect(url_for("settings_page"))

    @app.route("/settings/peer/add", methods=["POST"])
    @login_required
    @admin_required
    def peer_add():
        ip      = request.form.get("ip","").strip()
        nick    = request.form.get("nickname","").strip()
        api_key = request.form.get("api_key","").strip()
        if not ip or not api_key:
            flash("IP ומפתח API הם שדות חובה", "error")
        else:
            p_add(ip, nick, api_key)
            act("peer", f"נוסף Peer: {ip} ({nick})")
            flash(f"✅ Peer {ip} נוסף", "success")
        return redirect(url_for("settings_page"))

    @app.route("/settings/peer/remove/<int:pid>", methods=["POST"])
    @login_required
    @admin_required
    def peer_remove(pid):
        p_remove(pid); flash("Peer הוסר", "success")
        return redirect(url_for("settings_page"))

    @app.route("/settings/deploy", methods=["POST"])
    @login_required
    @admin_required
    def deploy_remote():
        target   = request.form.get("target_ip","").strip()
        au       = request.form.get("admin_user","Administrator").strip()
        ap       = request.form.get("admin_pw","")
        ok, msg  = deploy_windows(target, au, ap)
        flash(msg, "success" if ok else "error")
        return redirect(url_for("settings_page"))

    @app.route("/settings/agent/delete", methods=["POST"])
    @login_required
    @admin_required
    def agent_delete_route():
        hostname = request.form.get("hostname","").strip()
        os_user  = request.form.get("os_user","").strip()
        if hostname and os_user:
            s_delete_agent_shots(hostname, os_user)
            ag_delete(hostname, os_user)
            act("delete", f"מחיקת סוכן: {os_user}@{hostname}")
            flash(f"סוכן {os_user}@{hostname} נמחק", "success")
        else:
            flash("יש להזין hostname ו-os_user", "error")
        return redirect(url_for("settings_page"))

    # ── users ─────────────────────────────────────────────────────
    @app.route("/users")
    @login_required
    @admin_required
    def users_page():
        agents = ag_list()
        hostnames = sorted(set(a["hostname"] for a in agents))
        USR = """
<div class="sh"><h2>👥 ניהול משתמשים</h2></div>
<div class="tw" style="margin-bottom:24px">
<table>
<thead><tr><th>שם משתמש</th><th>תפקיד</th><th>ניהול</th><th>צפייה בהכל</th><th>מחשבים מורשים</th><th>כניסה אחרונה</th><th>פעולות</th></tr></thead>
<tbody>
{% for u in users %}
<tr>
  <form method="post" action="{{ url_for('user_update', uid=u.id) }}">
  <td><strong>{{ u.username }}</strong></td>
  <td><select name="role" style="width:auto">
    {% for r in ['viewer','admin','superadmin'] %}
    <option value="{{r}}" {{'selected' if u.role==r else ''}}>{{r}}</option>
    {% endfor %}
  </select></td>
  <td><input type="checkbox" name="can_manage" {{'checked' if u.can_manage else ''}} style="width:auto"></td>
  <td><input type="checkbox" name="can_view_all" {{'checked' if u.can_view_all else ''}} style="width:auto"></td>
  <td>
    <div class="cl">
    {% set allowed = u.allowed_keys|fromjson %}
    {% for hn in hostnames %}
      <label><input type="checkbox" name="allowed_keys" value="{{hn}}" {{'checked' if hn in allowed else ''}}> {{hn}}</label>
    {% endfor %}
    </div>
    <input type="password" name="new_pw" placeholder="סיסמה חדשה (אופציונלי)" style="margin-top:6px">
  </td>
  <td style="color:var(--dim);font-size:12px">{{ u.last_login[:16] if u.last_login else '—' }}</td>
  <td style="display:flex;gap:6px;align-items:center">
    <button class="btn sm ok" type="submit">💾</button>
  </form>
  <form method="post" action="{{ url_for('user_delete', uid=u.id) }}"
        onsubmit="return confirm('למחוק את {{ u.username }}?')">
    <button class="btn sm dng" type="submit" {{'disabled' if u.id==cur_uid else ''}}>🗑️</button>
  </form>
  </td>
</tr>
{% endfor %}
</tbody>
</table></div>

<div class="sh"><h2>➕ הוסף משתמש</h2></div>
<form method="post" action="{{ url_for('user_create') }}" style="max-width:540px">
  <div class="row2">
    <div class="fg"><label>שם משתמש</label><input type="text" name="username" required></div>
    <div class="fg"><label>סיסמה</label><input type="password" name="password" required></div>
  </div>
  <div class="fg"><label>תפקיד</label>
    <select name="role">
      <option value="viewer">צופה</option>
      <option value="admin">מנהל</option>
      <option value="superadmin">מנהל-על</option>
    </select>
  </div>
  <div class="fg">
    <label><input type="checkbox" name="can_manage" style="width:auto"> יכול לנהל משתמשים והגדרות</label>
  </div>
  <div class="fg">
    <label><input type="checkbox" name="can_view_all" style="width:auto" checked> יכול לצפות בכל המחשבים</label>
  </div>
  <div class="fg">
    <label>מחשבים מורשים (אם לא "צפייה בהכל")</label>
    <div class="cl">
    {% for hn in hostnames %}
      <label><input type="checkbox" name="allowed_keys" value="{{hn}}"> {{hn}}</label>
    {% endfor %}
    </div>
  </div>
  <button type="submit">➕ צור משתמש</button>
</form>"""
        return render("משתמשים", USR, users=u_list(),
                      hostnames=sorted(set(a["hostname"] for a in ag_list())),
                      cur_uid=cur_user()["id"])

    @app.route("/users/<int:uid>/update", methods=["POST"])
    @login_required
    @admin_required
    def user_update(uid):
        u_update(uid, request.form.get("role","viewer"),
                 request.form.get("can_manage")=="on",
                 request.form.get("can_view_all")=="on",
                 request.form.getlist("allowed_keys"))
        pw = request.form.get("new_pw","").strip()
        if pw: u_set_pw(uid, pw)
        flash("המשתמש עודכן", "success")
        return redirect(url_for("users_page"))

    @app.route("/users/create", methods=["POST"])
    @login_required
    @admin_required
    def user_create():
        username = request.form.get("username","").strip()
        password = request.form.get("password","")
        if not username or not password:
            flash("שם משתמש וסיסמה הם שדות חובה", "error")
        elif u_get(username=username):
            flash("שם המשתמש כבר קיים", "error")
        else:
            u_create(username, password,
                     request.form.get("role","viewer"),
                     request.form.get("can_manage")=="on",
                     request.form.get("can_view_all")=="on",
                     request.form.getlist("allowed_keys"))
            act("user_create", f"נוצר משתמש: {username}")
            flash(f"✅ משתמש {username} נוצר", "success")
        return redirect(url_for("users_page"))

    @app.route("/users/<int:uid>/delete", methods=["POST"])
    @login_required
    @admin_required
    def user_delete(uid):
        if uid == cur_user()["id"]:
            flash("לא ניתן למחוק את עצמך", "error")
        else:
            u = u_get(uid=uid)
            u_delete(uid)
            act("user_delete", f"נמחק משתמש: {u['username'] if u else uid}")
            flash("המשתמש נמחק", "success")
        return redirect(url_for("users_page"))

    # ── API endpoints ─────────────────────────────────────────────
    def api_auth():
        """Verify X-API-Key header matches local API key."""
        return request.headers.get("X-API-Key") == gs("api_key")

    @app.route("/api/live_frame")
    def api_live_frame():
        """Return a fresh JPEG frame. Called by live view polling."""
        # Accept session auth (browser) or API key auth (peer proxy)
        if not (cur_user() or api_auth()):
            return abort(401)

        hostname_req = request.args.get("h", socket.gethostname())
        os_user_req  = request.args.get("u", "")
        local_host   = socket.gethostname()

        if hostname_req == local_host:
            # Capture live from this machine
            png = take_png()
            if not png:
                return abort(503)
            quality = int(gs("live_quality","65") or "65")
            jpg = png2jpg(png, quality)
            return send_file(io.BytesIO(jpg), mimetype="image/jpeg",
                             max_age=0, last_modified=datetime.utcnow())
        else:
            # Proxy to peer that has this hostname
            import requests as req_lib
            for peer in p_list():
                if peer["hostname"] == hostname_req:
                    try:
                        resp = req_lib.get(
                            f"http://{peer['ip_address']}:{PORT}/api/live_frame",
                            headers={"X-API-Key": peer["api_key"]},
                            params={"h": hostname_req, "u": os_user_req},
                            timeout=6)
                        if resp.status_code == 200:
                            return Response(resp.content, mimetype="image/jpeg")
                    except Exception as e:
                        log(f"[live proxy] {peer['ip_address']}: {e}")
            # Fallback: return latest saved screenshot
            shot = s_latest(hostname_req, os_user_req or None)
            if shot:
                dkey = safe_dir_name(shot["hostname"], shot["os_user"])
                fpath = os.path.join(SHOTS_DIR, dkey, shot["filename"])
                if os.path.exists(fpath):
                    with open(fpath, "rb") as f:
                        png = f.read()
                    jpg = png2jpg(png, 70)
                    return send_file(io.BytesIO(jpg), mimetype="image/jpeg")
            return abort(404)

    @app.route("/api/upload", methods=["POST"])
    def api_upload():
        """Receive screenshot from a peer machine."""
        if not api_auth():
            return jsonify({"error": "unauthorized"}), 401
        hostname = request.form.get("hostname","").strip()
        os_user  = request.form.get("os_user","unknown").strip()
        taken_at = request.form.get("taken_at","")
        fhash    = request.form.get("file_hash","")
        if not hostname or "file" not in request.files:
            return jsonify({"error": "hostname and file required"}), 400
        try:
            dt = datetime.fromisoformat(taken_at) if taken_at else datetime.utcnow()
        except Exception:
            dt = datetime.utcnow()
        dkey  = safe_dir_name(hostname, os_user)
        ddir  = os.path.join(SHOTS_DIR, dkey)
        os.makedirs(ddir, exist_ok=True)
        fname = f"{dkey}_{dt:%Y%m%d_%H%M%S}.png"
        fpath = os.path.join(ddir, fname)
        request.files["file"].save(fpath)
        aid = ag_upsert(hostname, os_user, request.remote_addr, is_local=False)
        s_add(aid, hostname, os_user, fname, dt.isoformat(), fhash or None)
        try:
            mx = int(gs("max_shots","500") or "500")
            s_prune(hostname, os_user, mx)
        except Exception:
            pass
        return jsonify({"status": "ok", "filename": fname})

    @app.route("/api/heartbeat", methods=["POST"])
    def api_heartbeat():
        """Peer heartbeat — keeps agent online status fresh."""
        if not api_auth():
            return jsonify({"error": "unauthorized"}), 401
        data     = request.get_json(silent=True) or {}
        hostname = data.get("hostname","")
        os_user  = data.get("os_user","unknown")
        if not hostname:
            return jsonify({"error": "hostname required"}), 400
        ag_upsert(hostname, os_user, request.remote_addr, is_local=False)
        return jsonify({"status": "ok"})

    @app.route("/api/status")
    def api_status():
        """Return basic machine status — used by peers to probe health."""
        if not api_auth():
            return jsonify({"error": "unauthorized"}), 401
        agents = ag_list()
        return jsonify({
            "hostname": socket.gethostname(),
            "agents":   [{"hostname": a["hostname"], "os_user": a["os_user"],
                          "last_seen": a["last_seen"]} for a in agents],
            "total_shots": s_total(),
        })

    return app

# ════════════════════════════════════════════════════════════════
#  main
# ════════════════════════════════════════════════════════════════
def main():
    init_db()
    auto_install()

    t = threading.Thread(target=capture_worker, daemon=True)
    t.start()

    app = build_app()
    url = f"http://127.0.0.1:{PORT}"

    log("=" * 62)
    log(f"  Monitor System v2.0  →  {url}")
    log("  התחבר עם:  admin / admin123  (שנה סיסמה מיד!)")
    log("  כל ההגדרות נמצאות בעמוד ⚙️ הגדרות")
    log("=" * 62)

    def open_browser():
        time.sleep(1.8)
        try: webbrowser.open(url)
        except Exception: pass

    threading.Thread(target=open_browser, daemon=True).start()

    try:
        app.run(host="0.0.0.0", port=PORT, debug=False, threaded=True)
    finally:
        _stop.set()


if __name__ == "__main__":
    main()