version = 14
"""
Hostish Unified Agent
=====================

One Python agent for the complete Hostish client stack:

    * Website hosting  wss://hostish.site/ws/host
      static / proxy / hybrid, streamed HTTP, SSE, visitor WebSockets,
      host routes, static-file security, custom 404/favicon and IP masking.

    * SMTP mail        wss://hostish.site/ws/smtp
      inbound delivery, outbound local relay, plus optional built-in webmail.

    * Minecraft Manager (bundled v4)
      exact Hostish MC Manager app: installers, worlds, mods/plugins, console,
      players, backups, public dashboards, raw TCP and Simple Voice Chat UDP.

The local control panel defaults to:

    http://127.0.0.1:5000/

It has separate pages for Overview, Website, Mail, Minecraft, Scripts, Logs,
Manager settings and Update. CONTROL_PANEL_PORT is configurable and settings
are persisted in hostish-config.json beside this script.

Requirements:

    python -m pip install websockets certifi cryptography Flask requests

Hostish handles outbound SPF/DKIM authentication on its backend. For normal
USERNAME.hostish.site mail, no per-hoster DNS setup is needed. Linked custom
domains require one-time MX/SPF/DKIM/DMARC DNS records at that domain provider.

The cryptography package may still be used by legacy/update features and also
for optional Ed25519 agent-update verification.

Run:

    python hostish-unified-agent.py
"""

import asyncio
import base64
import contextvars
import fnmatch
import functools
import hashlib
import hmac
import importlib.util
import ipaddress
import json
import re
import os
import secrets
import smtplib
import socket
import ssl
import subprocess
import sys
import threading
import time
import traceback
import urllib.parse
import urllib.request
import io
import signal
import zipfile
from pathlib import Path
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime
from email.utils import parseaddr
from typing import Any, Dict, Optional

try:
    import websockets
except ImportError:
    print("Missing dependency: websockets")
    print("Install with: python -m pip install websockets certifi")
    sys.exit(1)

try:
    import certifi
except ImportError:
    certifi = None


# ============================================================
# HOSTER SETTINGS (edit these, or edit them in the control panel)
# ============================================================
#
# Anything you change in the control panel is saved to
# hostish-config.json next to this script and wins over the values here,
# so this file stays safe to replace when a new version comes out.

# ---------------- 1) ACCOUNT ----------------

USERNAME = "USERNAME"
PASSWORD = "PASSWORD"
# ---------------- 2) CONTROL PANEL ----------------

# The local webpage this agent hosts for you. Default 5000.
CONTROL_PANEL_ENABLED = True
CONTROL_PANEL_HOST = "127.0.0.1"
CONTROL_PANEL_PORT = 50001

# Optional password for the control panel. Required if you bind the panel
# to anything other than loopback.
CONTROL_PANEL_PASSWORD = ""

# Open the panel in your browser when the agent starts.
CONTROL_PANEL_OPEN_BROWSER = True

# ---------------- 3) WHICH TUNNELS TO RUN ----------------

WEB_ENABLED = True
SMTP_ENABLED = False
MINECRAFT_ENABLED = False  # legacy single-instance switch; migrated automatically

# ---------------- 4) WEB HOSTING ----------------

# "static" | "proxy" | "hybrid"
MODE = "hybrid"

WEBSITE_DIR = r""

PORT = 50002
UPSTREAM_BASE = f"http://127.0.0.1:{PORT}"

# Longest matching prefix wins. Example:
#   {"api/ai/": "http://127.0.0.1:8424", "api/": "http://127.0.0.1:5001"}
UPSTREAM_ROUTES = {}

# Hybrid mode only.
PROXY_PREFIXES = ["api/"]

# Explicit subdomains / custom subdomains. Root custom domains use the
# primary settings above and must NOT be listed here.
HOST_ROUTES = {}

# "loopback" | "private" | "any"
UPSTREAM_NETWORK_POLICY = "loopback"

PROXY_KEEPALIVE_ENABLED = True
PROXY_MAX_IDLE_CONNECTIONS = 8
PROXY_IDLE_CONNECTION_SECONDS = 60

# 0 = unlimited
MAX_CONCURRENT_REQUESTS = 0
MAX_QUEUED_REQUESTS = 0
MAX_REQUEST_BODY_BYTES = 0
MAX_FILE_BYTES = 0
MAX_PROXY_BYTES = 0

UPSTREAM_CONNECT_TIMEOUT_SECONDS = 30
UPSTREAM_RESPONSE_TIMEOUT_SECONDS = 0

TUNNEL_PROTOCOL = 3
TUNNEL_CHUNK_BYTES = 256 * 1024
WEBSOCKET_MAX_INCOMING_BYTES = 4 * 1024 * 1024
REQUEST_STREAM_BUFFER_CHUNKS = 8
USER_WEBSOCKET_PROXY_ENABLED = True
PROXY_READ_CHUNK_BYTES = 256 * 1024
STATIC_READ_CHUNK_BYTES = 256 * 1024

HANDLE_OPTIONS_LOCALLY = True
ADD_CORS_HEADER_TO_RESPONSES = True
CORS_ALLOW_ORIGIN = "*"
CORS_ALLOW_METHODS = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
CORS_ALLOW_HEADERS = "Content-Type, Authorization, Origin, Accept"

STATIC_CACHE_CONTROL = "no-store"

# "safe" | "custom" | "unrestricted"
STATIC_SECURITY_MODE = "safe"

STATIC_ALLOW_PATTERNS = [".well-known/**"]

STATIC_BLOCK_PATTERNS = [
    ".env", ".env.*", ".git/**", ".svn/**", ".hg/**", ".ssh/**",
    "**/*.pem", "**/*.key", "**/id_rsa", "**/id_ed25519",
    "**/credentials.json",
]

ALLOW_STATIC_SYMLINK_ESCAPE = False

CUSTOM_404_ENABLED = False
CUSTOM_404_PATH = r"404.html"

# ---------------- 5) SMTP MAIL ----------------

# Mail for anything@USERNAME.hostish.site is delivered to this local server.
LOCAL_DELIVERY_HOST = "127.0.0.1"
LOCAL_DELIVERY_PORT = 25

# "plain" | "starttls" | "ssl"
LOCAL_DELIVERY_MODE = "plain"
LOCAL_DELIVERY_USERNAME = ""
LOCAL_DELIVERY_PASSWORD = ""
LOCAL_DELIVERY_TLS_VERIFY = True

# Point your local mail server / app at this address to send mail out.
OUTBOUND_RELAY_ENABLED = True
LOCAL_RELAY_BIND_HOST = "127.0.0.1"
LOCAL_RELAY_BIND_PORT = 2525
LOCAL_RELAY_USERNAME = ""
LOCAL_RELAY_PASSWORD = ""

# Enable this only when you want email addresses on custom domains that are
# already linked to this Hostish account (for example hello@example.com).
# Hostish keeps the DKIM private key on its backend. You do NOT generate a
# custom key in this agent. The Mail page will show the one-time DNS records
# that must be added at the custom domain's DNS provider.
LINKED_CUSTOM_DOMAIN_MAIL_ENABLED = False

MAX_MESSAGE_BYTES = 25 * 1024 * 1024
MAX_RECIPIENTS = 100
WS_CHUNK_BYTES = 256 * 1024

# ---------------- 6) MINECRAFT ----------------

MINECRAFT_HOST = "127.0.0.1"
MINECRAFT_PORT = 25565

START_BAT = r""
AUTO_START_SERVER = True
SERVER_START_TIMEOUT = 120
SERVER_START_CHECK_INTERVAL = 2

# Optional HTML page shown to browsers on https://USERNAME.hostish.site/
# while the Minecraft tunnel is connected. Leave blank to disable.
# Ignored automatically when web hosting is also enabled.
HOMEPAGE_FILE = r""

SIMPLE_VOICE_CHAT_ENABLED = False
SIMPLE_VOICE_CHAT_HOST = "127.0.0.1"
SIMPLE_VOICE_CHAT_PORT = 24454
SIMPLE_VOICE_CHAT_PUBLIC_PORT = 0
SIMPLE_VOICE_CHAT_SERVICE_ID = "simple-voice-chat"
SIMPLE_VOICE_CHAT_CONFIG_FILE = r""

# Legacy Minecraft tunnel settings retained only for old config compatibility.
# The active Minecraft service is the embedded Hostish MC Manager v4.
# The control panel manages this list for you. Legacy single-server settings above
# are automatically migrated into one entry when MINECRAFT_ENABLED=True and this
# list is empty.
#
# Example:
# MINECRAFT_INSTANCES = [
#   {
#     "id": "survival", "name": "Survival", "enabled": True,
#     "username": "hostish_account_1", "password": "...",
#     "minecraft_host": "127.0.0.1", "minecraft_port": 25565,
#     "start_bat": r"C:\\mc\\survival\\start.bat",
#     "auto_start_server": True,
#   },
# ]
MINECRAFT_INSTANCES = []

# ---------------- 7) BUILT-IN WEBMAIL (part of the mail tab) ----------------
#
# A full LAN-reachable email client lives inside this agent's control panel
# at /webmail when this is on. It shares the SMTP tunnel: inbound mail is
# stored straight into local mailboxes (no separate mail server needed) and
# outbound mail is sent through the same Hostish relay used by SMTP_ENABLED.

WEBMAIL_ENABLED = False

# Shown as user@WEBMAIL_DOMAIN. Leave blank to use USERNAME.hostish.site.
WEBMAIL_DOMAIN = ""

WEBMAIL_DATA_DIR = r"./webmail-data"
WEBMAIL_SESSION_SECONDS = 7 * 24 * 60 * 60
WEBMAIL_REJECT_UNKNOWN_RECIPIENTS = True

# Portable single-file mail client. If this file exists, its contents replace
# the built-in client for Download/Copy on the Mail page. Relative paths are
# resolved beside this unified agent, making host customization easy.
MAIL_CLIENT_TEMPLATE_FILE = r"hostish-mail-client.html"

# ---------------- 7B) LEGACY LOCAL DKIM SETTINGS (unused) ----------------
#
# Kept only for old config-file compatibility. The unified agent does not sign
# outgoing mail. Hostish performs DKIM signing centrally. For linked custom
# domains, enable LINKED_CUSTOM_DOMAIN_MAIL_ENABLED and publish the DNS records
# shown on the Mail page.

MAIL_DKIM_ENABLED = False
MAIL_DKIM_DOMAIN = ""              # blank = WEBMAIL_DOMAIN or USERNAME.hostish.site
MAIL_DKIM_SELECTOR = "hostish"
MAIL_DKIM_PRIVATE_KEY_FILE = r"hostish-dkim-private.pem"
MAIL_DKIM_KEY_BITS = 2048
MAIL_DKIM_AUTO_GENERATE = True

# When True, outbound mail is intentionally blocked until you confirm that the
# displayed TXT record has been published. This prevents another Gmail 5.7.26
# caused by sending a signature whose public key is not yet in DNS.
MAIL_DKIM_REQUIRE_DNS_CONFIRMATION = False
MAIL_DKIM_DNS_CONFIRMED = False

# Refuse to sign a From: address that does not align with MAIL_DKIM_DOMAIN.
MAIL_DKIM_REQUIRE_FROM_ALIGNMENT = True

# Headers covered by the signature when present. From is always required.
MAIL_DKIM_SIGN_HEADERS = [
    "from", "to", "cc", "subject", "date", "message-id", "reply-to",
    "mime-version", "content-type", "content-transfer-encoding",
]

# ---------------- 8) WEBSITE EXTRAS: FAVICON + IP MASKING ----------------

FAVICON_BYPASS_ENABLED = False
FAVICON_BYPASS_URL = ""
FAVICON_MAX_BYTES = 2 * 1024 * 1024

# Replaces your current public IP with IP_PROTECTION_REPLACEMENT anywhere it
# appears in textual response bodies/headers, so a proxied local app never
# leaks the machine's real address to visitors.
IP_PROTECTION_ENABLED = False
IP_PROTECTION_REFRESH_SECONDS = 300
IP_PROTECTION_REPLACEMENT = "BLOCKED"

# ---------------- 9) MANAGED SCRIPTS ----------------
#
# Companion programs this agent can start for you and that you can restart
# individually at any time from the control panel's Scripts page. Each entry:
#   {"name": "backend", "command": "python backend.py", "cwd": "", "autostart": true}
# "command" is split with normal shell-style quoting. "cwd" defaults to the
# script's own folder when the command's first token is a file path.

MANAGED_SCRIPTS = []

# Compatibility with the original website agent. These scripts are imported
# into the managed-scripts system at startup, so the original feature is still
# available without maintaining a second process manager.
AUTO_START_PYTHON_SCRIPTS_ENABLED = False
AUTO_START_PYTHON_SCRIPTS = []

# ---------------- 10) SELF UPDATE ----------------

SELF_UPDATE_ENABLED = True
LATEST_AGENT_URL = "https://hostish.site/downloads/unified-agent.py"
SELF_UPDATE_BACKUP_ENABLED = True
SELF_UPDATE_BACKUP_DIR = ".hostish-agent-backups"
SELF_UPDATE_BACKUP_KEEP = 5
SELF_UPDATE_DOWNLOAD_TIMEOUT_SECONDS = 30
SELF_UPDATE_MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024
SELF_UPDATE_REQUIRE_HTTPS = True
SELF_UPDATE_ALLOW_CROSS_HOST_REDIRECTS = False
SELF_UPDATE_EXPECTED_SHA256 = ""
SELF_UPDATE_ED25519_PUBLIC_KEY_PEM = ""
SELF_UPDATE_SIGNATURE_SUFFIX = ".sig"
SELF_UPDATE_NONINTERACTIVE_ACTION = "continue"
SELF_UPDATE_PRESERVE_EXTRA_NAMES = []
SELF_UPDATE_CHECK_ON_STARTUP = True

# ---------------- 11) LOGGING ----------------

PRINT_REQUEST_HEADERS = False
PRINT_REQUEST_BODY_PREVIEW = False
BODY_PREVIEW_BYTES = 120
PRINT_SENSITIVE_HEADERS = False
REPEAT_ERROR_SUMMARY_EVERY = 10

# Original website-agent diagnostics / idle health checks.
PERIODIC_SUMMARY_ENABLED = False
PERIODIC_SUMMARY_SECONDS = 60
IDLE_STATUS_CHECK_ENABLED = True
IDLE_CHECK_INTERVAL_SECONDS = 120

SENSITIVE_HEADER_NAMES = {
    "authorization", "cookie", "set-cookie",
    "proxy-authorization", "x-api-key", "x-auth-token",
}

# ============================================================
# END HOSTER SETTINGS
# ============================================================


WEB_WS_URL = "wss://hostish.site/ws/host"
SMTP_WS_URL = "wss://hostish.site/ws/smtp"
MINECRAFT_WS_URL = "wss://hostish.site/ws/minecraft"

CONFIG_FILE = "hostish-config.json"
RECONNECT_SECONDS = 5

HOP_BY_HOP = {
    "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
    "te", "trailers", "transfer-encoding", "upgrade",
}

# Every name here can be edited from the control panel and is persisted.
EDITABLE_SETTINGS = [
    # Global Hostish account used by website + SMTP. Minecraft instances have
    # their own credentials in MINECRAFT_INSTANCES.
    "USERNAME", "PASSWORD",
    "CONTROL_PANEL_ENABLED", "CONTROL_PANEL_HOST", "CONTROL_PANEL_PORT",
    "CONTROL_PANEL_PASSWORD", "CONTROL_PANEL_OPEN_BROWSER",
    "WEB_ENABLED", "SMTP_ENABLED", "MINECRAFT_ENABLED",

    # Website agent: every editable setting from the original hoster section.
    "MODE", "WEBSITE_DIR", "PORT", "UPSTREAM_BASE", "UPSTREAM_ROUTES",
    "PROXY_PREFIXES", "HOST_ROUTES", "UPSTREAM_NETWORK_POLICY",
    "PROXY_KEEPALIVE_ENABLED", "PROXY_MAX_IDLE_CONNECTIONS",
    "PROXY_IDLE_CONNECTION_SECONDS", "MAX_CONCURRENT_REQUESTS",
    "MAX_QUEUED_REQUESTS", "MAX_REQUEST_BODY_BYTES", "MAX_FILE_BYTES",
    "MAX_PROXY_BYTES", "UPSTREAM_CONNECT_TIMEOUT_SECONDS",
    "UPSTREAM_RESPONSE_TIMEOUT_SECONDS", "TUNNEL_PROTOCOL",
    "TUNNEL_CHUNK_BYTES", "WEBSOCKET_MAX_INCOMING_BYTES",
    "REQUEST_STREAM_BUFFER_CHUNKS", "USER_WEBSOCKET_PROXY_ENABLED",
    "PROXY_READ_CHUNK_BYTES", "STATIC_READ_CHUNK_BYTES",
    "HANDLE_OPTIONS_LOCALLY", "ADD_CORS_HEADER_TO_RESPONSES",
    "CORS_ALLOW_ORIGIN", "CORS_ALLOW_METHODS", "CORS_ALLOW_HEADERS",
    "STATIC_CACHE_CONTROL", "STATIC_SECURITY_MODE",
    "STATIC_ALLOW_PATTERNS", "STATIC_BLOCK_PATTERNS",
    "ALLOW_STATIC_SYMLINK_ESCAPE", "FAVICON_BYPASS_ENABLED",
    "FAVICON_BYPASS_URL", "FAVICON_MAX_BYTES",
    "CUSTOM_404_ENABLED", "CUSTOM_404_PATH",
    "IP_PROTECTION_ENABLED", "IP_PROTECTION_REFRESH_SECONDS",
    "IP_PROTECTION_REPLACEMENT",

    # SMTP agent.
    "LOCAL_DELIVERY_HOST", "LOCAL_DELIVERY_PORT", "LOCAL_DELIVERY_MODE",
    "LOCAL_DELIVERY_USERNAME", "LOCAL_DELIVERY_PASSWORD",
    "LOCAL_DELIVERY_TLS_VERIFY", "OUTBOUND_RELAY_ENABLED",
    "LOCAL_RELAY_BIND_HOST", "LOCAL_RELAY_BIND_PORT", "LOCAL_RELAY_USERNAME",
    "LOCAL_RELAY_PASSWORD", "LINKED_CUSTOM_DOMAIN_MAIL_ENABLED",
    "MAX_MESSAGE_BYTES", "MAX_RECIPIENTS", "WS_CHUNK_BYTES", "RECONNECT_SECONDS",

    # Legacy single-Minecraft values are retained for config migration.
    "MINECRAFT_HOST", "MINECRAFT_PORT", "START_BAT", "AUTO_START_SERVER",
    "SERVER_START_TIMEOUT", "SERVER_START_CHECK_INTERVAL", "HOMEPAGE_FILE",
    "SIMPLE_VOICE_CHAT_ENABLED", "SIMPLE_VOICE_CHAT_HOST",
    "SIMPLE_VOICE_CHAT_PORT", "SIMPLE_VOICE_CHAT_PUBLIC_PORT",
    "SIMPLE_VOICE_CHAT_SERVICE_ID", "SIMPLE_VOICE_CHAT_CONFIG_FILE",
    "MINECRAFT_INSTANCES",

    # Built-in mail client.
    "WEBMAIL_ENABLED", "WEBMAIL_DOMAIN", "WEBMAIL_DATA_DIR",
    "WEBMAIL_SESSION_SECONDS", "WEBMAIL_REJECT_UNKNOWN_RECIPIENTS",
    "MAIL_CLIENT_TEMPLATE_FILE",

    # Outbound mail authentication.
    "MAIL_DKIM_ENABLED", "MAIL_DKIM_DOMAIN", "MAIL_DKIM_SELECTOR",
    "MAIL_DKIM_PRIVATE_KEY_FILE", "MAIL_DKIM_KEY_BITS",
    "MAIL_DKIM_AUTO_GENERATE", "MAIL_DKIM_REQUIRE_DNS_CONFIRMATION",
    "MAIL_DKIM_DNS_CONFIRMED", "MAIL_DKIM_REQUIRE_FROM_ALIGNMENT",
    "MAIL_DKIM_SIGN_HEADERS",

    # Process management.
    "MANAGED_SCRIPTS", "AUTO_START_PYTHON_SCRIPTS_ENABLED",
    "AUTO_START_PYTHON_SCRIPTS",

    # Self-update controls from the original website agent.
    "SELF_UPDATE_ENABLED", "LATEST_AGENT_URL", "SELF_UPDATE_BACKUP_ENABLED",
    "SELF_UPDATE_BACKUP_DIR", "SELF_UPDATE_BACKUP_KEEP",
    "SELF_UPDATE_DOWNLOAD_TIMEOUT_SECONDS", "SELF_UPDATE_MAX_DOWNLOAD_BYTES",
    "SELF_UPDATE_REQUIRE_HTTPS", "SELF_UPDATE_ALLOW_CROSS_HOST_REDIRECTS",
    "SELF_UPDATE_EXPECTED_SHA256", "SELF_UPDATE_ED25519_PUBLIC_KEY_PEM",
    "SELF_UPDATE_SIGNATURE_SUFFIX", "SELF_UPDATE_NONINTERACTIVE_ACTION",
    "SELF_UPDATE_PRESERVE_EXTRA_NAMES", "SELF_UPDATE_CHECK_ON_STARTUP",

    # Diagnostics.
    "PRINT_REQUEST_HEADERS", "PRINT_REQUEST_BODY_PREVIEW", "BODY_PREVIEW_BYTES",
    "PRINT_SENSITIVE_HEADERS", "SENSITIVE_HEADER_NAMES", "REPEAT_ERROR_SUMMARY_EVERY",
    "PERIODIC_SUMMARY_ENABLED", "PERIODIC_SUMMARY_SECONDS",
    "IDLE_STATUS_CHECK_ENABLED", "IDLE_CHECK_INTERVAL_SECONDS",
]

# Never sent to the browser in plain text.
SECRET_SETTINGS = {
    "PASSWORD", "CONTROL_PANEL_PASSWORD", "LOCAL_DELIVERY_PASSWORD",
    "LOCAL_RELAY_PASSWORD",
}


def _config_path() -> str:
    try:
        base = os.path.dirname(os.path.abspath(__file__))
    except Exception:
        base = os.getcwd()
    return os.path.join(base, CONFIG_FILE)


SECRET_VALUE_PREFIX = "enc:v1:"
SECRET_KEY_FILE = ".hostish-config.key"


def _secret_key_path() -> str:
    return os.path.join(os.path.dirname(_config_path()), SECRET_KEY_FILE)


def _load_or_create_secret_key() -> bytes:
    """Load the local Fernet key, creating it with owner-only permissions if needed."""
    try:
        from cryptography.fernet import Fernet
    except ImportError as error:
        raise RuntimeError(
            "Encrypted config requires cryptography. Install with: "
            "python -m pip install cryptography"
        ) from error

    path = _secret_key_path()
    if os.path.isfile(path):
        with open(path, "rb") as handle:
            key = handle.read().strip()
        # Validate before using it so a corrupt key fails loudly instead of
        # silently replacing credentials with unusable ciphertext.
        Fernet(key)
        return key

    key = Fernet.generate_key()
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    try:
        fd = os.open(path, flags, 0o600)
        with os.fdopen(fd, "wb") as handle:
            handle.write(key + b"\n")
            handle.flush()
            try:
                os.fsync(handle.fileno())
            except OSError:
                pass
    except FileExistsError:
        with open(path, "rb") as handle:
            key = handle.read().strip()
    try:
        os.chmod(path, 0o600)
    except Exception:
        pass
    Fernet(key)
    return key


def _encrypt_secret(value: Any) -> Any:
    if value in (None, ""):
        return value
    value = str(value)
    if value.startswith(SECRET_VALUE_PREFIX):
        return value
    from cryptography.fernet import Fernet
    token = Fernet(_load_or_create_secret_key()).encrypt(value.encode("utf-8")).decode("ascii")
    return SECRET_VALUE_PREFIX + token


def _decrypt_secret(value: Any) -> Any:
    if not isinstance(value, str) or not value.startswith(SECRET_VALUE_PREFIX):
        return value
    from cryptography.fernet import Fernet, InvalidToken
    token = value[len(SECRET_VALUE_PREFIX):]
    try:
        return Fernet(_load_or_create_secret_key()).decrypt(token.encode("ascii")).decode("utf-8")
    except InvalidToken as error:
        raise RuntimeError(
            f"Could not decrypt a saved password. Keep {SECRET_KEY_FILE} together with {CONFIG_FILE}."
        ) from error


def _encrypt_config_secrets(values: dict) -> dict:
    data = dict(values or {})
    for name in SECRET_SETTINGS:
        if name in data:
            data[name] = _encrypt_secret(data[name])
    instances = data.get("MINECRAFT_INSTANCES")
    if isinstance(instances, list):
        encrypted_instances = []
        for item in instances:
            if not isinstance(item, dict):
                encrypted_instances.append(item)
                continue
            item = dict(item)
            if "password" in item:
                item["password"] = _encrypt_secret(item.get("password"))
            encrypted_instances.append(item)
        data["MINECRAFT_INSTANCES"] = encrypted_instances
    return data


def _decrypt_config_secrets(values: dict) -> dict:
    data = dict(values or {})
    for name in SECRET_SETTINGS:
        if name in data:
            data[name] = _decrypt_secret(data[name])
    instances = data.get("MINECRAFT_INSTANCES")
    if isinstance(instances, list):
        decrypted_instances = []
        for item in instances:
            if not isinstance(item, dict):
                decrypted_instances.append(item)
                continue
            item = dict(item)
            if "password" in item:
                item["password"] = _decrypt_secret(item.get("password"))
            decrypted_instances.append(item)
        data["MINECRAFT_INSTANCES"] = decrypted_instances
    return data


def load_config_file() -> dict:
    path = _config_path()
    if not os.path.isfile(path):
        return {}
    try:
        with open(path, "r", encoding="utf-8-sig") as handle:
            data = json.load(handle)
        if not isinstance(data, dict):
            return {}
        return _decrypt_config_secrets(data)
    except Exception as error:
        print(f"[WARN] Could not read {CONFIG_FILE}: {error}")
        return {}


def _json_safe(value):
    """Return a recursively JSON-serializable copy of a config value.

    Some original Hostish settings intentionally use Python-only container
    types (notably SENSITIVE_HEADER_NAMES, which is a set).  The control
    panel persists settings as JSON, so normalize those values at the one
    boundary where they are written instead of relying on every caller to
    remember to convert them.  apply_settings() restores set-backed settings
    to their native type when the file is loaded again.
    """
    if isinstance(value, set):
        return sorted((_json_safe(item) for item in value), key=lambda item: str(item))
    if isinstance(value, (list, tuple)):
        return [_json_safe(item) for item in value]
    if isinstance(value, dict):
        return {str(key): _json_safe(item) for key, item in value.items()}
    if isinstance(value, os.PathLike):
        return os.fspath(value)
    return value


def save_config_file(values: dict) -> None:
    path = _config_path()
    temporary = path + ".tmp"
    persisted = _encrypt_config_secrets(values)
    with open(temporary, "w", encoding="utf-8") as handle:
        json.dump(_json_safe(persisted), handle, indent=2, ensure_ascii=False)
        handle.flush()
        try:
            os.fsync(handle.fileno())
        except OSError:
            pass
    os.replace(temporary, path)
    try:
        os.chmod(path, 0o600)
    except Exception:
        pass


def current_settings() -> dict:
    values = {}
    for name in EDITABLE_SETTINGS:
        value = globals().get(name)
        values[name] = sorted(value) if isinstance(value, set) else value
    return values


def apply_settings(values: dict) -> list[str]:
    """Copy known settings into module globals. Returns the names applied."""
    applied = []
    for name, value in (values or {}).items():
        if name not in EDITABLE_SETTINGS:
            continue
        current = globals().get(name)
        if isinstance(current, bool):
            value = bool(value)
        elif isinstance(current, int) and not isinstance(value, bool):
            try:
                value = int(value)
            except (TypeError, ValueError):
                continue
        elif isinstance(current, list) and not isinstance(value, list):
            continue
        elif isinstance(current, dict) and not isinstance(value, dict):
            continue
        elif isinstance(current, set):
            if not isinstance(value, (list, tuple, set)):
                continue
            value = {str(item) for item in value if str(item).strip()}
        globals()[name] = value
        applied.append(name)
    globals()["PROXY_PREFIXES"] = [
        _norm_prefix(str(prefix)) for prefix in (PROXY_PREFIXES or []) if str(prefix).strip()
    ]
    return applied


# ============================================================
# SHARED STATE / LOGGING
# ============================================================

START_TS = time.time()
LOG_LINES = deque(maxlen=500)
_REPEAT_ERRORS: dict = {}
_LOG_SEQUENCE = 0

STATS = {
    "web": {"connections": 0, "requests": 0, "static": 0, "proxy": 0,
            "bytes": 0, "inflight": 0, "statuses": {}},
    "smtp": {"connections": 0, "delivered": 0, "delivery_failed": 0,
             "sent": 0, "send_failed": 0},
    "minecraft": {"connections": 0, "players": 0, "peak_players": 0,
                  "voice_peers": 0},
}

SERVICE_STATE = {
    "web": {"status": "stopped", "detail": "", "since": 0.0},
    "smtp": {"status": "stopped", "detail": "", "since": 0.0},
    "minecraft": {"status": "stopped", "detail": "", "since": 0.0},
}

# Filled in by main(); the control panel talks to these.
SUPERVISORS: dict = {}

# Set by smtp_service() while the SMTP tunnel is connected; webmail sends
# outbound mail through this same tunnel instead of opening its own.
_SMTP_AGENT_INSTANCE = None


def ts() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def log(message: str, service: str = "agent") -> None:
    global _LOG_SEQUENCE
    _LOG_SEQUENCE += 1
    stamp = ts()
    LOG_LINES.append({
        "seq": _LOG_SEQUENCE,
        "time": stamp,
        "service": service,
        "text": str(message),
    })
    print(f"[{stamp}] [{service}] {message}", flush=True)


def log_repeatable_error(key: str, message: str, service: str = "agent") -> None:
    entry = _REPEAT_ERRORS.get(key)
    if entry is None or entry["last"] != message:
        _REPEAT_ERRORS[key] = {"count": 1, "last": message}
        log(message, service)
        return
    entry["count"] += 1
    if entry["count"] % max(1, int(REPEAT_ERROR_SUMMARY_EVERY)) == 0:
        log(f"[repeat x{entry['count']}] {message}", service)


def set_state(service: str, status: str, detail: str = "") -> None:
    state = SERVICE_STATE.setdefault(service, {})
    if state.get("status") != status:
        state["since"] = time.time()
    state["status"] = status
    state["detail"] = detail


def make_ssl_context() -> ssl.SSLContext:
    if certifi is not None:
        return ssl.create_default_context(cafile=certifi.where())
    return ssl.create_default_context()


def fmt_bytes(n: int) -> str:
    n = int(n or 0)
    if n < 1024:
        return f"{n} B"
    if n < 1024 * 1024:
        return f"{n / 1024:.1f} KB"
    if n < 1024 * 1024 * 1024:
        return f"{n / (1024 * 1024):.1f} MB"
    return f"{n / (1024 * 1024 * 1024):.2f} GB"


def _json(data: dict) -> str:
    return json.dumps(data, separators=(",", ":"), ensure_ascii=False)


def _norm_prefix(prefix: str) -> str:
    prefix = (prefix or "").lstrip("/")
    if prefix and not prefix.endswith("/"):
        prefix += "/"
    return prefix


def _limit_exceeded(limit, value) -> bool:
    try:
        limit_value = int(limit)
    except Exception:
        limit_value = 0
    return limit_value > 0 and int(value) > limit_value


def _is_loopback_host(host: str) -> bool:
    return str(host).strip().lower() in {"127.0.0.1", "localhost", "::1"}


async def run_in_thread(func, *args):
    loop = asyncio.get_running_loop()
    context = contextvars.copy_context()
    call = functools.partial(context.run, func, *args)
    return await loop.run_in_executor(None, call)


# ============================================================
# BUNDLED HOSTISH MINECRAFT MANAGER V4
# ============================================================
#
# The complete Minecraft manager project supplied by the user is embedded in
# this one Python file. It is extracted automatically into a private runtime
# folder and imported into THIS SAME Python process. Its Flask app is mounted
# beneath /minecraft through the unified listener; there is no iframe and no
# second Python process. Original routes/templates/static assets are preserved.

MINECRAFT_MANAGER_BUNDLE_VERSION = "4-unified-voicefix"
MINECRAFT_MANAGER_BUNDLE_SHA256 = "6894eeb7aaf1c29a3bfb88d0d16c84262c1286f9b3f39729d11d6877bff1ee0c"
MINECRAFT_MANAGER_ZIP_B64 = r"""
UEsDBBQAAAAIAAs8MV2eeYq9AQIAACQEAAAjAAAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9hcHAucHl9U99r2zAQfvdfcXgv9jDOD0pXAn7YsrQZ
LC0sfhgsQ8jW2RG1JSPJLaP0f59kOWmzrrHB1n333afT3Ym3nVQGpA4qJVuoGqrvgXvw2hmBd5RS4QEvpah4HQQBwwpKhdQgoV0XxYsA7GOXkPngiBBBWyQk
AYNt1zhmJRuGKgsPgA4T0IYaXh5d3gzjg1yq0W5jyD3+scpSpygeuJIirdFE4fpum3/brslmSbar5Y9VbgUtp1dUMNlG84v4Rcin/ivcfP5Jlne3+eo2J99X
tzf5OvxtpS/gI8ym839/8MF6br4kUEkFj1I1bNJKBn3XSMp0MKgPVXrEIlWyt4ciLeXiULCiA6rBIaTo/svWqB5Q6dOAEXwvhnb8lG+B97hdXzS8PKV7zEUc
y6Ow5tqgIkXTY6e4MNGYdXyO85LoWZrP7yzlmJNn2ab3SjiynTY/V6/nLQh4BYcRgyyDkAx1JyT0o1hWtQsZmp66ZhG/jrz8Xmpj/eFs/imd2ncWDvBQowwu
ry6vvD2kVoU7sbYBXO9hs4QNFbRGtRMAX6neF5IqtoC9Md1iMnlyys+LJ6f0vBPjJL+5SONpbZ+I6YXAZqCNTrO3J2Vc1AN4tNKct6iiWTpN3san9uYoQ1DQ
okEWezN6VfFeRC61zH2S4aCZ+yTAsOjr7Jo2GpNxM2RZrnqMg79QSwMEFAAAAAgAPCwyXQAAAAACAAAAAAAAACIAAABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2
ZXItYXBwL2NvcmUvAwBQSwMEFAAAAAgACzwxXQAAAAACAAAAAAAAAC0AAABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL2NvcmUvX19pbml0X18ucHkD
AFBLAwQUAAAACAA8LDJdAAAAAAIAAAAAAAAALgAAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29yZS9fX3B5Y2FjaGVfXy8DAFBLAwQUAAAACAA0
LDJdrDdlZoQAAACiAAAARgAAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29yZS9fX3B5Y2FjaGVfXy9fX2luaXRfXy5jcHl0aG9uLTMxMy5weWP7
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
AFopIieTdWSyzVM+ofUmtF4/QVWN87XkbEWEcxxI2bZgpl3xdK/H7WOxicjLVZqEfn2z/GuJycnQn5J5GpRZSAbi4I0WhPwcYFJOH14VTFx8sufV94d6LNKM
+XSY0Fm7HqwuWMP4Uc/wFoa05J1fgoSOSJTQxyXVJSLqGv/vf/+PjV1vFZFPJlRdY3frL6UKM7GDaJtIEeNlbQVb/7fM+S8bB5YHtLBZB9Y2w4/mCman6NCJ
0hdOBrqO0yqUEU19jzXlzWbXfxP76Zvaf2Tz2Ktm6YZtN5HWvwhwi62CvIw2h2SdCeCQtCqtjeFLt8OtITcXuS0UzK6qWk5f399jfMy++SZOeBpJ+1pu9+5/
c+GfkkJIJ61LySgpVyc7v5iEkg728T2UHDtBl/zxvNLci7fQBdSJ1MdSGO7oZxGIacjdtQuJV2WNk/oXA5oR/TuDgDXtTHNdn5K1oP45gL5tS7f8qynW7wdG
bEcHZHQ2pllFhOZ6odhl6BIp/iwhv9c2itjpNw9U1UINBUYHO0MaSGVw1b824AW2XJC19q8N3ErOf9jkTG7Qe45g2xzBgeRR753Lv51jgvq3RFqaQ2MCjZH3
OmHXpf4PUEsDBBQAAAAIAAs8MV1LyR2vOgAAAD0AAAAtAAAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9yZXF1aXJlbWVudHMudHh0c8tJLM62szXW
M+AqSi0sTS0uKbazNdIzNuQqT00qzk/OTgUJGBoB5ZNTi0oy0zKB0gZGJnpGekZcAFBLAwQUAAAACAA8LDJdAAAAAAIAAAAAAAAAJAAAAGhvc3Rpc2gtbWlu
ZWNyYWZ0LXNlcnZlci1hcHAvc3RhdGljLwMAUEsDBBQAAAAIAAs8MV1+fNQnFh4AACJrAAAqAAAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9zdGF0
aWMvYXBwLmpz3T3tcttGkv/9FCPGMYALAUrJJtmVRKkc2Tlrz19lyZsfXpcFEkMRMQjQACiZkVl1T3OvcP/vUe5Jrj9mgBkA/LCTvdu6rcqaAGZ6unt6+mt6
Ru5kkY7LOEuF64m7e0IMBuJyKkWWx9dxGiZiFqbhtczFbViI2zwuS5mKSZaLQSDOU1FC00UaT2IZCWiWliIuRRLfyEKEJUMbzOJUjvNwUgbi36ScizzLSj+X
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
AAAAAAAAABAA/UFCAgAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9jb3JlL1BLAQIUAxQAAAAIAAs8MV0AAAAAAgAAAAAAAAAtAAAAAAAAAAAAAACk
gYQCAABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL2NvcmUvX19pbml0X18ucHlQSwECFAMUAAAACAA8LDJdAAAAAAIAAAAAAAAALgAAAAAAAAAAABAA
/UHRAgAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9jb3JlL19fcHljYWNoZV9fL1BLAQIUAxQAAAAIADQsMl2sN2VmhAAAAKIAAABGAAAAAAAAAAAA
AACkgR8DAABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL2NvcmUvX19weWNhY2hlX18vX19pbml0X18uY3B5dGhvbi0zMTMucHljUEsBAhQDFAAAAAgA
NCwyXVZ/SaOjBgAAPwwAAEQAAAAAAAAAAAAAAKSBBwQAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29yZS9fX3B5Y2FjaGVfXy9jb25maWcuY3B5
dGhvbi0zMTMucHljUEsBAhQDFAAAAAgALSwyXVsVCIxvTAAAGawAAEwAAAAAAAAAAAAAAKSBDAsAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29y
ZS9fX3B5Y2FjaGVfXy9ob3N0aXNoX3R1bm5lbC5jcHl0aG9uLTMxMy5weWNQSwECFAMUAAAACAA0LDJdtwrOTo4iAABRSQAASgAAAAAAAAAAAAAApIHlVwAA
aG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9jb3JlL19fcHljYWNoZV9fL3NlcnZlcl9zdG9yZS5jcHl0aG9uLTMxMy5weWNQSwECFAMUAAAACAALPDFd
DmgBzXwCAACNBQAAKwAAAAAAAAAAAAAApIHbegAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC9jb3JlL2NvbmZpZy5weVBLAQIUAxQAAAAIAAs8MV3i
ovE6YA4AAO4wAAA0AAAAAAAAAAAAAACkgaB9AABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL2NvcmUvY29udGVudF9tYW5hZ2VyLnB5UEsBAhQDFAAA
AAgACzwxXeWtVqssBgAAOBQAAC8AAAAAAAAAAAAAAKSBUowAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29yZS9kb3dubG9hZGVyLnB5UEsBAhQD
FAAAAAgALSwyXecYV02+GAAAjXIAADMAAAAAAAAAAAAAAKSBy5IAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29yZS9ob3N0aXNoX3R1bm5lbC5w
eVBLAQIUAxQAAAAIAAs8MV3ZkKx8cwUAADQSAAAuAAAAAAAAAAAAAACkgdqrAABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL2NvcmUvaW5zdGFsbGVy
LnB5UEsBAhQDFAAAAAgACzwxXQsK2ZNoCQAAZx0AAC0AAAAAAAAAAAAAAKSBmbEAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29yZS9tY19maWxl
cy5weVBLAQIUAxQAAAAIAAs8MV3MJbcg+gwAAMcuAAA0AAAAAAAAAAAAAACkgUy7AABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL2NvcmUvcHJvY2Vz
c19tYW5hZ2VyLnB5UEsBAhQDFAAAAAgACzwxXffPtJnMEAAAGzEAADYAAAAAAAAAAAAAAKSBmMgAAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvY29y
ZS9wdWJsaWNfd2ViX3R1bm5lbC5weVBLAQIUAxQAAAAIAAs8MV1yuWTUTRMAAOw0AAAxAAAAAAAAAAAAAACkgbjZAABob3N0aXNoLW1pbmVjcmFmdC1zZXJ2
ZXItYXBwL2NvcmUvc2VydmVyX3N0b3JlLnB5UEsBAhQDFAAAAAgACzwxXUvJHa86AAAAPQAAAC0AAAAAAAAAAAAAAKSBVO0AAGhvc3Rpc2gtbWluZWNyYWZ0
LXNlcnZlci1hcHAvcmVxdWlyZW1lbnRzLnR4dFBLAQIUAxQAAAAIADwsMl0AAAAAAgAAAAAAAAAkAAAAAAAAAAAAEAD9QdntAABob3N0aXNoLW1pbmVjcmFm
dC1zZXJ2ZXItYXBwL3N0YXRpYy9QSwECFAMUAAAACAALPDFdfnzUJxYeAAAiawAAKgAAAAAAAAAAAAAApIEd7gAAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVy
LWFwcC9zdGF0aWMvYXBwLmpzUEsBAhQDFAAAAAgACzwxXXTP2+vCIQAAOKQAAC0AAAAAAAAAAAAAAKSBewwBAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1h
cHAvc3RhdGljL3N0eWxlLmNzc1BLAQIUAxQAAAAIADwsMl0AAAAAAgAAAAAAAAAnAAAAAAAAAAAAEAD9QYguAQBob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXIt
YXBwL3RlbXBsYXRlcy9QSwECFAMUAAAACAALPDFdoRBc7OQCAAD8BwAAOQAAAAAAAAAAAAAApIHPLgEAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC90
ZW1wbGF0ZXMvX2RvbmF0ZV9tb2RhbC5odG1sUEsBAhQDFAAAAAgACzwxXfsuH1aGBAAAfQsAADAAAAAAAAAAAAAAAKSBCjIBAGhvc3Rpc2gtbWluZWNyYWZ0
LXNlcnZlci1hcHAvdGVtcGxhdGVzL2Jhc2UuaHRtbFBLAQIUAxQAAAAIAAs8MV2AB/slvQYAADEWAAA1AAAAAAAAAAAAAACkgd42AQBob3N0aXNoLW1pbmVj
cmFmdC1zZXJ2ZXItYXBwL3RlbXBsYXRlcy9kYXNoYm9hcmQuaHRtbFBLAQIUAxQAAAAIAAs8MV2BmHbP1AEAAHkDAAAxAAAAAAAAAAAAAACkge49AQBob3N0
aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL3RlbXBsYXRlcy9sb2dpbi5odG1sUEsBAhQDFAAAAAgACzwxXbxGCBS8CQAABxwAAD4AAAAAAAAAAAAAAKSBEUAB
AGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvdGVtcGxhdGVzL3B1YmxpY19zZXJ2ZXJfZGFzaC5odG1sUEsBAhQDFAAAAAgACzwxXbSa9hmUAwAAfwcA
AD8AAAAAAAAAAAAAAKSBKUoBAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvdGVtcGxhdGVzL3B1YmxpY19zZXJ2ZXJfbG9naW4uaHRtbFBLAQIUAxQA
AAAIAC0sMl0vV2307iwAAJbDAAA5AAAAAAAAAAAAAACkgRpOAQBob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL3RlbXBsYXRlcy9zZXJ2ZXJfZGV0YWls
Lmh0bWxQSwECFAMUAAAACAALPDFdhgTG27IMAADuLQAANgAAAAAAAAAAAAAApIFfewEAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC90ZW1wbGF0ZXMv
c2VydmVyX25ldy5odG1sUEsBAhQDFAAAAAgACzwxXdNWvb8eBAAA+gsAADQAAAAAAAAAAAAAAKSBZYgBAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAv
dGVtcGxhdGVzL3NldHRpbmdzLmh0bWxQSwECFAMUAAAACAALPDFdUj+u464CAACLBQAAMQAAAAAAAAAAAAAApIHVjAEAaG9zdGlzaC1taW5lY3JhZnQtc2Vy
dmVyLWFwcC90ZW1wbGF0ZXMvc2V0dXAuaHRtbFBLAQIUAxQAAAAIADwsMl0AAAAAAgAAAAAAAAAhAAAAAAAAAAAAEAD9QdKPAQBob3N0aXNoLW1pbmVjcmFm
dC1zZXJ2ZXItYXBwL3dlYi9QSwECFAMUAAAACAALPDFdAAAAAAIAAAAAAAAALAAAAAAAAAAAAAAApIETkAEAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFw
cC93ZWIvX19pbml0X18ucHlQSwECFAMUAAAACAA8LDJdAAAAAAIAAAAAAAAALQAAAAAAAAAAABAA/UFfkAEAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFw
cC93ZWIvX19weWNhY2hlX18vUEsBAhQDFAAAAAgALSwyXdvdeDlJbwAA9SYBAEsAAAAAAAAAAAAAAKSBrJABAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1h
cHAvd2ViL19fcHljYWNoZV9fL3JvdXRlc19zZXJ2ZXJzLmNweXRob24tMzEzLnB5Y1BLAQIUAxQAAAAIAAs8MV0hK698GwkAACUiAAAuAAAAAAAAAAAAAACk
gV4AAgBob3N0aXNoLW1pbmVjcmFmdC1zZXJ2ZXItYXBwL3dlYi9yb3V0ZXNfYXBpLnB5UEsBAhQDFAAAAAgACzwxXTKhS277BgAAcBcAAC8AAAAAAAAAAAAA
AKSBxQkCAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvd2ViL3JvdXRlc19tYWluLnB5UEsBAhQDFAAAAAgACzwxXTM09CW1CwAA6SkAADEAAAAAAAAA
AAAAAKSBDRECAGhvc3Rpc2gtbWluZWNyYWZ0LXNlcnZlci1hcHAvd2ViL3JvdXRlc19wdWJsaWMucHlQSwECFAMUAAAACAALPDFdyCbKtUMmAADovAAAMgAA
AAAAAAAAAAAApIERHQIAaG9zdGlzaC1taW5lY3JhZnQtc2VydmVyLWFwcC93ZWIvcm91dGVzX3NlcnZlcnMucHlQSwUGAAAAACgAKAAbDwAApEMCAAAA
"""


def _minecraft_manager_root() -> str:
    return os.path.join(os.path.dirname(_config_path()), ".hostish-minecraft-manager-v4")


def _minecraft_manager_app_config_path() -> str:
    return os.path.join(_minecraft_manager_root(), "app_config.json")


def _minecraft_manager_read_config() -> dict:
    cfg = {
        "data_dir": "",
        "java_path": "java",
        "dashboard_password_hash": "",
    }
    path = _minecraft_manager_app_config_path()
    try:
        with open(path, "r", encoding="utf-8-sig") as handle:
            value = json.load(handle)
        if isinstance(value, dict):
            cfg.update(value)
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        pass
    # Old standalone versions may have persisted these. They are intentionally
    # ignored by the unified agent; the private WSGI port is automatic.
    cfg.pop("dashboard_host", None)
    cfg.pop("dashboard_port", None)
    return cfg


def _minecraft_manager_missing_dependencies() -> list:
    required = (("flask", "Flask"), ("requests", "requests"),
                ("websockets", "websockets"), ("certifi", "certifi"))
    missing = []
    for module_name, package_name in required:
        if importlib.util.find_spec(module_name) is None:
            missing.append(package_name)
    return missing


def _extract_minecraft_manager_bundle() -> str:
    root = os.path.abspath(_minecraft_manager_root())
    os.makedirs(root, exist_ok=True)
    marker = os.path.join(root, ".bundle-sha256")
    try:
        if os.path.isfile(marker):
            if Path(marker).read_text("utf-8").strip() == MINECRAFT_MANAGER_BUNDLE_SHA256:
                expected = os.path.join(root, "app.py")
                if os.path.isfile(expected):
                    return root
    except Exception:
        pass

    raw = base64.b64decode("".join(MINECRAFT_MANAGER_ZIP_B64.split()).encode("ascii"))
    if hashlib.sha256(raw).hexdigest() != MINECRAFT_MANAGER_BUNDLE_SHA256:
        raise RuntimeError("Embedded Minecraft manager bundle failed its SHA-256 integrity check")

    prefix = "hostish-minecraft-server-app/"
    with zipfile.ZipFile(io.BytesIO(raw), "r") as archive:
        for info in archive.infolist():
            name = info.filename.replace("\\", "/")
            if not name.startswith(prefix):
                continue
            relative = name[len(prefix):]
            if not relative:
                continue
            target = os.path.abspath(os.path.join(root, relative))
            try:
                if os.path.commonpath([root, target]) != root:
                    raise RuntimeError(f"Unsafe path in embedded Minecraft bundle: {name}")
            except ValueError:
                raise RuntimeError(f"Unsafe path in embedded Minecraft bundle: {name}")
            if info.is_dir() or relative.endswith("/"):
                os.makedirs(target, exist_ok=True)
                continue
            os.makedirs(os.path.dirname(target), exist_ok=True)
            with archive.open(info, "r") as source, open(target, "wb") as destination:
                while True:
                    chunk = source.read(1024 * 1024)
                    if not chunk:
                        break
                    destination.write(chunk)

    # Add a tiny local-only bridge back to the unified manager. This does not
    # alter any Minecraft functionality and stays hidden on *.hostish.site.
    try:
        base_template = os.path.join(root, "templates", "base.html")
        if os.path.isfile(base_template):
            html = Path(base_template).read_text("utf-8")
            marker_text = "HOSTISH_UNIFIED_BACK_LINK"
            if marker_text not in html:
                bridge = r"""
<script>
/* HOSTISH_UNIFIED_BACK_LINK */
document.addEventListener('DOMContentLoaded', () => {
  const host = String(location.hostname || '').toLowerCase();
  if (host.endsWith('.hostish.site')) return;
  const a = document.createElement('a');
  a.href = '/overview';
  a.textContent = 'Hostish Unified';
  a.style.cssText = 'position:fixed;right:14px;bottom:14px;z-index:99999;padding:8px 11px;border-radius:8px;background:#5865f2;color:white;text-decoration:none;font:600 12px system-ui;box-shadow:0 4px 18px #0008';
  document.body.appendChild(a);
});
</script>
"""
                html = html.replace("</body>", bridge + "\n</body>")
                Path(base_template).write_text(html, "utf-8")
    except Exception as error:
        log(f"Could not add unified navigation bridge to Minecraft templates: {error}", "minecraft")

    with open(marker, "w", encoding="utf-8") as handle:
        handle.write(MINECRAFT_MANAGER_BUNDLE_SHA256)
    return root


# Werkzeug's DispatcherMiddleware is used when the manager starts. It sets
# SCRIPT_NAME/PATH_INFO according to WSGI rules, so Flask url_for(), forms,
# redirects and static assets all stay under /minecraft.


class BundledMinecraftManager:
    """Runs the exact supplied Minecraft Flask application in this process."""

    def __init__(self) -> None:
        self.server = None
        self.thread = None
        self.flask_app = None
        self.module = None
        self.last_error = ""
        self.started_at = 0.0
        self.active_host = "127.0.0.1"
        self.active_port = 0

    @property
    def running(self) -> bool:
        return bool(self.server is not None and self.thread is not None and self.thread.is_alive())

    def status(self) -> dict:
        cfg = _minecraft_manager_read_config()
        missing = _minecraft_manager_missing_dependencies()
        return {
            "running": self.running,
            "status": "online" if self.running else ("error" if self.last_error else "stopped"),
            "detail": self.last_error or (
                "mounted at /minecraft" if self.running else "manager stopped"
            ),
            "pid": os.getpid() if self.running else None,
            "mount_path": "/minecraft",
            "root": _minecraft_manager_root(),
            "data_dir": str(cfg.get("data_dir") or ""),
            "java_path": str(cfg.get("java_path") or "java"),
            "missing_dependencies": missing,
            "bundle_version": MINECRAFT_MANAGER_BUNDLE_VERSION,
            "uptime_seconds": int(time.time() - self.started_at) if self.running and self.started_at else 0,
            "in_process": True,
        }

    def _import_manager(self, root: str):
        # The supplied project uses top-level imports such as "from core import".
        # Put only its private extracted root on sys.path, then import app.py
        # under a unique module name to avoid colliding with this unified file.
        if root not in sys.path:
            sys.path.insert(0, root)

        module_name = "_hostish_embedded_minecraft_app"
        module = sys.modules.get(module_name)
        if module is None:
            app_path = os.path.join(root, "app.py")
            spec = importlib.util.spec_from_file_location(module_name, app_path)
            if spec is None or spec.loader is None:
                raise RuntimeError("Could not load embedded Minecraft app.py")
            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module
            spec.loader.exec_module(module)

        app = getattr(module, "app", None)
        if app is None:
            create_app = getattr(module, "create_app", None)
            if not callable(create_app):
                raise RuntimeError("Embedded Minecraft app does not expose app/create_app")
            app = create_app()
        return module, app

    async def _wait_for_port(self, port: int, timeout: float = 10.0) -> bool:
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            if not self.running:
                return False
            try:
                reader, writer = await asyncio.wait_for(
                    asyncio.open_connection("127.0.0.1", int(port)), timeout=0.5)
                writer.close()
                try:
                    await writer.wait_closed()
                except Exception:
                    pass
                return True
            except Exception:
                await asyncio.sleep(0.15)
        return False

    async def start(self) -> tuple:
        if self.running:
            return False, "Minecraft manager is already running in this process"

        missing = _minecraft_manager_missing_dependencies()
        if missing:
            self.last_error = (
                "Missing Minecraft manager dependencies: " + ", ".join(missing) +
                ". Install with: python -m pip install Flask requests websockets certifi"
            )
            log(self.last_error, "minecraft")
            return False, self.last_error

        try:
            root = _extract_minecraft_manager_bundle()
            module, app = await asyncio.to_thread(self._import_manager, root)

            from werkzeug.middleware.dispatcher import DispatcherMiddleware
            from werkzeug.serving import make_server
            # The same Flask app serves root paths for Hostish public-dashboard
            # companions and /minecraft for the local unified manager. Port 0
            # asks the OS for a private free loopback port; users never set it.
            mounted_app = DispatcherMiddleware(app, {"/minecraft": app})
            server = await asyncio.to_thread(
                make_server, "127.0.0.1", 0, mounted_app, True
            )

            self.module = module
            self.flask_app = app
            self.server = server
            self.active_host = "127.0.0.1"
            self.active_port = int(server.server_port)
            os.environ["HOSTISH_MC_INTERNAL_PORT"] = str(self.active_port)
            self.started_at = time.time()
            self.last_error = ""

            def run_server():
                try:
                    server.serve_forever()
                except Exception as error:
                    self.last_error = f"Minecraft WSGI server stopped: {type(error).__name__}: {error}"
                    log(self.last_error, "minecraft")

            self.thread = threading.Thread(
                target=run_server,
                name="hostish-minecraft-wsgi",
                daemon=True,
            )
            self.thread.start()

            if not await self._wait_for_port(self.active_port):
                raise RuntimeError("Minecraft manager did not become ready")

            # app.py normally starts enabled public-web tunnels from __main__.
            # Because we import it in-process, explicitly perform that startup.
            try:
                public_web_tunnel = importlib.import_module("core.public_web_tunnel")
                threading.Timer(0.6, public_web_tunnel.start_enabled).start()
            except Exception as error:
                log(f"Minecraft public-web autostart warning: {error}", "minecraft")

            log(
                f"Bundled Minecraft Manager v{MINECRAFT_MANAGER_BUNDLE_VERSION} "
                f"mounted at /minecraft",
                "minecraft",
            )
            return True, "Minecraft manager mounted at /minecraft"
        except Exception as error:
            try:
                if self.server is not None:
                    await asyncio.to_thread(self.server.shutdown)
            except Exception:
                pass
            self.server = None
            self.thread = None
            self.last_error = f"Could not start Minecraft manager: {type(error).__name__}: {error}"
            log(self.last_error, "minecraft")
            return False, self.last_error

    async def stop(self) -> tuple:
        if not self.running:
            return False, "Minecraft manager is already stopped"
        try:
            # Stop the supplied manager's public-web companion tunnels first.
            try:
                public_web_tunnel = importlib.import_module("core.public_web_tunnel")
                stop_all = getattr(public_web_tunnel, "_stop_all", None)
                if callable(stop_all):
                    await asyncio.to_thread(stop_all)
            except Exception as error:
                log(f"Minecraft public-web shutdown warning: {error}", "minecraft")

            server = self.server
            if server is not None:
                await asyncio.to_thread(server.shutdown)
                await asyncio.to_thread(server.server_close)
            thread = self.thread
            if thread is not None and thread.is_alive():
                await asyncio.to_thread(thread.join, 4.0)
        except Exception as error:
            self.last_error = f"Error stopping Minecraft manager: {type(error).__name__}: {error}"
            return False, self.last_error
        finally:
            self.server = None
            self.thread = None
            self.flask_app = None
            self.started_at = 0.0
            os.environ.pop("HOSTISH_MC_INTERNAL_PORT", None)
        return True, "Minecraft manager stopped"

    async def restart(self) -> tuple:
        if self.running:
            await self.stop()
        await asyncio.sleep(0.2)
        return await self.start()


MINECRAFT_APP_MANAGER = BundledMinecraftManager()


async def _panel_minecraft_manager_action(payload: dict) -> dict:
    action = str(payload.get("action") or "status").strip().lower()
    if action == "status":
        return {"ok": True, "manager": MINECRAFT_APP_MANAGER.status()}
    if action == "start":
        ok, message = await MINECRAFT_APP_MANAGER.start()
    elif action == "stop":
        ok, message = await MINECRAFT_APP_MANAGER.stop()
    elif action == "restart":
        ok, message = await MINECRAFT_APP_MANAGER.restart()
    elif action == "extract":
        try:
            root = _extract_minecraft_manager_bundle()
            ok, message = True, f"Minecraft manager extracted to {root}"
        except Exception as error:
            ok, message = False, f"Extraction failed: {type(error).__name__}: {error}"
    else:
        return {"ok": False, "message": f"Unknown Minecraft manager action {action!r}"}
    return {"ok": bool(ok), "message": message, "manager": MINECRAFT_APP_MANAGER.status()}


# ============================================================
# WEB HOSTING TUNNEL
# ============================================================

_proxy_thread_local = threading.local()
_CURRENT_HOST_ROUTE = contextvars.ContextVar("hostish_host_route", default=None)


def _host_only_for_route(value: str) -> str:
    host = (value or "").strip().lower().rstrip(".")
    if host.startswith("["):
        end = host.find("]")
        return host[: end + 1] if end >= 0 else host
    if ":" in host:
        host = host.split(":", 1)[0]
    return host


def _normalize_host_routes_for_auth() -> list:
    routes = HOST_ROUTES if isinstance(HOST_ROUTES, dict) else {}
    out = []
    for raw in routes.keys():
        host = str(raw or "").strip().lower().rstrip(".")
        if not host or len(host) > 253:
            continue
        if host.startswith("*."):
            suffix = _host_only_for_route(host[2:])
            host = "*." + suffix if suffix else ""
        else:
            host = _host_only_for_route(host)
        if host and host not in out:
            out.append(host)
    return out


def _host_route_for_headers(headers: dict):
    routes = HOST_ROUTES if isinstance(HOST_ROUTES, dict) else {}
    if not routes:
        return None
    host = ""
    for key, value in (headers or {}).items():
        if str(key).lower() == "host":
            host = _host_only_for_route(str(value))
            break
    if not host:
        return None

    selected = None
    selected_pattern = ""
    for raw_pattern, config in routes.items():
        pattern = str(raw_pattern or "").strip().lower().rstrip(".")
        if not pattern:
            continue
        if pattern.startswith("*."):
            suffix = _host_only_for_route(pattern[2:])
            if suffix and host.endswith("." + suffix) and host != suffix:
                if len(suffix) > len(selected_pattern):
                    selected = config
                    selected_pattern = suffix
            continue
        if _host_only_for_route(pattern) == host:
            selected = config
            break

    if selected is None:
        return None
    if isinstance(selected, str):
        return {"mode": "proxy", "upstream": selected, "matched_host": host}
    if not isinstance(selected, dict):
        return {"matched_host": host}
    result = dict(selected)
    result["matched_host"] = host
    return result


def _route_setting(name: str, default):
    route = _CURRENT_HOST_ROUTE.get()
    if not isinstance(route, dict):
        return default
    aliases = {
        "mode": ("mode",),
        "website_dir": ("website_dir", "root"),
        "upstream": ("upstream", "upstream_base"),
        "proxy_prefixes": ("proxy_prefixes",),
        "upstream_routes": ("upstream_routes",),
    }
    for key in aliases.get(name, (name,)):
        if key in route and route[key] is not None:
            return route[key]
    return default


def _effective_mode() -> str:
    return str(_route_setting("mode", MODE) or MODE).strip().lower()


def _effective_website_dir() -> str:
    return os.path.expanduser(str(_route_setting("website_dir", WEBSITE_DIR) or WEBSITE_DIR))


def _effective_upstream_base() -> str:
    return str(_route_setting("upstream", UPSTREAM_BASE) or UPSTREAM_BASE)


def _effective_proxy_prefixes():
    raw = _route_setting("proxy_prefixes", PROXY_PREFIXES)
    if not isinstance(raw, (list, tuple)):
        return PROXY_PREFIXES
    return [_norm_prefix(str(prefix)) for prefix in raw if str(prefix or "").strip()]


def _effective_upstream_routes():
    raw = _route_setting("upstream_routes", UPSTREAM_ROUTES)
    return raw if isinstance(raw, dict) else UPSTREAM_ROUTES


def safe_join(root: str, rel_path: str) -> str:
    rel_path = urllib.parse.unquote(rel_path or "")
    second = urllib.parse.unquote(rel_path)
    if second != rel_path:
        rel_path = second
    rel_path = rel_path.replace("\\", "/").lstrip("/")
    if "\x00" in rel_path:
        raise ValueError("NUL byte blocked")

    root_abs = os.path.abspath(root)
    joined_abs = os.path.abspath(os.path.join(root_abs, rel_path))
    try:
        if os.path.commonpath([root_abs, joined_abs]) != root_abs:
            raise ValueError("path traversal blocked")
    except ValueError:
        raise ValueError("path traversal blocked")

    if ALLOW_STATIC_SYMLINK_ESCAPE:
        return joined_abs

    root_real = os.path.realpath(root_abs)
    full_real = os.path.realpath(joined_abs)
    try:
        if os.path.commonpath([root_real, full_real]) != root_real:
            raise ValueError("symlink escape blocked")
    except ValueError:
        raise ValueError("symlink escape blocked")
    return full_real


def should_proxy(rel_path: str) -> bool:
    rel_path = (rel_path or "").lstrip("/").lower()
    mode = _effective_mode()
    if mode == "proxy":
        return True
    if mode == "static":
        return False
    for prefix in _effective_proxy_prefixes():
        normalized = prefix.lower().strip("/")
        if not normalized:
            continue
        if rel_path == normalized or rel_path.startswith(normalized + "/"):
            return True
    return False


def guess_content_type(path: str) -> str:
    import mimetypes
    content_type, _ = mimetypes.guess_type(path)
    return content_type or "application/octet-stream"


def _preview_bytes(data: bytes, length: int = 120) -> str:
    if not data:
        return ""
    sample = data[:length]
    text = sample.decode("utf-8", errors="replace").replace("\n", "\\n").replace("\r", "\\r")
    if len(data) > length:
        text += "..."
    return text


def _redacted_headers_for_log(headers: dict) -> dict:
    if PRINT_SENSITIVE_HEADERS:
        return dict(headers or {})
    sensitive = {str(name).lower() for name in (SENSITIVE_HEADER_NAMES or set())}
    result = {}
    for key, value in (headers or {}).items():
        result[str(key)] = "[REDACTED]" if str(key).lower() in sensitive else value
    return result


def _sensitive_static_path(rel_path: str) -> bool:
    mode = str(STATIC_SECURITY_MODE or "safe").strip().lower()
    if mode == "unrestricted":
        return False

    normalized = urllib.parse.unquote(rel_path or "").replace("\\", "/").lstrip("/").lower()
    if not normalized:
        return False

    def matches(patterns) -> bool:
        for pattern in patterns or []:
            p = str(pattern or "").replace("\\", "/").lstrip("/").lower()
            if p and fnmatch.fnmatch(normalized, p):
                return True
        return False

    if matches(STATIC_ALLOW_PATTERNS):
        return False
    if mode == "custom":
        return matches(STATIC_BLOCK_PATTERNS)
    if matches(STATIC_BLOCK_PATTERNS):
        return True
    for segment in [s for s in normalized.split("/") if s]:
        if segment in {".git", ".svn", ".hg", ".ssh", ".env"}:
            return True
        if segment.startswith(".env."):
            return True
    return False


class StaticRangeNotSatisfiable(Exception):
    pass


def _parse_single_byte_range(range_header: str, size: int):
    value = str(range_header or "").strip()
    if not value or not value.lower().startswith("bytes="):
        return None
    spec = value[6:].strip()
    if not spec or "," in spec:
        raise StaticRangeNotSatisfiable("multiple/invalid byte ranges are unsupported")
    if size <= 0 or "-" not in spec:
        raise StaticRangeNotSatisfiable("range cannot be satisfied")

    first, last = (part.strip() for part in spec.split("-", 1))
    try:
        if first == "":
            suffix = int(last)
            if suffix <= 0:
                raise ValueError
            if suffix >= size:
                return 0, size - 1
            return size - suffix, size - 1
        start = int(first)
        if start < 0 or start >= size:
            raise StaticRangeNotSatisfiable("range start is outside the file")
        if last == "":
            end = size - 1
        else:
            end = int(last)
            if end < start:
                raise StaticRangeNotSatisfiable("range end precedes range start")
            end = min(end, size - 1)
        return start, end
    except StaticRangeNotSatisfiable:
        raise
    except (TypeError, ValueError):
        raise StaticRangeNotSatisfiable("invalid byte range")


def _static_entity_headers(full_path: str, size: int) -> dict:
    import email.utils
    stat_result = os.stat(full_path)
    return {
        "content-type": guess_content_type(full_path),
        "cache-control": str(STATIC_CACHE_CONTROL or "no-store"),
        "accept-ranges": "bytes",
        "etag": f'"{int(stat_result.st_mtime_ns):x}-{int(size):x}"',
        "last-modified": email.utils.formatdate(stat_result.st_mtime, usegmt=True),
    }


def _should_honor_if_range(headers: dict, entity_headers: dict) -> bool:
    if_range = ""
    for key, value in (headers or {}).items():
        if str(key).lower() == "if-range":
            if_range = str(value).strip()
            break
    if not if_range:
        return True
    return if_range in {
        str(entity_headers.get("etag", "")),
        str(entity_headers.get("last-modified", "")),
    }


def _resolve_custom_404_full_path() -> Optional[str]:
    path = (CUSTOM_404_PATH or "").strip()
    if not path:
        return None
    if os.path.isabs(path):
        return os.path.abspath(path)
    return safe_join(_effective_website_dir(), path)


def _serve_custom_404_if_configured():
    if not CUSTOM_404_ENABLED:
        return None
    try:
        full_path = _resolve_custom_404_full_path()
    except Exception:
        return None
    if not full_path or not os.path.isfile(full_path):
        return None
    try:
        if _limit_exceeded(MAX_FILE_BYTES, os.path.getsize(full_path)):
            return None
        with open(full_path, "rb") as handle:
            body = handle.read()
        return (
            404,
            {"content-type": guess_content_type(full_path), "cache-control": "no-store"},
            body,
            {"source": "custom-404", "served_rel": CUSTOM_404_PATH,
             "full_path": full_path, "size": len(body)},
        )
    except Exception:
        return None


def _hostish_default_404_redirect():
    return (
        302,
        {"location": "https://hostish.site/404/", "cache-control": "no-store"},
        b"",
        {"source": "agent-404-redirect", "target": "https://hostish.site/404/"},
    )


# ------------------------------------------------------------
# Favicon bypass
# ------------------------------------------------------------

_favicon_cache = {"body": None, "content_type": "image/x-icon", "fetched": False}


def fetch_custom_favicon() -> Optional[bytes]:
    if not FAVICON_BYPASS_ENABLED or not FAVICON_BYPASS_URL:
        return None
    if _favicon_cache["fetched"] and _favicon_cache["body"] is not None:
        return _favicon_cache["body"]
    try:
        request = urllib.request.Request(
            FAVICON_BYPASS_URL, headers={"User-Agent": f"HostishAgent/{version}"}, method="GET")
        with urllib.request.urlopen(request, timeout=10, context=make_ssl_context()) as response:
            body = response.read(max(1, int(FAVICON_MAX_BYTES)) + 1)
            if len(body) > max(1, int(FAVICON_MAX_BYTES)):
                raise ValueError("custom favicon exceeds FAVICON_MAX_BYTES")
            _favicon_cache["body"] = body
            _favicon_cache["content_type"] = response.headers.get("Content-Type", "image/x-icon")
        _favicon_cache["fetched"] = True
        log(f"Downloaded and cached custom favicon from {FAVICON_BYPASS_URL}", "web")
        return _favicon_cache["body"]
    except Exception as error:
        log_repeatable_error("favicon_fetch", f"Failed to retrieve custom favicon: {error}", "web")
        return None


def _is_favicon_request(rel_path: str) -> bool:
    normalized = (rel_path or "").lstrip("/").lower()
    return FAVICON_BYPASS_ENABLED and normalized in {"favicon.ico", "favicon.ico/"}


# ------------------------------------------------------------
# Public-IP masking
# ------------------------------------------------------------

PUBLIC_IP_CHECK_URLS = [
    "https://api.ipify.org", "https://checkip.amazonaws.com", "https://ipv4.icanhazip.com",
]
_public_ip_cache = {"last_fetch_ts": 0.0, "ip": None, "error": None}


def _normalize_ip_text(raw: str) -> Optional[str]:
    value = (raw or "").strip()
    if not value:
        return None
    value = value.splitlines()[0].strip().split()[0].strip()
    try:
        ipaddress.ip_address(value)
        return value
    except Exception:
        return None


def fetch_public_ip(force: bool = False) -> Optional[str]:
    now_ts = time.time()
    age = now_ts - float(_public_ip_cache.get("last_fetch_ts", 0.0) or 0.0)
    if not force and age < int(IP_PROTECTION_REFRESH_SECONDS) and _public_ip_cache.get("ip"):
        return _public_ip_cache.get("ip")

    last_error = None
    for url in PUBLIC_IP_CHECK_URLS:
        try:
            request = urllib.request.Request(
                url, headers={"User-Agent": f"HostishAgent/{version}"}, method="GET")
            with urllib.request.urlopen(request, timeout=8, context=make_ssl_context()) as response:
                raw = response.read(256).decode("utf-8", errors="replace")
            public_ip = _normalize_ip_text(raw)
            if not public_ip:
                raise ValueError("invalid IP response")
            _public_ip_cache["last_fetch_ts"] = now_ts
            _public_ip_cache["ip"] = public_ip
            _public_ip_cache["error"] = None
            return public_ip
        except Exception as error:
            last_error = f"{type(error).__name__}: {error}"

    _public_ip_cache["last_fetch_ts"] = now_ts
    _public_ip_cache["error"] = last_error
    return _public_ip_cache.get("ip")


def _mask_bytes(data: bytes, public_ip: str):
    if not data or not public_ip:
        return data, 0
    source = public_ip.encode("utf-8")
    count = data.count(source)
    if count <= 0:
        return data, 0
    replacement = IP_PROTECTION_REPLACEMENT.ljust(len(public_ip)).encode("utf-8")
    return data.replace(source, replacement), count


def _mask_headers(headers: dict, public_ip: str):
    if not headers or not public_ip:
        return headers or {}, 0
    replaced = 0
    output = {}
    replacement = IP_PROTECTION_REPLACEMENT.ljust(len(public_ip))
    for key, value in (headers or {}).items():
        value_string = str(value)
        count = value_string.count(public_ip)
        if count > 0:
            value_string = value_string.replace(public_ip, replacement)
            replaced += count
        output[str(key)] = value_string
    return output, replaced


def _apply_ip_protection_to_response(status, headers, body, meta):
    if not IP_PROTECTION_ENABLED:
        return status, headers, body, meta
    public_ip = fetch_public_ip(force=False)
    if not public_ip:
        return status, headers, body, meta

    content_type = ""
    for key, value in (headers or {}).items():
        if str(key).lower() == "content-type":
            content_type = str(value).split(";", 1)[0].strip().lower()
            break
    textual = (content_type.startswith("text/") or content_type in {
        "application/json", "application/ld+json", "application/javascript",
        "application/x-javascript", "application/xml", "application/xhtml+xml"}
        or content_type.endswith("+json") or content_type.endswith("+xml"))

    new_headers, header_hits = _mask_headers(headers or {}, public_ip)
    new_body, body_hits = _mask_bytes(body or b"", public_ip) if textual else (body or b"", 0)

    if header_hits + body_hits <= 0:
        return status, new_headers, new_body, meta
    for key in list(new_headers.keys()):
        if key.lower() == "content-length":
            new_headers[key] = str(len(new_body))
    return status, new_headers, new_body, meta


def _response_is_textual(headers: dict) -> bool:
    content_type = ""
    for key, value in (headers or {}).items():
        if str(key).lower() == "content-type":
            content_type = str(value).split(";", 1)[0].strip().lower()
            break
    return (content_type.startswith("text/") or content_type in {
        "application/json", "application/ld+json", "application/javascript",
        "application/x-javascript", "application/xml", "application/xhtml+xml"}
        or content_type.endswith("+json") or content_type.endswith("+xml"))


async def _mask_stream_chunks(chunks, headers: dict):
    """Length-preserving streaming IP replacement, including chunk boundaries."""
    if not IP_PROTECTION_ENABLED or not _response_is_textual(headers):
        async for chunk in chunks:
            yield chunk
        return

    public_ip = await asyncio.to_thread(fetch_public_ip, False)
    if not public_ip:
        async for chunk in chunks:
            yield chunk
        return

    needle = public_ip.encode("utf-8")
    replacement = IP_PROTECTION_REPLACEMENT.ljust(len(public_ip)).encode("utf-8")
    keep = max(0, len(needle) - 1)
    carry = b""
    async for chunk in chunks:
        data = carry + (chunk or b"")
        if keep and len(data) > keep:
            emit, carry = data[:-keep], data[-keep:]
        elif keep:
            carry = data
            continue
        else:
            emit, carry = data, b""
        yield emit.replace(needle, replacement)
    if carry:
        yield carry.replace(needle, replacement)


def read_static_file(rel_path: str, method: str = "GET", request_headers: Optional[dict] = None):
    requested_rel = rel_path
    request_headers = dict(request_headers or {})
    method_upper = str(method or "GET").upper()
    text_headers = {"content-type": "text/plain; charset=utf-8"}

    if _sensitive_static_path(rel_path):
        return 404, dict(text_headers, **{"cache-control": "no-store"}), b"Not Found", {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": None, "error": "sensitive static path blocked"}

    if rel_path == "" or rel_path.endswith("/"):
        rel_path += "index.html"

    try:
        full_path = safe_join(_effective_website_dir(), rel_path)
    except ValueError:
        return 403, dict(text_headers), (b"" if method_upper == "HEAD" else b"Forbidden"), {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": None, "error": "path traversal blocked"}

    if not os.path.isfile(full_path):
        return 404, dict(text_headers), (b"" if method_upper == "HEAD" else b"Not Found"), {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": full_path, "error": "not found"}

    size = os.path.getsize(full_path)
    if _limit_exceeded(MAX_FILE_BYTES, size):
        return 413, dict(text_headers), (b"" if method_upper == "HEAD" else b"File too large"), {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": full_path, "error": "too large"}

    output_headers = _static_entity_headers(full_path, size)
    range_header = ""
    for key, value in request_headers.items():
        if str(key).lower() == "range":
            range_header = str(value)
            break

    byte_range = None
    if method_upper in {"GET", "HEAD"} and range_header and _should_honor_if_range(request_headers, output_headers):
        try:
            byte_range = _parse_single_byte_range(range_header, size)
        except StaticRangeNotSatisfiable as error:
            output_headers["content-range"] = f"bytes */{size}"
            output_headers["content-length"] = "0"
            return 416, output_headers, b"", {
                "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
                "full_path": full_path, "error": str(error), "size": size}

    if byte_range is not None:
        start, end = byte_range
        length = end - start + 1
        output_headers["content-range"] = f"bytes {start}-{end}/{size}"
        output_headers["content-length"] = str(length)
        if method_upper == "HEAD":
            body = b""
        else:
            with open(full_path, "rb") as handle:
                handle.seek(start)
                body = handle.read(length)
        status = 206
    else:
        output_headers["content-length"] = str(size)
        if method_upper == "HEAD":
            body = b""
        else:
            with open(full_path, "rb") as handle:
                body = handle.read()
        status = 200

    return status, output_headers, body, {
        "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
        "full_path": full_path, "size": size, "range": byte_range}


def _select_upstream(rel_path: str) -> str:
    normalized_rel = (rel_path or "").lstrip("/").lower()
    best_target = _effective_upstream_base()
    best_len = -1
    for prefix, target in (_effective_upstream_routes() or {}).items():
        normalized_prefix = str(prefix or "").lstrip("/").lower().rstrip("/")
        if not normalized_prefix:
            continue
        if normalized_rel == normalized_prefix or normalized_rel.startswith(normalized_prefix + "/"):
            if len(normalized_prefix) > best_len:
                best_len = len(normalized_prefix)
                best_target = str(target)
    return best_target


def parse_upstream_base(upstream: str):
    parsed = urllib.parse.urlparse(upstream)
    if parsed.scheme not in ("http", "https"):
        raise ValueError("upstream must start with http:// or https://")
    host = parsed.hostname or "127.0.0.1"
    port = parsed.port or (443 if parsed.scheme == "https" else 80)
    base_path = parsed.path or ""
    if base_path.endswith("/"):
        base_path = base_path[:-1]
    return parsed.scheme, host, port, base_path


def _upstream_host_allowed(host: str) -> bool:
    policy = str(UPSTREAM_NETWORK_POLICY or "loopback").strip().lower()
    if policy == "any":
        return True
    if policy not in {"loopback", "private"}:
        return False

    host_text = (host or "").strip().strip("[]")
    if not host_text:
        return False

    addresses = []
    try:
        addresses.append(ipaddress.ip_address(host_text))
    except ValueError:
        try:
            infos = socket.getaddrinfo(host_text, None, type=socket.SOCK_STREAM)
        except OSError:
            return False
        seen = set()
        for info in infos:
            try:
                address = ipaddress.ip_address(info[4][0])
            except ValueError:
                continue
            if address not in seen:
                seen.add(address)
                addresses.append(address)

    if not addresses:
        return False

    def allowed(address):
        if address.is_loopback:
            return True
        if policy == "private":
            return bool(address.is_private or address.is_link_local)
        return False

    return all(allowed(address) for address in addresses)


def _proxy_cache() -> dict:
    cache = getattr(_proxy_thread_local, "connections", None)
    if cache is None:
        cache = {}
        _proxy_thread_local.connections = cache
    return cache


def _new_proxy_connection(scheme: str, host: str, port: int):
    import http.client
    timeout = None if float(UPSTREAM_CONNECT_TIMEOUT_SECONDS) <= 0 else float(UPSTREAM_CONNECT_TIMEOUT_SECONDS)
    if scheme == "https":
        return http.client.HTTPSConnection(host, port, timeout=timeout)
    return http.client.HTTPConnection(host, port, timeout=timeout)


def _get_proxy_connection(scheme: str, host: str, port: int):
    key = (scheme, host, port)
    if not PROXY_KEEPALIVE_ENABLED:
        return key, _new_proxy_connection(scheme, host, port)

    cache = _proxy_cache()
    now = time.time()
    entry = cache.get(key)
    connection = None
    if isinstance(entry, tuple) and len(entry) == 2:
        candidate, last_used = entry
        if float(PROXY_IDLE_CONNECTION_SECONDS) > 0 and now - float(last_used) > float(PROXY_IDLE_CONNECTION_SECONDS):
            try:
                candidate.close()
            except Exception:
                pass
            cache.pop(key, None)
        else:
            connection = candidate

    if connection is None:
        connection = _new_proxy_connection(scheme, host, port)
    cache[key] = (connection, now)

    max_idle = int(PROXY_MAX_IDLE_CONNECTIONS)
    if max_idle > 0 and len(cache) > max_idle:
        ordered = sorted(cache.items(), key=lambda item: item[1][1] if isinstance(item[1], tuple) else 0)
        for stale_key, stale_entry in ordered[: max(0, len(cache) - max_idle)]:
            if stale_key == key:
                continue
            cache.pop(stale_key, None)
            try:
                (stale_entry[0] if isinstance(stale_entry, tuple) else stale_entry).close()
            except Exception:
                pass
    return key, connection


def _drop_proxy_connection(key, connection=None) -> None:
    cached = _proxy_cache().pop(key, None)
    if isinstance(cached, tuple):
        cached = cached[0]
    target = cached if cached is not None else connection
    if target is not None:
        try:
            target.close()
        except Exception:
            pass


def proxy_request(method: str, rel_path: str, query: str, headers: dict, body: bytes):
    upstream_url = _select_upstream(rel_path)
    scheme, host, port, base_path = parse_upstream_base(upstream_url)

    if not _upstream_host_allowed(host):
        return 400, {"content-type": "text/plain; charset=utf-8"}, \
            b"UPSTREAM blocked by network policy", {
                "source": "proxy",
                "error": f"blocked by UPSTREAM_NETWORK_POLICY={UPSTREAM_NETWORK_POLICY!r}",
                "upstream": f"{scheme}://{host}:{port}"}

    encoded_rel = urllib.parse.quote((rel_path or "").lstrip("/"), safe="/%:@-._~!$&'()*+,;=")
    clean_query = (query or "").replace("\r", "").replace("\n", "")
    path = "/" + encoded_rel
    if base_path:
        path = base_path + path
    if clean_query:
        path += "?" + clean_query

    output_headers = {}
    for key, value in (headers or {}).items():
        lower_key = str(key).lower()
        if lower_key in HOP_BY_HOP or lower_key in {"host", "content-length"}:
            continue
        safe_key = str(key).replace("\r", "").replace("\n", "")
        if safe_key:
            output_headers[safe_key] = str(value).replace("\r", "").replace("\n", "")

    host_header = host
    if (scheme == "http" and port != 80) or (scheme == "https" and port != 443):
        host_header = f"{host}:{port}"
    output_headers["Host"] = host_header
    output_headers["Content-Length"] = str(len(body))

    key, connection = _get_proxy_connection(scheme, host, port)
    reusable = PROXY_KEEPALIVE_ENABLED

    try:
        connection.request(method, path, body=body, headers=output_headers)
        if connection.sock is not None:
            connection.sock.settimeout(
                None if float(UPSTREAM_RESPONSE_TIMEOUT_SECONDS) <= 0
                else float(UPSTREAM_RESPONSE_TIMEOUT_SECONDS))
        response = connection.getresponse()
        status = int(response.status)

        chunks = []
        total = 0
        read_size = max(16 * 1024, int(PROXY_READ_CHUNK_BYTES))
        while True:
            chunk = response.read(read_size)
            if not chunk:
                break
            total += len(chunk)
            if _limit_exceeded(MAX_PROXY_BYTES, total):
                _drop_proxy_connection(key, connection)
                return 502, {"content-type": "text/plain; charset=utf-8"}, \
                    b"Upstream response too large", {
                        "source": "proxy", "error": "response too large",
                        "upstream": upstream_url, "upstream_path": path, "status": status}
            chunks.append(chunk)

        response_body = b"".join(chunks)
        response_headers = {}
        for header_key, header_value in response.getheaders():
            if header_key.lower() in HOP_BY_HOP:
                continue
            response_headers[header_key] = header_value

        if getattr(response, "will_close", False):
            reusable = False
        if not reusable:
            _drop_proxy_connection(key, connection)
        elif PROXY_KEEPALIVE_ENABLED:
            _proxy_cache()[key] = (connection, time.time())

        return status, response_headers, response_body, {
            "source": "proxy", "upstream": upstream_url, "upstream_path": path,
            "status": status, "resp_len": len(response_body)}
    except Exception as error:
        _drop_proxy_connection(key, connection)
        log_repeatable_error(
            "proxy_request",
            f"[PROXY] Failed upstream='{upstream_url}' path='{path}' "
            f"error={type(error).__name__}: {error}", "web")
        return 502, {"content-type": "text/plain; charset=utf-8",
                     "access-control-allow-origin": "*"}, \
            f"Upstream error: {type(error).__name__}: {error}".encode("utf-8", "replace"), {
                "source": "proxy", "error": f"{type(error).__name__}: {error}",
                "upstream": upstream_url, "upstream_path": path}


def _build_tunnel_response(req_id, status, headers, body, encode_body: bool) -> dict:
    response = {"type": "resp", "id": req_id, "status": int(status),
                "headers": dict(headers or {})}
    if encode_body:
        response["body_b64"] = base64.b64encode(body or b"").decode("ascii")
    else:
        response["_body_raw"] = body or b""
    return response


def _status_count_inc(code: int) -> None:
    counts = STATS["web"]["statuses"]
    key = str(int(code))
    counts[key] = counts.get(key, 0) + 1


async def handle_req(msg: dict, encode_body: bool = True) -> dict:
    """Protocol v1/v2 buffered request handler."""
    req_id = msg.get("id")
    method = msg.get("method", "GET")
    rel_path = msg.get("path", "")
    query = msg.get("query", "")
    headers = msg.get("headers", {}) or {}
    body_base64 = msg.get("body_b64", "") or ""
    route_token = _CURRENT_HOST_ROUTE.set(_host_route_for_headers(headers))

    if str(method).upper() == "OPTIONS" and HANDLE_OPTIONS_LOCALLY:
        _CURRENT_HOST_ROUTE.reset(route_token)
        return _build_tunnel_response(req_id, 200, {
            "access-control-allow-origin": str(CORS_ALLOW_ORIGIN),
            "access-control-allow-methods": str(CORS_ALLOW_METHODS),
            "access-control-allow-headers": str(CORS_ALLOW_HEADERS),
            "access-control-max-age": "86400"}, b"", encode_body)

    request_start = time.time()
    STATS["web"]["inflight"] += 1

    try:
        try:
            body = base64.b64decode(body_base64.encode("ascii")) if body_base64 else b""
        except Exception:
            body = b""

        if _limit_exceeded(MAX_REQUEST_BODY_BYTES, len(body)):
            return _build_tunnel_response(
                req_id, 413, {"content-type": "text/plain; charset=utf-8"},
                b"Request body exceeds this agent's configured limit", encode_body)

        portable_route = _portable_mail_api_route(rel_path)
        if portable_route is not None:
            status, output_body, content_type, output_headers = await _portable_mail_dispatch(
                method, portable_route, headers, query, body)
            encoded_body = _portable_mail_encode_body(output_body)
            output_headers = dict(output_headers or {})
            output_headers.setdefault("content-type", content_type)
            STATS["web"]["requests"] += 1
            STATS["web"]["bytes"] += len(encoded_body)
            _status_count_inc(status)
            return _build_tunnel_response(req_id, status, output_headers, encoded_body, encode_body)

        if not WEB_ENABLED:
            return _build_tunnel_response(
                req_id, 404, {"content-type": "text/plain; charset=utf-8"},
                b"Website hosting is disabled", encode_body)

        route_mode = "custom-favicon" if _is_favicon_request(rel_path) else \
            ("proxy" if should_proxy(rel_path) else "static")
        log(f"[REQ] START | {req_id} | {method} /{rel_path}"
            + (f"?{query}" if query else "") + f" | {route_mode}", "web")

        if PRINT_REQUEST_HEADERS and headers:
            log("   -> headers: " + _json(_redacted_headers_for_log(headers)), "web")
        if PRINT_REQUEST_BODY_PREVIEW and body:
            log("   -> body: " + _preview_bytes(body, int(BODY_PREVIEW_BYTES)), "web")

        if route_mode == "custom-favicon":
            favicon_body = await run_in_thread(fetch_custom_favicon)
            if favicon_body is not None:
                status, output_body = 200, favicon_body
                output_headers = {"content-type": _favicon_cache.get("content_type", "image/x-icon"),
                                  "cache-control": "public, max-age=86400"}
                meta = {"source": "custom-favicon"}
            else:
                route_mode = "proxy" if should_proxy(rel_path) else "static"

        if route_mode == "proxy":
            status, output_headers, output_body, meta = await run_in_thread(
                proxy_request, method, rel_path, query, headers, body)
        elif route_mode == "static":
            status, output_headers, output_body, meta = await run_in_thread(
                read_static_file, rel_path, method, headers)

        if status == 404 and meta.get("source") not in {"agent-404-redirect"}:
            custom = await run_in_thread(_serve_custom_404_if_configured) if CUSTOM_404_ENABLED else None
            if custom is not None:
                status, output_headers, output_body, meta = custom
            elif not CUSTOM_404_ENABLED:
                status, output_headers, output_body, meta = _hostish_default_404_redirect()

        status, output_headers, output_body, meta = await run_in_thread(
            _apply_ip_protection_to_response, status, output_headers, output_body, meta)

        output_headers = dict(output_headers or {})
        if ADD_CORS_HEADER_TO_RESPONSES:
            output_headers.setdefault("access-control-allow-origin", str(CORS_ALLOW_ORIGIN))

        elapsed_ms = int((time.time() - request_start) * 1000)
        STATS["web"]["requests"] += 1
        STATS["web"]["bytes"] += len(output_body)
        _status_count_inc(status)
        if route_mode == "proxy":
            STATS["web"]["proxy"] += 1
        elif route_mode == "static":
            STATS["web"]["static"] += 1

        log(f"[REQ] DONE | {req_id} | {status} | {fmt_bytes(len(output_body))} | {elapsed_ms}ms", "web")
        return _build_tunnel_response(req_id, status, output_headers, output_body, encode_body)

    except Exception as error:
        log_repeatable_error("handle_req",
                             f"[REQ] ERROR | {req_id} | {type(error).__name__}: {error}", "web")
        return _build_tunnel_response(
            req_id, 500, {"content-type": "text/plain; charset=utf-8"},
            b"Internal agent error", encode_body)
    finally:
        STATS["web"]["inflight"] = max(0, STATS["web"]["inflight"] - 1)
        _CURRENT_HOST_ROUTE.reset(route_token)


# ------------------------------------------------------------
# Protocol v3: full request/response streaming
# ------------------------------------------------------------

class RequestBodyLimitExceeded(Exception):
    pass


class V3ResponseAborted(Exception):
    pass


class ProxyTargetBlocked(Exception):
    pass


def _prepare_static_stream(rel_path: str, method: str = "GET", request_headers: Optional[dict] = None):
    requested_rel = rel_path
    request_headers = dict(request_headers or {})
    method_upper = str(method or "GET").upper()
    text_headers = {"content-type": "text/plain; charset=utf-8"}

    if _sensitive_static_path(rel_path):
        return 404, dict(text_headers), None, {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": None, "error": "sensitive static path blocked"}

    if rel_path == "" or rel_path.endswith("/"):
        rel_path += "index.html"
    try:
        full_path = safe_join(_effective_website_dir(), rel_path)
    except ValueError as error:
        return 403, dict(text_headers), None, {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": None, "error": str(error)}
    if not os.path.isfile(full_path):
        return 404, dict(text_headers), None, {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": full_path, "error": "not found"}

    size = os.path.getsize(full_path)
    if _limit_exceeded(MAX_FILE_BYTES, size):
        return 413, dict(text_headers), None, {
            "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
            "full_path": full_path, "error": "file exceeds configured limit", "size": size}

    output_headers = _static_entity_headers(full_path, size)
    range_header = ""
    for key, value in request_headers.items():
        if str(key).lower() == "range":
            range_header = str(value)
            break

    byte_range = None
    if method_upper in {"GET", "HEAD"} and range_header and _should_honor_if_range(request_headers, output_headers):
        try:
            byte_range = _parse_single_byte_range(range_header, size)
        except StaticRangeNotSatisfiable as error:
            output_headers["content-range"] = f"bytes */{size}"
            output_headers["content-length"] = "0"
            return 416, output_headers, None, {
                "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
                "full_path": full_path, "error": str(error), "size": size, "head_only": True}

    if byte_range is not None:
        start, end = byte_range
        length = end - start + 1
        output_headers["content-range"] = f"bytes {start}-{end}/{size}"
        output_headers["content-length"] = str(length)
        status = 206
    else:
        start = 0
        length = size
        output_headers["content-length"] = str(size)
        status = 200

    return status, output_headers, full_path, {
        "source": "static", "served_rel": rel_path, "requested_rel": requested_rel,
        "full_path": full_path, "size": size, "range": byte_range,
        "range_start": start, "range_length": length,
        "head_only": method_upper == "HEAD"}


async def _file_chunks(full_path: str, start: int = 0, length: Optional[int] = None):
    handle = await asyncio.to_thread(open, full_path, "rb")
    try:
        if int(start or 0) > 0:
            await asyncio.to_thread(handle.seek, int(start))
        remaining = None if length is None else max(0, int(length))
        read_size = max(16 * 1024, int(STATIC_READ_CHUNK_BYTES))
        while remaining is None or remaining > 0:
            amount = read_size if remaining is None else min(read_size, remaining)
            chunk = await asyncio.to_thread(handle.read, amount)
            if not chunk:
                break
            if remaining is not None:
                remaining -= len(chunk)
            yield chunk
    finally:
        await asyncio.to_thread(handle.close)


async def _single_body_chunks(body: bytes):
    if body:
        yield body


def _proxy_target_details(rel_path: str, query: str, headers: dict):
    upstream_url = _select_upstream(rel_path)
    scheme, host, port, base_path = parse_upstream_base(upstream_url)
    if not _upstream_host_allowed(host):
        raise ProxyTargetBlocked(
            f"UPSTREAM {host!r} blocked by UPSTREAM_NETWORK_POLICY={UPSTREAM_NETWORK_POLICY!r}")

    encoded_rel = urllib.parse.quote((rel_path or "").lstrip("/"), safe="/%:@-._~!$&'()*+,;=")
    clean_query = (query or "").replace("\r", "").replace("\n", "")
    path = "/" + encoded_rel
    if base_path:
        path = base_path + path
    if clean_query:
        path += "?" + clean_query

    output_headers = {}
    for key, value in (headers or {}).items():
        lower = str(key).lower()
        if lower in HOP_BY_HOP or lower == "host":
            continue
        safe_key = str(key).replace("\r", "").replace("\n", "")
        if safe_key:
            output_headers[safe_key] = str(value).replace("\r", "").replace("\n", "")

    host_header = f"[{host}]" if ":" in host and not host.startswith("[") else host
    if (scheme == "http" and port != 80) or (scheme == "https" and port != 443):
        host_header = f"{host_header}:{port}"
    output_headers["Host"] = host_header
    return upstream_url, scheme, host, port, path, output_headers


def _new_stream_proxy_connection(scheme: str, host: str, port: int):
    import http.client
    timeout = None if float(UPSTREAM_CONNECT_TIMEOUT_SECONDS) <= 0 else float(UPSTREAM_CONNECT_TIMEOUT_SECONDS)
    if scheme == "https":
        return http.client.HTTPSConnection(host, port, timeout=timeout)
    return http.client.HTTPConnection(host, port, timeout=timeout)


async def _proxy_stream_v3(method: str, rel_path: str, query: str, headers: dict, body_chunks):
    upstream_url, scheme, host, port, path, output_headers = _proxy_target_details(
        rel_path, query, headers)

    content_length = None
    for key, value in output_headers.items():
        if str(key).lower() == "content-length":
            try:
                content_length = max(0, int(value))
            except (TypeError, ValueError):
                content_length = None
            break
    if content_length is not None and _limit_exceeded(MAX_REQUEST_BODY_BYTES, content_length):
        raise RequestBodyLimitExceeded("request body exceeds this agent's configured limit")

    connection = _new_stream_proxy_connection(scheme, host, port)
    total_in = 0
    total_out = 0
    chunked_request = content_length is None and str(method).upper() not in {"GET", "HEAD"}

    def begin_request():
        connection.putrequest(str(method).upper(), path, skip_host=True, skip_accept_encoding=True)
        for key, value in output_headers.items():
            lower = str(key).lower()
            if lower in HOP_BY_HOP:
                continue
            if lower == "content-length" and chunked_request:
                continue
            connection.putheader(str(key), str(value))
        if chunked_request:
            connection.putheader("Transfer-Encoding", "chunked")
        connection.endheaders()
        if connection.sock is not None:
            connection.sock.settimeout(
                None if float(UPSTREAM_RESPONSE_TIMEOUT_SECONDS) <= 0
                else float(UPSTREAM_RESPONSE_TIMEOUT_SECONDS))

    try:
        await asyncio.to_thread(begin_request)
        async for chunk in body_chunks:
            if not chunk:
                continue
            total_in += len(chunk)
            if _limit_exceeded(MAX_REQUEST_BODY_BYTES, total_in):
                raise RequestBodyLimitExceeded("request body exceeds this agent's configured limit")
            if chunked_request:
                framed = f"{len(chunk):X}\r\n".encode("ascii") + chunk + b"\r\n"
                await asyncio.to_thread(connection.send, framed)
            else:
                await asyncio.to_thread(connection.send, chunk)
        if chunked_request:
            await asyncio.to_thread(connection.send, b"0\r\n\r\n")

        response = await asyncio.to_thread(connection.getresponse)
        status = int(response.status)
        response_headers = {}
        for key, value in response.getheaders():
            if key.lower() in HOP_BY_HOP:
                continue
            response_headers[key] = value

        async def response_chunks():
            nonlocal total_out
            read_size = max(16 * 1024, int(PROXY_READ_CHUNK_BYTES))
            try:
                while True:
                    chunk = await asyncio.to_thread(response.read, read_size)
                    if not chunk:
                        break
                    total_out += len(chunk)
                    if _limit_exceeded(MAX_PROXY_BYTES, total_out):
                        raise RuntimeError("upstream response exceeds configured limit")
                    yield chunk
            finally:
                try:
                    await asyncio.to_thread(connection.close)
                except Exception:
                    pass

        return status, response_headers, response_chunks(), {
            "source": "proxy", "upstream": upstream_url, "upstream_path": path,
            "request_bytes": total_in}
    except Exception:
        try:
            await asyncio.to_thread(connection.close)
        except Exception:
            pass
        raise


async def _send_v3_response(request_id, status, headers, chunks,
                            send_text_locked, send_bytes_locked, wait_response_credit):
    output_headers = dict(headers or {})
    if ADD_CORS_HEADER_TO_RESPONSES:
        output_headers.setdefault("access-control-allow-origin", str(CORS_ALLOW_ORIGIN))
    if IP_PROTECTION_ENABLED:
        public_ip = await asyncio.to_thread(fetch_public_ip, False)
        if public_ip:
            output_headers, _hits = _mask_headers(output_headers, public_ip)

    await send_text_locked({"type": "resp_start", "id": request_id,
                            "status": int(status), "headers": output_headers})

    prefix = b"R3" + bytes.fromhex(request_id)
    total = 0
    chunk_size = max(16 * 1024, int(TUNNEL_CHUNK_BYTES))
    try:
        masked = _mask_stream_chunks(chunks, output_headers)
        async for original in masked:
            if not original:
                continue
            view = memoryview(original)
            for offset in range(0, len(view), chunk_size):
                chunk = bytes(view[offset: offset + chunk_size])
                await wait_response_credit(request_id)
                await send_bytes_locked(prefix + chunk)
                total += len(chunk)
                STATS["web"]["bytes"] += len(chunk)
    except asyncio.CancelledError:
        raise
    except Exception as error:
        try:
            await send_text_locked({"type": "resp_abort", "id": request_id,
                                    "error": f"{type(error).__name__}: {error}"[:512]})
        except Exception:
            pass
        raise V3ResponseAborted(str(error)) from error

    await send_text_locked({"type": "resp_end", "id": request_id})
    return total


async def handle_req_v3(msg, body_chunks, send_text_locked, send_bytes_locked, wait_response_credit):
    request_id = str(msg.get("id", ""))
    method = str(msg.get("method", "GET")).upper()
    rel_path = str(msg.get("path", ""))
    query = str(msg.get("query", ""))
    headers = dict(msg.get("headers", {}) or {})
    request_start = time.time()
    STATS["web"]["inflight"] += 1
    route_token = _CURRENT_HOST_ROUTE.set(_host_route_for_headers(headers))

    try:
        portable_route = _portable_mail_api_route(rel_path)
        if portable_route is not None:
            body = bytearray()
            async for chunk in body_chunks:
                if chunk:
                    body.extend(chunk)
                    if len(body) > PORTABLE_MAIL_MAX_JSON_BYTES:
                        raise RequestBodyLimitExceeded("portable mail request body is too large")
            status, response_body, content_type, output_headers = await _portable_mail_dispatch(
                method, portable_route, headers, query, bytes(body))
            encoded_body = _portable_mail_encode_body(response_body)
            output_headers = dict(output_headers or {})
            output_headers.setdefault("content-type", content_type)
            total = await _send_v3_response(
                request_id, status, output_headers, _single_body_chunks(encoded_body),
                send_text_locked, send_bytes_locked, wait_response_credit)
            STATS["web"]["requests"] += 1
            STATS["web"]["bytes"] += len(encoded_body)
            _status_count_inc(status)
            elapsed_ms = int((time.time() - request_start) * 1000)
            log(f"[REQ] DONE | {request_id} | {status} | {fmt_bytes(total)} | {elapsed_ms}ms | portable-mail", "web")
            return

        if not WEB_ENABLED:
            await _send_v3_response(
                request_id, 404, {"content-type": "text/plain; charset=utf-8"},
                _single_body_chunks(b"Website hosting is disabled"),
                send_text_locked, send_bytes_locked, wait_response_credit)
            STATS["web"]["requests"] += 1
            _status_count_inc(404)
            return

        route_mode = "custom-favicon" if _is_favicon_request(rel_path) else \
            ("proxy" if should_proxy(rel_path) else "static")
        log(f"[REQ] START | {request_id} | {method} /{rel_path}"
            + (f"?{query}" if query else "") + f" | {route_mode}", "web")
        if PRINT_REQUEST_HEADERS and headers:
            log("   -> headers: " + _json(_redacted_headers_for_log(headers)), "web")

        if method == "OPTIONS" and HANDLE_OPTIONS_LOCALLY:
            status = 200
            output_headers = {
                "access-control-allow-origin": str(CORS_ALLOW_ORIGIN),
                "access-control-allow-methods": str(CORS_ALLOW_METHODS),
                "access-control-allow-headers": str(CORS_ALLOW_HEADERS),
                "access-control-max-age": "86400"}
            chunks = _single_body_chunks(b"")
            meta = {"source": "options"}
        else:
            if route_mode == "custom-favicon":
                favicon_body = await asyncio.to_thread(fetch_custom_favicon)
                if favicon_body is not None:
                    status = 200
                    output_headers = {"content-type": _favicon_cache.get("content_type", "image/x-icon"),
                                      "cache-control": "public, max-age=86400",
                                      "content-length": str(len(favicon_body))}
                    chunks = _single_body_chunks(b"" if method == "HEAD" else favicon_body)
                    meta = {"source": "custom-favicon"}
                else:
                    route_mode = "proxy" if should_proxy(rel_path) else "static"

            if route_mode == "proxy":
                status, output_headers, chunks, meta = await _proxy_stream_v3(
                    method, rel_path, query, headers, body_chunks)
            elif route_mode == "static":
                status, output_headers, full_path, meta = await asyncio.to_thread(
                    _prepare_static_stream, rel_path, method, headers)
                if full_path and not meta.get("head_only"):
                    chunks = _file_chunks(full_path, meta.get("range_start", 0), meta.get("range_length"))
                else:
                    chunks = _single_body_chunks(
                        b"" if meta.get("head_only") else (b"Forbidden" if status == 403 else b"Not Found"))

        if status == 404 and meta.get("source") != "agent-404-redirect":
            if CUSTOM_404_ENABLED:
                custom = await asyncio.to_thread(_serve_custom_404_if_configured)
                if custom is not None:
                    status, output_headers, output_body, meta = custom
                    chunks = _single_body_chunks(output_body)
            else:
                status, output_headers, output_body, meta = _hostish_default_404_redirect()
                chunks = _single_body_chunks(output_body)

        total = await _send_v3_response(request_id, status, output_headers, chunks,
                                        send_text_locked, send_bytes_locked, wait_response_credit)
        STATS["web"]["requests"] += 1
        _status_count_inc(status)
        if route_mode == "proxy":
            STATS["web"]["proxy"] += 1
        elif route_mode == "static":
            STATS["web"]["static"] += 1

        elapsed_ms = int((time.time() - request_start) * 1000)
        log(f"[REQ] DONE | {request_id} | {status} | {fmt_bytes(total)} | {elapsed_ms}ms", "web")

    except RequestBodyLimitExceeded as error:
        await _send_v3_response(request_id, 413, {"content-type": "text/plain; charset=utf-8"},
                                _single_body_chunks(str(error).encode()),
                                send_text_locked, send_bytes_locked, wait_response_credit)
    except ProxyTargetBlocked as error:
        await _send_v3_response(request_id, 400, {"content-type": "text/plain; charset=utf-8"},
                                _single_body_chunks(str(error).encode()),
                                send_text_locked, send_bytes_locked, wait_response_credit)
    except V3ResponseAborted as error:
        log_repeatable_error("v3_abort", f"[REQ] STREAM ABORT | {request_id} | {error}", "web")
    except asyncio.CancelledError:
        raise
    except Exception as error:
        log_repeatable_error("handle_req_v3",
                             f"[REQ] ERROR | {request_id} | {type(error).__name__}: {error}", "web")
        try:
            await _send_v3_response(
                request_id, 502, {"content-type": "text/plain; charset=utf-8"},
                _single_body_chunks(f"Upstream error: {type(error).__name__}: {error}".encode("utf-8", "replace")),
                send_text_locked, send_bytes_locked, wait_response_credit)
        except Exception:
            pass
    finally:
        STATS["web"]["inflight"] = max(0, STATS["web"]["inflight"] - 1)
        _CURRENT_HOST_ROUTE.reset(route_token)


def _websocket_upstream_url(rel_path: str, query: str) -> str:
    upstream = _select_upstream(rel_path)
    scheme, host, port, base_path = parse_upstream_base(upstream)
    if not _upstream_host_allowed(host):
        raise ProxyTargetBlocked(
            f"WebSocket upstream blocked by UPSTREAM_NETWORK_POLICY={UPSTREAM_NETWORK_POLICY!r}")
    ws_scheme = "wss" if scheme == "https" else "ws"
    encoded_rel = urllib.parse.quote((rel_path or "").lstrip("/"), safe="/%:@-._~!$&'()*+,;=")
    path = "/" + encoded_rel
    if base_path:
        path = base_path + path
    if query:
        path += "?" + query.replace("\r", "").replace("\n", "")
    host_text = f"[{host}]" if ":" in host and not host.startswith("[") else host
    default_port = 443 if ws_scheme == "wss" else 80
    authority = host_text if port == default_port else f"{host_text}:{port}"
    return f"{ws_scheme}://{authority}{path}"


async def handle_user_websocket_bridge(msg, inbound_queue, send_text_locked, send_bytes_locked):
    request_id = str(msg.get("id", ""))
    rel_path = str(msg.get("path", ""))
    query = str(msg.get("query", ""))
    headers = dict(msg.get("headers", {}) or {})
    subprotocols = [str(x) for x in (msg.get("subprotocols") or []) if x]
    route_token = _CURRENT_HOST_ROUTE.set(_host_route_for_headers(headers))

    if not USER_WEBSOCKET_PROXY_ENABLED or not should_proxy(rel_path):
        _CURRENT_HOST_ROUTE.reset(route_token)
        await send_text_locked({"type": "user_ws_accept", "id": request_id, "ok": False,
                                "code": 1008, "reason": "WebSocket path is not proxied"})
        return

    try:
        target = _websocket_upstream_url(rel_path, query)
        extra_headers = []
        skip = {"host", "content-length", "sec-websocket-key", "sec-websocket-version",
                "sec-websocket-extensions", "sec-websocket-protocol"}
        for key, value in headers.items():
            if str(key).lower() in HOP_BY_HOP or str(key).lower() in skip:
                continue
            extra_headers.append((str(key), str(value)))

        connect_kwargs = {
            "subprotocols": subprotocols or None,
            "open_timeout": None if float(UPSTREAM_CONNECT_TIMEOUT_SECONDS) <= 0
            else float(UPSTREAM_CONNECT_TIMEOUT_SECONDS)}
        try:
            import inspect
            if "additional_headers" in inspect.signature(websockets.connect).parameters:
                connect_kwargs["additional_headers"] = extra_headers
            else:
                connect_kwargs["extra_headers"] = extra_headers
        except Exception:
            connect_kwargs["extra_headers"] = extra_headers

        async with websockets.connect(target, **connect_kwargs) as local_ws:
            await send_text_locked({"type": "user_ws_accept", "id": request_id, "ok": True,
                                    "subprotocol": getattr(local_ws, "subprotocol", None)})

            async def local_to_vps():
                prefix = b"WR" + bytes.fromhex(request_id)
                async for payload in local_ws:
                    if isinstance(payload, bytes):
                        await send_bytes_locked(prefix + payload)
                    else:
                        await send_text_locked({"type": "user_ws_text", "id": request_id,
                                                "data": str(payload)})

            async def vps_to_local():
                while True:
                    event = await inbound_queue.get()
                    if event[0] in {"text", "bytes"}:
                        await local_ws.send(event[1])
                    elif event[0] == "close":
                        await local_ws.close(code=int(event[1] or 1000))
                        return

            tasks = {asyncio.create_task(local_to_vps()), asyncio.create_task(vps_to_local())}
            done, rest = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
            for task in rest:
                task.cancel()
            await asyncio.gather(*rest, return_exceptions=True)
            for task in done:
                task.result()
    except Exception as error:
        log_repeatable_error("user_ws_proxy",
                             f"[USER-WS] {request_id} failed: {type(error).__name__}: {error}", "web")
        try:
            await send_text_locked({"type": "user_ws_accept", "id": request_id, "ok": False,
                                    "code": 1011, "reason": "Local WebSocket connection failed"})
        except Exception:
            pass
    finally:
        try:
            await send_text_locked({"type": "user_ws_close", "id": request_id,
                                    "code": 1000, "reason": ""})
        except Exception:
            pass
        _CURRENT_HOST_ROUTE.reset(route_token)


async def web_connect_once() -> None:
    ssl_context = make_ssl_context() if WEB_WS_URL.lower().startswith("wss://") else None
    set_state("web", "connecting")
    log(f"[WS] Connecting to {WEB_WS_URL}", "web")

    async with websockets.connect(
        WEB_WS_URL, ssl=ssl_context,
        max_size=max(1_500_000, int(WEBSOCKET_MAX_INCOMING_BYTES)),
        ping_interval=20, ping_timeout=20, open_timeout=15, close_timeout=5,
    ) as websocket:
        STATS["web"]["connections"] += 1

        capabilities = ["cancel"]
        if isinstance(HOST_ROUTES, dict):
            capabilities.append("host_routes")
        if int(TUNNEL_PROTOCOL) >= 2:
            capabilities.append("stream_response")
        if int(TUNNEL_PROTOCOL) >= 3:
            capabilities.append("stream_request")
            if USER_WEBSOCKET_PROXY_ENABLED:
                capabilities.append("user_websocket")

        await websocket.send(_json({
            "username": USERNAME, "password": PASSWORD,
            "protocol": int(TUNNEL_PROTOCOL), "agent_version": int(version),
            "capabilities": capabilities,
            "host_routes": _normalize_host_routes_for_auth()}))

        authentication = json.loads(await websocket.recv())
        if authentication.get("type") != "auth" or not authentication.get("ok"):
            detail = authentication.get("error") or "bad credentials"
            set_state("web", "error", f"authentication failed: {detail}")
            raise RuntimeError(f"authentication failed ({detail}); check USERNAME/PASSWORD")

        server_protocol = int(authentication.get("protocol", 1) or 1)
        negotiated = min(max(1, server_protocol), max(1, int(TUNNEL_PROTOCOL)))
        negotiated = 3 if negotiated >= 3 else (2 if negotiated >= 2 else 1)

        set_state("web", "online", f"https://{USERNAME}.hostish.site/")
        log(f"[AUTH] Authenticated as '{USERNAME}' | mode={MODE} | protocol v{negotiated}", "web")
        log(f"[LIVE] https://{USERNAME}.hostish.site/", "web")

        send_lock = asyncio.Lock()
        request_tasks: dict = {}
        incoming_bodies: dict = {}
        response_credits: dict = {}
        user_ws_queues: dict = {}
        user_ws_tasks: dict = {}

        max_active = int(MAX_CONCURRENT_REQUESTS)
        request_semaphore = asyncio.Semaphore(max_active) if max_active > 0 else None

        def over_agent_capacity() -> bool:
            if max_active <= 0:
                return False
            max_queue = int(MAX_QUEUED_REQUESTS)
            if max_queue <= 0:
                return False
            return len(request_tasks) >= max_active + max_queue

        async def send_text_locked(payload: dict) -> None:
            async with send_lock:
                await websocket.send(_json(payload))

        async def send_bytes_locked(payload: bytes) -> None:
            async with send_lock:
                await websocket.send(payload)

        async def send_response_v2(response: dict) -> None:
            body_raw = response.pop("_body_raw", b"") or b""
            request_id = str(response.get("id", ""))
            try:
                request_id_raw = bytes.fromhex(request_id)
            except ValueError:
                request_id_raw = b""
            if len(request_id_raw) != 16:
                await send_text_locked(_build_tunnel_response(
                    request_id, 500, {"content-type": "text/plain; charset=utf-8"},
                    b"Invalid request id", True))
                return
            await send_text_locked({
                "type": "resp_start", "id": request_id,
                "status": int(response.get("status", 502)),
                "headers": dict(response.get("headers", {}) or {}),
                "size": len(body_raw)})
            prefix = b"H2" + request_id_raw
            chunk_size = max(16 * 1024, int(TUNNEL_CHUNK_BYTES))
            view = memoryview(body_raw)
            for offset in range(0, len(view), chunk_size):
                await send_bytes_locked(prefix + bytes(view[offset: offset + chunk_size]))
            await send_text_locked({"type": "resp_end", "id": request_id})

        async def wait_response_credit(request_id: str) -> None:
            state = response_credits.setdefault(request_id, {"credit": 0, "event": asyncio.Event()})
            while int(state["credit"]) <= 0:
                state["event"].clear()
                if int(state["credit"]) > 0:
                    state["event"].set()
                    break
                await state["event"].wait()
            state["credit"] = int(state["credit"]) - 1

        async def send_busy_response(message_payload: dict) -> None:
            request_id = str(message_payload.get("id", ""))
            body = b"Host is busy. Try again shortly."
            busy_headers = {"content-type": "text/plain; charset=utf-8", "retry-after": "1"}
            if negotiated >= 3:
                response_credits[request_id] = {"credit": 1, "event": asyncio.Event()}
                response_credits[request_id]["event"].set()
                await _send_v3_response(request_id, 503, busy_headers,
                                        _single_body_chunks(body), send_text_locked,
                                        send_bytes_locked, wait_response_credit)
                response_credits.pop(request_id, None)
            elif negotiated >= 2:
                await send_response_v2(_build_tunnel_response(request_id, 503, busy_headers, body, False))
            else:
                await send_text_locked(_build_tunnel_response(request_id, 503, busy_headers, body, True))

        async def body_iterator(request_id: str, queue):
            total = 0
            while True:
                item = await queue.get()
                if item is None:
                    return
                total += len(item)
                await send_text_locked({"type": "req_credit", "id": request_id, "credit": 1})
                if _limit_exceeded(MAX_REQUEST_BODY_BYTES, total):
                    raise RequestBodyLimitExceeded("request body exceeds configured limit")
                yield item

        async def run_request_task(message_payload: dict, queue=None):
            request_id = str(message_payload.get("id", ""))

            async def work():
                if negotiated >= 3:
                    await handle_req_v3(message_payload, body_iterator(request_id, queue),
                                        send_text_locked, send_bytes_locked, wait_response_credit)
                else:
                    response = await handle_req(message_payload, encode_body=(negotiated < 2))
                    if negotiated >= 2:
                        await send_response_v2(response)
                    else:
                        await send_text_locked(response)

            try:
                if request_semaphore is None:
                    await work()
                else:
                    async with request_semaphore:
                        await work()
            except asyncio.CancelledError:
                raise
            except Exception as error:
                log_repeatable_error("task_error",
                                     f"Error in request task {request_id}: "
                                     f"{type(error).__name__}: {error}", "web")
            finally:
                incoming_bodies.pop(request_id, None)
                response_credits.pop(request_id, None)

        def attach(registry: dict, request_id: str, task) -> None:
            registry[request_id] = task

            def done_callback(done_task, rid=request_id):
                registry.pop(rid, None)
                incoming_bodies.pop(rid, None)
                response_credits.pop(rid, None)
                user_ws_queues.pop(rid, None)
                try:
                    done_task.exception()
                except (asyncio.CancelledError, Exception):
                    pass

            task.add_done_callback(done_callback)

        try:
            while True:
                raw = await websocket.recv()

                if isinstance(raw, bytes):
                    if len(raw) < 18:
                        continue
                    magic = raw[:2]
                    request_id = raw[2:18].hex()
                    payload = raw[18:]
                    if magic == b"Q3" and negotiated >= 3:
                        queue = incoming_bodies.get(request_id)
                        if queue is not None:
                            await queue.put(payload)
                    elif magic == b"WQ" and negotiated >= 3:
                        queue = user_ws_queues.get(request_id)
                        if queue is not None:
                            await queue.put(("bytes", payload))
                    continue

                message = json.loads(raw)
                message_type = message.get("type")

                if message_type in {"ping", "pong"}:
                    continue

                if message_type == "server_restart":
                    log("[WS] Hostish server is restarting; reconnecting...", "web")
                    await websocket.close()
                    break

                if message_type == "cancel":
                    task = request_tasks.get(str(message.get("id", "")))
                    if task is not None and not task.done():
                        task.cancel()
                    continue

                if message_type == "resp_credit" and negotiated >= 3:
                    request_id = str(message.get("id", ""))
                    state = response_credits.setdefault(
                        request_id, {"credit": 0, "event": asyncio.Event()})
                    try:
                        credit = max(0, int(message.get("credit", 0)))
                    except (TypeError, ValueError):
                        credit = 0
                    if credit:
                        state["credit"] = int(state["credit"]) + credit
                        state["event"].set()
                    continue

                if message_type == "req_end" and negotiated >= 3:
                    queue = incoming_bodies.get(str(message.get("id", "")))
                    if queue is not None:
                        await queue.put(None)
                    continue

                if message_type == "req_start" and negotiated >= 3:
                    request_id = str(message.get("id", ""))
                    if over_agent_capacity():
                        await send_busy_response(message)
                        continue
                    queue = asyncio.Queue(maxsize=max(1, int(REQUEST_STREAM_BUFFER_CHUNKS)))
                    incoming_bodies[request_id] = queue
                    response_credits[request_id] = {"credit": 0, "event": asyncio.Event()}
                    await send_text_locked({"type": "req_credit", "id": request_id,
                                            "credit": max(1, int(REQUEST_STREAM_BUFFER_CHUNKS))})
                    attach(request_tasks, request_id,
                           asyncio.create_task(run_request_task(message, queue)))
                    continue

                if message_type == "req":
                    request_id = str(message.get("id", ""))
                    if over_agent_capacity():
                        await send_busy_response(message)
                        continue
                    attach(request_tasks, request_id,
                           asyncio.create_task(run_request_task(message, None)))
                    continue

                if message_type == "user_ws_open" and negotiated >= 3:
                    request_id = str(message.get("id", ""))
                    if not USER_WEBSOCKET_PROXY_ENABLED:
                        await send_text_locked({"type": "user_ws_accept", "id": request_id,
                                                "ok": False, "code": 1008,
                                                "reason": "User WebSocket proxy disabled"})
                        continue
                    queue = asyncio.Queue(maxsize=max(2, int(REQUEST_STREAM_BUFFER_CHUNKS)))
                    user_ws_queues[request_id] = queue
                    attach(user_ws_tasks, request_id, asyncio.create_task(
                        handle_user_websocket_bridge(message, queue, send_text_locked,
                                                     send_bytes_locked)))
                    continue

                if message_type == "user_ws_text" and negotiated >= 3:
                    queue = user_ws_queues.get(str(message.get("id", "")))
                    if queue is not None:
                        await queue.put(("text", str(message.get("data", ""))))
                    continue

                if message_type == "user_ws_close" and negotiated >= 3:
                    queue = user_ws_queues.get(str(message.get("id", "")))
                    if queue is not None:
                        await queue.put(("close", int(message.get("code", 1000) or 1000)))
                    continue

                log(f"[WS] Ignoring unknown message type: {message_type!r}", "web")
        finally:
            for task in list(request_tasks.values()) + list(user_ws_tasks.values()):
                if not task.done():
                    task.cancel()
            pending = list(request_tasks.values()) + list(user_ws_tasks.values())
            if pending:
                await asyncio.gather(*pending, return_exceptions=True)


# ============================================================
# OUTBOUND MAIL AUTHENTICATION (DKIM)
# ============================================================

_DKIM_DEFAULT_HEADERS = [
    "from", "to", "cc", "subject", "date", "message-id", "reply-to",
    "mime-version", "content-type", "content-transfer-encoding",
]


def _dkim_domain() -> str:
    configured = str(MAIL_DKIM_DOMAIN or "").strip().lower().rstrip(".")
    if configured:
        return configured
    custom = str(WEBMAIL_DOMAIN or "").strip().lower().rstrip(".")
    if custom:
        return custom
    return f"{USERNAME}.hostish.site".strip().lower().rstrip(".")


def _dkim_selector() -> str:
    return str(MAIL_DKIM_SELECTOR or "hostish").strip().lower().rstrip(".")


def _dkim_record_name() -> str:
    return f"{_dkim_selector()}._domainkey.{_dkim_domain()}"


def _dkim_private_key_path() -> str:
    value = os.path.expanduser(str(MAIL_DKIM_PRIVATE_KEY_FILE or "hostish-dkim-private.pem").strip())
    if os.path.isabs(value):
        return os.path.abspath(value)
    try:
        base = os.path.dirname(os.path.abspath(__file__))
    except Exception:
        base = os.getcwd()
    return os.path.abspath(os.path.join(base, value))


def _dkim_crypto():
    try:
        from cryptography.hazmat.primitives import hashes, serialization
        from cryptography.hazmat.primitives.asymmetric import padding, rsa
    except ImportError as error:
        raise RuntimeError(
            "DKIM requires the cryptography package. Install it with: "
            "python -m pip install cryptography"
        ) from error
    return hashes, serialization, padding, rsa


def _load_dkim_private_key():
    _hashes, serialization, _padding, _rsa = _dkim_crypto()
    path = _dkim_private_key_path()
    with open(path, "rb") as handle:
        key = serialization.load_pem_private_key(handle.read(), password=None)
    if not hasattr(key, "sign") or not hasattr(key, "public_key"):
        raise ValueError("DKIM private key is not an RSA signing key")
    return key


def _dkim_public_key_b64(private_key=None) -> str:
    _hashes, serialization, _padding, _rsa = _dkim_crypto()
    key = private_key or _load_dkim_private_key()
    pem = key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    ).decode("ascii")
    return "".join(
        line.strip() for line in pem.splitlines()
        if line and not line.startswith("-----")
    )


def _dkim_record_value(private_key=None) -> str:
    return f"v=DKIM1; k=rsa; p={_dkim_public_key_b64(private_key)}"


def generate_dkim_key(force: bool = False) -> dict:
    _hashes, serialization, _padding, rsa = _dkim_crypto()
    path = _dkim_private_key_path()
    if os.path.exists(path) and not force:
        raise FileExistsError(
            f"DKIM private key already exists: {path}. Use Rotate key if you intend to replace it."
        )
    bits = int(MAIL_DKIM_KEY_BITS or 2048)
    if bits < 1024:
        raise ValueError("MAIL_DKIM_KEY_BITS must be at least 1024; 2048 is recommended")
    # Keep upper bounds reasonable for an interactive control panel.
    if bits > 4096:
        raise ValueError("MAIL_DKIM_KEY_BITS cannot exceed 4096")
    key = rsa.generate_private_key(public_exponent=65537, key_size=bits)
    pem = key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    temporary = path + ".tmp"
    with open(temporary, "wb") as handle:
        handle.write(pem)
        handle.flush()
        try:
            os.fsync(handle.fileno())
        except OSError:
            pass
    os.replace(temporary, path)
    try:
        os.chmod(path, 0o600)
    except Exception:
        pass
    return {
        "path": path,
        "bits": bits,
        "record_name": _dkim_record_name(),
        "record_value": _dkim_record_value(key),
    }


def _dkim_dns_lookup() -> dict:
    """Best-effort TXT verification. dnspython is optional."""
    wanted = _dkim_record_value()
    name = _dkim_record_name()
    try:
        import dns.resolver
    except ImportError:
        return {
            "available": False,
            "published": None,
            "message": "Automatic DNS check unavailable; install dnspython or confirm manually.",
        }
    try:
        answers = dns.resolver.resolve(name, "TXT", lifetime=6.0)
        values = []
        for answer in answers:
            strings = getattr(answer, "strings", None)
            if strings is not None:
                value = b"".join(strings).decode("utf-8", "replace")
            else:
                value = str(answer).strip('"').replace('" "', '')
            values.append(value)
        normalize = lambda value: re.sub(r"\s+", "", str(value or "")).lower()
        published = any(normalize(value) == normalize(wanted) for value in values)
        return {
            "available": True,
            "published": published,
            "answers": values,
            "message": "DKIM TXT record matches DNS." if published else "TXT record was not found or does not match this key.",
        }
    except Exception as error:
        return {
            "available": True,
            "published": False,
            "answers": [],
            "message": f"DNS lookup failed: {type(error).__name__}: {error}",
        }


def dkim_status(check_dns: bool = False) -> dict:
    domain = _dkim_domain()
    selector = _dkim_selector()
    path = _dkim_private_key_path()
    result = {
        "enabled": bool(MAIL_DKIM_ENABLED),
        "domain": domain,
        "selector": selector,
        "record_name": f"{selector}._domainkey.{domain}",
        "private_key_file": path,
        "key_exists": os.path.isfile(path),
        "dns_confirmed": bool(MAIL_DKIM_DNS_CONFIRMED),
        "dns_confirmation_required": bool(MAIL_DKIM_REQUIRE_DNS_CONFIRMATION),
        "from_alignment_required": bool(MAIL_DKIM_REQUIRE_FROM_ALIGNMENT),
        "ready": False,
        "error": "",
    }
    if not result["key_exists"]:
        result["error"] = "Private key has not been generated yet."
        return result
    try:
        result["record_value"] = _dkim_record_value()
    except Exception as error:
        result["error"] = f"Could not load DKIM key: {type(error).__name__}: {error}"
        return result
    if check_dns:
        result["dns"] = _dkim_dns_lookup()
    result["ready"] = bool(
        MAIL_DKIM_ENABLED
        and result["key_exists"]
        and (not MAIL_DKIM_REQUIRE_DNS_CONFIRMATION or MAIL_DKIM_DNS_CONFIRMED)
    )
    if MAIL_DKIM_ENABLED and MAIL_DKIM_REQUIRE_DNS_CONFIRMATION and not MAIL_DKIM_DNS_CONFIRMED:
        result["error"] = "Publish the displayed TXT record, then mark DNS confirmed before sending."
    return result


def _dkim_split_message(raw: bytes):
    if b"\r\n\r\n" in raw:
        header_blob, body = raw.split(b"\r\n\r\n", 1)
        lines = header_blob.split(b"\r\n")
    elif b"\n\n" in raw:
        header_blob, body = raw.split(b"\n\n", 1)
        lines = header_blob.replace(b"\r\n", b"\n").replace(b"\r", b"\n").split(b"\n")
    else:
        raise ValueError("message has no RFC 5322 header/body separator")

    chunks = []
    current = None
    for line in lines:
        if line.startswith((b" ", b"\t")) and current is not None:
            current += b"\r\n" + line
        else:
            if current is not None:
                chunks.append(current)
            current = line
    if current is not None:
        chunks.append(current)

    fields = []
    for chunk in chunks:
        if b":" not in chunk:
            continue
        name, value = chunk.split(b":", 1)
        decoded = name.decode("ascii", "ignore").strip().lower()
        if decoded:
            fields.append((decoded, value))
    return fields, body


def _dkim_relaxed_header(name: str, value: bytes) -> bytes:
    unfolded = re.sub(br"\r\n[ \t]+", b" ", value)
    unfolded = unfolded.replace(b"\r", b"").replace(b"\n", b" ")
    unfolded = re.sub(br"[ \t]+", b" ", unfolded).strip(b" \t")
    return name.lower().encode("ascii") + b":" + unfolded + b"\r\n"


def _dkim_relaxed_body(body: bytes) -> bytes:
    lines = re.split(br"\r\n|\n|\r", body)
    normalized = []
    for line in lines:
        line = re.sub(br"[ \t]+", b" ", line).rstrip(b" \t")
        normalized.append(line)
    while normalized and normalized[-1] == b"":
        normalized.pop()
    if not normalized:
        return b"\r\n"
    return b"\r\n".join(normalized) + b"\r\n"


def _dkim_from_domain(message: bytes) -> str:
    try:
        from email import policy as _mail_policy
        from email.parser import BytesParser as _HeaderParser
        parsed = _HeaderParser(policy=_mail_policy.default).parsebytes(message, headersonly=True)
        _display, address = parseaddr(str(parsed.get("From", "")))
    except Exception:
        address = ""
    if "@" not in address:
        return ""
    return address.rsplit("@", 1)[1].strip().lower().rstrip(".")


def _dkim_domains_align(from_domain: str, signing_domain: str) -> bool:
    left = str(from_domain or "").strip().lower().rstrip(".")
    right = str(signing_domain or "").strip().lower().rstrip(".")
    if not left or not right:
        return False
    return left == right or left.endswith("." + right) or right.endswith("." + left)


def dkim_sign_message(message: bytes) -> bytes:
    if not MAIL_DKIM_ENABLED:
        return message

    path = _dkim_private_key_path()
    if not os.path.isfile(path):
        if MAIL_DKIM_AUTO_GENERATE:
            generated = generate_dkim_key(force=False)
            log(f"Generated DKIM private key: {generated['path']}", "smtp")
            log(f"Publish TXT {generated['record_name']} = {generated['record_value']}", "smtp")
        else:
            raise RuntimeError("DKIM is enabled but the private key file does not exist")

    status = dkim_status(check_dns=False)
    if MAIL_DKIM_REQUIRE_DNS_CONFIRMATION and not MAIL_DKIM_DNS_CONFIRMED:
        raise RuntimeError(
            "DKIM setup is incomplete. Publish this DNS TXT record first, then enable "
            f"'DNS record published/confirmed' on the Mail page: {status.get('record_name')} "
            f"TXT {status.get('record_value', '')}"
        )

    signing_domain = _dkim_domain()
    from_domain = _dkim_from_domain(message)
    if not from_domain:
        raise RuntimeError("Outbound message is missing a valid From: address, so it cannot be DKIM-signed")
    if MAIL_DKIM_REQUIRE_FROM_ALIGNMENT and not _dkim_domains_align(from_domain, signing_domain):
        raise RuntimeError(
            f"From domain {from_domain!r} does not align with DKIM signing domain {signing_domain!r}. "
            "Set MAIL_DKIM_DOMAIN/WEBMAIL_DOMAIN to the From domain or disable alignment checking only if intentional."
        )

    fields, body = _dkim_split_message(message)
    by_name = {}
    for index, (name, value) in enumerate(fields):
        by_name.setdefault(name, []).append(index)

    requested_headers = [
        str(name).strip().lower() for name in (MAIL_DKIM_SIGN_HEADERS or _DKIM_DEFAULT_HEADERS)
        if str(name).strip()
    ]
    if "from" not in requested_headers:
        requested_headers.insert(0, "from")

    selected_names = []
    canonical_headers = bytearray()
    use_count = {}
    for name in requested_headers:
        indexes = by_name.get(name, [])
        count = use_count.get(name, 0)
        if len(indexes) <= count:
            continue
        index = indexes[-1 - count]
        use_count[name] = count + 1
        selected_names.append(name)
        canonical_headers.extend(_dkim_relaxed_header(name, fields[index][1]))

    if "from" not in selected_names:
        raise RuntimeError("Outbound message has no From: header to sign")

    body_hash = base64.b64encode(hashlib.sha256(_dkim_relaxed_body(body)).digest()).decode("ascii")
    unsigned_value = (
        f"v=1; a=rsa-sha256; c=relaxed/relaxed; d={signing_domain}; s={_dkim_selector()}; "
        f"t={int(time.time())}; h={':'.join(selected_names)}; bh={body_hash}; b="
    )
    canonical_headers.extend(_dkim_relaxed_header("dkim-signature", unsigned_value.encode("ascii")))

    hashes, _serialization, padding, _rsa = _dkim_crypto()
    key = _load_dkim_private_key()
    signature = key.sign(bytes(canonical_headers), padding.PKCS1v15(), hashes.SHA256())
    signature_b64 = base64.b64encode(signature).decode("ascii")
    header = f"DKIM-Signature: {unsigned_value}{signature_b64}\r\n".encode("ascii")
    return header + message


async def _panel_dkim_action(payload: dict) -> dict:
    action = str((payload or {}).get("action", "status")).strip().lower()
    try:
        if action == "status":
            return {"ok": True, "status": dkim_status(check_dns=False)}
        if action == "check_dns":
            status = dkim_status(check_dns=True)
            dns_result = status.get("dns") or {}
            if dns_result.get("published") is True and not MAIL_DKIM_DNS_CONFIRMED:
                apply_settings({"MAIL_DKIM_DNS_CONFIRMED": True})
                stored = load_config_file()
                stored["MAIL_DKIM_DNS_CONFIRMED"] = True
                save_config_file(stored)
                status = dkim_status(check_dns=True)
            return {"ok": True, "status": status, "message": dns_result.get("message", "DNS check complete")}
        if action in {"generate", "rotate"}:
            generated = await asyncio.to_thread(generate_dkim_key, action == "rotate")
            apply_settings({"MAIL_DKIM_DNS_CONFIRMED": False})
            stored = load_config_file()
            stored["MAIL_DKIM_DNS_CONFIRMED"] = False
            save_config_file(stored)
            log(f"DKIM key {'rotated' if action == 'rotate' else 'generated'} for {_dkim_domain()}", "smtp")
            return {
                "ok": True,
                "status": dkim_status(check_dns=False),
                "message": "DKIM key generated. Publish the TXT record before sending." if action == "generate"
                           else "DKIM key rotated. Replace the DNS TXT record before sending.",
            }
        return {"ok": False, "message": f"Unknown DKIM action {action!r}"}
    except Exception as error:
        return {"ok": False, "message": f"{type(error).__name__}: {error}", "status": dkim_status(check_dns=False)}


# ============================================================
# SMTP MAIL TUNNEL
# ============================================================

DELIVERY_MAGIC = b"SM"
OUTBOUND_MAGIC = b"SO"
FRAME_HEADER_BYTES = 18


def local_tls_context() -> ssl.SSLContext:
    if LOCAL_DELIVERY_TLS_VERIFY:
        return ssl.create_default_context()
    return ssl._create_unverified_context()


def _id_bytes(message_id: str) -> bytes:
    raw = bytes.fromhex(message_id)
    if len(raw) != 16:
        raise ValueError("id must be 16 bytes")
    return raw


def _normalize_address(value: str) -> str:
    _name, address = parseaddr(value or "")
    return (address or value or "").strip()


def deliver_to_local_sync(mail_from: str, recipients: list, message: bytes):
    server = None
    try:
        mode = str(LOCAL_DELIVERY_MODE).strip().lower()
        if mode == "ssl":
            server = smtplib.SMTP_SSL(LOCAL_DELIVERY_HOST, LOCAL_DELIVERY_PORT,
                                      timeout=30, context=local_tls_context())
        else:
            server = smtplib.SMTP(LOCAL_DELIVERY_HOST, LOCAL_DELIVERY_PORT, timeout=30)
            server.ehlo()
            if mode == "starttls":
                server.starttls(context=local_tls_context())
                server.ehlo()
            elif mode != "plain":
                return False, "LOCAL_DELIVERY_MODE must be plain, starttls, or ssl"

        if LOCAL_DELIVERY_USERNAME:
            server.login(LOCAL_DELIVERY_USERNAME, LOCAL_DELIVERY_PASSWORD)

        refused = server.sendmail(mail_from, recipients, message)
        if refused:
            return False, f"local SMTP refused recipients: {refused}"
        return True, ""
    except Exception as error:
        return False, f"{type(error).__name__}: {error}"
    finally:
        if server is not None:
            try:
                server.quit()
            except Exception:
                try:
                    server.close()
                except Exception:
                    pass


@dataclass
class DeliveryBuffer:
    mail_from: str
    recipients: list
    expected_size: int
    body: bytearray = field(default_factory=bytearray)


class SMTPAgent:
    def __init__(self) -> None:
        self.ws = None
        self.send_lock = asyncio.Lock()
        self.delivery_buffers: Dict[str, DeliveryBuffer] = {}
        self.outbound_waiters: Dict[str, asyncio.Future] = {}
        self.mail_domain = ""
        self.custom_mail_domains = []
        self.custom_domain_dns = {}
        self.max_server_message_bytes = MAX_MESSAGE_BYTES
        self.connected_event = asyncio.Event()

    async def send_json(self, data: dict) -> None:
        if self.ws is None:
            raise ConnectionError("not connected to Hostish")
        async with self.send_lock:
            await self.ws.send(_json(data))

    async def send_binary(self, data: bytes) -> None:
        if self.ws is None:
            raise ConnectionError("not connected to Hostish")
        async with self.send_lock:
            await self.ws.send(data)

    async def submit_outbound(self, mail_from: str, recipients: list, message: bytes):
        if not self.connected_event.is_set() or self.ws is None:
            return False, "Hostish SMTP agent is offline"
        if not recipients or len(recipients) > int(MAX_RECIPIENTS):
            return False, "invalid recipient count"

        # Hostish handles SPF/DKIM and final sender authentication on its backend.
        # Keep the client message byte-for-byte intact when uploading it to Hostish.
        message = bytes(message)
        if len(message) > min(int(MAX_MESSAGE_BYTES), self.max_server_message_bytes):
            return False, "message too large"

        outbound_id = secrets.token_hex(16)
        future = asyncio.get_running_loop().create_future()
        self.outbound_waiters[outbound_id] = future

        try:
            await self.send_json({"type": "outbound_start", "id": outbound_id,
                                  "mail_from": mail_from, "recipients": recipients,
                                  "size": len(message)})
            raw_id = _id_bytes(outbound_id)
            chunk_size = max(1024, int(WS_CHUNK_BYTES))
            for offset in range(0, len(message), chunk_size):
                await self.send_binary(OUTBOUND_MAGIC + raw_id + message[offset: offset + chunk_size])
            await self.send_json({"type": "outbound_end", "id": outbound_id})
            result = await asyncio.wait_for(future, timeout=180.0)
            ok = bool(result.get("ok"))
            if ok:
                STATS["smtp"]["sent"] += 1
            else:
                STATS["smtp"]["send_failed"] += 1
            error_text = str(result.get("error") or "")
            return ok, error_text
        except Exception as error:
            STATS["smtp"]["send_failed"] += 1
            return False, f"{type(error).__name__}: {error}"
        finally:
            self.outbound_waiters.pop(outbound_id, None)

    async def handle_binary(self, data: bytes) -> None:
        if len(data) < FRAME_HEADER_BYTES or data[:2] != DELIVERY_MAGIC:
            return
        delivery_id = data[2:18].hex()
        delivery = self.delivery_buffers.get(delivery_id)
        if delivery is None:
            return
        chunk = data[18:]
        limit = min(int(MAX_MESSAGE_BYTES), self.max_server_message_bytes)
        if len(delivery.body) + len(chunk) > limit:
            self.delivery_buffers.pop(delivery_id, None)
            await self.send_json({"type": "deliver_result", "id": delivery_id,
                                  "ok": False, "error": "message too large for SMTP agent"})
            return
        delivery.body.extend(chunk)

    async def handle_text(self, raw: str) -> None:
        try:
            message = json.loads(raw)
        except json.JSONDecodeError:
            return

        message_type = str(message.get("type", ""))

        if message_type == "ping":
            await self.send_json({"type": "pong"})
            return

        if message_type == "server_restart":
            raise ConnectionError("Hostish SMTP server is restarting")

        if message_type == "deliver_start":
            delivery_id = str(message.get("id", ""))
            try:
                _id_bytes(delivery_id)
            except Exception:
                return
            recipients = message.get("recipients", [])
            if not isinstance(recipients, list):
                return
            self.delivery_buffers[delivery_id] = DeliveryBuffer(
                mail_from=str(message.get("mail_from", "")),
                recipients=[str(item) for item in recipients],
                expected_size=int(message.get("size", 0) or 0))
            return

        if message_type == "deliver_end":
            delivery = self.delivery_buffers.pop(str(message.get("id", "")), None)
            if delivery is not None:
                asyncio.create_task(self.finish_delivery(str(message.get("id", "")), delivery))
            return

        if message_type == "outbound_result":
            future = self.outbound_waiters.get(str(message.get("id", "")))
            if future is not None and not future.done():
                future.set_result(message)
            return

    async def finish_delivery(self, delivery_id: str, delivery: DeliveryBuffer) -> None:
        # When the built-in webmail client is enabled it becomes the local
        # mail store, so inbound mail is written straight into mailboxes
        # instead of being handed off to an external SMTP server.
        if WEBMAIL_ENABLED:
            ok, error = await asyncio.to_thread(
                webmail_deliver_inbound, delivery.mail_from, delivery.recipients, bytes(delivery.body))
        else:
            ok, error = await asyncio.to_thread(
                deliver_to_local_sync, delivery.mail_from, delivery.recipients, bytes(delivery.body))
        try:
            await self.send_json({"type": "deliver_result", "id": delivery_id,
                                  "ok": bool(ok), "error": str(error or "")[:1000]})
        except Exception:
            pass
        if ok:
            STATS["smtp"]["delivered"] += 1
            log(f"Inbound mail delivered: {', '.join(delivery.recipients)}", "smtp")
        else:
            STATS["smtp"]["delivery_failed"] += 1
            log(f"Inbound local delivery failed: {error}", "smtp")

    async def connect_once(self) -> None:
        ssl_context = make_ssl_context() if SMTP_WS_URL.lower().startswith("wss://") else None
        set_state("smtp", "connecting")
        async with websockets.connect(SMTP_WS_URL, ssl=ssl_context, max_size=None,
                                      ping_interval=20, ping_timeout=20, close_timeout=5) as ws:
            self.ws = ws
            await self.send_json({"username": USERNAME, "password": PASSWORD,
                                  "agent": "smtp", "version": version,
                                  "allow_custom_domains": bool(LINKED_CUSTOM_DOMAIN_MAIL_ENABLED)})

            raw = await asyncio.wait_for(ws.recv(), timeout=20.0)
            if not isinstance(raw, str):
                raise RuntimeError("invalid authentication response")
            auth = json.loads(raw)
            if auth.get("type") != "auth" or not auth.get("ok"):
                set_state("smtp", "error", str(auth.get("error", "authentication failed")))
                raise RuntimeError(f"authentication failed: {auth.get('error', 'unknown error')}")

            STATS["smtp"]["connections"] += 1
            self.mail_domain = str(auth.get("mail_domain", f"{USERNAME}.hostish.site"))
            self.custom_mail_domains = [str(x) for x in (auth.get("custom_mail_domains") or [])]
            self.custom_domain_dns = dict(auth.get("custom_domain_dns") or {})
            self.max_server_message_bytes = int(
                auth.get("max_message_bytes", MAX_MESSAGE_BYTES) or MAX_MESSAGE_BYTES)
            self.connected_event.set()
            set_state("smtp", "online", f"*@{self.mail_domain}")

            log("SMTP tunnel connected", "smtp")
            log(f"Mail domain : {self.mail_domain}", "smtp")
            log(f"Inbound     : *@{self.mail_domain} -> "
                f"{LOCAL_DELIVERY_HOST}:{LOCAL_DELIVERY_PORT}", "smtp")
            if OUTBOUND_RELAY_ENABLED:
                log(f"Outbound    : {LOCAL_RELAY_BIND_HOST}:{LOCAL_RELAY_BIND_PORT} -> Hostish", "smtp")
            if LINKED_CUSTOM_DOMAIN_MAIL_ENABLED:
                if self.custom_mail_domains:
                    log("Linked custom-domain mail is enabled for: " + ", ".join(self.custom_mail_domains), "smtp")
                    dns = self.custom_domain_dns
                    for domain in self.custom_mail_domains:
                        log(f"Custom mail DNS for {domain}: MX @ -> {dns.get('mx_target','hostish.site')}", "smtp")
                        log(f"Custom mail DNS for {domain}: TXT @ -> {dns.get('spf_value','')}", "smtp")
                        log(f"Custom mail DNS for {domain}: CNAME {dns.get('dkim_selector','hostish')}._domainkey -> {dns.get('dkim_cname_target','')}", "smtp")
                        log(f"Custom mail DNS for {domain}: TXT _dmarc -> {dns.get('dmarc_value','v=DMARC1; p=none')}", "smtp")
                else:
                    log("Linked custom-domain mail is enabled, but this Hostish account has no linked custom domains.", "smtp")

            try:
                async for event in ws:
                    if isinstance(event, bytes):
                        await self.handle_binary(event)
                    else:
                        await self.handle_text(event)
            finally:
                self.connected_event.clear()
                self.ws = None
                self.delivery_buffers.clear()
                for future in list(self.outbound_waiters.values()):
                    if not future.done():
                        future.set_result({"ok": False, "error": "Hostish connection lost"})


# ---- Local outbound SMTP relay ----

async def relay_reply(writer, code: int, text: str) -> None:
    writer.write(f"{code} {text}\r\n".encode("utf-8", "replace"))
    await writer.drain()


async def relay_multiline(writer, code: int, lines: list) -> None:
    for index, line in enumerate(lines):
        separator = " " if index == len(lines) - 1 else "-"
        writer.write(f"{code}{separator}{line}\r\n".encode("utf-8", "replace"))
    await writer.drain()


def parse_smtp_path(argument: str, keyword: str) -> Optional[str]:
    prefix = keyword.upper() + ":"
    text = (argument or "").strip()
    if not text.upper().startswith(prefix):
        return None
    rest = text[len(prefix):].lstrip()
    if not rest.startswith("<") or ">" not in rest:
        return None
    return rest[1:rest.index(">")].strip()


async def read_relay_data(reader) -> bytes:
    body = bytearray()
    too_large = False
    while True:
        line = await asyncio.wait_for(reader.readline(), timeout=300.0)
        if not line:
            raise ConnectionError("client disconnected during DATA")
        if line in (b".\r\n", b".\n"):
            break
        if line.startswith(b".."):
            line = line[1:]
        if not too_large:
            body.extend(line)
            if len(body) > int(MAX_MESSAGE_BYTES):
                too_large = True
                body.clear()
    if too_large:
        raise OverflowError("message too large")
    return bytes(body)


def decode_auth_plain(value: str):
    decoded = base64.b64decode(value.encode("ascii"), validate=True).decode("utf-8", "replace")
    parts = decoded.split("\x00")
    if len(parts) < 3:
        raise ValueError("invalid AUTH PLAIN")
    return parts[-2], parts[-1]


async def handle_local_relay(agent: "SMTPAgent", reader, writer) -> None:
    authenticated = not bool(LOCAL_RELAY_USERNAME)
    greeted = False
    mail_from: Optional[str] = None
    recipients: list = []

    try:
        await relay_reply(writer, 220, "Hostish unified agent local relay")
        while True:
            raw = await asyncio.wait_for(reader.readline(), timeout=300.0)
            if not raw:
                break
            if len(raw) > 8192:
                await relay_reply(writer, 500, "command too long")
                break
            line = raw.decode("utf-8", "replace").rstrip("\r\n")
            command, argument = (line.split(" ", 1) + [""])[:2] if " " in line else (line, "")
            command = command.upper()

            if command in {"EHLO", "HELO"}:
                greeted = True
                mail_from = None
                recipients.clear()
                if command == "EHLO":
                    lines = ["hostish-unified-agent", f"SIZE {int(MAX_MESSAGE_BYTES)}",
                             "8BITMIME", "SMTPUTF8"]
                    if LOCAL_RELAY_USERNAME:
                        lines.append("AUTH PLAIN")
                    await relay_multiline(writer, 250, lines)
                else:
                    await relay_reply(writer, 250, "hostish-unified-agent")
                continue

            if command == "AUTH":
                parts = argument.split()
                if len(parts) != 2 or parts[0].upper() != "PLAIN":
                    await relay_reply(writer, 504, "Only AUTH PLAIN is supported")
                    continue
                try:
                    username, password = decode_auth_plain(parts[1])
                    authenticated = (secrets.compare_digest(username, LOCAL_RELAY_USERNAME)
                                     and secrets.compare_digest(password, LOCAL_RELAY_PASSWORD))
                except Exception:
                    authenticated = False
                await relay_reply(writer, 235 if authenticated else 535,
                                  "Authentication successful" if authenticated else "Authentication failed")
                continue

            if command == "NOOP":
                await relay_reply(writer, 250, "OK")
                continue
            if command == "RSET":
                mail_from = None
                recipients.clear()
                await relay_reply(writer, 250, "OK")
                continue
            if command == "QUIT":
                await relay_reply(writer, 221, "Bye")
                break
            if not greeted:
                await relay_reply(writer, 503, "Send EHLO/HELO first")
                continue
            if not authenticated:
                await relay_reply(writer, 530, "Authentication required")
                continue

            if command == "MAIL":
                parsed = parse_smtp_path(argument, "FROM")
                if parsed is None:
                    await relay_reply(writer, 501, "Syntax: MAIL FROM:<address>")
                    continue
                mail_from = _normalize_address(parsed)
                recipients.clear()
                await relay_reply(writer, 250, "OK")
                continue

            if command == "RCPT":
                if mail_from is None:
                    await relay_reply(writer, 503, "Need MAIL FROM first")
                    continue
                recipient = _normalize_address(parse_smtp_path(argument, "TO") or "")
                if not recipient or "@" not in recipient:
                    await relay_reply(writer, 501, "Syntax: RCPT TO:<address>")
                    continue
                if len(recipients) >= int(MAX_RECIPIENTS):
                    await relay_reply(writer, 452, "Too many recipients")
                    continue
                recipients.append(recipient)
                await relay_reply(writer, 250, "Accepted")
                continue

            if command == "DATA":
                if mail_from is None or not recipients:
                    await relay_reply(writer, 503, "Need MAIL FROM and RCPT TO first")
                    continue
                await relay_reply(writer, 354, "End data with <CR><LF>.<CR><LF>")
                try:
                    message = await read_relay_data(reader)
                except OverflowError:
                    await relay_reply(writer, 552, "Message too large")
                    mail_from = None
                    recipients.clear()
                    continue

                ok, error = await agent.submit_outbound(mail_from, recipients, message)
                if ok:
                    log(f"Outbound mail accepted: {', '.join(recipients)}", "smtp")
                    await relay_reply(writer, 250, "Message accepted by Hostish relay")
                else:
                    log(f"Outbound mail failed: {error}", "smtp")
                    await relay_reply(writer, 451, f"Hostish relay failed: {error[:300]}")
                mail_from = None
                recipients.clear()
                continue

            await relay_reply(writer, 502, "Command not implemented")
    except (asyncio.TimeoutError, ConnectionError):
        pass
    except Exception as error:
        log(f"Local relay client error: {type(error).__name__}: {error}", "smtp")
    finally:
        try:
            writer.close()
            await writer.wait_closed()
        except Exception:
            pass


async def smtp_service() -> None:
    """Runs the SMTP tunnel plus the local outbound relay until cancelled."""
    if OUTBOUND_RELAY_ENABLED and not _is_loopback_host(LOCAL_RELAY_BIND_HOST):
        if not LOCAL_RELAY_USERNAME or not LOCAL_RELAY_PASSWORD:
            set_state("smtp", "error", "relay bound off-loopback without credentials")
            log("Refusing to expose the local SMTP relay beyond loopback without "
                "LOCAL_RELAY_USERNAME and LOCAL_RELAY_PASSWORD.", "smtp")
            return

    global _SMTP_AGENT_INSTANCE
    agent = SMTPAgent()
    _SMTP_AGENT_INSTANCE = agent
    relay_server = None

    if OUTBOUND_RELAY_ENABLED:
        try:
            relay_server = await asyncio.start_server(
                lambda reader, writer: handle_local_relay(agent, reader, writer),
                LOCAL_RELAY_BIND_HOST, int(LOCAL_RELAY_BIND_PORT))
            log(f"Local outbound SMTP relay ready on "
                f"{LOCAL_RELAY_BIND_HOST}:{LOCAL_RELAY_BIND_PORT}", "smtp")
        except Exception as error:
            log(f"Could not start local relay: {type(error).__name__}: {error}", "smtp")

    try:
        while True:
            try:
                await agent.connect_once()
                set_state("smtp", "connecting", "reconnecting")
            except asyncio.CancelledError:
                raise
            except Exception as error:
                agent.connected_event.clear()
                agent.ws = None
                set_state("smtp", "error", f"{type(error).__name__}: {error}")
                log(f"Connection error: {type(error).__name__}: {error}", "smtp")
            await asyncio.sleep(max(1, int(RECONNECT_SECONDS)))
    finally:
        if relay_server is not None:
            relay_server.close()
            try:
                await relay_server.wait_closed()
            except Exception:
                pass
        if _SMTP_AGENT_INSTANCE is agent:
            _SMTP_AGENT_INSTANCE = None
        set_state("smtp", "stopped")


# ============================================================
# MINECRAFT TUNNEL
# ============================================================

MC_MAGIC = b"MC"
MC_HEADER_SIZE = 18
UDP_MAGIC = b"UD"
UDP_HEADER_SIZE = 34
UDP_MAX_DATAGRAM_BYTES = 65535
TCP_CHUNK_SIZE = 64 * 1024
MC_MAX_WEBSOCKET_MESSAGE = 4 * 1024 * 1024
MAX_HOMEPAGE_BYTES = 512 * 1024
VOICE_CHAT_UDP_OPEN_TIMEOUT = 20
MINECRAFT_AGENT_VERSION = 6

_VOICE_CHAT_LAST_PUBLIC_PORT = 0


def normalized_path_setting(value: str) -> str:
    configured = str(value or "").strip()
    if not configured:
        return ""
    return os.path.abspath(os.path.expanduser(configured))


def update_voice_chat_public_address(public_host: str, public_port: int) -> bool:
    path = normalized_path_setting(SIMPLE_VOICE_CHAT_CONFIG_FILE)
    if not path:
        raise RuntimeError("SIMPLE_VOICE_CHAT_CONFIG_FILE is blank.")
    if not os.path.isfile(path):
        raise RuntimeError(f"Simple Voice Chat config file was not found: {path}")

    host = str(public_host or "").strip()
    port = int(public_port)
    if not host:
        raise RuntimeError("Hostish returned a blank UDP hostname.")
    if port < 1 or port > 65535:
        raise RuntimeError("Hostish returned an invalid UDP port.")

    wanted = f"{host}:{port}"
    try:
        with open(path, "r", encoding="utf-8-sig", newline="") as handle:
            original = handle.read()
    except OSError as error:
        raise RuntimeError(f"Could not read Simple Voice Chat config: {error}") from error

    newline = "\r\n" if "\r\n" in original else "\n"
    lines = original.replace("\r\n", "\n").replace("\r", "\n").split("\n")

    found = False
    changed = False
    new_lines = []
    for line in lines:
        stripped = line.lstrip()
        if not stripped.startswith("#") and "=" in stripped:
            key = stripped.split("=", 1)[0].strip()
            if key == "voice_host":
                found = True
                new_line = f"voice_host={wanted}"
                if line != new_line:
                    changed = True
                new_lines.append(new_line)
                continue
        new_lines.append(line)

    if not found:
        if new_lines and new_lines[-1] != "":
            new_lines.append("")
        new_lines.append(f"voice_host={wanted}")
        changed = True

    if not changed:
        log(f"Simple Voice Chat voice_host already correct: {wanted}", "minecraft")
        return False

    temporary = path + ".hostish.tmp"
    try:
        with open(temporary, "w", encoding="utf-8", newline="") as handle:
            handle.write(newline.join(new_lines))
            handle.flush()
            try:
                os.fsync(handle.fileno())
            except OSError:
                pass
        os.replace(temporary, path)
    except OSError as error:
        try:
            if os.path.exists(temporary):
                os.remove(temporary)
        except OSError:
            pass
        raise RuntimeError(f"Could not update Simple Voice Chat config: {error}") from error

    log(f"Updated Simple Voice Chat config: voice_host={wanted}", "minecraft")
    return True


def load_homepage():
    configured = str(HOMEPAGE_FILE or "").strip()
    if not configured:
        return False, "", ""
    path = normalized_path_setting(configured)
    try:
        size = os.path.getsize(path)
    except OSError as error:
        raise RuntimeError(f"HOMEPAGE_FILE could not be opened: {path} ({error})") from error
    if size > MAX_HOMEPAGE_BYTES:
        raise RuntimeError(f"HOMEPAGE_FILE is too large (max {MAX_HOMEPAGE_BYTES // 1024} KB).")
    try:
        with open(path, "r", encoding="utf-8-sig") as handle:
            html = handle.read()
    except OSError as error:
        raise RuntimeError(f"Could not read HOMEPAGE_FILE: {error}") from error
    if not html.strip():
        raise RuntimeError("HOMEPAGE_FILE is empty.")
    return True, html, path


async def test_local_minecraft(timeout: float = 3) -> bool:
    try:
        _reader, writer = await asyncio.wait_for(
            asyncio.open_connection(MINECRAFT_HOST, int(MINECRAFT_PORT)), timeout=timeout)
        writer.close()
        try:
            await writer.wait_closed()
        except Exception:
            pass
        return True
    except Exception:
        return False


def start_minecraft_server() -> bool:
    start_bat = normalized_path_setting(START_BAT)
    if not start_bat:
        log("START_BAT is blank; Minecraft will not be started automatically.", "minecraft")
        return False
    if not os.path.isfile(start_bat):
        log(f"START_BAT was not found: {start_bat}", "minecraft")
        return False

    working_directory = os.path.dirname(start_bat)
    log(f"Starting Minecraft server: {start_bat}", "minecraft")
    try:
        if os.name == "nt":
            subprocess.Popen(["cmd.exe", "/c", start_bat], cwd=working_directory,
                             creationflags=subprocess.CREATE_NEW_CONSOLE)
        else:
            subprocess.Popen(["/bin/sh", start_bat], cwd=working_directory, start_new_session=True)
        return True
    except Exception as error:
        log(f"Could not start Minecraft server: {type(error).__name__}: {error}", "minecraft")
        return False


async def ensure_minecraft_server() -> bool:
    log(f"Checking local Minecraft server {MINECRAFT_HOST}:{MINECRAFT_PORT}...", "minecraft")
    if await test_local_minecraft():
        log("Local Minecraft server detected.", "minecraft")
        return True
    if not AUTO_START_SERVER or not normalized_path_setting(START_BAT):
        log("Minecraft is not running and automatic start is unavailable.", "minecraft")
        return False
    if not start_minecraft_server():
        return False

    timeout = max(1, int(SERVER_START_TIMEOUT))
    interval = max(0.5, float(SERVER_START_CHECK_INTERVAL))
    started_at = time.monotonic()
    log(f"Waiting for Minecraft server {MINECRAFT_HOST}:{MINECRAFT_PORT}...", "minecraft")
    while True:
        if await test_local_minecraft(timeout=2):
            log(f"Minecraft server online after {time.monotonic() - started_at:.1f}s.", "minecraft")
            return True
        if time.monotonic() - started_at >= timeout:
            log(f"Minecraft server did not start within {timeout}s.", "minecraft")
            return False
        await asyncio.sleep(interval)


class MinecraftConnection:
    def __init__(self, connection_id: str, websocket, send_lock: asyncio.Lock):
        self.id = connection_id
        self.websocket = websocket
        self.send_lock = send_lock
        self.reader = None
        self.writer = None
        self.read_task = None
        self.closed = False

    async def send_json(self, data: dict) -> None:
        async with self.send_lock:
            await self.websocket.send(_json(data))

    async def send_binary(self, data: bytes) -> None:
        async with self.send_lock:
            await self.websocket.send(data)

    async def open(self) -> bool:
        try:
            self.reader, self.writer = await asyncio.wait_for(
                asyncio.open_connection(MINECRAFT_HOST, int(MINECRAFT_PORT)), timeout=8)
            await self.send_json({"type": "tcp_open_result", "id": self.id, "ok": True})
            STATS["minecraft"]["players"] += 1
            STATS["minecraft"]["peak_players"] = max(
                STATS["minecraft"]["peak_players"], STATS["minecraft"]["players"])
            log(f"Player connected | {self.id[:8]}", "minecraft")
            self.read_task = asyncio.create_task(self.minecraft_to_hostish())
            return True
        except Exception as error:
            try:
                await self.send_json({"type": "tcp_open_result", "id": self.id,
                                      "ok": False, "error": str(error)})
            except Exception:
                pass
            log(f"Could not connect player to local Minecraft server: {error}", "minecraft")
            return False

    async def minecraft_to_hostish(self) -> None:
        try:
            connection_bytes = bytes.fromhex(self.id)
            if len(connection_bytes) != 16:
                raise ValueError("Hostish returned an invalid connection ID")
            prefix = MC_MAGIC + connection_bytes
            while not self.closed:
                data = await self.reader.read(TCP_CHUNK_SIZE)
                if not data:
                    break
                await self.send_binary(prefix + data)
        except asyncio.CancelledError:
            return
        except Exception as error:
            if not self.closed:
                log(f"Player connection error | {self.id[:8]} | {error}", "minecraft")
        finally:
            await self.close(notify_hostish=True)

    async def hostish_to_minecraft(self, data: bytes) -> None:
        if self.closed or self.writer is None or not data:
            return
        try:
            self.writer.write(data)
            await self.writer.drain()
        except Exception:
            await self.close(notify_hostish=True)

    async def close(self, notify_hostish: bool = False) -> None:
        if self.closed:
            return
        self.closed = True
        STATS["minecraft"]["players"] = max(0, STATS["minecraft"]["players"] - 1)

        if self.read_task is not None and self.read_task is not asyncio.current_task():
            self.read_task.cancel()
        if self.writer is not None:
            try:
                self.writer.close()
                await self.writer.wait_closed()
            except Exception:
                pass
        if notify_hostish:
            try:
                await self.send_json({"type": "tcp_close", "id": self.id})
            except Exception:
                pass
        log(f"Player disconnected | {self.id[:8]}", "minecraft")


class VoiceChatPeer:
    def __init__(self, peer_id: str, service_token: str, websocket, send_lock: asyncio.Lock):
        self.peer_id = peer_id
        self.service_token = service_token
        self.websocket = websocket
        self.send_lock = send_lock
        self.socket = None
        self.read_task = None
        self.closed = False

    async def send_binary(self, data: bytes) -> None:
        async with self.send_lock:
            await self.websocket.send(data)

    async def open(self) -> bool:
        if self.closed:
            return False
        if self.socket is not None:
            return True
        try:
            loop = asyncio.get_running_loop()
            family = socket.AF_INET6 if ":" in str(SIMPLE_VOICE_CHAT_HOST) else socket.AF_INET
            sock = socket.socket(family, socket.SOCK_DGRAM)
            sock.setblocking(False)
            await loop.sock_connect(sock, (str(SIMPLE_VOICE_CHAT_HOST), int(SIMPLE_VOICE_CHAT_PORT)))
            self.socket = sock
            STATS["minecraft"]["voice_peers"] += 1
            self.read_task = asyncio.create_task(self.local_to_hostish())
            return True
        except Exception as error:
            log(f"Voice chat peer {self.peer_id[:8]} could not reach the local UDP "
                f"server: {error}", "minecraft")
            await self.close()
            return False

    async def hostish_to_local(self, payload: bytes) -> None:
        if self.closed or not payload:
            return
        if self.socket is None and not await self.open():
            return
        try:
            await asyncio.get_running_loop().sock_sendall(self.socket, payload)
        except Exception as error:
            if not self.closed:
                log(f"Voice chat UDP send error | {self.peer_id[:8]} | {error}", "minecraft")
            await self.close()

    async def local_to_hostish(self) -> None:
        try:
            loop = asyncio.get_running_loop()
            prefix = (UDP_MAGIC + bytes.fromhex(self.service_token) + bytes.fromhex(self.peer_id))
            while not self.closed:
                payload = await loop.sock_recv(self.socket, UDP_MAX_DATAGRAM_BYTES)
                if not payload:
                    continue
                await self.send_binary(prefix + payload)
        except asyncio.CancelledError:
            return
        except Exception as error:
            if not self.closed:
                log(f"Voice chat UDP receive error | {self.peer_id[:8]} | {error}", "minecraft")
        finally:
            await self.close()

    async def close(self) -> None:
        if self.closed:
            return
        self.closed = True
        if self.socket is not None:
            STATS["minecraft"]["voice_peers"] = max(0, STATS["minecraft"]["voice_peers"] - 1)
        if self.read_task is not None and self.read_task is not asyncio.current_task():
            self.read_task.cancel()
        if self.socket is not None:
            try:
                self.socket.close()
            except Exception:
                pass
            self.socket = None


class VoiceChatTunnel:
    def __init__(self, websocket, send_lock: asyncio.Lock):
        self.websocket = websocket
        self.send_lock = send_lock
        self.enabled = bool(SIMPLE_VOICE_CHAT_ENABLED)
        self.service_id = str(SIMPLE_VOICE_CHAT_SERVICE_ID)
        self.service_token = ""
        self.public_host = ""
        self.public_port = 0
        self.peers: dict = {}

    async def send_json(self, data: dict) -> None:
        async with self.send_lock:
            await self.websocket.send(_json(data))

    async def request_open(self, requested_public_port: Optional[int] = None) -> None:
        if not self.enabled:
            return
        if requested_public_port is None:
            requested_public_port = int(SIMPLE_VOICE_CHAT_PUBLIC_PORT)
        if requested_public_port:
            log(f"Requesting Hostish UDP port {requested_public_port}...", "minecraft")
        else:
            log("Requesting a dynamic Hostish UDP port...", "minecraft")
        await self.send_json({"type": "udp_open", "service_id": self.service_id,
                              "requested_public_port": int(requested_public_port)})

    async def handle_open_result(self, event: dict) -> bool:
        if not self.enabled or str(event.get("service_id", "")) != self.service_id:
            return False
        if not event.get("ok"):
            log(f"Voice chat UDP could not be opened: "
                f"{event.get('error', 'unknown error')}", "minecraft")
            return False

        service_token = str(event.get("service_token", ""))
        try:
            token_bytes = bytes.fromhex(service_token)
        except Exception:
            token_bytes = b""
        if len(token_bytes) != 16:
            log("Voice chat UDP returned an invalid service token.", "minecraft")
            return False

        self.service_token = service_token
        self.public_host = str(event.get("public_host", ""))
        self.public_port = int(event.get("public_port", 0) or 0)
        log(f"Voice chat public address: {self.public_host}:{self.public_port}", "minecraft")
        return True

    async def handle_peer_open(self, event: dict) -> None:
        if not self.enabled or not self.service_token:
            return
        if str(event.get("service_id", "")) != self.service_id:
            return
        if str(event.get("service_token", "")) != self.service_token:
            return

        peer_id = str(event.get("peer_id", ""))
        try:
            if len(bytes.fromhex(peer_id)) != 16:
                return
        except Exception:
            return

        old = self.peers.pop(peer_id, None)
        if old is not None:
            await old.close()

        peer = VoiceChatPeer(peer_id, self.service_token, self.websocket, self.send_lock)
        self.peers[peer_id] = peer
        await peer.open()

    async def handle_peer_close(self, event: dict) -> None:
        peer = self.peers.pop(str(event.get("peer_id", "")), None)
        if peer is not None:
            await peer.close()

    async def handle_udp_closed(self, event: dict) -> None:
        if str(event.get("service_id", "")) != self.service_id:
            return
        await self.close_peers()
        self.service_token = ""
        self.public_host = ""
        self.public_port = 0
        if self.enabled:
            log(f"Voice chat UDP tunnel closed: {event.get('reason', 'closed')}", "minecraft")

    async def handle_binary(self, message: bytes) -> bool:
        if not self.enabled or not self.service_token:
            return False
        if len(message) < UDP_HEADER_SIZE or message[:2] != UDP_MAGIC:
            return False
        if message[2:18].hex() != self.service_token:
            return False

        peer_id = message[18:34].hex()
        payload = message[UDP_HEADER_SIZE:]
        if not payload:
            return True

        peer = self.peers.get(peer_id)
        if peer is None:
            peer = VoiceChatPeer(peer_id, self.service_token, self.websocket, self.send_lock)
            self.peers[peer_id] = peer
        await peer.hostish_to_local(payload)
        return True

    async def close_peers(self) -> None:
        if not self.peers:
            return
        peers = list(self.peers.values())
        self.peers.clear()
        await asyncio.gather(*(peer.close() for peer in peers), return_exceptions=True)

    async def close(self) -> None:
        await self.close_peers()
        self.service_token = ""
        self.public_host = ""
        self.public_port = 0


async def prepare_voice_chat_before_server(websocket, send_lock, voice_chat: VoiceChatTunnel):
    global _VOICE_CHAT_LAST_PUBLIC_PORT

    if not SIMPLE_VOICE_CHAT_ENABLED:
        return "", 0

    configured_port = int(SIMPLE_VOICE_CHAT_PUBLIC_PORT)
    requested_port = configured_port or int(_VOICE_CHAT_LAST_PUBLIC_PORT or 0)
    await voice_chat.request_open(requested_public_port=requested_port)

    loop = asyncio.get_running_loop()
    deadline = loop.time() + float(VOICE_CHAT_UDP_OPEN_TIMEOUT)
    tried_dynamic_fallback = False

    while True:
        remaining = deadline - loop.time()
        if remaining <= 0:
            raise RuntimeError("Timed out waiting for Hostish to allocate the voice chat UDP port.")

        message = await asyncio.wait_for(websocket.recv(), timeout=remaining)
        if isinstance(message, bytes):
            continue
        try:
            event = json.loads(message)
        except Exception:
            continue

        if event.get("type") == "ping":
            async with send_lock:
                await websocket.send(_json({"type": "pong"}))
            continue
        if event.get("type") != "udp_open_result":
            continue
        if str(event.get("service_id", "")) != str(SIMPLE_VOICE_CHAT_SERVICE_ID):
            continue

        if not await voice_chat.handle_open_result(event):
            if requested_port and not configured_port and not tried_dynamic_fallback:
                tried_dynamic_fallback = True
                requested_port = 0
                log("Previous UDP port could not be reclaimed; requesting a new one...", "minecraft")
                await voice_chat.request_open(requested_public_port=0)
                deadline = loop.time() + float(VOICE_CHAT_UDP_OPEN_TIMEOUT)
                continue
            raise RuntimeError("Hostish could not open the voice chat UDP service.")

        _VOICE_CHAT_LAST_PUBLIC_PORT = int(voice_chat.public_port)
        update_voice_chat_public_address(voice_chat.public_host, voice_chat.public_port)
        return voice_chat.public_host, voice_chat.public_port


async def send_homepage_state(websocket, send_lock) -> None:
    # The web tunnel owns https://USERNAME.hostish.site/ when it is enabled,
    # so the Minecraft homepage is only offered when web hosting is off.
    if WEB_ENABLED:
        enabled, html, path = False, "", ""
    else:
        enabled, html, path = load_homepage()

    async with send_lock:
        await websocket.send(_json({"type": "homepage_update", "enabled": enabled,
                                    "html": html if enabled else ""}))
    if enabled:
        log(f"Browser homepage sent to Hostish: {path}", "minecraft")


async def minecraft_connect_once() -> None:
    set_state("minecraft", "connecting")
    log("Connecting to Hostish...", "minecraft")

    async with websockets.connect(
        MINECRAFT_WS_URL, ssl=make_ssl_context(), max_size=MC_MAX_WEBSOCKET_MESSAGE,
        ping_interval=20, ping_timeout=20, open_timeout=15, close_timeout=5,
    ) as websocket:
        send_lock = asyncio.Lock()
        connections: dict = {}
        voice_chat = VoiceChatTunnel(websocket, send_lock)

        await websocket.send(_json({
            "username": str(USERNAME).strip(), "password": str(PASSWORD),
            "agent": "minecraft", "agent_version": MINECRAFT_AGENT_VERSION}))

        response_raw = await websocket.recv()
        if not isinstance(response_raw, str):
            raise RuntimeError("Invalid authentication response")
        response = json.loads(response_raw)
        if response.get("type") != "auth" or not response.get("ok"):
            set_state("minecraft", "error", str(response.get("error", "authentication failed")))
            raise RuntimeError(response.get("error", "Authentication failed"))

        STATS["minecraft"]["connections"] += 1
        hostname = response.get("hostname", f"{USERNAME}.hostish.site")
        port = int(response.get("port", 25565) or 25565)
        address = hostname if port == 25565 else f"{hostname}:{port}"
        set_state("minecraft", "online", address)
        log(f"Authenticated as {USERNAME}", "minecraft")
        log(f"Minecraft address: {address}", "minecraft")

        await send_homepage_state(websocket, send_lock)

        if SIMPLE_VOICE_CHAT_ENABLED:
            await prepare_voice_chat_before_server(websocket, send_lock, voice_chat)
            server_online = await ensure_minecraft_server()
        else:
            server_online = await test_local_minecraft()

        if not server_online:
            log("WARNING: Minecraft is not responding. The tunnel stays connected, "
                "but players cannot join yet.", "minecraft")

        log("Minecraft tunnel is online.", "minecraft")

        try:
            async for message in websocket:
                if isinstance(message, bytes):
                    if await voice_chat.handle_binary(message):
                        continue
                    if len(message) < MC_HEADER_SIZE or message[:2] != MC_MAGIC:
                        continue
                    connection = connections.get(message[2:18].hex())
                    if connection is not None:
                        await connection.hostish_to_minecraft(message[MC_HEADER_SIZE:])
                    continue

                try:
                    event = json.loads(message)
                except Exception:
                    continue

                event_type = event.get("type")

                if event_type == "udp_open_result":
                    await voice_chat.handle_open_result(event)
                elif event_type == "udp_peer_open":
                    await voice_chat.handle_peer_open(event)
                elif event_type == "udp_peer_close":
                    await voice_chat.handle_peer_close(event)
                elif event_type == "udp_closed":
                    await voice_chat.handle_udp_closed(event)
                elif event_type == "tcp_open":
                    connection_id = str(event.get("id", ""))
                    if not connection_id:
                        continue
                    old = connections.pop(connection_id, None)
                    if old is not None:
                        await old.close()
                    connection = MinecraftConnection(connection_id, websocket, send_lock)
                    connections[connection_id] = connection
                    if not await connection.open():
                        connections.pop(connection_id, None)
                elif event_type == "tcp_close":
                    connection = connections.pop(str(event.get("id", "")), None)
                    if connection is not None:
                        await connection.close()
                elif event_type == "ping":
                    async with send_lock:
                        await websocket.send(_json({"type": "pong"}))
        finally:
            await voice_chat.close()
            if connections:
                await asyncio.gather(*(c.close() for c in connections.values()),
                                     return_exceptions=True)


# ============================================================
# BUILT-IN WEBMAIL
# ============================================================
#
# A self-contained mail client, adapted from LocalEmail, that lives inside
# this agent. When WEBMAIL_ENABLED, it becomes the local mail store for the
# SMTP tunnel: inbound mail is written straight into per-user mailboxes here
# instead of being relayed to an external mail server, and outbound mail is
# sent through the same Hostish SMTP tunnel via _SMTP_AGENT_INSTANCE.

import hashlib
import hmac as _hmac
import uuid as _uuid
from email import policy as _email_policy
from email.message import EmailMessage as _EmailMessage
from email.parser import BytesParser as _BytesParser
from email.utils import formatdate as _formatdate, make_msgid as _make_msgid

_WEBMAIL_USERNAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
_WEBMAIL_SAFE_ID_RE = re.compile(r"^[a-f0-9-]{20,80}$")


def _webmail_domain() -> str:
    return str(WEBMAIL_DOMAIN or "").strip() or f"{USERNAME}.hostish.site"


def _webmail_data_dir() -> "os.PathLike":
    from pathlib import Path
    path = Path(os.path.expanduser(str(WEBMAIL_DATA_DIR or "./webmail-data")))
    path.mkdir(parents=True, exist_ok=True)
    return path


def _webmail_users_file():
    return _webmail_data_dir() / "users.json"


def _webmail_sessions_file():
    return _webmail_data_dir() / "sessions.json"


def _webmail_mailboxes_dir():
    path = _webmail_data_dir() / "mailboxes"
    path.mkdir(parents=True, exist_ok=True)
    return path


def _webmail_load_json(path, default):
    try:
        return json.loads(path.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return default


def _webmail_save_json(path, value) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(value, indent=2, ensure_ascii=False), "utf-8")
    tmp.replace(path)


def _webmail_hash_password(password: str, salt: bytes = None) -> str:
    salt = salt or secrets.token_bytes(16)
    iterations = 310_000
    digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
    return f"pbkdf2_sha256${iterations}${base64.b64encode(salt).decode()}${base64.b64encode(digest).decode()}"


def _webmail_verify_password(password: str, encoded: str) -> bool:
    try:
        algo, iters, salt_b64, digest_b64 = encoded.split("$", 3)
        if algo != "pbkdf2_sha256":
            return False
        salt = base64.b64decode(salt_b64)
        expected = base64.b64decode(digest_b64)
        actual = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, int(iters))
        return _hmac.compare_digest(actual, expected)
    except Exception:
        return False


def _webmail_normalize_username(value: str) -> str:
    return (value or "").strip().lower()


def _webmail_email_for(username: str) -> str:
    return f"{username}@{_webmail_domain()}"


def _webmail_mailbox_dir(username: str):
    path = _webmail_mailboxes_dir() / username
    (path / "inbox").mkdir(parents=True, exist_ok=True)
    (path / "sent").mkdir(parents=True, exist_ok=True)
    (path / "attachments").mkdir(parents=True, exist_ok=True)
    return path


def _webmail_index_file(username: str):
    return _webmail_mailbox_dir(username) / "index.json"


def _webmail_load_index(username: str) -> list:
    data = _webmail_load_json(_webmail_index_file(username), [])
    return data if isinstance(data, list) else []


def _webmail_save_index(username: str, items: list) -> None:
    _webmail_save_json(_webmail_index_file(username), items)


def webmail_get_users() -> dict:
    data = _webmail_load_json(_webmail_users_file(), {})
    return data if isinstance(data, dict) else {}


def _webmail_save_users(users: dict) -> None:
    _webmail_save_json(_webmail_users_file(), users)


def _webmail_get_sessions() -> dict:
    data = _webmail_load_json(_webmail_sessions_file(), {})
    now = time.time()
    changed = False
    for token, info in list(data.items()):
        if not isinstance(info, dict) or float(info.get("expires", 0)) <= now:
            data.pop(token, None)
            changed = True
    if changed:
        _webmail_save_json(_webmail_sessions_file(), data)
    return data


def _webmail_create_session(username: str) -> str:
    sessions = _webmail_get_sessions()
    token = secrets.token_urlsafe(32)
    sessions[token] = {"username": username, "expires": time.time() + int(WEBMAIL_SESSION_SECONDS),
                       "created": datetime.now().isoformat()}
    _webmail_save_json(_webmail_sessions_file(), sessions)
    return token


def _webmail_remove_session(token: str) -> None:
    sessions = _webmail_get_sessions()
    sessions.pop(token, None)
    _webmail_save_json(_webmail_sessions_file(), sessions)


def _webmail_clean_header(value) -> str:
    return " ".join(str(value or "").replace("\r", " ").replace("\n", " ").split())


def _webmail_plain_and_html(msg) -> tuple:
    plain_parts, html_parts = [], []
    parts = msg.walk() if msg.is_multipart() else [msg]
    for part in parts:
        if part.get_content_disposition() == "attachment":
            continue
        content_type = part.get_content_type()
        if content_type not in {"text/plain", "text/html"}:
            continue
        try:
            content = part.get_content()
        except Exception:
            continue
        if isinstance(content, str):
            (html_parts if content_type == "text/html" else plain_parts).append(content)
    return "\n".join(plain_parts).strip(), "\n".join(html_parts).strip()


def _webmail_save_attachments(username: str, message_id: str, msg) -> list:
    saved = []
    base = _webmail_mailbox_dir(username) / "attachments" / message_id
    for part in msg.walk():
        filename = part.get_filename()
        if not filename:
            continue
        filename = os.path.basename(filename).replace("\x00", "") or "attachment"
        payload = part.get_payload(decode=True) or b""
        if len(payload) > int(MAX_MESSAGE_BYTES):
            continue
        base.mkdir(parents=True, exist_ok=True)
        target = base / filename
        counter = 1
        while target.exists():
            stem, suffix = os.path.splitext(filename)
            target = base / f"{stem}_{counter}{suffix}"
            counter += 1
        target.write_bytes(payload)
        saved.append({"filename": target.name, "size": len(payload), "content_type": part.get_content_type()})
    return saved


def webmail_store_message(username: str, folder: str, raw: bytes,
                          envelope_from: str = "", envelope_to: list = None) -> dict:
    if folder not in {"inbox", "sent"}:
        raise ValueError("invalid folder")
    message_id = str(_uuid.uuid4())
    (_webmail_mailbox_dir(username) / folder / f"{message_id}.eml").write_bytes(raw)
    msg = _BytesParser(policy=_email_policy.default).parsebytes(raw)
    plain_text, html_text = _webmail_plain_and_html(msg)
    meta = {
        "id": message_id, "folder": folder, "received_at": datetime.now().isoformat(),
        "date": _webmail_clean_header(msg.get("Date")),
        "from": _webmail_clean_header(msg.get("From")) or envelope_from,
        "to": _webmail_clean_header(msg.get("To")) or ", ".join(envelope_to or []),
        "cc": _webmail_clean_header(msg.get("Cc")),
        "subject": _webmail_clean_header(msg.get("Subject")) or "(no subject)",
        "message_id": _webmail_clean_header(msg.get("Message-ID")),
        "preview": (plain_text or re.sub(r"<[^>]+>", " ", html_text))[:300],
        "has_html": bool(html_text),
        "attachments": _webmail_save_attachments(username, message_id, msg),
        "read": folder == "sent",
        "size": len(raw),
    }
    items = _webmail_load_index(username)
    items.insert(0, meta)
    _webmail_save_index(username, items)
    return meta


def _webmail_local_recipient_username(address: str):
    _name, addr = parseaddr(address)
    if "@" not in addr:
        return None
    local, domain = addr.rsplit("@", 1)
    if domain.lower() != _webmail_domain().lower():
        return None
    username = _webmail_normalize_username(local)
    return username if username in webmail_get_users() else None


def _webmail_first_admin():
    return next((u for u, info in webmail_get_users().items() if info.get("admin")), None)


def webmail_ensure_first_admin() -> None:
    if not WEBMAIL_ENABLED:
        return
    _webmail_mailboxes_dir()
    if webmail_get_users():
        return
    password = secrets.token_urlsafe(12)
    users = {"admin": {"password_hash": _webmail_hash_password(password), "admin": True,
                       "created": datetime.now().isoformat()}}
    _webmail_save_users(users)
    _webmail_mailbox_dir("admin")
    log("=" * 60, "mail")
    log("First-run webmail admin account created:", "mail")
    log("  Username : admin", "mail")
    log(f"  Password : {password}", "mail")
    log(f"  Address  : {_webmail_email_for('admin')}", "mail")
    log("  Save this password now -- it is only shown once.", "mail")
    log("=" * 60, "mail")


def webmail_deliver_inbound(mail_from: str, recipients: list, raw: bytes):
    """Called from the SMTP tunnel's inbound path when WEBMAIL_ENABLED."""
    try:
        if len(raw) > int(MAX_MESSAGE_BYTES):
            return False, "message too large"
        delivered = 0
        for recipient in recipients:
            username = _webmail_local_recipient_username(recipient)
            if username:
                webmail_store_message(username, "inbox", raw, mail_from, recipients)
                delivered += 1
        if delivered == 0 and not WEBMAIL_REJECT_UNKNOWN_RECIPIENTS:
            admin = _webmail_first_admin()
            if admin:
                webmail_store_message(admin, "inbox", raw, mail_from, recipients)
                delivered += 1
        if delivered:
            return True, ""
        return False, "no such local mailbox"
    except Exception as error:
        return False, f"{type(error).__name__}: {error}"


def _webmail_split_addresses(value: str) -> list:
    result = []
    for part in (value or "").replace(";", ",").split(","):
        addr = parseaddr(part.strip())[1]
        if addr and "@" in addr:
            result.append(addr)
    return result


async def webmail_send(username: str, to_value: str, cc_value: str, subject: str, body_text: str):
    to_list = _webmail_split_addresses(to_value)
    cc_list = _webmail_split_addresses(cc_value)
    recipients = list(dict.fromkeys(to_list + cc_list))
    if not recipients:
        return False, "At least one valid recipient is required", None
    if len(recipients) > int(MAX_RECIPIENTS):
        return False, "Too many recipients", None

    from_addr = _webmail_email_for(username)
    msg = _EmailMessage()
    msg["From"] = from_addr
    msg["To"] = ", ".join(to_list)
    if cc_list:
        msg["Cc"] = ", ".join(cc_list)
    msg["Subject"] = _webmail_clean_header(subject)[:500] or "(no subject)"
    msg["Date"] = _formatdate(localtime=True)
    msg["Message-ID"] = _make_msgid(domain=_webmail_domain())
    msg.set_content(body_text or "")
    raw = msg.as_bytes(policy=_email_policy.SMTP)
    if len(raw) > int(MAX_MESSAGE_BYTES):
        return False, "Message too large", None

    agent = _SMTP_AGENT_INSTANCE
    if agent is None:
        return False, "The mail tunnel is not connected right now", None

    ok, error = await agent.submit_outbound(from_addr, recipients, raw)
    if not ok:
        return False, f"Send failed: {error}", None

    saved = webmail_store_message(username, "sent", raw, from_addr, recipients)
    return True, "", saved


# ---- portable Hostish Mail client + public web gateway ----

PORTABLE_MAIL_CLIENT_HTML = r'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#101217" id="themeColorMeta">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no">
<title>Hostish Mail</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' x2='1' y1='0' y2='1'%3E%3Cstop stop-color='%237c8cff'/%3E%3Cstop offset='1' stop-color='%235865f2'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='16' fill='%23101217'/%3E%3Cpath d='M12 19c0-4 3-7 7-7h26c4 0 7 3 7 7v26c0 4-3 7-7 7H19c-4 0-7-3-7-7V19Z' fill='url(%23g)'/%3E%3Cpath d='m17 22 15 12 15-12v20c0 3-2 5-5 5H22c-3 0-5-2-5-5V22Z' fill='%23fff' fill-opacity='.94'/%3E%3Cpath d='m17 22 15 12 15-12' fill='none' stroke='%23c8ceff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
<style>
:root{
 --accent:#5865f2;--accent-rgb:88,101,242;--bg:#0f1115;--bg2:#151821;--panel:#191d27;--panel2:#202531;
 --text:#f5f7fb;--muted:#98a1b2;--border:rgba(255,255,255,.09);--danger:#f45b69;--good:#33c481;--warn:#f3b94f;
 --radius:18px;--shadow:0 24px 80px rgba(0,0,0,.32);--font-scale:1;--row-pad:15px;
 font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",sans-serif;
 color-scheme:dark;
}
:root[data-theme="light"]{--bg:#eef1f7;--bg2:#f8f9fc;--panel:#fff;--panel2:#f1f3f8;--text:#1c2130;--muted:#697386;--border:rgba(18,28,45,.10);--shadow:0 22px 60px rgba(24,35,56,.12);color-scheme:light}
@media(prefers-color-scheme:light){:root[data-theme="auto"]{--bg:#eef1f7;--bg2:#f8f9fc;--panel:#fff;--panel2:#f1f3f8;--text:#1c2130;--muted:#697386;--border:rgba(18,28,45,.10);--shadow:0 22px 60px rgba(24,35,56,.12);color-scheme:light}}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);font-size:calc(16px * var(--font-scale));overscroll-behavior:none}
body{min-height:100dvh;padding-top:env(safe-area-inset-top);padding-right:env(safe-area-inset-right);padding-bottom:env(safe-area-inset-bottom);padding-left:env(safe-area-inset-left);background:radial-gradient(circle at 12% -10%,rgba(var(--accent-rgb),.20),transparent 32rem),var(--bg)}
button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.55;cursor:not-allowed}
a{color:inherit}.hidden{display:none!important}
.view{min-height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));animation:fade .22s ease both}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@keyframes pop{from{opacity:0;transform:scale(.97) translateY(8px)}to{opacity:1;transform:none}}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(51,196,129,.28)}50%{box-shadow:0 0 0 8px rgba(51,196,129,0)}}
.loginWrap{min-height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));display:grid;place-items:center;padding:24px}
.loginCard{width:min(480px,100%);background:color-mix(in srgb,var(--panel) 96%,transparent);border:1px solid var(--border);border-radius:28px;padding:28px;box-shadow:var(--shadow);backdrop-filter:blur(18px);animation:pop .32s cubic-bezier(.2,.8,.2,1)}
.logo{display:flex;align-items:center;gap:13px;margin-bottom:10px}.logoMark{width:46px;height:46px;border-radius:14px;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 78%,#fff),var(--accent));display:grid;place-items:center;box-shadow:0 10px 25px rgba(var(--accent-rgb),.25)}
.logoMark svg{width:25px;height:25px}.brandName{font-size:1.45rem;font-weight:820;letter-spacing:-.035em}.tag{color:var(--muted);margin:0 0 24px;line-height:1.55}
label{display:block;font-size:.78rem;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin:14px 0 6px}
input,textarea,select{width:100%;border:1px solid var(--border);background:var(--panel2);color:var(--text);border-radius:13px;padding:12px 13px;outline:none;transition:border .18s,box-shadow .18s,transform .18s}
input:focus,textarea:focus,select:focus{border-color:color-mix(in srgb,var(--accent) 80%,#fff);box-shadow:0 0 0 4px rgba(var(--accent-rgb),.12)}
.passwordRow{position:relative}.passwordRow input{padding-right:72px}.ghostInside{position:absolute;right:7px;top:7px;width:auto;padding:6px 9px;border:0;background:transparent;color:var(--muted);font-size:.78rem;font-weight:800}
.checkRow{display:flex;gap:9px;align-items:center;margin:15px 0;color:var(--muted);font-size:.86rem}.checkRow input{width:18px;height:18px;accent-color:var(--accent);margin:0}
.primary,.secondary,.dangerBtn,.iconBtn{border:0;border-radius:13px;padding:11px 14px;font-weight:800;transition:transform .15s,filter .15s,background .15s}.primary{background:var(--accent);color:#fff;box-shadow:0 9px 25px rgba(var(--accent-rgb),.24)}.primary:hover{filter:brightness(1.07)}.primary:active,.secondary:active,.dangerBtn:active,.iconBtn:active{transform:scale(.98)}.secondary{background:var(--panel2);color:var(--text);border:1px solid var(--border)}.dangerBtn{background:rgba(244,91,105,.13);color:var(--danger);border:1px solid rgba(244,91,105,.18)}.iconBtn{width:42px;height:42px;padding:0;background:var(--panel2);color:var(--text);border:1px solid var(--border);display:grid;place-items:center}.wide{width:100%}
.statusLine{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:.82rem;margin-top:14px;min-height:20px}.statusDot{width:9px;height:9px;border-radius:50%;background:#596171;flex:none}.statusDot.online{background:var(--good);animation:pulse 2.3s infinite}.statusDot.warn{background:var(--warn)}.statusDot.offline{background:var(--danger)}
.errorText{color:var(--danger);font-size:.86rem;min-height:20px;margin-top:9px;white-space:pre-wrap}.small{font-size:.78rem;color:var(--muted)}
.shell{display:grid;grid-template-columns:230px 360px minmax(0,1fr);height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));overflow:hidden}.side,.listPane,.readerPane{min-width:0;background:color-mix(in srgb,var(--panel) 97%,transparent)}.side{border-right:1px solid var(--border);padding:18px 14px;display:flex;flex-direction:column;gap:12px}.side .logo{padding:3px 7px 10px;margin:0}.side .logoMark{width:38px;height:38px;border-radius:12px}.side .logoMark svg{width:21px}.side .brandName{font-size:1.05rem}.navBtn{width:100%;border:0;background:transparent;color:var(--muted);border-radius:13px;padding:11px 12px;display:flex;align-items:center;gap:10px;text-align:left;font-weight:760;transition:.16s}.navBtn:hover{background:var(--panel2);color:var(--text)}.navBtn.active{background:rgba(var(--accent-rgb),.15);color:color-mix(in srgb,var(--accent) 70%,#fff)}.navBtn svg{width:19px;height:19px;flex:none}.sideBottom{margin-top:auto;padding-top:12px;border-top:1px solid var(--border)}.identity{padding:4px 10px 12px;overflow:hidden}.identity strong,.identity span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.identity span{color:var(--muted);font-size:.75rem;margin-top:3px}
.listPane{border-right:1px solid var(--border);display:flex;flex-direction:column}.paneHead{min-height:76px;padding:15px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:12px}.paneTitle{font-size:1.18rem;font-weight:850;letter-spacing:-.025em}.messages{overflow:auto;flex:1}.messageRow{padding:var(--row-pad) 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .14s,transform .14s}.messageRow:hover{background:rgba(var(--accent-rgb),.07)}.messageRow.selected{background:rgba(var(--accent-rgb),.13)}.messageRow.unread .msgSubject,.messageRow.unread .msgWho{font-weight:850;color:var(--text)}.msgWho,.msgSubject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.msgWho{font-size:.87rem;color:var(--text);font-weight:720}.msgSubject{font-size:.9rem;margin-top:5px;color:color-mix(in srgb,var(--text) 90%,var(--muted))}.msgMeta{display:flex;justify-content:space-between;gap:8px;margin-top:7px;color:var(--muted);font-size:.72rem}.empty{padding:42px 22px;text-align:center;color:var(--muted)}
.readerPane{overflow:auto;background:var(--bg2)}.readerEmpty{height:100%;display:grid;place-items:center;color:var(--muted);text-align:center;padding:30px}.reader{padding:26px clamp(20px,4vw,52px);max-width:1050px;margin:auto}.readerTop{display:flex;gap:12px;justify-content:space-between;align-items:flex-start;border-bottom:1px solid var(--border);padding-bottom:18px}.reader h1{font-size:clamp(1.35rem,2.5vw,2rem);line-height:1.18;margin:3px 0 12px;letter-spacing:-.035em}.mailMeta{font-size:.84rem;line-height:1.65;color:var(--muted)}.mailMeta b{color:var(--text)}.readerBody{white-space:pre-wrap;word-break:break-word;line-height:1.7;padding:23px 0}.htmlFrame{width:100%;min-height:520px;background:white;border:1px solid var(--border);border-radius:14px;margin-top:20px}.attach{display:inline-flex;align-items:center;gap:7px;padding:8px 10px;border-radius:10px;background:var(--panel);border:1px solid var(--border);margin:8px 7px 0 0;color:var(--text);text-decoration:none;font-size:.78rem}
.mobileTop{display:none}.bottomNav{display:none}.badge{padding:4px 8px;border-radius:999px;background:rgba(var(--accent-rgb),.13);color:color-mix(in srgb,var(--accent) 65%,#fff);font-size:.7rem;font-weight:850}.serviceBadge{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:.76rem}
.modal{position:fixed;inset:0;background:rgba(3,5,9,.58);backdrop-filter:blur(8px);display:grid;place-items:center;padding:calc(18px + env(safe-area-inset-top)) calc(18px + env(safe-area-inset-right)) calc(18px + env(safe-area-inset-bottom)) calc(18px + env(safe-area-inset-left));z-index:50;animation:fade .16s ease}.modalCard{width:min(680px,100%);max-height:min(88dvh,820px);overflow:auto;background:var(--panel);border:1px solid var(--border);border-radius:24px;padding:22px;box-shadow:var(--shadow);animation:pop .2s ease}.modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.modalHead h2{margin:0;font-size:1.25rem}.modalActions{display:flex;gap:9px;justify-content:flex-end;flex-wrap:wrap;margin-top:18px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}.composeBody{min-height:260px;resize:vertical}.themeSwatches{display:flex;gap:8px;flex-wrap:wrap}.swatch{width:34px;height:34px;border-radius:50%;border:3px solid var(--panel);outline:1px solid var(--border);cursor:pointer}.toast{position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom));transform:translate(-50%,16px);background:var(--text);color:var(--bg);padding:10px 15px;border-radius:999px;font-size:.8rem;font-weight:800;opacity:0;pointer-events:none;transition:.2s;z-index:100;box-shadow:var(--shadow)}.toast.show{opacity:1;transform:translate(-50%,0)}
[data-density="compact"]{--row-pad:10px}[data-density="comfortable"]{--row-pad:18px}
@media(max-width:1040px){.shell{grid-template-columns:190px 330px minmax(0,1fr)}.side{padding-left:9px;padding-right:9px}.navBtn{padding-left:9px;padding-right:9px}}
@media(max-width:820px){.shell{grid-template-columns:84px minmax(0,1fr)}.side .brandName,.side .navBtn span,.identity{display:none}.side .logo{justify-content:center}.side .logoMark{margin:auto}.navBtn{justify-content:center;padding:11px}.listPane{border-right:0}.readerPane{position:fixed;inset:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) 84px;z-index:15;transform:translateX(0);transition:transform .22s;background:var(--bg2)}.readerPane.mobileHidden{transform:translateX(110%)}.mobileTop{display:inline-grid}}
@media(max-width:560px){body{background:var(--panel)}.shell{display:block;height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));padding-bottom:66px}.side{display:none}.listPane{height:100%;border:0}.paneHead{min-height:66px;padding:10px 13px}.readerPane{inset:env(safe-area-inset-top) env(safe-area-inset-right) calc(62px + env(safe-area-inset-bottom)) env(safe-area-inset-left);background:var(--bg2)}.reader{padding:18px 16px}.bottomNav{position:fixed;display:grid;grid-template-columns:repeat(4,1fr);left:env(safe-area-inset-left);right:env(safe-area-inset-right);bottom:env(safe-area-inset-bottom);height:62px;background:color-mix(in srgb,var(--panel) 94%,transparent);backdrop-filter:blur(18px);border-top:1px solid var(--border);z-index:30}.bottomNav button{border:0;background:transparent;color:var(--muted);font-size:.67rem;font-weight:780;display:grid;place-items:center;align-content:center;gap:2px}.bottomNav button.active{color:var(--accent)}.bottomNav svg{width:21px;height:21px}.loginWrap{padding:18px}.loginCard{padding:22px 18px;border-radius:22px}.grid2{grid-template-columns:1fr}.modal{padding-top:calc(12px + env(safe-area-inset-top));padding-bottom:calc(12px + env(safe-area-inset-bottom))}.modalCard{padding:18px;border-radius:20px;max-height:92dvh}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
</style>
</head>
<body>
<section id="loginView" class="view">
 <div class="loginWrap"><div class="loginCard">
  <div class="logo"><div class="logoMark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M3.5 6.5 12 13l8.5-6.5M5 5h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div><div class="brandName" data-brand>Hostish Mail</div><div class="small">Portable mail client</div></div></div>
  <p class="tag">Connect to any Hostish mail domain or linked domain. Your password is sent only to the Hostish agent you choose and is never saved by this file.</p>
  <label for="serverInput">Hostish domain or agent URL</label><input id="serverInput" type="text" inputmode="url" autocapitalize="none" autocomplete="url" placeholder="example.hostish.site">
  <div class="statusLine"><span id="loginDot" class="statusDot"></span><span id="loginStatus">Enter a server to check availability.</span></div>
  <label for="usernameInput">Account</label><input id="usernameInput" type="text" autocapitalize="none" autocomplete="username" placeholder="admin">
  <label for="passwordInput">Password</label><div class="passwordRow"><input id="passwordInput" type="password" autocomplete="current-password"><button class="ghostInside" id="showPasswordBtn" type="button">Show</button></div>
  <label class="checkRow"><input id="rememberInput" type="checkbox"><span>Keep me signed in on this device</span></label>
  <button id="loginBtn" class="primary wide">Sign in</button><div id="loginError" class="errorText"></div>
  <div class="modalActions" style="justify-content:space-between;margin-top:10px"><button id="loginCustomize" class="secondary" type="button">Customize</button><span class="small">Single-file webapp</span></div>
 </div></div>
</section>
<section id="appView" class="view hidden">
 <div class="shell">
  <aside class="side">
   <div class="logo"><div class="logoMark"><svg viewBox="0 0 24 24" fill="none"><path d="M3.5 6.5 12 13l8.5-6.5M5 5h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div><div class="brandName" data-brand>Hostish Mail</div><div class="serviceBadge"><span id="sideDot" class="statusDot"></span><span id="sideStatus">Checking</span></div></div></div>
   <button class="primary wide" id="composeBtn">Compose</button>
   <button class="navBtn active" data-folder="inbox"><svg viewBox="0 0 24 24" fill="none"><path d="M4 5h16v14H4zM4 13h4l2 3h4l2-3h4" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span>Inbox</span></button>
   <button class="navBtn" data-folder="sent"><svg viewBox="0 0 24 24" fill="none"><path d="m3 11 18-8-7 18-3-7-8-3Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span>Sent</span></button>
   <button class="navBtn" id="usersBtn"><svg viewBox="0 0 24 24" fill="none"><path d="M8 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm7-1a3 3 0 1 0 0-6M2 21v-3c0-3 2-5 6-5s6 2 6 5v3m2-8c3 0 5 2 5 5v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span>Accounts</span></button>
   <div class="sideBottom"><div class="identity"><strong id="identityName"></strong><span id="identityEmail"></span></div><button class="navBtn" id="settingsBtn"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM19 12l2-1-2-4-2 .5L15.5 6 15 4h-6l-.5 2L7 7.5 5 7l-2 4 2 1v2l-2 1 2 4 2-.5L8.5 20 9 22h6l.5-2 1.5-1.5 2 .5 2-4-2-1v-2Z" stroke="currentColor" stroke-width="1.45" stroke-linejoin="round"/></svg><span>Settings</span></button><button class="navBtn" id="logoutBtn"><svg viewBox="0 0 24 24" fill="none"><path d="M14 8V5H4v14h10v-3m-3-4h10m-3-3 3 3-3 3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Sign out</span></button></div>
  </aside>
  <main class="listPane"><div class="paneHead"><div><div id="folderTitle" class="paneTitle">Inbox</div><div id="folderSub" class="small">Loading messages…</div></div><div style="display:flex;gap:8px"><button id="refreshBtn" class="iconBtn" title="Refresh"><svg width="18" viewBox="0 0 24 24" fill="none"><path d="M20 11a8 8 0 1 0-2 5.5M20 4v7h-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button><button id="mobileCompose" class="iconBtn" title="Compose"><svg width="18" viewBox="0 0 24 24" fill="none"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-5 1 1-5L16.5 3.5Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></button></div></div><div id="messageList" class="messages"></div></main>
  <article id="readerPane" class="readerPane mobileHidden"><div id="reader" class="readerEmpty"><div><div style="font-size:2rem;opacity:.35">✉</div><div style="margin-top:8px">Select a message</div></div></div></article>
 </div>
 <nav class="bottomNav"><button class="active" data-mobile-folder="inbox"><svg viewBox="0 0 24 24" fill="none"><path d="M4 5h16v14H4zM4 13h4l2 3h4l2-3h4" stroke="currentColor" stroke-width="1.8"/></svg>Inbox</button><button data-mobile-folder="sent"><svg viewBox="0 0 24 24" fill="none"><path d="m3 11 18-8-7 18-3-7-8-3Z" stroke="currentColor" stroke-width="1.8"/></svg>Sent</button><button id="bottomCompose"><svg viewBox="0 0 24 24" fill="none"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-5 1 1-5L16.5 3.5Z" stroke="currentColor" stroke-width="1.8"/></svg>Compose</button><button id="bottomSettings"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z" stroke="currentColor" stroke-width="1.8"/></svg>Settings</button></nav>
</section>
<div id="modalRoot"></div><div id="toast" class="toast"></div>
<script>
(()=>{'use strict';
const $=id=>document.getElementById(id), $$=sel=>Array.from(document.querySelectorAll(sel));
const ICON_CLOSE='<svg width="18" viewBox="0 0 24 24" fill="none"><path d="m6 6 12 12M18 6 6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
const PREF_KEY='hostishPortableMailPrefs.v1', AUTH_KEY='hostishPortableMailAuth.v1';
let prefs={server:'',theme:'auto',accent:'#5865f2',density:'normal',brand:'Hostish Mail',fontScale:1};
let auth={token:'',apiBase:'',origin:'',remember:false};let me=null,currentFolder='inbox',selectedId='',pollTimer=null;
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function toast(msg){const t=$('toast');t.textContent=msg||'';t.classList.add('show');clearTimeout(t._timer);t._timer=setTimeout(()=>t.classList.remove('show'),2400)}
function loadPrefs(){try{prefs={...prefs,...JSON.parse(localStorage.getItem(PREF_KEY)||'{}')}}catch{};applyPrefs();$('serverInput').value=prefs.server||''}
function savePrefs(){localStorage.setItem(PREF_KEY,JSON.stringify(prefs))}
function hexRgb(hex){const m=String(hex).replace('#','').match(/^([0-9a-f]{6})$/i);if(!m)return [88,101,242];const n=parseInt(m[1],16);return[(n>>16)&255,(n>>8)&255,n&255]}
function applyPrefs(){const root=document.documentElement;root.dataset.theme=prefs.theme||'auto';root.dataset.density=prefs.density||'normal';root.style.setProperty('--accent',prefs.accent||'#5865f2');root.style.setProperty('--accent-rgb',hexRgb(prefs.accent||'#5865f2').join(','));root.style.setProperty('--font-scale',String(Number(prefs.fontScale)||1));$$('[data-brand]').forEach(e=>e.textContent=prefs.brand||'Hostish Mail');document.title=(prefs.brand||'Hostish Mail');$('themeColorMeta').content=root.dataset.theme==='light'?'#eef1f7':'#101217'}
function authStore(){const data=JSON.stringify(auth);if(auth.remember){localStorage.setItem(AUTH_KEY,data);sessionStorage.removeItem(AUTH_KEY)}else{sessionStorage.setItem(AUTH_KEY,data);localStorage.removeItem(AUTH_KEY)}}
function loadAuth(){let raw=sessionStorage.getItem(AUTH_KEY)||localStorage.getItem(AUTH_KEY)||'';try{const x=JSON.parse(raw);if(x&&x.token&&x.apiBase)auth={...auth,...x}}catch{}}
function clearAuth(){auth={token:'',apiBase:'',origin:'',remember:false};sessionStorage.removeItem(AUTH_KEY);localStorage.removeItem(AUTH_KEY)}
function normalizeOrigin(raw){let value=String(raw||'').trim();if(!value)throw new Error('Enter a Hostish domain or agent URL.');if(!/^https?:\/\//i.test(value))value='https://'+value;let u;try{u=new URL(value)}catch{throw new Error('That server address is not valid.')}return u.origin}
function timeoutFetch(url,opt={},ms=9000){const ctrl=new AbortController();const timer=setTimeout(()=>ctrl.abort(),ms);return fetch(url,{...opt,signal:ctrl.signal}).finally(()=>clearTimeout(timer))}
async function fetchJson(url,opt={}){const r=await timeoutFetch(url,opt);const txt=await r.text();let data;try{data=txt?JSON.parse(txt):{}}catch{data={error:txt||('HTTP '+r.status)}}if(!r.ok)throw Object.assign(new Error(data.error||data.message||('HTTP '+r.status)),{status:r.status,data});return data}
async function discover(raw,quiet=false){const origin=normalizeOrigin(raw);const candidates=[origin+'/.hostish/mail',origin+'/api/mail'];let last='Mail service not found.';for(const base of candidates){try{const status=await fetchJson(base+'/status',{headers:{Accept:'application/json'}},6500);if(status&&status.service==='hostish-mail'){return{origin,base,status}}last='Hostish mail API not available at '+origin}catch(e){last=e.name==='AbortError'?'Connection timed out.':e.message}}if(!quiet)throw new Error(last);return null}
function setService(dotId,textId,state,text){const dot=$(dotId);if(dot)dot.className='statusDot '+state;const label=$(textId);if(label)label.textContent=text}
function describeStatus(status){if(!status)return['offline','Offline'];if(status.online&&status.smtp_online)return['online','Online'];if(status.online)return['warn','Mailbox online · relay offline'];return['offline','Offline']}
async function updateStatus(){const raw=(auth.origin||prefs.server||$('serverInput').value).trim();if(!raw)return;try{const d=await discover(raw,true);if(!d)throw new Error('offline');if(auth.token&&!auth.apiBase)auth.apiBase=d.base;const [state,text]=describeStatus(d.status);setService('loginDot','loginStatus',state,text+(d.status.domain?' · '+d.status.domain:''));setService('sideDot','sideStatus',state,text)}catch{setService('loginDot','loginStatus','offline','Service unavailable');setService('sideDot','sideStatus','offline','Offline')}}
async function api(path,opt={}){if(!auth.apiBase)throw new Error('No mail server selected.');const headers={Accept:'application/json',...(opt.headers||{})};if(auth.token)headers.Authorization='Bearer '+auth.token;if(opt.body&&typeof opt.body!=='string'){headers['Content-Type']='application/json';opt.body=JSON.stringify(opt.body)}const r=await timeoutFetch(auth.apiBase+path,{...opt,headers},30000);const text=await r.text();let data;try{data=text?JSON.parse(text):{}}catch{data={error:text||('HTTP '+r.status)}}if(!r.ok){if(r.status===401&&path!='/login'){await signOut(false)}throw new Error(data.error||data.message||('HTTP '+r.status))}return data}
async function apiBlob(path){const r=await timeoutFetch(auth.apiBase+path,{headers:{Authorization:'Bearer '+auth.token}},30000);if(!r.ok)throw new Error(await r.text()||('HTTP '+r.status));return{blob:await r.blob(),disposition:r.headers.get('content-disposition')||''}}
async function login(){const btn=$('loginBtn'),err=$('loginError');err.textContent='';btn.disabled=true;btn.textContent='Connecting…';try{const found=await discover($('serverInput').value);prefs.server=found.origin;savePrefs();auth.origin=found.origin;auth.apiBase=found.base;const result=await fetchJson(found.base+'/login',{method:'POST',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({username:$('usernameInput').value.trim(),password:$('passwordInput').value})},15000);auth.token=result.token;auth.remember=$('rememberInput').checked;authStore();$('passwordInput').value='';await enterApp()}catch(e){err.textContent=e.name==='AbortError'?'Connection timed out.':e.message}finally{btn.disabled=false;btn.textContent='Sign in'}}
async function restore(){loadAuth();if(!auth.token)return false;try{me=await api('/me');prefs.server=auth.origin||prefs.server;savePrefs();return true}catch{return false}}
async function enterApp(){me=await api('/me');$('identityName').textContent=me.username;$('identityEmail').textContent=me.email;$('usersBtn').classList.toggle('hidden',!me.admin);$('loginView').classList.add('hidden');$('appView').classList.remove('hidden');currentFolder='inbox';selectedId='';await loadFolder('inbox');await updateStatus();clearInterval(pollTimer);pollTimer=setInterval(()=>{updateStatus();if(!document.hidden)refreshList(true).catch(()=>{})},20000)}
async function signOut(remote=true){if(remote&&auth.token){try{await api('/logout',{method:'POST'})}catch{}}clearInterval(pollTimer);clearAuth();me=null;$('appView').classList.add('hidden');$('loginView').classList.remove('hidden');$('passwordInput').value='';await updateStatus()}
function folderButtons(folder){$$('[data-folder]').forEach(b=>b.classList.toggle('active',b.dataset.folder===folder));$$('[data-mobile-folder]').forEach(b=>b.classList.toggle('active',b.dataset.mobileFolder===folder))}
async function loadFolder(folder){currentFolder=folder;selectedId='';folderButtons(folder);$('folderTitle').textContent=folder==='inbox'?'Inbox':'Sent';$('readerPane').classList.add('mobileHidden');$('reader').className='readerEmpty';$('reader').innerHTML='<div><div style="font-size:2rem;opacity:.35">✉</div><div style="margin-top:8px">Select a message</div></div>';await refreshList(false)}
async function refreshList(silent=false){if(!silent)$('folderSub').textContent='Loading messages…';try{const items=await api('/messages?folder='+encodeURIComponent(currentFolder));renderList(items);$('folderSub').textContent=items.length+(items.length===1?' message':' messages')}catch(e){$('folderSub').textContent='Could not refresh';if(!silent)toast(e.message)}}
function renderList(items){const box=$('messageList');if(!items.length){box.innerHTML='<div class="empty">No messages here yet.</div>';return}box.innerHTML=items.map(m=>{const who=currentFolder==='inbox'?m.from:m.to;const date=m.date||m.received_at||'';return`<article class="messageRow ${m.read?'':'unread'} ${m.id===selectedId?'selected':''}" data-msg="${esc(m.id)}"><div class="msgWho">${esc(who||'(unknown)')}</div><div class="msgSubject">${esc(m.subject||'(no subject)')}</div><div class="msgMeta"><span>${esc((m.preview||'').slice(0,55))}</span><span>${esc(date)}</span></div></article>`}).join('');$$('[data-msg]').forEach(row=>row.onclick=()=>openMessage(row.dataset.msg))}
async function openMessage(id){selectedId=id;$$('[data-msg]').forEach(r=>r.classList.toggle('selected',r.dataset.msg===id));$('readerPane').classList.remove('mobileHidden');$('reader').className='reader';$('reader').innerHTML='<div class="empty">Loading message…</div>';try{const m=await api('/message/'+encodeURIComponent(id));const at=(m.attachments||[]).map(a=>`<a href="#" class="attach" data-attach="${esc(a.filename)}" data-mid="${esc(m.id)}">↧ ${esc(a.filename)} <span class="small">${Math.ceil((a.size||0)/1024)} KB</span></a>`).join('');const body=m.html?'<iframe id="htmlFrame" class="htmlFrame" sandbox="allow-popups allow-popups-to-escape-sandbox"></iframe>':`<div class="readerBody">${esc(m.text||'')}</div>`;$('reader').innerHTML=`<div class="readerTop"><div><button class="secondary mobileTop" id="backReader" style="margin-bottom:12px">← Back</button><h1>${esc(m.subject||'(no subject)')}</h1><div class="mailMeta"><div><b>From:</b> ${esc(m.from)}</div><div><b>To:</b> ${esc(m.to)}</div>${m.cc?`<div><b>Cc:</b> ${esc(m.cc)}</div>`:''}<div>${esc(m.date||m.received_at||'')}</div></div>${at}</div><button class="dangerBtn" id="deleteMessageBtn">Delete</button></div>${body}`;if(m.html)$('htmlFrame').srcdoc=m.html;$('backReader')?.addEventListener('click',()=>$('readerPane').classList.add('mobileHidden'));$('deleteMessageBtn').onclick=()=>deleteMessage(m.id);$$('[data-attach]').forEach(a=>a.onclick=async e=>{e.preventDefault();await downloadAttachment(a.dataset.mid,a.dataset.attach)});await refreshList(true)}catch(e){$('reader').innerHTML=`<div class="empty">${esc(e.message)}</div>`}}
async function deleteMessage(id){if(!confirm('Delete this message?'))return;try{await api('/message/'+encodeURIComponent(id),{method:'DELETE'});$('readerPane').classList.add('mobileHidden');selectedId='';await refreshList(false);toast('Message deleted')}catch(e){toast(e.message)}}
async function downloadAttachment(mid,name){try{const r=await apiBlob('/attachment/'+encodeURIComponent(mid)+'/'+encodeURIComponent(name));const url=URL.createObjectURL(r.blob),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1200)}catch(e){toast(e.message)}}
function modal(title,body,actions=''){const root=$('modalRoot');root.innerHTML=`<div class="modal" id="modalBackdrop"><section class="modalCard" role="dialog" aria-modal="true"><div class="modalHead"><h2>${esc(title)}</h2><button class="iconBtn" id="modalClose">${ICON_CLOSE}</button></div>${body}${actions?`<div class="modalActions">${actions}</div>`:''}</section></div>`;$('modalClose').onclick=closeModal;$('modalBackdrop').onclick=e=>{if(e.target.id==='modalBackdrop')closeModal()}}
function closeModal(){$('modalRoot').innerHTML=''}
function openCompose(){modal('New message',`<label>To</label><input id="composeTo" inputmode="email" autocomplete="off" placeholder="person@example.com"><label>Cc</label><input id="composeCc" inputmode="email" autocomplete="off"><label>Subject</label><input id="composeSubject"><label>Message</label><textarea id="composeBody" class="composeBody"></textarea><div id="composeError" class="errorText"></div>`,`<button class="secondary" id="composeCancel">Cancel</button><button class="primary" id="composeSend">Send</button>`);$('composeCancel').onclick=closeModal;$('composeSend').onclick=sendMessage;setTimeout(()=>$('composeTo')?.focus(),30)}
async function sendMessage(){const b=$('composeSend'),e=$('composeError');e.textContent='';b.disabled=true;b.textContent='Sending…';try{await api('/send',{method:'POST',body:{to:$('composeTo').value,cc:$('composeCc').value,subject:$('composeSubject').value,body:$('composeBody').value}});closeModal();toast('Message sent');if(currentFolder==='sent')await refreshList(false)}catch(x){e.textContent=x.message}finally{if(b){b.disabled=false;b.textContent='Send'}}}
async function openUsers(){if(!me?.admin)return;modal('Mailbox accounts','<div id="usersList" class="small">Loading…</div><h3 style="margin-top:22px">Create account</h3><div class="grid2"><div><label>Username</label><input id="newUser"></div><div><label>Password</label><input id="newPass" type="password"></div></div><label class="checkRow"><input id="newAdmin" type="checkbox"><span>Administrator</span></label><div id="userError" class="errorText"></div>',`<button class="secondary" id="usersClose">Close</button><button class="primary" id="userAdd">Create</button>`);$('usersClose').onclick=closeModal;$('userAdd').onclick=addUser;await refreshUsers()}
async function refreshUsers(){try{const users=await api('/users');$('usersList').innerHTML=users.map(u=>`<div style="display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--border)"><div style="flex:1;min-width:0"><strong style="color:var(--text)">${esc(u.username)}</strong><div class="small">${esc(u.email)}${u.admin?' · administrator':''}</div></div>${u.username!==me.username?`<button class="dangerBtn" data-del-user="${esc(u.username)}">Delete</button>`:''}</div>`).join('')||'No accounts';$$('[data-del-user]').forEach(b=>b.onclick=()=>deleteUser(b.dataset.delUser))}catch(e){$('usersList').textContent=e.message}}
async function addUser(){const err=$('userError');err.textContent='';try{await api('/users',{method:'POST',body:{username:$('newUser').value,password:$('newPass').value,admin:$('newAdmin').checked}});$('newUser').value='';$('newPass').value='';$('newAdmin').checked=false;await refreshUsers();toast('Account created')}catch(e){err.textContent=e.message}}
async function deleteUser(user){if(!confirm('Delete '+user+'? Mailbox files are kept on the host.'))return;try{await api('/users/'+encodeURIComponent(user),{method:'DELETE'});await refreshUsers();toast('Account removed')}catch(e){toast(e.message)}}
function openSettings(){const swatches=['#5865f2','#7c5cff','#1fa7a0','#2aa76b','#d28b2c','#d65776'];modal('Settings',`<div class="grid2"><div><label>Theme</label><select id="prefTheme"><option value="auto">System</option><option value="dark">Dark</option><option value="light">Light</option></select></div><div><label>Message spacing</label><select id="prefDensity"><option value="compact">Compact</option><option value="normal">Normal</option><option value="comfortable">Comfortable</option></select></div><div><label>App name</label><input id="prefBrand" maxlength="32"></div><div><label>Text size</label><select id="prefScale"><option value="0.9">90%</option><option value="1">100%</option><option value="1.1">110%</option><option value="1.2">120%</option></select></div></div><label>Accent</label><div class="themeSwatches">${swatches.map(c=>`<button class="swatch" data-color="${c}" style="background:${c}" title="${c}"></button>`).join('')}<input id="prefAccent" type="color" style="width:46px;height:36px;padding:2px"></div><h3 style="margin-top:24px">Account password</h3><div class="grid2"><div><label>Current password</label><input id="oldPassword" type="password"></div><div><label>New password</label><input id="newPassword" type="password" placeholder="8+ characters"></div></div><div id="settingsError" class="errorText"></div><div class="small" style="margin-top:12px">Appearance and server preferences are stored only in this browser using localStorage. Your password is never stored.</div>`,`<button class="secondary" id="settingsCancel">Close</button><button class="secondary" id="passwordChange">Change password</button><button class="primary" id="settingsSave">Save appearance</button>`);$('prefTheme').value=prefs.theme;$('prefDensity').value=prefs.density;$('prefBrand').value=prefs.brand;$('prefScale').value=String(prefs.fontScale);$('prefAccent').value=prefs.accent;$('settingsCancel').onclick=closeModal;$('settingsSave').onclick=()=>{prefs.theme=$('prefTheme').value;prefs.density=$('prefDensity').value;prefs.brand=$('prefBrand').value.trim()||'Hostish Mail';prefs.fontScale=Number($('prefScale').value)||1;prefs.accent=$('prefAccent').value;savePrefs();applyPrefs();toast('Appearance saved');closeModal()};$$('[data-color]').forEach(b=>b.onclick=()=>{$('prefAccent').value=b.dataset.color});$('passwordChange').onclick=changePassword}
async function changePassword(){const err=$('settingsError');err.textContent='';const oldp=$('oldPassword').value,newp=$('newPassword').value;if(!oldp||newp.length<8){err.textContent='Enter your current password and a new password of at least 8 characters.';return}try{await api('/change-password',{method:'POST',body:{old_password:oldp,new_password:newp}});$('oldPassword').value=$('newPassword').value='';toast('Password changed')}catch(e){err.textContent=e.message}}
function openLoginCustomize(){const wasLogged=!!me;modal('Customize',`<div class="grid2"><div><label>Theme</label><select id="prefTheme"><option value="auto">System</option><option value="dark">Dark</option><option value="light">Light</option></select></div><div><label>App name</label><input id="prefBrand" maxlength="32"></div></div><label>Accent</label><div class="themeSwatches"><input id="prefAccent" type="color" style="width:54px;height:40px;padding:2px"></div><div class="small" style="margin-top:15px">Saved on this device with localStorage.</div>`,`<button class="secondary" id="customClose">Cancel</button><button class="primary" id="customSave">Save</button>`);$('prefTheme').value=prefs.theme;$('prefBrand').value=prefs.brand;$('prefAccent').value=prefs.accent;$('customClose').onclick=closeModal;$('customSave').onclick=()=>{prefs.theme=$('prefTheme').value;prefs.brand=$('prefBrand').value.trim()||'Hostish Mail';prefs.accent=$('prefAccent').value;savePrefs();applyPrefs();closeModal();if(wasLogged)toast('Appearance saved')}}
$('loginBtn').onclick=login;$('passwordInput').addEventListener('keydown',e=>{if(e.key==='Enter')login()});$('serverInput').addEventListener('change',()=>updateStatus());$('serverInput').addEventListener('blur',()=>updateStatus());$('showPasswordBtn').onclick=()=>{const p=$('passwordInput');p.type=p.type==='password'?'text':'password';$('showPasswordBtn').textContent=p.type==='password'?'Show':'Hide'};$('loginCustomize').onclick=openLoginCustomize;
$('composeBtn').onclick=openCompose;$('mobileCompose').onclick=openCompose;$('bottomCompose').onclick=openCompose;$('refreshBtn').onclick=()=>refreshList(false);$('logoutBtn').onclick=()=>signOut(true);$('settingsBtn').onclick=openSettings;$('bottomSettings').onclick=openSettings;$('usersBtn').onclick=openUsers;$$('[data-folder]').forEach(b=>b.onclick=()=>loadFolder(b.dataset.folder));$$('[data-mobile-folder]').forEach(b=>b.onclick=()=>loadFolder(b.dataset.mobileFolder));
window.addEventListener('online',updateStatus);window.addEventListener('offline',()=>{setService('loginDot','loginStatus','offline','Device offline');setService('sideDot','sideStatus','offline','Offline')});document.addEventListener('visibilitychange',()=>{if(!document.hidden){updateStatus();if(auth.token)refreshList(true).catch(()=>{})}});
(async()=>{loadPrefs();loadAuth();if(prefs.server)$('serverInput').value=prefs.server;await updateStatus();if(await restore()){await enterApp()}else{clearAuth();$('loginView').classList.remove('hidden');$('appView').classList.add('hidden')}})();
})();
</script>
</body>
</html>
'''
PORTABLE_MAIL_PUBLIC_PREFIX = ".hostish/mail"
PORTABLE_MAIL_MAX_JSON_BYTES = 2 * 1024 * 1024


def _mail_client_template_path() -> str:
    raw = str(MAIL_CLIENT_TEMPLATE_FILE or "hostish-mail-client.html").strip()
    raw = os.path.expanduser(raw or "hostish-mail-client.html")
    if os.path.isabs(raw):
        return os.path.abspath(raw)
    try:
        base = os.path.dirname(os.path.abspath(__file__))
    except Exception:
        base = os.getcwd()
    return os.path.abspath(os.path.join(base, raw))


def _mail_client_source() -> tuple:
    path = _mail_client_template_path()
    if os.path.isfile(path):
        try:
            return Path(path).read_text("utf-8"), True, path
        except Exception as error:
            log(f"Could not read custom mail client template {path}: {error}", "mail")
    return PORTABLE_MAIL_CLIENT_HTML, False, path


def _export_mail_client_template(overwrite: bool = False) -> tuple:
    path = _mail_client_template_path()
    if os.path.isfile(path) and not overwrite:
        return False, f"Template already exists: {path}", path
    Path(path).parent.mkdir(parents=True, exist_ok=True)
    Path(path).write_text(PORTABLE_MAIL_CLIENT_HTML, "utf-8")
    return True, f"Editable mail client template written to {path}", path


def _mail_cors_headers(extra: Optional[dict] = None) -> dict:
    out = {
        "access-control-allow-origin": "*",
        "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
        "access-control-allow-headers": "Content-Type, Authorization",
        "access-control-expose-headers": "Content-Disposition, Content-Type",
        "access-control-max-age": "86400",
        "cache-control": "no-store",
    }
    out.update(extra or {})
    return out


def _portable_mail_api_route(rel_path: str) -> Optional[str]:
    value = str(rel_path or "").replace("\\", "/").lstrip("/")
    prefix = PORTABLE_MAIL_PUBLIC_PREFIX
    if value == prefix or value == prefix + "/":
        return "/api/mail/status"
    if not value.startswith(prefix + "/"):
        return None
    tail = value[len(prefix) + 1:].strip("/")
    if not tail:
        tail = "status"
    return "/api/mail/" + tail


def _portable_mail_query(raw_query: str) -> dict:
    return {k: v[0] for k, v in urllib.parse.parse_qs(str(raw_query or ""), keep_blank_values=True).items()}


async def _portable_mail_dispatch(method: str, api_route: str, headers: dict, raw_query: str, body: bytes):
    method = str(method or "GET").upper()
    if method == "OPTIONS":
        return 204, b"", "text/plain; charset=utf-8", _mail_cors_headers()
    payload = None
    if body:
        if len(body) > PORTABLE_MAIL_MAX_JSON_BYTES:
            return 413, {"error": "request body too large"}, "application/json", _mail_cors_headers()
        try:
            payload = json.loads(body.decode("utf-8"))
        except Exception:
            return 400, {"error": "invalid JSON"}, "application/json", _mail_cors_headers()
    status, response_body, content_type, extra = await webmail_api(
        method, api_route, headers, _portable_mail_query(raw_query), payload)
    return status, response_body, content_type, _mail_cors_headers(extra)


def _portable_mail_encode_body(value) -> bytes:
    if isinstance(value, (bytes, bytearray)):
        return bytes(value)
    return _json(value).encode("utf-8")


# ---- webmail HTTP API (mounted under /webmail and /api/mail/*) ----

def _webmail_token_from_headers(headers: dict) -> str:
    auth = str(headers.get("authorization", ""))
    return auth[7:].strip() if auth.lower().startswith("bearer ") else ""


class WebmailHTTPError(Exception):
    def __init__(self, status: int, message: str):
        super().__init__(message)
        self.status = status
        self.message = message


def _webmail_require_user(headers: dict):
    token = _webmail_token_from_headers(headers)
    session = _webmail_get_sessions().get(token)
    if not session:
        raise WebmailHTTPError(401, "Not logged in")
    username = session.get("username", "")
    info = webmail_get_users().get(username)
    if not info:
        raise WebmailHTTPError(401, "Account no longer exists")
    return username, info


def _webmail_require_admin(headers: dict):
    username, info = _webmail_require_user(headers)
    if not info.get("admin"):
        raise WebmailHTTPError(403, "Admin required")
    return username, info


async def webmail_api(method: str, route: str, headers: dict, query: dict, payload) -> tuple:
    """Returns (status, json_body_or_bytes, content_type, extra_headers)."""
    if route == "/api/mail/status" and method == "GET":
        smtp_online = bool(
            _SMTP_AGENT_INSTANCE is not None
            and getattr(_SMTP_AGENT_INSTANCE, "connected_event", None) is not None
            and _SMTP_AGENT_INSTANCE.connected_event.is_set()
        )
        return 200, {
            "service": "hostish-mail",
            "online": bool(WEBMAIL_ENABLED),
            "smtp_online": smtp_online,
            "domain": _webmail_domain() if WEBMAIL_ENABLED else "",
            "version": version,
        }, "application/json", {}

    if not WEBMAIL_ENABLED:
        return 404, {"error": "webmail is disabled"}, "application/json", {}

    try:
        if route == "/api/mail/login" and method == "POST":
            username = _webmail_normalize_username(str((payload or {}).get("username", "")))
            password = str((payload or {}).get("password", ""))
            info = webmail_get_users().get(username)
            if not info or not await asyncio.to_thread(
                    _webmail_verify_password, password, str(info.get("password_hash", ""))):
                await asyncio.sleep(0.15)
                raise WebmailHTTPError(401, "Invalid username or password")
            token = await asyncio.to_thread(_webmail_create_session, username)
            return 200, {"token": token, "username": username, "email": _webmail_email_for(username),
                        "admin": bool(info.get("admin"))}, "application/json", {}

        if route == "/api/mail/logout" and method == "POST":
            token = _webmail_token_from_headers(headers)
            if token:
                await asyncio.to_thread(_webmail_remove_session, token)
            return 200, {"ok": True}, "application/json", {}

        if route == "/api/mail/me" and method == "GET":
            username, info = _webmail_require_user(headers)
            return 200, {"username": username, "email": _webmail_email_for(username),
                        "admin": bool(info.get("admin")), "domain": _webmail_domain()}, "application/json", {}

        if route == "/api/mail/messages" and method == "GET":
            username, _info = _webmail_require_user(headers)
            folder = query.get("folder", "inbox")
            if folder not in {"inbox", "sent"}:
                raise WebmailHTTPError(400, "Invalid folder")
            items = await asyncio.to_thread(_webmail_load_index, username)
            return 200, [x for x in items if x.get("folder") == folder], "application/json", {}

        if route.startswith("/api/mail/message/") and method == "GET":
            username, _info = _webmail_require_user(headers)
            message_id = route.rsplit("/", 1)[-1]
            if not _WEBMAIL_SAFE_ID_RE.fullmatch(message_id):
                raise WebmailHTTPError(404, "Not found")
            items = await asyncio.to_thread(_webmail_load_index, username)
            meta = next((x for x in items if x.get("id") == message_id), None)
            if not meta:
                raise WebmailHTTPError(404, "Message not found")
            path = _webmail_mailbox_dir(username) / meta["folder"] / f"{message_id}.eml"
            if not path.is_file():
                raise WebmailHTTPError(404, "Message file missing")
            raw = await asyncio.to_thread(path.read_bytes)
            msg = _BytesParser(policy=_email_policy.default).parsebytes(raw)
            plain_text, html_text = _webmail_plain_and_html(msg)
            if not meta.get("read"):
                meta["read"] = True
                await asyncio.to_thread(_webmail_save_index, username, items)
            result = dict(meta)
            result["text"] = plain_text
            result["html"] = html_text
            return 200, result, "application/json", {}

        if route.startswith("/api/mail/message/") and method == "DELETE":
            username, _info = _webmail_require_user(headers)
            message_id = route.rsplit("/", 1)[-1]
            if not _WEBMAIL_SAFE_ID_RE.fullmatch(message_id):
                raise WebmailHTTPError(404, "Not found")
            items = await asyncio.to_thread(_webmail_load_index, username)
            meta = next((x for x in items if x.get("id") == message_id), None)
            if not meta:
                raise WebmailHTTPError(404, "Message not found")

            def do_delete():
                _webmail_save_index(username, [x for x in items if x.get("id") != message_id])
                (_webmail_mailbox_dir(username) / meta["folder"] / f"{message_id}.eml").unlink(missing_ok=True)
                attach_dir = _webmail_mailbox_dir(username) / "attachments" / message_id
                if attach_dir.exists():
                    for entry in attach_dir.iterdir():
                        if entry.is_file():
                            entry.unlink(missing_ok=True)
                    try:
                        attach_dir.rmdir()
                    except OSError:
                        pass

            await asyncio.to_thread(do_delete)
            return 200, {"ok": True}, "application/json", {}

        if route.startswith("/api/mail/attachment/") and method == "GET":
            username, _info = _webmail_require_user(headers)
            tail = route[len("/api/mail/attachment/"):]
            if "/" not in tail:
                raise WebmailHTTPError(404, "Not found")
            message_id, filename = tail.split("/", 1)
            filename = os.path.basename(urllib.parse.unquote(filename))
            if not _WEBMAIL_SAFE_ID_RE.fullmatch(message_id) or not filename:
                raise WebmailHTTPError(404, "Not found")
            items = await asyncio.to_thread(_webmail_load_index, username)
            meta = next((x for x in items if x.get("id") == message_id), None)
            if not meta or filename not in {a.get("filename") for a in meta.get("attachments", [])}:
                raise WebmailHTTPError(404, "Not found")
            path = _webmail_mailbox_dir(username) / "attachments" / message_id / filename
            if not path.is_file():
                raise WebmailHTTPError(404, "Not found")
            data = await asyncio.to_thread(path.read_bytes)
            return 200, data, guess_content_type(filename), {
                "content-disposition": f'attachment; filename="{filename}"'}

        if route == "/api/mail/send" and method == "POST":
            username, _info = _webmail_require_user(headers)
            body = payload or {}
            ok, message, saved = await webmail_send(
                username, str(body.get("to", "")), str(body.get("cc", "")),
                str(body.get("subject", "")), str(body.get("body", "")))
            if not ok:
                raise WebmailHTTPError(400 if "recipient" in message.lower() else 502, message)
            log(f"Webmail: {username} sent mail to {body.get('to','')}", "mail")
            return 200, {"ok": True, "message": saved}, "application/json", {}

        if route == "/api/mail/users" and method == "GET":
            _webmail_require_admin(headers)
            users = webmail_get_users()
            return 200, [{"username": u, "email": _webmail_email_for(u), "admin": bool(i.get("admin")),
                         "created": i.get("created", "")} for u, i in sorted(users.items())], \
                "application/json", {}

        if route == "/api/mail/users" and method == "POST":
            _webmail_require_admin(headers)
            body = payload or {}
            username = _webmail_normalize_username(str(body.get("username", "")))
            password = str(body.get("password", ""))
            if not _WEBMAIL_USERNAME_RE.fullmatch(username):
                raise WebmailHTTPError(400, "Username may contain letters, numbers, dot, underscore and dash")
            if len(password) < 8:
                raise WebmailHTTPError(400, "Password must be at least 8 characters")
            users = webmail_get_users()
            if username in users:
                raise WebmailHTTPError(409, "User already exists")
            users[username] = {"password_hash": _webmail_hash_password(password),
                               "admin": bool(body.get("admin", False)), "created": datetime.now().isoformat()}
            await asyncio.to_thread(_webmail_save_users, users)
            await asyncio.to_thread(_webmail_mailbox_dir, username)
            log(f"Webmail: account created for {username}", "mail")
            return 200, {"ok": True, "username": username, "email": _webmail_email_for(username)}, \
                "application/json", {}

        if route.startswith("/api/mail/users/") and method == "DELETE":
            actor, _info = _webmail_require_admin(headers)
            target = _webmail_normalize_username(route.rsplit("/", 1)[-1])
            if target == actor:
                raise WebmailHTTPError(400, "You cannot delete your own account while logged in")
            users = webmail_get_users()
            if target not in users:
                raise WebmailHTTPError(404, "User not found")
            if users[target].get("admin") and sum(1 for u in users.values() if u.get("admin")) <= 1:
                raise WebmailHTTPError(400, "Cannot delete the last administrator")
            users.pop(target)
            await asyncio.to_thread(_webmail_save_users, users)
            sessions = {t: s for t, s in _webmail_get_sessions().items() if s.get("username") != target}
            await asyncio.to_thread(_webmail_save_json, _webmail_sessions_file(), sessions)
            log(f"Webmail: account removed for {target}", "mail")
            return 200, {"ok": True, "note": "Account removed; mailbox files retained on disk"}, \
                "application/json", {}

        if route == "/api/mail/change-password" and method == "POST":
            username, _info = _webmail_require_user(headers)
            body = payload or {}
            users = webmail_get_users()
            info = users[username]
            if not await asyncio.to_thread(
                    _webmail_verify_password, str(body.get("old_password", "")), info.get("password_hash", "")):
                raise WebmailHTTPError(401, "Current password is incorrect")
            new_password = str(body.get("new_password", ""))
            if len(new_password) < 8:
                raise WebmailHTTPError(400, "New password must be at least 8 characters")
            info["password_hash"] = _webmail_hash_password(new_password)
            users[username] = info
            await asyncio.to_thread(_webmail_save_users, users)
            return 200, {"ok": True}, "application/json", {}

        return 404, {"error": "not found"}, "application/json", {}

    except WebmailHTTPError as error:
        return error.status, {"error": error.message}, "application/json", {}


# ============================================================
# SERVICE SUPERVISORS
# ============================================================

class Supervisor:
    """Keeps one tunnel alive, and lets the control panel start/stop it."""

    def __init__(self, name: str, runner, reconnecting: bool = True):
        self.name = name
        self.runner = runner
        self.reconnecting = reconnecting
        self.task: Optional[asyncio.Task] = None

    @property
    def running(self) -> bool:
        return self.task is not None and not self.task.done()

    async def _loop(self) -> None:
        backoff = 2
        try:
            while True:
                try:
                    await self.runner()
                    backoff = 2
                    if not self.reconnecting:
                        return
                except asyncio.CancelledError:
                    raise
                except Exception as error:
                    set_state(self.name, "error", f"{type(error).__name__}: {error}")
                    log_repeatable_error(f"{self.name}_disconnect",
                                         f"Disconnected: {type(error).__name__}: {error}",
                                         self.name)
                set_state(self.name, "connecting", f"reconnecting in {backoff}s")
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 15)
        finally:
            set_state(self.name, "stopped")

    def start(self) -> bool:
        if self.running:
            return False
        set_state(self.name, "connecting", "starting")
        self.task = asyncio.create_task(self._loop())
        log(f"{self.name} tunnel started", "agent")
        return True

    async def stop(self) -> bool:
        if not self.running:
            return False
        self.task.cancel()
        try:
            await self.task
        except (asyncio.CancelledError, Exception):
            pass
        self.task = None
        set_state(self.name, "stopped")
        log(f"{self.name} tunnel stopped", "agent")
        return True

    async def restart(self) -> None:
        await self.stop()
        self.start()


def build_supervisors() -> dict:
    # Minecraft is managed separately because each server needs an isolated set
    # of module globals (especially USERNAME/PASSWORD and UDP voice-chat state).
    return {
        "web": Supervisor("web", web_connect_once),
        # smtp_service runs its own reconnect loop and owns the local relay socket.
        "smtp": Supervisor("smtp", smtp_service, reconnecting=False),
    }


# ============================================================
# MULTI-ACCOUNT MINECRAFT INSTANCES
# ============================================================
#
# The original Minecraft agent is written around module-level settings. Rather
# than weakening it by rewriting the tunnel protocol, every configured server
# gets an isolated in-memory copy of this module. That gives every server its
# own Hostish credentials, local TCP endpoint, homepage, voice-chat UDP state,
# statistics and reconnect loop while the user still launches one Python file.

MINECRAFT_RUNNERS: dict = {}

MINECRAFT_INSTANCE_FIELDS = (
    "id", "name", "enabled", "username", "password",
    "minecraft_host", "minecraft_port", "start_bat", "auto_start_server",
    "server_start_timeout", "server_start_check_interval", "homepage_file",
    "simple_voice_chat_enabled", "simple_voice_chat_host",
    "simple_voice_chat_port", "simple_voice_chat_public_port",
    "simple_voice_chat_service_id", "simple_voice_chat_config_file",
)


def _new_minecraft_instance_id() -> str:
    return "mc-" + secrets.token_hex(6)


def _minecraft_instance_defaults() -> dict:
    return {
        "id": _new_minecraft_instance_id(),
        "name": "Minecraft server",
        "enabled": True,
        "username": "",
        "password": "",
        "minecraft_host": "127.0.0.1",
        "minecraft_port": 25565,
        "start_bat": "",
        "auto_start_server": True,
        "server_start_timeout": 120,
        "server_start_check_interval": 2,
        "homepage_file": "",
        "simple_voice_chat_enabled": False,
        "simple_voice_chat_host": "127.0.0.1",
        "simple_voice_chat_port": 24454,
        "simple_voice_chat_public_port": 0,
        "simple_voice_chat_service_id": "simple-voice-chat",
        "simple_voice_chat_config_file": "",
    }


def _normalize_minecraft_instance(raw: dict, index: int = 0) -> dict:
    cfg = _minecraft_instance_defaults()
    if isinstance(raw, dict):
        for key in MINECRAFT_INSTANCE_FIELDS:
            if key in raw:
                cfg[key] = raw[key]
    cfg["id"] = str(cfg.get("id") or _new_minecraft_instance_id()).strip()
    cfg["name"] = str(cfg.get("name") or f"Minecraft {index + 1}").strip()[:80]
    cfg["enabled"] = bool(cfg.get("enabled", True))
    cfg["username"] = str(cfg.get("username") or "").strip()
    cfg["password"] = str(cfg.get("password") or "")
    cfg["minecraft_host"] = str(cfg.get("minecraft_host") or "127.0.0.1").strip()
    cfg["start_bat"] = str(cfg.get("start_bat") or "")
    cfg["homepage_file"] = str(cfg.get("homepage_file") or "")
    cfg["simple_voice_chat_host"] = str(
        cfg.get("simple_voice_chat_host") or "127.0.0.1").strip()
    cfg["simple_voice_chat_service_id"] = str(
        cfg.get("simple_voice_chat_service_id") or "simple-voice-chat").strip()
    cfg["simple_voice_chat_config_file"] = str(
        cfg.get("simple_voice_chat_config_file") or "")
    for key, default in (
        ("minecraft_port", 25565), ("server_start_timeout", 120),
        ("server_start_check_interval", 2), ("simple_voice_chat_port", 24454),
        ("simple_voice_chat_public_port", 0),
    ):
        try:
            cfg[key] = int(cfg.get(key, default))
        except (TypeError, ValueError):
            cfg[key] = default
    cfg["auto_start_server"] = bool(cfg.get("auto_start_server", True))
    cfg["simple_voice_chat_enabled"] = bool(cfg.get("simple_voice_chat_enabled", False))
    return cfg


def configured_minecraft_instances() -> list:
    raw = MINECRAFT_INSTANCES if isinstance(MINECRAFT_INSTANCES, list) else []
    if raw:
        return [_normalize_minecraft_instance(item, i) for i, item in enumerate(raw)
                if isinstance(item, dict)]

    # Backward-compatible migration path for the old single Minecraft section.
    if MINECRAFT_ENABLED:
        return [_normalize_minecraft_instance({
            "id": "legacy-minecraft",
            "name": "Minecraft server",
            "enabled": True,
            "username": USERNAME,
            "password": PASSWORD,
            "minecraft_host": MINECRAFT_HOST,
            "minecraft_port": MINECRAFT_PORT,
            "start_bat": START_BAT,
            "auto_start_server": AUTO_START_SERVER,
            "server_start_timeout": SERVER_START_TIMEOUT,
            "server_start_check_interval": SERVER_START_CHECK_INTERVAL,
            "homepage_file": HOMEPAGE_FILE,
            "simple_voice_chat_enabled": SIMPLE_VOICE_CHAT_ENABLED,
            "simple_voice_chat_host": SIMPLE_VOICE_CHAT_HOST,
            "simple_voice_chat_port": SIMPLE_VOICE_CHAT_PORT,
            "simple_voice_chat_public_port": SIMPLE_VOICE_CHAT_PUBLIC_PORT,
            "simple_voice_chat_service_id": SIMPLE_VOICE_CHAT_SERVICE_ID,
            "simple_voice_chat_config_file": SIMPLE_VOICE_CHAT_CONFIG_FILE,
        })]
    return []


def _minecraft_public_config(cfg: dict) -> dict:
    out = dict(cfg)
    out["password"] = "********" if out.get("password") else ""
    return out


def _minecraft_config_errors(cfg: dict, all_configs: Optional[list] = None) -> list:
    errors = []
    label = cfg.get("name") or cfg.get("id") or "Minecraft"
    username = str(cfg.get("username") or "").strip()
    password = str(cfg.get("password") or "")
    if not username:
        errors.append(f"[minecraft:{label}] Hostish username is required")
    if not password:
        errors.append(f"[minecraft:{label}] Hostish password is required")
    try:
        port = int(cfg.get("minecraft_port", 0))
        if not 1 <= port <= 65535:
            raise ValueError
    except Exception:
        errors.append(f"[minecraft:{label}] Minecraft port must be 1-65535")

    if cfg.get("simple_voice_chat_enabled"):
        try:
            voice_port = int(cfg.get("simple_voice_chat_port", 0))
            if not 1 <= voice_port <= 65535:
                raise ValueError
            public_port = int(cfg.get("simple_voice_chat_public_port", 0))
            if not 0 <= public_port <= 65535:
                raise ValueError
        except Exception:
            errors.append(f"[minecraft:{label}] voice-chat ports are invalid")
        config_file = normalized_path_setting(cfg.get("simple_voice_chat_config_file", ""))
        if not config_file:
            errors.append(f"[minecraft:{label}] voicechat-server.properties path is required")
        elif not os.path.isfile(config_file):
            errors.append(f"[minecraft:{label}] voice-chat config does not exist: {config_file}")

    start_bat = normalized_path_setting(cfg.get("start_bat", ""))
    if cfg.get("auto_start_server") and start_bat and not os.path.isfile(start_bat):
        errors.append(f"[minecraft:{label}] start script does not exist: {start_bat}")

    if all_configs and username:
        duplicates = [c for c in all_configs
                      if str(c.get("id")) != str(cfg.get("id"))
                      and str(c.get("username") or "").strip().lower() == username.lower()]
        if duplicates:
            errors.append(
                f"[minecraft:{label}] Hostish account {username!r} is already used by another Minecraft server")
    return errors


class MinecraftInstanceRunner:
    """Runs one isolated copy of the integrated Minecraft agent."""

    def __init__(self, config: dict):
        self.config = _normalize_minecraft_instance(config)
        self.task: Optional[asyncio.Task] = None
        self.module = None
        self.state = {"status": "stopped", "detail": "", "since": 0.0}
        self.last_error = ""

    @property
    def id(self) -> str:
        return str(self.config.get("id"))

    @property
    def running(self) -> bool:
        return self.task is not None and not self.task.done()

    def _set_state(self, status: str, detail: str = "") -> None:
        if self.state.get("status") != status:
            self.state["since"] = time.time()
        self.state["status"] = status
        self.state["detail"] = str(detail or "")

    def _child_log(self, message: str, service: str = "minecraft") -> None:
        log(message, f"minecraft:{self.id}")

    def _child_set_state(self, _service: str, status: str, detail: str = "") -> None:
        self._set_state(status, detail)

    def _load_isolated_module(self):
        script_path = _current_script_path()
        safe_id = "".join(ch if ch.isalnum() else "_" for ch in self.id)[:40]
        module_name = f"_hostish_mc_{safe_id}_{secrets.token_hex(4)}"
        spec = importlib.util.spec_from_file_location(module_name, script_path)
        if spec is None or spec.loader is None:
            raise RuntimeError("Could not create isolated Minecraft runtime")
        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        try:
            spec.loader.exec_module(module)
        except Exception:
            sys.modules.pop(module_name, None)
            raise

        cfg = self.config
        mapping = {
            "USERNAME": "username", "PASSWORD": "password",
            "MINECRAFT_HOST": "minecraft_host", "MINECRAFT_PORT": "minecraft_port",
            "START_BAT": "start_bat", "AUTO_START_SERVER": "auto_start_server",
            "SERVER_START_TIMEOUT": "server_start_timeout",
            "SERVER_START_CHECK_INTERVAL": "server_start_check_interval",
            "HOMEPAGE_FILE": "homepage_file",
            "SIMPLE_VOICE_CHAT_ENABLED": "simple_voice_chat_enabled",
            "SIMPLE_VOICE_CHAT_HOST": "simple_voice_chat_host",
            "SIMPLE_VOICE_CHAT_PORT": "simple_voice_chat_port",
            "SIMPLE_VOICE_CHAT_PUBLIC_PORT": "simple_voice_chat_public_port",
            "SIMPLE_VOICE_CHAT_SERVICE_ID": "simple_voice_chat_service_id",
            "SIMPLE_VOICE_CHAT_CONFIG_FILE": "simple_voice_chat_config_file",
        }
        for global_name, key in mapping.items():
            setattr(module, global_name, cfg.get(key))

        # A Minecraft instance has its own Hostish account, so its optional
        # browser homepage must not be suppressed just because the parent agent
        # happens to host a website under a different account.
        module.WEB_ENABLED = False
        module.SMTP_ENABLED = False
        module.MINECRAFT_ENABLED = True
        module.MINECRAFT_INSTANCES = []
        module.MINECRAFT_WS_URL = MINECRAFT_WS_URL
        module.log = self._child_log
        module.set_state = self._child_set_state
        return module

    async def _loop(self) -> None:
        backoff = 2
        try:
            self.module = self._load_isolated_module()
            while True:
                try:
                    await self.module.minecraft_connect_once()
                    backoff = 2
                except asyncio.CancelledError:
                    raise
                except Exception as error:
                    self.last_error = f"{type(error).__name__}: {error}"
                    self._set_state("error", self.last_error)
                    log_repeatable_error(
                        f"minecraft_instance_{self.id}",
                        f"{self.config.get('name')}: disconnected: {self.last_error}",
                        f"minecraft:{self.id}")
                self._set_state("connecting", f"reconnecting in {backoff}s")
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 15)
        finally:
            module = self.module
            self.module = None
            self._set_state("stopped")
            if module is not None:
                sys.modules.pop(getattr(module, "__name__", ""), None)

    def start(self) -> tuple:
        if self.running:
            return False, "already running"
        errors = _minecraft_config_errors(self.config, configured_minecraft_instances())
        if errors:
            self._set_state("error", errors[0])
            return False, errors[0]
        self._set_state("connecting", "starting")
        self.task = asyncio.create_task(self._loop())
        log(f"Started Minecraft instance {self.config.get('name')} ({self.config.get('username')})", "agent")
        return True, "started"

    async def stop(self) -> tuple:
        if not self.running:
            self._set_state("stopped")
            return False, "not running"
        self.task.cancel()
        try:
            await self.task
        except (asyncio.CancelledError, Exception):
            pass
        self.task = None
        self._set_state("stopped")
        log(f"Stopped Minecraft instance {self.config.get('name')}", "agent")
        return True, "stopped"

    async def restart(self) -> tuple:
        await self.stop()
        return self.start()

    def status(self) -> dict:
        stats = {"connections": 0, "players": 0, "peak_players": 0, "voice_peers": 0}
        if self.module is not None:
            try:
                stats.update(dict(self.module.STATS.get("minecraft", {})))
            except Exception:
                pass
        return {
            "id": self.id,
            "name": self.config.get("name"),
            "enabled": bool(self.config.get("enabled")),
            "username": self.config.get("username"),
            "address": (f"{self.config.get('username')}.hostish.site"
                        if self.config.get("username") else ""),
            "minecraft_host": self.config.get("minecraft_host"),
            "minecraft_port": self.config.get("minecraft_port"),
            "voice_enabled": bool(self.config.get("simple_voice_chat_enabled")),
            "running": self.running,
            "state": dict(self.state),
            "stats": stats,
            "last_error": self.last_error,
        }


async def sync_minecraft_runners(start_enabled: bool = False, restart_changed: bool = False) -> None:
    desired = {cfg["id"]: cfg for cfg in configured_minecraft_instances()}

    for instance_id in list(MINECRAFT_RUNNERS):
        if instance_id not in desired:
            await MINECRAFT_RUNNERS[instance_id].stop()
            MINECRAFT_RUNNERS.pop(instance_id, None)

    for instance_id, cfg in desired.items():
        current = MINECRAFT_RUNNERS.get(instance_id)
        if current is None:
            current = MinecraftInstanceRunner(cfg)
            MINECRAFT_RUNNERS[instance_id] = current
        elif current.config != cfg:
            was_running = current.running
            if was_running:
                await current.stop()
            current = MinecraftInstanceRunner(cfg)
            MINECRAFT_RUNNERS[instance_id] = current
            if restart_changed and was_running:
                current.start()
                continue

        if start_enabled and cfg.get("enabled") and not current.running:
            current.start()
        elif not cfg.get("enabled") and current.running:
            await current.stop()


def minecraft_runner_statuses() -> list:
    return [runner.status() for runner in MINECRAFT_RUNNERS.values()]


def minecraft_aggregate_state() -> dict:
    statuses = minecraft_runner_statuses()
    if not statuses:
        return {"status": "stopped", "detail": "no servers configured", "running": False}
    states = [item["state"].get("status", "stopped") for item in statuses]
    if any(state == "online" for state in states):
        status = "online"
    elif any(state == "connecting" for state in states):
        status = "connecting"
    elif any(state == "error" for state in states):
        status = "error"
    else:
        status = "stopped"
    running = sum(1 for item in statuses if item.get("running"))
    return {"status": status, "detail": f"{running}/{len(statuses)} running", "running": running > 0}


def minecraft_aggregate_stats() -> dict:
    total = {"connections": 0, "players": 0, "peak_players": 0, "voice_peers": 0}
    for item in minecraft_runner_statuses():
        stats = item.get("stats", {})
        for key in total:
            total[key] += int(stats.get(key, 0) or 0)
    return total


async def _panel_minecraft_action(payload: dict) -> dict:
    action = str(payload.get("action", "list"))
    instance_id = str(payload.get("id", "")).strip()

    if action == "list":
        configs = configured_minecraft_instances()
        return {"ok": True, "instances": [_minecraft_public_config(c) for c in configs],
                "statuses": minecraft_runner_statuses()}

    if action == "save":
        raw = payload.get("instance")
        if not isinstance(raw, dict):
            return {"ok": False, "message": "Invalid Minecraft instance"}
        configs = configured_minecraft_instances()
        existing = next((c for c in configs if c.get("id") == raw.get("id")), None)
        if raw.get("password") in (None, "", "********"):
            raw = dict(raw)
            raw["password"] = existing.get("password", "") if existing else ""
        cfg = _normalize_minecraft_instance(raw, len(configs))
        others = [c for c in configs if c.get("id") != cfg.get("id")]
        errors = _minecraft_config_errors(cfg, others + [cfg])
        if errors:
            return {"ok": False, "message": errors[0], "errors": errors}
        configs = others + [cfg]
        globals()["MINECRAFT_INSTANCES"] = configs
        globals()["MINECRAFT_ENABLED"] = False
        stored = load_config_file()
        stored["MINECRAFT_INSTANCES"] = configs
        stored["MINECRAFT_ENABLED"] = False
        save_config_file(stored)
        await sync_minecraft_runners(start_enabled=False, restart_changed=True)
        return {"ok": True, "message": f"Saved {cfg['name']}", "id": cfg["id"]}

    if action == "delete":
        configs = configured_minecraft_instances()
        target = next((c for c in configs if c.get("id") == instance_id), None)
        if target is None:
            return {"ok": False, "message": "Minecraft instance not found"}
        runner = MINECRAFT_RUNNERS.get(instance_id)
        if runner is not None:
            await runner.stop()
        configs = [c for c in configs if c.get("id") != instance_id]
        globals()["MINECRAFT_INSTANCES"] = configs
        stored = load_config_file()
        stored["MINECRAFT_INSTANCES"] = configs
        save_config_file(stored)
        await sync_minecraft_runners()
        return {"ok": True, "message": f"Removed {target.get('name')}"}

    if action in {"start_all", "stop_all", "restart_all"}:
        changed = 0
        for runner in list(MINECRAFT_RUNNERS.values()):
            if action == "start_all":
                if runner.config.get("enabled") and runner.start()[0]:
                    changed += 1
            elif action == "stop_all":
                if (await runner.stop())[0]:
                    changed += 1
            else:
                if runner.config.get("enabled"):
                    await runner.restart()
                    changed += 1
        return {"ok": True, "message": f"{action.replace('_', ' ')}: {changed} server(s)"}

    runner = MINECRAFT_RUNNERS.get(instance_id)
    if runner is None:
        return {"ok": False, "message": "Minecraft instance not found"}
    if action == "start":
        ok, message = runner.start()
    elif action == "stop":
        ok, message = await runner.stop()
    elif action == "restart":
        ok, message = await runner.restart()
    else:
        return {"ok": False, "message": f"Unknown Minecraft action {action!r}"}
    return {"ok": ok, "message": message}


# ============================================================
# MANAGED SCRIPTS
# ============================================================
#
# Companion processes (a game server watchdog, a bot, a backend the web
# tunnel proxies to, and so on) that this agent can start for you and that
# you can start/stop/restart individually at any time from the panel's
# Scripts tab, independently of the three tunnels above.

import shlex
import shutil


SCRIPT_EXTENSIONS = {".py", ".pyw", ".bat", ".cmd", ".ps1",
                     ".js", ".mjs", ".cjs", ".sh", ".jar", ".exe", ".com"}


def normalized_script_file_path(value: str) -> str:
    value = str(value or "").strip()
    if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
        value = value[1:-1].strip()
    if not value or "\x00" in value or "\n" in value or "\r" in value:
        return ""
    return normalized_path_setting(value)


def validate_script_file(path: str) -> str:
    if not path:
        return "Enter a file path first"
    if not os.path.isfile(path):
        return f"File not found on this computer: {path}"
    ext = os.path.splitext(path)[1].lower()
    if ext not in SCRIPT_EXTENSIONS and not (os.name != "nt" and os.access(path, os.X_OK)):
        return "Unsupported file type. Use .py, .bat, .cmd, .ps1, .js, .sh, .jar or an executable"
    if ext in {".bat", ".cmd", ".ps1", ".exe", ".com"} and os.name != "nt":
        return f"{ext} files require Windows"
    return ""


def script_launch_command(path: str) -> list[str]:
    ext = os.path.splitext(path)[1].lower()
    if ext in {".py", ".pyw"}:
        return [sys.executable, path]
    if ext in {".bat", ".cmd"}:
        return ["cmd.exe", "/d", "/c", path]
    if ext == ".ps1":
        executable = shutil.which("powershell.exe") or shutil.which("pwsh.exe")
        if not executable:
            raise ValueError("PowerShell is not installed on this computer")
        return [executable, "-NoProfile", "-File", path]
    if ext in {".js", ".mjs", ".cjs"}:
        executable = shutil.which("node")
        if not executable:
            raise ValueError("Node.js is not installed on this computer")
        return [executable, path]
    if ext == ".sh":
        if os.name == "nt":
            executable = shutil.which("bash")
            if not executable:
                raise ValueError("Bash is required to run .sh files on Windows")
        else:
            executable = "/bin/sh"
        return [executable, path]
    if ext == ".jar":
        executable = shutil.which("java")
        if not executable:
            raise ValueError("Java is not installed on this computer")
        return [executable, "-jar", path]
    return [path]


class ManagedScriptRunner:
    """Owns one external process and streams its output into the shared log."""

    def __init__(self, entry: dict):
        self.name = str(entry.get("name") or "script")
        self.command = str(entry.get("command") or "")
        self.cwd = str(entry.get("cwd") or "")
        self.autostart = bool(entry.get("autostart"))
        # Easy one-path-per-line Python entries keep the .py path as the
        # source of truth, so a Python reinstall cannot leave a stale
        # absolute python.exe path in the saved config.
        self.source = str(entry.get("_source") or "")
        self.script_path = str(entry.get("path") or "")
        self.process: Optional[asyncio.subprocess.Process] = None
        self.pump_task: Optional[asyncio.Task] = None
        self.wait_task: Optional[asyncio.Task] = None
        self.started_at = 0.0
        self.exit_code: Optional[int] = None
        self.last_error = ""

    @property
    def log_tag(self) -> str:
        return f"script:{self.name}"

    @property
    def running(self) -> bool:
        return self.process is not None and self.process.returncode is None

    def status(self) -> dict:
        return {
            "name": self.name, "command": self.command, "cwd": self.cwd,
            "autostart": self.autostart, "running": self.running,
            "source": self.source, "path": self.script_path,
            "pid": self.process.pid if self.process else None,
            "started_at": self.started_at, "exit_code": self.exit_code,
            "last_error": self.last_error,
        }

    async def _pump_output(self) -> None:
        assert self.process is not None
        stream = self.process.stdout
        try:
            while True:
                line = await stream.readline()
                if not line:
                    break
                text = line.decode("utf-8", "replace").rstrip("\r\n")
                if text:
                    log(text, self.log_tag)
        except Exception:
            pass

    async def start(self) -> tuple:
        if self.running:
            return False, "already running"

        # Path-managed scripts need no manual command, interpreter, or cwd.
        # Each supported file type launches using the appropriate local runner.
        if self.source == "auto_python_path" or self.script_path.strip():
            script = normalized_script_file_path(self.script_path)
            error = validate_script_file(script)
            if error:
                self.last_error = error
                log(f"Could not start: {error}", self.log_tag)
                return False, error
            try:
                parts = script_launch_command(script)
            except ValueError as error:
                self.last_error = str(error)
                log(f"Could not start: {error}", self.log_tag)
                return False, self.last_error
            cwd = os.path.dirname(script) or None
        else:
            if not self.command.strip():
                return False, "no command configured"

            try:
                parts = shlex.split(self.command, posix=(os.name != "nt"))
            except ValueError as error:
                return False, f"could not parse command: {error}"
            if not parts:
                return False, "no command configured"

            cwd = normalized_path_setting(self.cwd) if self.cwd else None
            if cwd and not os.path.isdir(cwd):
                self.last_error = f"Working directory was not found: {cwd}"
                log(f"Could not start: {self.last_error}", self.log_tag)
                return False, self.last_error

            executable = parts[0]
            looks_like_path = os.path.isabs(executable) or "/" in executable or "\\" in executable
            if looks_like_path:
                executable_path = normalized_path_setting(executable)
                if not os.path.isfile(executable_path):
                    self.last_error = f"Executable was not found: {executable_path}"
                    log(f"Could not start: {self.last_error}", self.log_tag)
                    return False, self.last_error
                parts[0] = executable_path

            if not cwd:
                for arg in parts[1:]:
                    candidate = normalized_path_setting(arg)
                    if os.path.isfile(candidate):
                        cwd = os.path.dirname(candidate) or None
                        break

        try:
            self.process = await asyncio.create_subprocess_exec(
                *parts, cwd=cwd or None,
                stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
            self.started_at = time.time()
            self.exit_code = None
            self.last_error = ""
            log(f"Started ({' '.join(parts)}) pid={self.process.pid}", self.log_tag)
            self.pump_task = asyncio.create_task(self._pump_output())
            self.wait_task = asyncio.create_task(self._watch_exit())
            return True, "started"
        except FileNotFoundError as error:
            self.last_error = (
                f"Could not launch process. executable={parts[0]!r}, "
                f"cwd={cwd!r}: {error}"
            )
            log(f"Could not start: {self.last_error}", self.log_tag)
            return False, self.last_error
        except Exception as error:
            self.last_error = f"{type(error).__name__}: {error}"
            log(f"Could not start: {self.last_error}", self.log_tag)
            return False, self.last_error

    async def _watch_exit(self) -> None:
        process = self.process
        if process is None:
            return
        code = await process.wait()
        self.exit_code = code
        log(f"Exited with code {code}", self.log_tag)

    async def stop(self) -> tuple:
        if not self.running:
            return False, "not running"
        process = self.process
        try:
            process.terminate()
        except Exception:
            pass
        try:
            await asyncio.wait_for(process.wait(), timeout=6)
        except asyncio.TimeoutError:
            try:
                process.kill()
            except Exception:
                pass
            try:
                await asyncio.wait_for(process.wait(), timeout=3)
            except Exception:
                pass
        if self.pump_task is not None:
            self.pump_task.cancel()
        log("Stopped", self.log_tag)
        return True, "stopped"

    async def restart(self) -> tuple:
        if self.running:
            await self.stop()
        return await self.start()


MANAGED_SCRIPT_RUNNERS: Dict[str, ManagedScriptRunner] = {}


def sync_managed_script_runners() -> None:
    """Rebuild the runner registry from settings, keeping live processes."""
    wanted = {}
    for entry in (MANAGED_SCRIPTS or []):
        if isinstance(entry, dict) and str(entry.get("name") or "").strip():
            wanted[str(entry["name"]).strip()] = entry

    for name in list(MANAGED_SCRIPT_RUNNERS.keys()):
        if name not in wanted:
            runner = MANAGED_SCRIPT_RUNNERS.pop(name)
            if runner.running:
                asyncio.create_task(runner.stop())

    for name, entry in wanted.items():
        existing = MANAGED_SCRIPT_RUNNERS.get(name)
        if existing is None:
            MANAGED_SCRIPT_RUNNERS[name] = ManagedScriptRunner(entry)
        else:
            existing.command = str(entry.get("command") or "")
            existing.cwd = str(entry.get("cwd") or "")
            existing.autostart = bool(entry.get("autostart"))
            existing.source = str(entry.get("_source") or "")
            existing.script_path = str(entry.get("path") or "")


def merge_legacy_auto_start_scripts() -> None:
    """Synchronize the easy one-path-per-line list into MANAGED_SCRIPTS.

    Entries generated from AUTO_START_PYTHON_SCRIPTS carry an internal source
    marker so editing/removing/reordering paths on the Scripts page does not
    leave stale legacy-python-N processes behind.  Manually-created advanced
    managed commands are preserved untouched.
    """
    existing = [
        dict(item) for item in (MANAGED_SCRIPTS or [])
        if isinstance(item, dict) and item.get("_source") != "auto_python_path"
    ]
    if AUTO_START_PYTHON_SCRIPTS_ENABLED:
        used_names = {str(item.get("name", "")).strip() for item in existing}
        seen_paths = set()
        for script in (AUTO_START_PYTHON_SCRIPTS or []):
            path = str(script or "").strip()
            if not path:
                continue
            normalized = normalized_path_setting(path)
            identity = os.path.normcase(normalized)
            if identity in seen_paths:
                continue
            seen_paths.add(identity)
            stem = os.path.splitext(os.path.basename(normalized))[0] or "python-script"
            base_name = stem
            name = base_name
            suffix = 2
            while name in used_names:
                name = f"{base_name}-{suffix}"
                suffix += 1
            used_names.add(name)
            existing.append({
                "name": name,
                "command": f'python "{normalized}"',
                "cwd": os.path.dirname(normalized),
                "autostart": True,
                "_source": "auto_python_path",
                "path": normalized,
            })
    globals()["MANAGED_SCRIPTS"] = existing


async def start_autostart_scripts() -> None:
    merge_legacy_auto_start_scripts()
    sync_managed_script_runners()
    for runner in MANAGED_SCRIPT_RUNNERS.values():
        if runner.autostart:
            await runner.start()


async def stop_all_managed_scripts() -> None:
    for runner in list(MANAGED_SCRIPT_RUNNERS.values()):
        if runner.running:
            await runner.stop()


# ============================================================
# SELF UPDATE
# ============================================================
#
# Because every setting lives in hostish-config.json rather than inside this
# file, updating is simple: back the current script up, download the newer
# one, swap it in atomically, and restart. Configuration is untouched.

_version_cache = {"last_fetch_ts": 0.0, "newest_version": None, "error": None}


def _current_script_path() -> str:
    candidates = []
    try:
        candidates.append(os.path.abspath(__file__))
    except Exception:
        pass
    if sys.argv and sys.argv[0]:
        candidates.append(os.path.abspath(sys.argv[0]))
    for candidate in candidates:
        if candidate and os.path.isfile(candidate):
            return candidate
    raise RuntimeError("Could not determine the current agent script path")


def _extract_version_from_first_line(line: str) -> Optional[int]:
    value = (line or "").strip()
    if not value.lower().startswith("version"):
        return None
    parts = value.split("=", 1)
    if len(parts) != 2:
        return None
    rhs = parts[1].strip().strip("\"' ")
    return int(rhs) if rhs.isdigit() else None


def fetch_latest_agent_version(force: bool = False) -> Optional[int]:
    now_ts = time.time()
    if not force and now_ts - _version_cache["last_fetch_ts"] < 30 and _version_cache["newest_version"]:
        return _version_cache["newest_version"]
    try:
        request = urllib.request.Request(
            LATEST_AGENT_URL, headers={"User-Agent": f"HostishAgent/{version}",
                                       "Cache-Control": "no-cache"}, method="GET")
        with urllib.request.urlopen(request, timeout=8, context=make_ssl_context()) as response:
            chunk = response.read(256)
        first_line = chunk.splitlines()[0].decode("utf-8", "replace") if chunk else ""
        newest = _extract_version_from_first_line(first_line)
        _version_cache["last_fetch_ts"] = now_ts
        _version_cache["newest_version"] = newest
        _version_cache["error"] = None
        return newest
    except Exception as error:
        _version_cache["last_fetch_ts"] = now_ts
        _version_cache["error"] = f"{type(error).__name__}: {error}"
        return _version_cache["newest_version"]


def _download_latest_agent_source() -> tuple:
    parsed = urllib.parse.urlparse(LATEST_AGENT_URL)
    initial_scheme = (parsed.scheme or "").lower()
    initial_host = (parsed.hostname or "").lower()
    if initial_scheme not in {"http", "https"} or not initial_host:
        raise ValueError("LATEST_AGENT_URL must be a valid HTTP(S) URL")
    if SELF_UPDATE_REQUIRE_HTTPS and initial_scheme != "https":
        raise ValueError("Self-update requires an HTTPS LATEST_AGENT_URL")

    request = urllib.request.Request(
        LATEST_AGENT_URL, headers={
            "User-Agent": f"HostishAgent/{version}",
            "Accept": "text/x-python, text/plain, application/octet-stream;q=0.9, */*;q=0.1",
            "Cache-Control": "no-cache"}, method="GET")

    timeout = max(5, int(SELF_UPDATE_DOWNLOAD_TIMEOUT_SECONDS))
    max_bytes = max(1024, int(SELF_UPDATE_MAX_DOWNLOAD_BYTES))
    with urllib.request.urlopen(request, timeout=timeout, context=make_ssl_context()) as response:
        final_url = response.geturl()
        final = urllib.parse.urlparse(final_url)
        if SELF_UPDATE_REQUIRE_HTTPS and (final.scheme or "").lower() != "https":
            raise ValueError("Update redirect did not remain on HTTPS")
        if (not SELF_UPDATE_ALLOW_CROSS_HOST_REDIRECTS
                and (final.hostname or "").lower() != initial_host):
            raise ValueError("Update redirect changed host")
        raw = response.read(max_bytes + 1)

    if len(raw) > max_bytes:
        raise ValueError(f"Downloaded agent exceeds SELF_UPDATE_MAX_DOWNLOAD_BYTES ({max_bytes} bytes)")
    if not raw:
        raise ValueError("Downloaded agent file is empty")

    # Optional local SHA-256 pin. Leave blank to skip this check.
    actual_sha = hashlib.sha256(raw).hexdigest().lower()
    expected_sha = str(SELF_UPDATE_EXPECTED_SHA256 or "").strip().lower()
    if expected_sha and not hmac.compare_digest(actual_sha, expected_sha):
        raise ValueError("Downloaded agent SHA-256 did not match SELF_UPDATE_EXPECTED_SHA256")

    public_key_pem = str(SELF_UPDATE_ED25519_PUBLIC_KEY_PEM or "").strip()
    if public_key_pem:
        try:
            from cryptography.hazmat.primitives import serialization
            from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
        except ImportError as error:
            raise ValueError(
                "Ed25519 update verification is configured but the optional cryptography package is not installed") from error
        signature_url = final_url + str(SELF_UPDATE_SIGNATURE_SUFFIX or ".sig")
        sig_parsed = urllib.parse.urlparse(signature_url)
        if SELF_UPDATE_REQUIRE_HTTPS and sig_parsed.scheme.lower() != "https":
            raise ValueError("Update signature URL must use HTTPS")
        if (not SELF_UPDATE_ALLOW_CROSS_HOST_REDIRECTS
                and (sig_parsed.hostname or "").lower() != initial_host):
            raise ValueError("Update signature URL changed host")
        sig_request = urllib.request.Request(
            signature_url, headers={"User-Agent": f"HostishAgent/{version}",
                                    "Cache-Control": "no-cache"}, method="GET")
        with urllib.request.urlopen(sig_request, timeout=timeout, context=make_ssl_context()) as sig_response:
            sig_final = urllib.parse.urlparse(sig_response.geturl())
            if SELF_UPDATE_REQUIRE_HTTPS and sig_final.scheme.lower() != "https":
                raise ValueError("Update signature redirect did not remain on HTTPS")
            if (not SELF_UPDATE_ALLOW_CROSS_HOST_REDIRECTS
                    and (sig_final.hostname or "").lower() != initial_host):
                raise ValueError("Update signature redirect changed host")
            signature_raw = sig_response.read(8193)
        if len(signature_raw) > 8192:
            raise ValueError("Update signature file is unexpectedly large")
        signature = signature_raw.strip()
        try:
            decoded = base64.b64decode(signature, validate=True)
            if decoded:
                signature = decoded
        except Exception:
            pass
        key = serialization.load_pem_public_key(public_key_pem.encode("utf-8"))
        if not isinstance(key, Ed25519PublicKey):
            raise ValueError("Configured update public key is not Ed25519")
        try:
            key.verify(signature, raw)
        except Exception as error:
            raise ValueError("Downloaded agent Ed25519 signature verification failed") from error

    try:
        source = raw.decode("utf-8-sig")
    except UnicodeDecodeError as error:
        raise ValueError(f"Downloaded agent is not valid UTF-8: {error}") from error
    return source, final_url


def _make_update_backup(script_path: str, old_source: str) -> Optional[str]:
    if not SELF_UPDATE_BACKUP_ENABLED:
        return None
    script_directory = os.path.dirname(script_path)
    backup_directory_setting = str(SELF_UPDATE_BACKUP_DIR or "").strip() or ".hostish-agent-backups"
    backup_directory = (os.path.abspath(os.path.expanduser(backup_directory_setting))
                        if os.path.isabs(backup_directory_setting)
                        else os.path.join(script_directory, backup_directory_setting))
    os.makedirs(backup_directory, exist_ok=True)

    base_name = os.path.basename(script_path)
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    backup_path = os.path.join(backup_directory, f"{base_name}.v{version}.{stamp}.backup.py")
    with open(backup_path, "wb") as handle:
        handle.write(old_source.encode("utf-8"))

    try:
        keep = max(1, int(SELF_UPDATE_BACKUP_KEEP))
        prefix, suffix = f"{base_name}.v", ".backup.py"
        candidates = sorted(
            ((os.path.getmtime(os.path.join(backup_directory, name)), os.path.join(backup_directory, name))
             for name in os.listdir(backup_directory)
             if name.startswith(prefix) and name.endswith(suffix)), reverse=True)
        for _mtime, path in candidates[keep:]:
            try:
                os.remove(path)
            except Exception:
                pass
    except Exception:
        pass
    return backup_path


def install_latest_agent_update(expected_newest: Optional[int] = None) -> dict:
    script_path = _current_script_path()
    with open(script_path, "r", encoding="utf-8-sig", newline="") as handle:
        old_source = handle.read()

    new_source, final_url = _download_latest_agent_source()
    downloaded_version = _extract_version_from_first_line(new_source.splitlines()[0] if new_source else "")
    if downloaded_version is None:
        raise ValueError("Downloaded script does not declare an integer 'version'")
    if downloaded_version <= version:
        raise ValueError(f"Downloaded version {downloaded_version} is not newer than {version}")
    if expected_newest is not None and downloaded_version < int(expected_newest):
        raise ValueError(f"Downloaded version {downloaded_version} is older than advertised {expected_newest}")

    compile(new_source, final_url, "exec")

    backup_path = _make_update_backup(script_path, old_source)

    script_directory = os.path.dirname(script_path)
    fd, temp_path = __import__("tempfile").mkstemp(
        prefix=f".{os.path.basename(script_path)}.update-", suffix=".tmp", dir=script_directory)
    try:
        with os.fdopen(fd, "wb") as handle:
            handle.write(new_source.encode("utf-8"))
        os.replace(temp_path, script_path)
    finally:
        if os.path.exists(temp_path):
            try:
                os.remove(temp_path)
            except Exception:
                pass

    return {"script_path": script_path, "backup_path": backup_path,
            "download_url": final_url, "downloaded_version": downloaded_version}


def restart_into_updated_agent(script_path: str) -> None:
    try:
        sys.stdout.flush()
        sys.stderr.flush()
    except Exception:
        pass
    arguments = [sys.executable, script_path] + list(sys.argv[1:])
    try:
        os.execv(sys.executable, arguments)
    except Exception:
        subprocess.Popen(arguments, cwd=os.path.dirname(script_path) or None)
        os._exit(0)


# ============================================================
# PERIODIC SUMMARY / IDLE WEB HEALTH CHECK
# ============================================================

async def periodic_summary_loop() -> None:
    previous_requests = 0
    previous_bytes = 0
    while True:
        await asyncio.sleep(max(5, int(PERIODIC_SUMMARY_SECONDS)))
        if not PERIODIC_SUMMARY_ENABLED:
            previous_requests = int(STATS["web"].get("requests", 0) or 0)
            previous_bytes = int(STATS["web"].get("bytes", 0) or 0)
            continue
        requests_now = int(STATS["web"].get("requests", 0) or 0)
        bytes_now = int(STATS["web"].get("bytes", 0) or 0)
        log("[STATS] Periodic | "
            f"window={PERIODIC_SUMMARY_SECONDS}s | "
            f"reqs={requests_now - previous_requests} | "
            f"bytes={fmt_bytes(bytes_now - previous_bytes)} | "
            f"total_reqs={requests_now} | "
            f"inflight={STATS['web'].get('inflight', 0)}", "web")
        previous_requests = requests_now
        previous_bytes = bytes_now


def _check_hostish_web_status_sync() -> Optional[bool]:
    username = str(USERNAME or "").strip()
    if not username:
        return None
    url = "https://hostish.site/api/status/" + urllib.parse.quote(username, safe="")
    try:
        request = urllib.request.Request(
            url, headers={"User-Agent": f"HostishAgent/{version}", "Accept": "application/json"},
            method="GET")
        with urllib.request.urlopen(request, timeout=10, context=make_ssl_context()) as response:
            raw = response.read(64 * 1024)
        data = json.loads(raw.decode("utf-8", "replace"))
        if not isinstance(data, dict):
            return None
        for key in ("hosting", "online"):
            if key in data and isinstance(data[key], bool):
                return bool(data[key])
        status_value = str(data.get("status", "") or "").strip().lower()
        if status_value in {"online", "hosting", "live", "connected"}:
            return True
        if status_value in {"offline", "not_hosting", "not-hosting", "disconnected"}:
            return False
        return None
    except Exception as error:
        log_repeatable_error("idle_status_check",
                             f"Idle status check was inconclusive: {type(error).__name__}: {error}",
                             "web")
        return None


async def idle_status_checker_loop() -> None:
    last_requests = int(STATS["web"].get("requests", 0) or 0)
    while True:
        await asyncio.sleep(max(5, int(IDLE_CHECK_INTERVAL_SECONDS)))
        if not IDLE_STATUS_CHECK_ENABLED or not WEB_ENABLED:
            continue
        web_supervisor = SUPERVISORS.get("web")
        if not web_supervisor or not web_supervisor.running:
            continue
        if SERVICE_STATE.get("web", {}).get("status") != "online":
            continue
        current_requests = int(STATS["web"].get("requests", 0) or 0)
        if current_requests != last_requests:
            last_requests = current_requests
            continue
        first = await run_in_thread(_check_hostish_web_status_sync)
        if first is not False:
            continue
        log("Hostish reported the idle website offline once; confirming before reconnecting.", "web")
        await asyncio.sleep(3)
        if not web_supervisor.running or SERVICE_STATE.get("web", {}).get("status") != "online":
            continue
        second = await run_in_thread(_check_hostish_web_status_sync)
        if second is False:
            log("Hostish reported the website offline twice while the tunnel appears connected; restarting only the web tunnel.", "web")
            await web_supervisor.restart()


# ============================================================
# LOCAL CONTROL PANEL
# ============================================================

CONTROL_PANEL_HTML = r"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#5865f2">
<title>Hostish Unified Agent</title>
<style>
:root{
  --bg:#313338;--card:#2b2d31;--panel:#1e1f22;--txt:#dbdee1;--muted:#949ba4;
  --brand:#5865f2;--brand-hover:#4752c4;--brand-soft:rgba(88,101,242,.10);
  --brand-border:rgba(88,101,242,.30);--brand-text:#c9ceff;
  --minecraft:#23a559;--minecraft-hover:#1d8f4c;--minecraft-soft:rgba(35,165,89,.09);
  --minecraft-border:rgba(35,165,89,.30);--minecraft-text:#d9ffea;
  --mail:#f2b53f;--mail-soft:rgba(242,181,63,.09);--mail-border:rgba(242,181,63,.30);
  --red:#f23f42;--red-soft:rgba(242,63,66,.10);--border:rgba(255,255,255,.08)
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--txt);font-family:'gg sans','Noto Sans','Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.5}
a{color:inherit}.shell{max-width:1120px;margin:auto;padding:18px}.hero,.card{background:var(--card);border:1px solid var(--border);border-radius:9px;padding:18px;margin-bottom:14px}.hero{border-color:var(--brand-border)}
.top{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.top h1{margin:0;color:#fff;font-size:30px}.subtitle{margin:6px 0 0;color:var(--muted);max-width:78ch}.brandLine{display:flex;align-items:center;gap:10px}.brandMark{width:12px;height:12px;border-radius:4px;background:var(--brand);box-shadow:0 0 0 4px var(--brand-soft)}
.nav{display:flex;gap:6px;flex-wrap:wrap;margin:14px 0}.nav a{background:var(--panel);border:1px solid var(--border);color:var(--muted);text-decoration:none;border-radius:6px;padding:8px 11px;font-weight:750;font-size:12px}.nav a:hover,.nav a.active{background:var(--brand-soft);border-color:var(--brand-border);color:#fff}
.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.card h2{margin:0 0 8px;color:#fff;font-size:18px}.card h3{margin:14px 0 6px;color:#fff;font-size:15px}.card p{color:var(--muted);margin:6px 0}.mcCard{border-color:var(--minecraft-border)}.mailCard{border-color:var(--mail-border)}
.actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:14px}.btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:var(--brand);color:#fff;border:0;border-radius:6px;padding:9px 13px;font-weight:750;font-size:13px;cursor:pointer;text-decoration:none;font-family:inherit}.btn:hover{background:var(--brand-hover)}.btn.gray{background:#4e5058}.btn.gray:hover{background:#3f4147}.btn.green{background:var(--minecraft)}.btn.green:hover{background:var(--minecraft-hover)}.btn.yellow{background:#9d741f}.btn.yellow:hover{background:#856218}.btn.danger{background:#a12d2f}.btn.danger:hover{background:#8b2527}.btn[disabled]{opacity:.45;cursor:not-allowed}
.flow,code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.flow{background:var(--panel);border:1px solid #111;border-left:3px solid var(--brand);border-radius:7px;padding:11px;margin-top:10px;color:#b5bac1;white-space:pre-wrap;font-size:13px;word-break:break-word}.mcCard .flow,.flow.mc{border-left-color:var(--minecraft)}.mailCard .flow,.flow.mail{border-left-color:var(--mail)}
.pill{display:inline-block;border:1px solid var(--brand-border);background:var(--brand-soft);color:var(--brand-text);padding:3px 8px;border-radius:999px;font-size:12px;font-weight:750;margin:3px 4px 3px 0}.pill.mc{border-color:var(--minecraft-border);background:var(--minecraft-soft);color:var(--minecraft-text)}.pill.mail{border-color:var(--mail-border);background:var(--mail-soft);color:#ffe5a8}
.dot{width:10px;height:10px;border-radius:50%;display:inline-block;background:#4e5058;margin-right:8px;box-shadow:0 0 0 3px rgba(255,255,255,.04)}.dot.online{background:var(--minecraft)}.dot.connecting{background:var(--mail)}.dot.error{background:var(--red)}
.stats{display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:12px;font-size:12px;color:var(--muted)}.stats b{color:#fff}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 14px}.full{grid-column:1/-1}label{display:block;font-size:11px;font-weight:800;color:var(--muted);margin:11px 0 4px;text-transform:uppercase;letter-spacing:.04em}input[type=text],input[type=number],input[type=password],select,textarea{width:100%;background:var(--panel);border:1px solid #111;color:var(--txt);border-radius:5px;padding:9px 10px;font-family:inherit;font-size:14px}textarea{font-family:ui-monospace,Consolas,monospace;font-size:12px;min-height:92px;resize:vertical}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--brand)}.check{display:flex;align-items:center;gap:9px;margin:11px 0;min-height:38px}.check input{width:17px;height:17px;accent-color:var(--brand)}.check span{font-size:14px}.hint{font-size:12px;color:var(--muted);margin-top:4px}.note{border-left:3px solid var(--brand);padding:8px 10px;background:var(--brand-soft);color:var(--muted);margin-top:12px;border-radius:4px;font-size:13px}.note.mc{border-left-color:var(--minecraft);background:var(--minecraft-soft)}.note.mail{border-left-color:var(--mail);background:var(--mail-soft)}
.sectionHead{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}.sectionHead h2{margin:0}.table{width:100%;border-collapse:collapse;margin-top:10px}.table th,.table td{text-align:left;padding:9px 8px;border-bottom:1px solid var(--border);font-size:13px}.table th{color:var(--muted);font-size:11px;text-transform:uppercase}.table td{color:var(--txt)}
#logBox{background:var(--panel);border:1px solid #111;border-radius:7px;padding:11px;height:520px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;color:#b5bac1;white-space:pre-wrap}.logline{padding:1px 0}.logline .t{color:#6d6f78}.logline .s{font-weight:700}.s.web{color:var(--brand-text)}.s.smtp,.s.mail{color:#f2b53f}.s.agent{color:#949ba4}.logline .s[class*="minecraft"]{color:#7fe0a5}
.toast{position:fixed;right:16px;bottom:16px;background:var(--panel);border:1px solid var(--brand-border);color:#fff;padding:11px 15px;border-radius:7px;font-size:13px;opacity:0;transform:translateY(6px);transition:.18s;pointer-events:none;max-width:420px}.toast.show{opacity:1;transform:translateY(0)}.footer{color:var(--muted);font-size:12px;text-align:center;margin:18px 0}.hidden{display:none!important}.lock{max-width:400px;margin:12vh auto}.dangerText{color:#ff9c9e}.muted{color:var(--muted)}
@media(max-width:900px){.grid{grid-template-columns:1fr}.grid2{grid-template-columns:1fr}.fields{grid-template-columns:1fr}.top{flex-direction:column}.shell{padding:10px}.top h1{font-size:26px}.full{grid-column:auto}}

/* Web Hub: simpler layout using the existing Hostish design tokens. */
.webIntro{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
.webUrl{display:inline-flex;align-items:center;gap:8px;min-width:0;color:var(--brand-text);font-weight:700;text-decoration:none;overflow-wrap:anywhere}
.webUrl:hover{text-decoration:underline}
.webStatus{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--muted)}
.webStatus strong{color:#fff}
.webModes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:10px}
.webMode{position:relative;display:block;cursor:pointer;background:var(--panel);border:1px solid var(--border);border-radius:7px;padding:13px;min-height:104px}
.webMode:has(input:checked){border-color:var(--brand);background:var(--brand-soft)}
.webMode:focus-within{outline:2px solid var(--brand);outline-offset:2px}
.webMode input{position:absolute;right:12px;top:14px;accent-color:var(--brand)}
.webMode strong{display:block;color:#fff;font-size:14px;padding-right:18px}
.webMode span{display:block;color:var(--muted);font-size:12px;margin-top:5px;line-height:1.4}
.webField{margin-top:16px}.webField label{font-size:12px;text-transform:none;letter-spacing:0;color:var(--txt)}
.webField input,.webField textarea{margin-top:2px}.webField .hint{max-width:72ch}
.webRows{display:grid;gap:9px;margin-top:10px}.webRoute{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.6fr) auto;gap:8px;align-items:center}
.webRoute input{min-width:0}.webRoute .btn{padding:9px 11px}
.webDisclosure{margin-bottom:14px;background:var(--card);border:1px solid var(--border);border-radius:9px;overflow:hidden}
.webDisclosure>summary{list-style:none;cursor:pointer;padding:16px 18px;display:flex;align-items:center;justify-content:space-between;gap:12px;color:#fff;font-size:15px;font-weight:750}
.webDisclosure>summary::-webkit-details-marker{display:none}
.webDisclosure>summary::after{content:'⌄';font-size:19px;color:var(--muted);line-height:1;transition:transform .15s}
.webDisclosure[open]>summary::after{transform:rotate(180deg)}
.webDisclosure>summary small{display:block;font-size:12px;font-weight:400;color:var(--muted);margin-top:3px}
.webDisclosure .webDisclosureBody{padding:0 18px 18px}
.webDisclosure .card{background:var(--panel);margin:10px 0;padding:14px}
.webDisclosure .card input,.webDisclosure .card textarea,.webDisclosure .card select{background:var(--card)}
.webSave{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;background:var(--card);border:1px solid var(--brand-border);border-radius:9px;padding:12px 16px;margin:0 0 16px;position:sticky;bottom:8px;z-index:5;box-shadow:0 6px 24px #0006}
.webSave .actions{margin-top:0}.webSave .hint{margin:0}.webSave .dirty{color:#ffe5a8}
.webToggle{display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;color:var(--txt);font-size:13px;font-weight:700}
.webToggle input{width:17px;height:17px;accent-color:var(--brand)}
.webSimpleTitle{margin:0;color:#fff;font-size:18px}.webSimpleDesc{color:var(--muted);font-size:13px;margin:4px 0 0}
.webAdvancedHint{font-size:12px;color:var(--muted);margin:10px 0}
.webRouteEmpty{font-size:12px;color:var(--muted);margin:0}
@media(max-width:650px){.webModes{grid-template-columns:1fr}.webMode{min-height:0}.webRoute{grid-template-columns:minmax(0,1fr) auto}.webRoute input:nth-child(2){grid-column:1 / -1;grid-row:2}.webRoute button{grid-column:2;grid-row:1}.webSave{bottom:0;border-radius:9px 9px 0 0}.webIntro{align-items:flex-start}}

/* Scripts: compact file-path manager using existing Hostish colors. */
.scriptAddRow{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:9px;align-items:end;margin-top:10px}
.scriptAddRow label{margin-top:0}
.scriptAddRow .btn{min-height:39px}
.scriptList{display:grid;gap:10px;margin-top:14px}
.scriptItem{display:flex;gap:12px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;margin:0}
.scriptInfo{min-width:220px;flex:1 1 340px}
.scriptTitle{display:flex;align-items:center;gap:3px;flex-wrap:wrap;margin-bottom:3px}
.scriptTitle h2{font-size:15px;margin:0}
.scriptState{color:var(--muted);font-size:12px;margin-left:7px}
.scriptPath{font-family:ui-monospace,Consolas,monospace;word-break:break-all;font-size:12px;color:var(--muted);margin-top:6px}
.scriptItem .actions{margin:0;align-items:center}
.scriptItem .btn{padding:7px 11px}
.scriptAuto{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);cursor:pointer;margin-top:10px}
.scriptAuto input{width:16px;height:16px;accent-color:var(--brand)}
.scriptEdit{display:flex;gap:8px;align-items:center;margin-top:12px}
.scriptEdit input{flex:1;min-width:0}
.scriptNotice{font-size:12px;color:var(--muted);margin-top:9px}
.scriptError{color:#ff9c9e;font-size:12px;margin-top:8px;word-break:break-word}
@media(max-width:650px){.scriptAddRow{grid-template-columns:1fr}.scriptItem .actions{width:100%}.scriptEdit{flex-wrap:wrap}.scriptEdit input{flex-basis:100%}}
</style>
</head>
<body>
<div class="shell">
<div id="lock" class="hidden"><section class="hero lock"><h1>Hostish</h1><p class="subtitle">This local control panel is password protected.</p><label>Panel password</label><input id="lockPass" type="password" autocomplete="current-password"><div class="actions"><button class="btn" id="lockBtn">Unlock</button></div><p id="lockError" class="hint dangerText"></p></section></div>
<div id="app" class="hidden">
<header class="hero">
 <div class="top"><div><div class="brandLine"><span class="brandMark"></span><h1>Hostish Unified Agent</h1></div><p id="headerSub" class="subtitle">Web hosting, SMTP and multiple Minecraft servers from one local agent.</p></div><div><span class="pill" id="pillAccount">account</span><span class="pill" id="pillUptime">uptime</span><span class="pill" id="pillVersion">version</span></div></div>
 <nav class="nav" id="nav"></nav>
</header>
<main id="page"></main>
<div class="footer">Hostish unified agent &mdash; local control panel on the port you choose.</div>
</div></div>
<div class="toast" id="toast"></div>
<script>
const NAV=[['overview','Overview','/overview'],['website','Website','/website'],['mail','Mail','/mail'],['minecraft','Minecraft','/minecraft'],['scripts','Scripts','/scripts'],['logs','Logs','/logs'],['settings','Manager','/settings'],['update','Update','/update']];
const pathPage=(location.pathname.replace(/^\//,'')||'overview').replace('index.html','overview');
const PAGE=NAV.some(n=>n[0]===pathPage)?pathPage:'overview';
let KEY=sessionStorage.getItem('hostishKey')||'',STATE=null,SETTINGS={},MC_CONFIGS=[],MC_STATUSES=[],MC_EDIT_ID='',LOG_SEQ=0,LOG_FILTER='all';
const SECRET_MASK='********';
function h(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function toast(msg){const t=document.getElementById('toast');t.textContent=msg||'';t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3000)}
async function api(path,opt={}){opt.headers=Object.assign({'Content-Type':'application/json'},opt.headers||{});if(KEY)opt.headers['X-Hostish-Auth']=KEY;const r=await fetch(path,opt);let data={};try{data=await r.json()}catch(e){data={error:'Invalid response from local agent'}}if(r.status===401&&r.headers.get('X-Hostish-Panel-Auth')==='required'){showLock();throw new Error('Panel authentication required')}if(!r.ok)throw new Error(data.message||data.error||('HTTP '+r.status));return data}
function showLock(){lock.classList.remove('hidden');app.classList.add('hidden')}function showApp(){lock.classList.add('hidden');app.classList.remove('hidden')}
lockBtn.onclick=async()=>{KEY=lockPass.value;try{STATE=await api('/api/state');sessionStorage.setItem('hostishKey',KEY);showApp();await boot()}catch(e){lockError.textContent='Wrong password.'}};
function statusText(s){return ({online:'Online',connecting:'Connecting',stopped:'Stopped',error:'Error'})[s]||s||'Stopped'}
function stat(k,v){return `<span>${h(k)} <b>${h(v)}</b></span>`}
function serviceActions(id,state){const st=state?.status||'stopped';return `<div class="actions"><button class="btn ${id==='minecraft'?'green':''}" data-service="${id}" data-action="${st==='stopped'?'start':'restart'}">${st==='stopped'?'Start':'Restart'}</button><button class="btn gray" data-service="${id}" data-action="stop" ${st==='stopped'?'disabled':''}>Stop</button></div>`}
function bindServiceButtons(){document.querySelectorAll('[data-service]').forEach(b=>b.onclick=async()=>{b.disabled=true;const r=await api('/api/service',{method:'POST',body:JSON.stringify({service:b.dataset.service,action:b.dataset.action})});toast(r.message);await refreshState();renderCurrent()})}
function nav(){navEl=document.getElementById('nav');navEl.innerHTML=NAV.map(n=>`<a class="${PAGE===n[0]?'active':''}" href="${n[2]}">${n[1]}</a>`).join('')}
function header(){pillAccount.textContent=(SETTINGS.USERNAME&&SETTINGS.USERNAME!=='your_username')?SETTINGS.USERNAME:'global account not set';pillUptime.textContent='up '+(STATE?.uptime||'0s');pillVersion.textContent='agent v'+(STATE?.version||'?')}
async function refreshState(){STATE=await api('/api/state');SETTINGS=STATE.settings||{};header();return STATE}

const WEB_BASIC=[
 ['WEB_ENABLED','bool','Enable website tunnel'],['MODE','select:static|proxy|hybrid','Static files, local-app proxy, or both'],['WEBSITE_DIR','text','Static/hybrid website directory'],['PORT','number','Convenience local app port'],['UPSTREAM_BASE','text','Default proxy target'],['PROXY_PREFIXES','list','Hybrid proxy prefixes, one per line'],['UPSTREAM_ROUTES','routes','One route per line: api/ = http://127.0.0.1:5001'],['HOST_ROUTES','hostroutes','One host per line: sub.example.com = http://127.0.0.1:5001. JSON values are also accepted on the right side.']];
const WEB_PROXY=[['UPSTREAM_NETWORK_POLICY','select:loopback|private|any','Allowed upstream network scope'],['PROXY_KEEPALIVE_ENABLED','bool','Reuse upstream HTTP connections'],['PROXY_MAX_IDLE_CONNECTIONS','number','Maximum cached idle upstream connections'],['PROXY_IDLE_CONNECTION_SECONDS','number','Idle connection lifetime'],['MAX_CONCURRENT_REQUESTS','number','0 = unlimited'],['MAX_QUEUED_REQUESTS','number','0 = unlimited'],['MAX_REQUEST_BODY_BYTES','number','0 = unlimited'],['MAX_PROXY_BYTES','number','0 = unlimited'],['UPSTREAM_CONNECT_TIMEOUT_SECONDS','number','Proxy connect timeout'],['UPSTREAM_RESPONSE_TIMEOUT_SECONDS','number','0 = unlimited for SSE/long jobs']];
const WEB_TRANSPORT=[['TUNNEL_PROTOCOL','select:1|2|3','Hostish tunnel protocol'],['TUNNEL_CHUNK_BYTES','number','Tunnel stream chunk size'],['WEBSOCKET_MAX_INCOMING_BYTES','number','Maximum incoming WebSocket message size'],['REQUEST_STREAM_BUFFER_CHUNKS','number','Request-stream backpressure buffer'],['USER_WEBSOCKET_PROXY_ENABLED','bool','Proxy visitor WebSockets'],['PROXY_READ_CHUNK_BYTES','number','Upstream response read chunk'],['STATIC_READ_CHUNK_BYTES','number','Static-file read chunk']];
const WEB_HTTP=[['HANDLE_OPTIONS_LOCALLY','bool','Answer CORS OPTIONS locally'],['ADD_CORS_HEADER_TO_RESPONSES','bool','Add CORS response header'],['CORS_ALLOW_ORIGIN','text','Access-Control-Allow-Origin'],['CORS_ALLOW_METHODS','text','Allowed methods'],['CORS_ALLOW_HEADERS','text','Allowed headers'],['STATIC_CACHE_CONTROL','text','Cache-Control for static files'],['STATIC_SECURITY_MODE','select:safe|custom|unrestricted','Static secret/source protection'],['STATIC_ALLOW_PATTERNS','list','Static allow patterns'],['STATIC_BLOCK_PATTERNS','list','Static block patterns'],['ALLOW_STATIC_SYMLINK_ESCAPE','bool','Allow symlinks outside website root'],['MAX_FILE_BYTES','number','0 = unlimited static file size']];
const WEB_EXTRAS=[['FAVICON_BYPASS_ENABLED','bool','Fetch a custom favicon directly'],['FAVICON_BYPASS_URL','text','Custom favicon URL'],['FAVICON_MAX_BYTES','number','Favicon download limit'],['CUSTOM_404_ENABLED','bool','Use custom 404 file'],['CUSTOM_404_PATH','text','Custom 404 path'],['IP_PROTECTION_ENABLED','bool','Mask the host public IP in textual responses'],['IP_PROTECTION_REFRESH_SECONDS','number','Public-IP refresh interval'],['IP_PROTECTION_REPLACEMENT','text','Replacement text']];
const MAIL_SMTP=[['SMTP_ENABLED','bool','Enable Hostish SMTP tunnel'],['LINKED_CUSTOM_DOMAIN_MAIL_ENABLED','bool','Linked custom-domain email — only enable when using a linked custom domain for email; Hostish manages the DKIM private key and you add the shown one-time DNS records'],['LOCAL_DELIVERY_HOST','text','Inbound local SMTP server'],['LOCAL_DELIVERY_PORT','number','Inbound local SMTP port'],['LOCAL_DELIVERY_MODE','select:plain|starttls|ssl','Inbound delivery transport'],['LOCAL_DELIVERY_USERNAME','text','Optional local SMTP username'],['LOCAL_DELIVERY_PASSWORD','password','Optional local SMTP password'],['LOCAL_DELIVERY_TLS_VERIFY','bool','Verify local TLS certificates'],['OUTBOUND_RELAY_ENABLED','bool','Run local outbound SMTP relay'],['LOCAL_RELAY_BIND_HOST','text','Local relay bind host'],['LOCAL_RELAY_BIND_PORT','number','Local relay port'],['LOCAL_RELAY_USERNAME','text','Optional relay AUTH username'],['LOCAL_RELAY_PASSWORD','password','Optional relay AUTH password'],['MAX_MESSAGE_BYTES','number','Maximum message bytes'],['MAX_RECIPIENTS','number','Maximum recipients'],['WS_CHUNK_BYTES','number','SMTP tunnel chunk bytes'],['RECONNECT_SECONDS','number','SMTP reconnect delay']];
const WEBMAIL_FIELDS=[['WEBMAIL_ENABLED','bool','Use built-in webmail/mailbox storage'],['WEBMAIL_DOMAIN','text','Blank = USERNAME.hostish.site'],['WEBMAIL_DATA_DIR','text','Mailbox data directory'],['WEBMAIL_SESSION_SECONDS','number','Webmail session lifetime'],['WEBMAIL_REJECT_UNKNOWN_RECIPIENTS','bool','Reject unknown local recipients'],['MAIL_CLIENT_TEMPLATE_FILE','text','Optional custom HTML file used by Download/Copy; relative paths are beside this agent']];
const MANAGER_FIELDS=[['USERNAME','text','Global Hostish account for website + SMTP'],['PASSWORD','password','Global Hostish account password'],['CONTROL_PANEL_ENABLED','bool','Enable local management webpage'],['CONTROL_PANEL_HOST','text','Control-panel bind address'],['CONTROL_PANEL_PORT','number','Control-panel port; default 5000'],['CONTROL_PANEL_PASSWORD','password','Required when binding beyond loopback'],['CONTROL_PANEL_OPEN_BROWSER','bool','Open panel in browser at startup']];
const DIAG_FIELDS=[['PRINT_REQUEST_HEADERS','bool','Log visitor request headers'],['PRINT_REQUEST_BODY_PREVIEW','bool','Log request body preview'],['BODY_PREVIEW_BYTES','number','Body preview bytes'],['PRINT_SENSITIVE_HEADERS','bool','Do not redact secrets in logged headers'],['REPEAT_ERROR_SUMMARY_EVERY','number','Repeat-error summary interval'],['PERIODIC_SUMMARY_ENABLED','bool','Periodic website stats summary'],['PERIODIC_SUMMARY_SECONDS','number','Stats summary interval'],['IDLE_STATUS_CHECK_ENABLED','bool','Confirm idle website tunnel with Hostish'],['IDLE_CHECK_INTERVAL_SECONDS','number','Idle status check interval']];
const RAW_COMPLEX_FIELDS=[['PROXY_PREFIXES','list','Raw proxy-prefix list, one value per line'],['UPSTREAM_ROUTES','json','Raw UPSTREAM_ROUTES JSON object'],['HOST_ROUTES','json','Raw HOST_ROUTES JSON object; use this for advanced per-host mode/root/upstream objects'],['STATIC_ALLOW_PATTERNS','list','Raw static allow patterns'],['STATIC_BLOCK_PATTERNS','list','Raw static block patterns'],['SENSITIVE_HEADER_NAMES','list','Raw redacted-header-name set, one value per line']];
const UPDATE_FIELDS=[['SELF_UPDATE_ENABLED','bool','Allow self-update'],['LATEST_AGENT_URL','text','Unified agent update URL'],['SELF_UPDATE_BACKUP_ENABLED','bool','Back up the current script'],['SELF_UPDATE_BACKUP_DIR','text','Backup directory'],['SELF_UPDATE_BACKUP_KEEP','number','Backups to retain'],['SELF_UPDATE_DOWNLOAD_TIMEOUT_SECONDS','number','Update download timeout'],['SELF_UPDATE_MAX_DOWNLOAD_BYTES','number','Maximum update size'],['SELF_UPDATE_REQUIRE_HTTPS','bool','Require HTTPS'],['SELF_UPDATE_ALLOW_CROSS_HOST_REDIRECTS','bool','Allow cross-host redirects'],['SELF_UPDATE_EXPECTED_SHA256','text','Optional SHA-256 pin'],['SELF_UPDATE_ED25519_PUBLIC_KEY_PEM','textarea','Optional Ed25519 public key PEM'],['SELF_UPDATE_SIGNATURE_SUFFIX','text','Signature URL suffix'],['SELF_UPDATE_NONINTERACTIVE_ACTION','select:continue|update|exit','Startup behavior when newer version exists'],['SELF_UPDATE_PRESERVE_EXTRA_NAMES','list','Extra setting names to preserve'],['SELF_UPDATE_CHECK_ON_STARTUP','bool','Check for update on startup']];
const MC_FIELDS=[['name','text','Display name'],['enabled','bool','Auto-start this tunnel when the unified agent starts'],['username','text','Dedicated Hostish username'],['password','password','Dedicated Hostish password'],['minecraft_host','text','Local Minecraft host'],['minecraft_port','number','Local Minecraft TCP port'],['start_bat','text','start.bat/start.sh path; blank = manual'],['auto_start_server','bool','Start local server automatically'],['server_start_timeout','number','Startup timeout seconds'],['server_start_check_interval','number','Port-check interval seconds'],['homepage_file','text','Optional browser homepage for this Hostish account'],['simple_voice_chat_enabled','bool','Tunnel Simple Voice Chat UDP'],['simple_voice_chat_host','text','Local voice-chat host'],['simple_voice_chat_port','number','Local voice-chat UDP port'],['simple_voice_chat_public_port','number','0 = dynamic public UDP port'],['simple_voice_chat_service_id','text','Hostish UDP service id'],['simple_voice_chat_config_file','text','voicechat-server.properties path']];
function mapLines(value,advanced=false){return Object.entries(value||{}).map(([k,v])=>{if(!advanced&&typeof v==='string')return k+' = '+v;return k+' = '+(typeof v==='string'?v:JSON.stringify(v))}).join('\n')}
function parseMapLines(text,name,advanced=false){const out={};for(const raw of String(text||'').split('\n')){const line=raw.trim();if(!line||line.startsWith('#'))continue;const at=line.indexOf('=');if(at<1)throw new Error(name+': expected key = value on line: '+line);const key=line.slice(0,at).trim(),rhs=line.slice(at+1).trim();if(!key||!rhs)throw new Error(name+': key and value are required on line: '+line);if(advanced&&(rhs.startsWith('{')||rhs.startsWith('['))){try{out[key]=JSON.parse(rhs)}catch(e){throw new Error(name+': invalid JSON value for '+key)}}else out[key]=rhs}return out}
const FIELD_NAMES={WEB_ENABLED:'Website hosting',WEBSITE_DIR:'Website folder',UPSTREAM_BASE:'Local app URL',PORT:'Local app port',PROXY_PREFIXES:'Paths sent to your app',UPSTREAM_ROUTES:'Custom proxy routes',HOST_ROUTES:'Domains and subdomains',CUSTOM_404_PATH:'404 page file',IP_PROTECTION_ENABLED:'Hide your public IP',FAVICON_BYPASS_URL:'Favicon URL',STATIC_SECURITY_MODE:'File protection',UPSTREAM_NETWORK_POLICY:'Allowed upstream addresses',MAX_FILE_BYTES:'Maximum file size',MAX_CONCURRENT_REQUESTS:'Maximum simultaneous requests',PROXY_KEEPALIVE_ENABLED:'Reuse proxy connections',TUNNEL_PROTOCOL:'Tunnel protocol',USER_WEBSOCKET_PROXY_ENABLED:'Visitor WebSocket support',CORS_ALLOW_ORIGIN:'Allowed website origins',ADD_CORS_HEADER_TO_RESPONSES:'Enable CORS headers'};
function settingLabel(name){return FIELD_NAMES[name]||name.toLowerCase().split('_').map(s=>s.toUpperCase()==='IP'?'IP':s.charAt(0).toUpperCase()+s.slice(1)).join(' ')}
function field(name,kind,hint,value){const id='f_'+name;if(kind==='bool')return `<div class="check"><input type="checkbox" id="${id}" data-setting="${name}" data-kind="bool" ${value?'checked':''}><span>${h(hint)}</span></div>`;let input='';if(kind.startsWith('select:')){input=`<select id="${id}" data-setting="${name}" data-kind="str">`+kind.slice(7).split('|').map(o=>`<option ${String(o)===String(value)?'selected':''}>${h(o)}</option>`).join('')+'</select>'}else if(kind==='list'){input=`<textarea id="${id}" data-setting="${name}" data-kind="list">${h((value||[]).join('\n'))}</textarea>`}else if(kind==='routes'){input=`<textarea id="${id}" data-setting="${name}" data-kind="routes" placeholder="api/ = http://127.0.0.1:5001">${h(mapLines(value,false))}</textarea>`}else if(kind==='hostroutes'){input=`<textarea id="${id}" data-setting="${name}" data-kind="hostroutes" placeholder="sub.example.com = http://127.0.0.1:5001">${h(mapLines(value,true))}</textarea>`}else if(kind==='json'){input=`<textarea id="${id}" data-setting="${name}" data-kind="json">${h(JSON.stringify(value||{},null,2))}</textarea>`}else if(kind==='textarea'){input=`<textarea id="${id}" data-setting="${name}" data-kind="str">${h(value||'')}</textarea>`}else{const secret=kind==='password'&&value===SECRET_MASK;input=`<input type="${kind}" id="${id}" data-setting="${name}" data-kind="${kind==='number'?'num':'str'}" value="${secret?'':h(value==null?'':value)}" ${secret?'placeholder="Saved — leave blank to keep"':''}>`}return `<div><label for="${id}">${h(settingLabel(name))}</label>${input}<div class="hint">${h(hint)}</div></div>`}
function group(title,fields,desc=''){return `<section class="card"><h2>${h(title)}</h2>${desc?`<p>${h(desc)}</p>`:''}<div class="fields">${fields.map(f=>field(f[0],f[1],f[2],SETTINGS[f[0]])).join('')}</div></section>`}
function collectSettings(fields){const out={};for(const f of fields){const el=document.getElementById('f_'+f[0]);if(!el)continue;const kind=el.dataset.kind;if(kind==='bool')out[f[0]]=el.checked;else if(kind==='num')out[f[0]]=parseInt(el.value||'0',10)||0;else if(kind==='list')out[f[0]]=el.value.split('\n').map(v=>v.trim()).filter(Boolean);else if(kind==='routes')out[f[0]]=parseMapLines(el.value,f[0],false);else if(kind==='hostroutes')out[f[0]]=parseMapLines(el.value,f[0],true);else if(kind==='json'){try{out[f[0]]=JSON.parse(el.value||'{}')}catch(e){throw new Error(f[0]+' is not valid JSON')}}else{if(el.type==='password'&&!el.value)continue;out[f[0]]=el.value}}return out}
async function saveGroups(fields,restart=false){try{const values=collectSettings(fields);const r=await api('/api/settings',{method:'POST',body:JSON.stringify({settings:values,restart})});if(!r.ok)throw new Error(r.message||'Could not save settings');toast(r.message);await refreshState();await renderCurrent()}catch(e){toast(e.message)}}
function saveBar(fields){window._saveFields=fields;return `<section class="card"><div class="actions"><button class="btn" id="savePage">Save settings</button><button class="btn gray" id="saveRestart">Save + restart services</button></div><div class="note">Changes that affect the panel bind address or local relay socket may require a full agent restart.</div></section>`}
function bindSave(){if(document.getElementById('savePage'))savePage.onclick=()=>saveGroups(window._saveFields,false);if(document.getElementById('saveRestart'))saveRestart.onclick=()=>saveGroups(window._saveFields,true)}
function serviceCard(id,title,cls,info,stats=''){const st=STATE.services?.[id]||{};return `<section class="card ${cls}"><h2><span class="dot ${h(st.status)}"></span>${h(title)}</h2><p><b style="color:#fff">${h(statusText(st.status))}</b>${st.detail?' · '+h(st.detail):''}</p><div class="flow ${id==='minecraft'?'mc':id==='smtp'?'mail':''}">${h(info)}</div><div class="stats">${stats}</div>${serviceActions(id,st)}</section>`}
function renderOverview(){const s=SETTINGS,st=STATE.stats||{},mc=STATE.minecraft_manager||{};page.innerHTML=`<section class="hero"><div class="sectionHead"><div><h2>Overview</h2><p>Website hosting, Hostish mail, the complete bundled Minecraft Manager, and managed scripts in one agent.</p></div><div class="actions"><button class="btn gray" data-service="all" data-action="start">Start all services</button><button class="btn danger" data-service="all" data-action="stop">Stop all</button></div></div><div class="flow">Website   ${s.WEB_ENABLED?'https://'+s.USERNAME+'.hostish.site/ ('+s.MODE+')':'disabled'}\nMail      ${(s.SMTP_ENABLED||s.WEBMAIL_ENABLED)?'*@'+s.USERNAME+'.hostish.site':'disabled'}\nMinecraft ${mc.running?'manager online on port '+mc.port:(mc.detail||'manager stopped')}\nManager   http://${s.CONTROL_PANEL_HOST}:${s.CONTROL_PANEL_PORT}/</div></section><div class="grid">${serviceCard('web','Website','',s.MODE==='proxy'?s.UPSTREAM_BASE:s.WEBSITE_DIR,stat('requests',st.web?.requests||0)+stat('sent',STATE.web_bytes||'0 B'))}${serviceCard('smtp','Mail','mailCard',s.LOCAL_DELIVERY_HOST+':'+s.LOCAL_DELIVERY_PORT,stat('delivered',st.smtp?.delivered||0)+stat('sent',st.smtp?.sent||0))}${serviceCard('minecraft','Minecraft Manager','mcCard',mc.running?('Dashboard port '+mc.port):(mc.detail||'Bundled manager stopped'),stat('bundle','v'+(mc.bundle_version||'4'))+stat('pid',mc.pid||'—'))}</div>`;bindServiceButtons()}
const WEB_ALL_FIELDS=[...WEB_BASIC,...WEB_PROXY,...WEB_TRANSPORT,...WEB_HTTP,...WEB_EXTRAS];
const WEB_VISIBLE_FIELDS=WEB_BASIC.filter(f=>['PORT'].includes(f[0]));
let WEB_DIRTY=false,WEB_SAVING=false;
function webField(name,label,description,placeholder=''){
 const value=SETTINGS[name]??'';
 return `<div class="webField"><label for="f_${name}">${h(label)}</label><input id="f_${name}" type="text" data-kind="str" value="${h(value)}" placeholder="${h(placeholder)}" autocomplete="off"><div class="hint">${h(description)}</div></div>`;
}
function webRoutes(name){
 const data=SETTINGS[name]||{};
 const rows=Object.entries(data).map(([key,target])=>webRouteRow(name,key,target)).join('');
 return `<div class="webRows" id="webRows_${name}">${rows||'<p class="webRouteEmpty">None added yet.</p>'}</div><div class="actions"><button type="button" class="btn gray" data-route-add="${name}">+ Add ${name==='UPSTREAM_ROUTES'?'route':'domain'}</button></div>`;
}
function webRouteRow(name,key='',target=''){
 const val=typeof target==='string'?target:JSON.stringify(target);
 return `<div class="webRoute" data-route="${name}"><input type="text" aria-label="${name==='UPSTREAM_ROUTES'?'Path prefix':'Domain'}" data-route-key placeholder="${name==='UPSTREAM_ROUTES'?'api/':'shop.example.com'}" value="${h(key)}"><input type="text" aria-label="${name==='UPSTREAM_ROUTES'?'Destination URL':'Domain destination'}" data-route-value placeholder="http://127.0.0.1:5001" value="${h(val)}"><button type="button" class="btn gray" data-route-remove aria-label="Remove row">Remove</button></div>`;
}
function webReadRoutes(name){
 const data={};
 for(const row of document.querySelectorAll(`[data-route="${name}"]`)){
  const key=row.querySelector('[data-route-key]').value.trim(),value=row.querySelector('[data-route-value]').value.trim();
  if(!key&&!value)continue;
  if(!key||!value)throw new Error((name==='HOST_ROUTES'?'Domain':'Route')+' needs both a name and destination');
  if(Object.prototype.hasOwnProperty.call(data,key))throw new Error('Duplicate '+(name==='HOST_ROUTES'?'domain: ':'route: ')+key);
  if(name==='HOST_ROUTES'&&(value.startsWith('{')||value.startsWith('['))){try{data[key]=JSON.parse(value)}catch(e){throw new Error('Invalid advanced config for '+key)}}
  else data[key]=value;
 }
 return data;
}
function webMode(){return document.querySelector('input[name="websiteMode"]:checked')?.value||SETTINGS.MODE||'static'}
function webValues(){
 const values=collectSettings(WEB_ALL_FIELDS);
 values.WEB_ENABLED=!!document.getElementById('f_WEB_ENABLED')?.checked;
 values.MODE=webMode();
 values.UPSTREAM_ROUTES=webReadRoutes('UPSTREAM_ROUTES');
 values.HOST_ROUTES=webReadRoutes('HOST_ROUTES');
 return values;
}
function webChanges(){
 const values=webValues(),result={};
 for(const [key,value] of Object.entries(values)){const previous=SETTINGS[key];if(typeof previous==='number'&&typeof value==='string'&&String(previous)===value)continue;if(JSON.stringify(value)!==JSON.stringify(previous))result[key]=value;}
 return result;
}
function webModeVisibility(){
 const mode=webMode();
 document.getElementById('webStaticSettings')?.classList.toggle('hidden',mode==='proxy');
 document.getElementById('webAppSettings')?.classList.toggle('hidden',mode==='static');
 document.getElementById('webHybridSettings')?.classList.toggle('hidden',mode!=='hybrid');
 document.getElementById('webModeHelp').textContent=({static:'Host a folder of HTML, CSS, JS, and other files.',proxy:'Show your locally running app on your Hostish address.',hybrid:'Serve website files, but send selected paths to your local app.'})[mode];
}
function webDirtyState(){
 const text=document.getElementById('webSaveHint'),btn=document.getElementById('webSaveApply');
 if(!text||!btn)return;
 try{WEB_DIRTY=Object.keys(webChanges()).length>0;text.textContent=WEB_DIRTY?'Unsaved changes':'All changes saved';text.classList.toggle('dirty',WEB_DIRTY)}
 catch(e){WEB_DIRTY=true;text.textContent=e.message;text.classList.add('dirty')}
 btn.disabled=!WEB_DIRTY||WEB_SAVING;
 document.getElementById('webUndo').disabled=!WEB_DIRTY||WEB_SAVING;
}
async function webSave(apply=true){
 if(WEB_SAVING)return;
 let changes;
 try{
  changes=webChanges();
  if(!Object.keys(changes).length){toast('No changes to save');return}
  if(changes.WEB_ENABLED||SETTINGS.WEB_ENABLED){
   if((changes.MODE||webMode())!=='proxy'&&!document.getElementById('f_WEBSITE_DIR').value.trim())throw new Error('Choose a website folder');
   if((changes.MODE||webMode())!=='static'&&!document.getElementById('f_UPSTREAM_BASE').value.trim())throw new Error('Enter the local app URL');
  }
  WEB_SAVING=true;webDirtyState();
  const r=await api('/api/settings',{method:'POST',body:JSON.stringify({settings:changes,...(apply?{restart_service:'web'}:{})})});
  if(!r.ok)throw new Error(r.message||'Could not save settings');
  toast(apply?'Saved. Website service updated.':'Changes saved. Apply when ready.');
  await refreshState();await renderCurrent();
 }catch(e){toast(e.message)}finally{WEB_SAVING=false;webDirtyState()}
}
function renderWebsite(){
 const s=SETTINGS,st=STATE.services?.web||{},stats=STATE.stats?.web||{};
 const url=s.USERNAME&&s.USERNAME!=='your_username'?'https://'+encodeURIComponent(s.USERNAME)+'.hostish.site/':'';
 WEB_DIRTY=false;
 page.innerHTML=`
 <section class="hero">
  <div class="webIntro"><div><h2 class="webSimpleTitle">Web Hub</h2><p class="webSimpleDesc">Manage what visitors see at your Hostish address.</p></div><label class="webToggle"><input id="f_WEB_ENABLED" type="checkbox" ${s.WEB_ENABLED?'checked':''}> Website enabled</label></div>
  <div class="webStatus" style="margin-top:14px"><span class="dot ${h(st.status)}"></span><strong>${h(statusText(st.status))}</strong><span>${s.WEB_ENABLED?h(st.detail||'Website tunnel'):'Website hosting is off'}</span></div>
  ${url?`<div class="actions"><a class="webUrl" href="${h(url)}" target="_blank" rel="noopener noreferrer">${h(url)} ↗</a><button type="button" class="btn gray" id="webCopyUrl">Copy link</button></div>`:'<div class="note">Set your Hostish account under Manager to get your website address.</div>'}
  <div class="stats">${stat('Requests',stats.requests||0)}${stat('Data sent',STATE.web_bytes||'0 B')}</div>
 </section>
 <section class="card"><h2 class="webSimpleTitle">What are you hosting?</h2><p class="webSimpleDesc" id="webModeHelp">Choose how your site works.</p>
  <div class="webModes" role="radiogroup" aria-label="Hosting type">
   ${[['static','Static website','HTML, CSS, images, and other files'],['proxy','Local app','A website or backend running on your computer'],['hybrid','Both','Static files plus app routes']].map(([mode,label,description])=>`<label class="webMode"><input type="radio" name="websiteMode" value="${mode}" ${s.MODE===mode?'checked':''}><strong>${label}</strong><span>${description}</span></label>`).join('')}
  </div>
  <div id="webStaticSettings">${webField('WEBSITE_DIR','Website folder','Choose the local folder containing your website files.','C:\\Sites\\my-website')}</div>
  <div id="webAppSettings">${webField('UPSTREAM_BASE','Local app URL','The address where your app is already running, for example http://127.0.0.1:3000.','http://127.0.0.1:3000')}</div>
  <div id="webHybridSettings" class="webField"><label for="f_PROXY_PREFIXES">Paths handled by your app</label><textarea id="f_PROXY_PREFIXES" data-kind="list" rows="2" placeholder="api/">${h((s.PROXY_PREFIXES||[]).join('\n'))}</textarea><div class="hint">One path per line, such as api/. Other paths serve files from your website folder.</div></div>
 </section>
 <details class="webDisclosure" id="webRoutesSection"><summary><span>Custom routes & domains<small>Optional — only if you need more than one app, domain, or subdomain</small></span></summary><div class="webDisclosureBody">
  <h3 class="webSimpleTitle">App routes</h3><p class="webSimpleDesc">Send particular paths to a different local app. Longer matching prefixes win.</p>${webRoutes('UPSTREAM_ROUTES')}
  <h3 class="webSimpleTitle" style="margin-top:22px">Domains & subdomains</h3><p class="webSimpleDesc">Map an already-linked hostname to an app URL. Advanced host configs can still be entered as JSON in the destination field.</p>${webRoutes('HOST_ROUTES')}
 </div></details>
 <details class="webDisclosure" id="webAdvancedSection"><summary><span>Advanced settings<small>Connection limits, CORS, security, streaming, and extras</small></span></summary><div class="webDisclosureBody">
  <p class="webAdvancedHint">Defaults work for most sites. Only change these if you need a specific behavior.</p>
  ${group('Connections & limits',[...WEB_VISIBLE_FIELDS,...WEB_PROXY])}
  ${group('HTTP & file security',WEB_HTTP)}
  ${group('Streaming & WebSockets',WEB_TRANSPORT)}
  ${group('Extras',WEB_EXTRAS)}
 </div></details>
 <div class="webSave" role="group" aria-label="Save website settings"><div class="hint" id="webSaveHint" aria-live="polite">All changes saved</div><div class="actions"><button type="button" class="btn gray" id="webUndo" disabled>Discard changes</button><button type="button" class="btn" id="webSaveApply" disabled>Save & apply</button></div></div>`;
 webModeVisibility();
 page.querySelectorAll('input[name="websiteMode"]').forEach(el=>el.addEventListener('change',()=>{webModeVisibility();webDirtyState()}));
 page.querySelectorAll('input:not([name="websiteMode"]),textarea,select').forEach(el=>{
  el.addEventListener('input',webDirtyState);el.addEventListener('change',webDirtyState);
 });
 page.querySelectorAll('[data-route-add]').forEach(button=>button.onclick=()=>{
  const name=button.dataset.routeAdd,container=document.getElementById('webRows_'+name);
  container.querySelector('.webRouteEmpty')?.remove();container.insertAdjacentHTML('beforeend',webRouteRow(name));
  container.lastElementChild.querySelector('[data-route-key]').focus();webDirtyState();
 });
 page.querySelectorAll('.webRows').forEach(container=>{
  container.addEventListener('input',webDirtyState);
  container.addEventListener('click',e=>{if(!e.target.closest('[data-route-remove]'))return;e.target.closest('.webRoute').remove();if(!container.children.length)container.innerHTML='<p class="webRouteEmpty">None added yet.</p>';webDirtyState()});
 });
 document.getElementById('webSaveApply').onclick=()=>webSave(true);
 document.getElementById('webUndo').onclick=()=>{if(!WEB_DIRTY)return;renderWebsite()};
 const copy=document.getElementById('webCopyUrl');if(copy)copy.onclick=async()=>{try{await navigator.clipboard.writeText(url);toast('Link copied')}catch(e){if(copyTextFallback(url))toast('Link copied');else toast('Could not copy link')}};
 webDirtyState();
}

async function loadMailUsers(){
 const box=document.getElementById('mailUsers');if(!box)return;
 try{
  const r=await api('/api/webmail/users');
  const users=r.users||[];
  box.innerHTML=users.length?`<table class="table"><thead><tr><th>Account</th><th>Role</th><th></th></tr></thead><tbody>${users.map(u=>`<tr><td><b>${h(u.username)}</b><div class="hint">${h(u.email)}</div></td><td>${u.admin?'Administrator':'Mailbox'}</td><td><button class="btn danger" data-mail-delete="${h(u.username)}">Delete</button></td></tr>`).join('')}</tbody></table>`:'<div class="note mail">No webmail accounts yet.</div>';
  document.querySelectorAll('[data-mail-delete]').forEach(b=>b.onclick=async()=>{if(!confirm('Delete webmail account '+b.dataset.mailDelete+'? Mailbox files are kept on disk.'))return;try{const q=await api('/api/webmail/users/'+encodeURIComponent(b.dataset.mailDelete),{method:'DELETE',body:'{}'});toast(q.message);if(q.ok)await loadMailUsers()}catch(e){toast(e.message)}});
 }catch(e){box.innerHTML=`<div class="note mail"><span class="dangerText">${h(e.message)}</span></div>`}
}
let MAIL_CLIENT_SOURCE=null;
async function getMailClientSource(force=false){if(MAIL_CLIENT_SOURCE&&!force)return MAIL_CLIENT_SOURCE;MAIL_CLIENT_SOURCE=await api('/api/mail-client',{method:'GET'});return MAIL_CLIENT_SOURCE}
function closeMailClientModal(){document.getElementById('mailClientModal')?.remove()}
function copyTextFallback(text){const ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();let ok=false;try{ok=document.execCommand('copy')}catch(e){}ta.remove();return ok}
async function copyMailClientHtml(){try{const d=await getMailClientSource(true);if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(d.html);else if(!copyTextFallback(d.html))throw new Error('Clipboard access was blocked');toast('Mail client HTML copied')}catch(e){toast(e.message)}}
async function downloadMailClientHtml(){try{const d=await getMailClientSource(true),blob=new Blob([d.html],{type:'text/html;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=d.filename||'hostish-mail.html';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1500);toast('Mail client downloaded')}catch(e){toast(e.message)}}
async function exportMailClientTemplate(){try{const d=await api('/api/mail-client',{method:'POST',body:JSON.stringify({action:'export',overwrite:false})});toast(d.message);MAIL_CLIENT_SOURCE=null;const info=document.getElementById('mailClientTemplateInfo');if(info)info.textContent=d.message}catch(e){toast(e.message)}}
async function openMailClientModal(){let d;try{d=await getMailClientSource(true)}catch(e){toast(e.message);return}const wrap=document.createElement('div');wrap.id='mailClientModal';wrap.style.cssText='position:fixed;inset:0;background:#000a;z-index:9999;display:grid;place-items:center;padding:18px';wrap.innerHTML=`<section class="card mailCard" style="width:min(720px,100%);max-height:90vh;overflow:auto;margin:0;box-shadow:0 24px 80px #0008"><div class="sectionHead"><div><h2>Portable Hostish Mail client</h2><p>One self-contained HTML webapp. It asks users for a Hostish domain or linked domain, then signs into that agent's webmail account.</p></div><button class="btn gray" id="mailClientClose">Close</button></div><div class="flow mail">Public API path: /.hostish/mail/\nDirect/LAN fallback: /api/mail/\nTemplate: ${h(d.template_path)}\nSource: ${d.using_custom?'custom template file':'built-in default'}</div><div class="note mail">The downloaded file stores appearance/server preferences in localStorage. Passwords are never stored. iPhone/iPad safe areas are built in.</div><div class="actions"><button class="btn yellow" id="mailClientDownload">Download HTML</button><button class="btn" id="mailClientCopy">Copy HTML code</button><button class="btn gray" id="mailClientExport">Create editable template file</button></div><div id="mailClientTemplateInfo" class="hint">To customize future downloads, edit the template file shown above. If it exists, Download and Copy use it exactly.</div></section>`;wrap.onclick=e=>{if(e.target===wrap)closeMailClientModal()};document.body.appendChild(wrap);document.getElementById('mailClientClose').onclick=closeMailClientModal;document.getElementById('mailClientDownload').onclick=downloadMailClientHtml;document.getElementById('mailClientCopy').onclick=copyMailClientHtml;document.getElementById('mailClientExport').onclick=exportMailClientTemplate}
async function renderMail(){
 const fields=[...MAIL_SMTP,...WEBMAIL_FIELDS],s=SETTINGS,st=STATE.stats?.smtp||{};
 page.innerHTML=`${serviceCard('smtp','SMTP tunnel','mailCard','inbound -> '+s.LOCAL_DELIVERY_HOST+':'+s.LOCAL_DELIVERY_PORT+' ('+s.LOCAL_DELIVERY_MODE+')\noutbound '+(s.OUTBOUND_RELAY_ENABLED?s.LOCAL_RELAY_BIND_HOST+':'+s.LOCAL_RELAY_BIND_PORT+' -> Hostish':'disabled'),stat('delivered',st.delivered||0)+stat('delivery failed',st.delivery_failed||0)+stat('sent',st.sent||0)+stat('send failed',st.send_failed||0))}
 ${group('SMTP agent',MAIL_SMTP,'Inbound delivery and the optional local outbound relay. Hostish handles SPF/DKIM and outbound DNS authentication on its backend.')}
 ${group('Built-in webmail',WEBMAIL_FIELDS,'Optional mailbox storage and browser mail client using the same Hostish SMTP tunnel.')}
 ${s.LINKED_CUSTOM_DOMAIN_MAIL_ENABLED?`<section class="card mailCard"><h2>Linked custom-domain email DNS</h2><p>This is only needed when you use a linked custom domain for email. Hostish owns the DKIM private key; add these records once at your domain DNS provider.</p>${(STATE.smtp_custom?.domains||[]).length?(STATE.smtp_custom.domains||[]).map(d=>`<div class="flow">${h(d)}
MX @ 10 ${h(STATE.smtp_custom?.dns?.mx_target||'hostish.site')}
TXT @ ${h(STATE.smtp_custom?.dns?.spf_value||'')}
CNAME ${h(STATE.smtp_custom?.dns?.dkim_selector||'hostish')}._domainkey ${h(STATE.smtp_custom?.dns?.dkim_cname_target||'')}
TXT _dmarc ${h(STATE.smtp_custom?.dns?.dmarc_value||'v=DMARC1; p=none')}</div>`).join(''):'<div class="note">No linked custom domains were reported by Hostish yet. Link the domain on Hostish first, then restart the SMTP service.</div>'}</section>`:''}
 <section class="card mailCard"><div class="sectionHead"><div><h2>Webmail accounts</h2><p>${STATE.webmail?.enabled?'Domain: '+h(STATE.webmail.domain):'Enable WEBMAIL_ENABLED and save first.'}</p></div><a class="btn yellow" href="/webmail">Open webmail</a></div><div id="mailUsers"></div>${STATE.webmail?.enabled?`<h3>Add account</h3><div class="fields"><div><label>Username</label><input id="newMailUser" type="text" autocomplete="off"></div><div><label>Password</label><input id="newMailPass" type="password" autocomplete="new-password"></div></div><div class="check"><input id="newMailAdmin" type="checkbox"><span>Administrator</span></div><div class="actions"><button class="btn yellow" id="addMailUser">Create account</button></div><div id="mailUserResult" class="hint"></div>`:''}</section>
 <section class="card mailCard"><div class="sectionHead"><div><h2>Portable mail client</h2><p>Give users one HTML file that connects to any Hostish mail-enabled agent/domain. Responsive, install-like webapp UI, online status, local customization, and iPhone safe-area support.</p></div><button class="btn yellow" id="mailClientButton">Get HTML client</button></div><div class="flow mail">Users enter: yourname.hostish.site or a linked custom domain
Gateway: https://&lt;domain&gt;/.hostish/mail/</div><div class="note mail">If MAIL_CLIENT_TEMPLATE_FILE exists, its exact HTML is downloaded/copied. Use “Create editable template file” once, then edit that file however you want.</div></section>${saveBar(fields)}`;
 bindServiceButtons();bindSave();
 const mailClientButton=document.getElementById('mailClientButton');if(mailClientButton)mailClientButton.onclick=openMailClientModal;
 if(STATE.webmail?.enabled){
  await loadMailUsers();
  const add=document.getElementById('addMailUser');
  if(add)add.onclick=async()=>{const user=newMailUser.value.trim(),pass=newMailPass.value;mailUserResult.textContent='';if(!user){mailUserResult.textContent='Enter a username.';return}if(pass.length<8){mailUserResult.textContent='Password must be at least 8 characters.';return}add.disabled=true;try{const r=await api('/api/webmail/users',{method:'POST',body:JSON.stringify({username:user,password:pass,admin:newMailAdmin.checked})});mailUserResult.textContent=r.message||'';toast(r.message);if(r.ok){newMailUser.value='';newMailPass.value='';newMailAdmin.checked=false;await refreshState();await loadMailUsers()}}catch(e){mailUserResult.textContent=e.message;toast(e.message)}finally{add.disabled=false}};
 }
}

async function loadMinecraft(){
 const r=await api('/api/minecraft');
 MC_CONFIGS=Array.isArray(r.instances)?r.instances:[];
 MC_STATUSES=Array.isArray(r.statuses)?r.statuses:[];
 return r;
}
function mcStatus(id){return MC_STATUSES.find(x=>String(x.id)===String(id))||null}
function mcDefault(){return {id:'',name:'Minecraft server',enabled:true,username:'',password:'',minecraft_host:'127.0.0.1',minecraft_port:25565,start_bat:'',auto_start_server:true,server_start_timeout:120,server_start_check_interval:2,homepage_file:'',simple_voice_chat_enabled:false,simple_voice_chat_host:'127.0.0.1',simple_voice_chat_port:24454,simple_voice_chat_public_port:0,simple_voice_chat_service_id:'simple-voice-chat',simple_voice_chat_config_file:''}}
function mcEditor(cfg){const c=Object.assign(mcDefault(),cfg||{});return `<section class="card mcCard" id="mcEditor"><div class="sectionHead"><div><h2>${cfg?'Edit server':'Add Minecraft server'}</h2><p>Each server uses its own Hostish account. Leave a saved password blank to keep it.</p></div></div><div class="fields">
<div><label>Name</label><input id="mc_name" type="text" value="${h(c.name)}"></div><div class="check"><input id="mc_enabled" type="checkbox" ${c.enabled?'checked':''}><span>Start tunnel automatically</span></div>
<div><label>Hostish username</label><input id="mc_username" type="text" value="${h(c.username)}" autocomplete="off"></div><div><label>Hostish password</label><input id="mc_password" type="password" value="" placeholder="${c.password===SECRET_MASK?'Saved — leave blank to keep':'Enter password'}" autocomplete="new-password"></div>
<div><label>Local Minecraft host</label><input id="mc_minecraft_host" type="text" value="${h(c.minecraft_host)}"></div><div><label>Local Minecraft port</label><input id="mc_minecraft_port" type="number" value="${h(c.minecraft_port)}"></div>
<div class="full"><label>Server start script (.bat/.cmd/.sh)</label><input id="mc_start_bat" type="text" value="${h(c.start_bat)}"></div><div class="check"><input id="mc_auto_start_server" type="checkbox" ${c.auto_start_server?'checked':''}><span>Automatically start the local Minecraft server</span></div>
<div><label>Server startup timeout</label><input id="mc_server_start_timeout" type="number" value="${h(c.server_start_timeout)}"></div><div><label>Port check interval</label><input id="mc_server_start_check_interval" type="number" value="${h(c.server_start_check_interval)}"></div>
<div class="full"><label>Optional browser homepage HTML</label><input id="mc_homepage_file" type="text" value="${h(c.homepage_file)}"></div>
<div class="check full"><input id="mc_simple_voice_chat_enabled" type="checkbox" ${c.simple_voice_chat_enabled?'checked':''}><span>Enable Simple Voice Chat UDP tunnel</span></div>
<div><label>Voice host</label><input id="mc_simple_voice_chat_host" type="text" value="${h(c.simple_voice_chat_host)}"></div><div><label>Voice local UDP port</label><input id="mc_simple_voice_chat_port" type="number" value="${h(c.simple_voice_chat_port)}"></div>
<div><label>Voice public port</label><input id="mc_simple_voice_chat_public_port" type="number" value="${h(c.simple_voice_chat_public_port)}"><div class="hint">0 = let Hostish choose.</div></div><div><label>Voice service ID</label><input id="mc_simple_voice_chat_service_id" type="text" value="${h(c.simple_voice_chat_service_id)}"></div>
<div class="full"><label>voicechat-server.properties</label><input id="mc_simple_voice_chat_config_file" type="text" value="${h(c.simple_voice_chat_config_file)}"></div>
</div><div class="actions"><button class="btn green" id="mcSave">Save server</button><button class="btn gray" id="mcCancel">Cancel</button></div><div id="mcEditorResult" class="hint"></div></section>`}
function mcVal(id,fallback=''){const el=document.getElementById('mc_'+id);return el?el.value:fallback}
function mcNum(id,fallback){const n=parseInt(mcVal(id,String(fallback)),10);return Number.isFinite(n)?n:fallback}
function mcCheck(id,fallback=false){const el=document.getElementById('mc_'+id);return el?!!el.checked:fallback}
function collectMc(){const existing=MC_CONFIGS.find(x=>String(x.id)===String(MC_EDIT_ID));return {id:MC_EDIT_ID||'',name:mcVal('name','Minecraft server').trim(),enabled:mcCheck('enabled',true),username:mcVal('username','').trim(),password:mcVal('password',''),minecraft_host:mcVal('minecraft_host','127.0.0.1').trim()||'127.0.0.1',minecraft_port:mcNum('minecraft_port',25565),start_bat:mcVal('start_bat',''),auto_start_server:mcCheck('auto_start_server',true),server_start_timeout:mcNum('server_start_timeout',120),server_start_check_interval:mcNum('server_start_check_interval',2),homepage_file:mcVal('homepage_file',''),simple_voice_chat_enabled:mcCheck('simple_voice_chat_enabled',false),simple_voice_chat_host:mcVal('simple_voice_chat_host','127.0.0.1').trim()||'127.0.0.1',simple_voice_chat_port:mcNum('simple_voice_chat_port',24454),simple_voice_chat_public_port:mcNum('simple_voice_chat_public_port',0),simple_voice_chat_service_id:mcVal('simple_voice_chat_service_id','simple-voice-chat').trim()||'simple-voice-chat',simple_voice_chat_config_file:mcVal('simple_voice_chat_config_file','')}}

async function renderMinecraft(){location.replace('/minecraft');}
function showMcEditor(cfg){MC_EDIT_ID=cfg?.id||'';const mount=document.getElementById('mcEditMount');mount.innerHTML=mcEditor(cfg);document.getElementById('mcCancel').onclick=()=>{mount.innerHTML='';MC_EDIT_ID=''};document.getElementById('mcSave').onclick=async()=>{const result=document.getElementById('mcEditorResult');result.textContent='Saving...';try{const r=await api('/api/minecraft',{method:'POST',body:JSON.stringify({action:'save',instance:collectMc()})});result.textContent=r.message||'';toast(r.message);if(r.ok){MC_EDIT_ID='';await refreshState();await renderMinecraft()}}catch(e){result.textContent=e.message;toast(e.message)}};document.getElementById('mcEditor').scrollIntoView({behavior:'smooth',block:'start'})}
async function mcAction(id,action){const r=await api('/api/minecraft',{method:'POST',body:JSON.stringify({action,id})});toast(r.message);await refreshState();await renderMinecraft()}
function scriptDisplayName(x){return x.path?(x.path.split(/[\\/]/).pop()||x.name):x.name}
function scriptCard(x){const running=!!x.running;const status=running?'Running':x.last_error?'Error':x.exit_code!=null?('Stopped · exit '+x.exit_code):'Stopped';const legacy=!x.path;return `<section class="card scriptItem" data-script-card="${h(x.name)}"><div class="scriptInfo"><div class="scriptTitle"><span class="dot ${running?'online':x.last_error?'error':'stopped'}"></span><h2>${h(scriptDisplayName(x))}</h2><span class="scriptState">${h(status)}</span></div><div class="scriptPath" title="${h(x.path||x.command)}">${h(x.path||x.command)}</div><div class="scriptError ${x.last_error?'':'hidden'}">${h(x.last_error||'')}</div><label class="scriptAuto"><input type="checkbox" data-script-auto="${h(x.name)}" ${x.autostart?'checked':''}> Start with Hostish</label>${legacy?'<div class="scriptNotice">Existing custom command · start/stop only</div>':''}<div class="scriptEdit hidden" data-script-editor="${h(x.name)}"><input type="text" aria-label="New file path" data-script-path="${h(x.name)}" value="${h(x.path||'')}" spellcheck="false"><button class="btn" type="button" data-script-save="${h(x.name)}">Save</button><button class="btn gray" type="button" data-script-cancel="${h(x.name)}">Cancel</button></div></div><div class="actions"><button class="btn" type="button" data-script-action="${running?'restart':'start'}" data-script="${h(x.name)}">${running?'Restart':'Start'}</button><button class="btn gray" type="button" data-script-action="stop" data-script="${h(x.name)}" ${running?'':'disabled'}>Stop</button>${legacy?'':`<button class="btn gray" type="button" data-script-edit="${h(x.name)}">Edit path</button>`}<button class="btn danger" type="button" data-script-action="remove" data-script="${h(x.name)}">Remove</button></div></section>`}
async function scriptRequest(payload){const r=await api('/api/scripts',{method:'POST',body:JSON.stringify(payload)});toast(r.message);return r}
function updateScriptStatuses(scripts){for(const x of scripts||[]){const card=[...document.querySelectorAll('[data-script-card]')].find(e=>e.dataset.scriptCard===x.name);if(!card)continue;const dot=card.querySelector('.dot'),state=card.querySelector('.scriptState'),start=card.querySelector('[data-script-action="start"], [data-script-action="restart"]'),stop=card.querySelector('[data-script-action="stop"]'),err=card.querySelector('.scriptError');if(!dot||!state||!start||!stop)return;dot.className='dot '+(x.running?'online':x.last_error?'error':'stopped');state.textContent=x.running?'Running':x.last_error?'Error':x.exit_code!=null?'Stopped · exit '+x.exit_code:'Stopped';start.dataset.scriptAction=x.running?'restart':'start';start.textContent=x.running?'Restart':'Start';stop.disabled=!x.running;if(err){err.textContent=x.last_error||'';err.classList.toggle('hidden',!x.last_error)}}}
async function renderScripts(){const r=await api('/api/scripts');const scripts=r.scripts||[];page.innerHTML=`<section class="hero"><div class="sectionHead"><div><h2>Scripts</h2><p class="subtitle">Add a file from this computer, then start, stop, or remove it here.</p></div><span class="pill">${scripts.length} script${scripts.length===1?'':'s'}</span></div></section><section class="card"><h2>Add a script</h2><div class="scriptAddRow"><div><label for="scriptNewPath">File path</label><input id="scriptNewPath" type="text" placeholder="C:\\Projects\\bot.py or /home/me/bot.py" autocomplete="off" spellcheck="false"></div><button class="btn" id="scriptAddPath" type="button">Add &amp; start</button></div><label class="scriptAuto"><input id="scriptNewAutostart" type="checkbox" checked> Start automatically with Hostish</label><div class="scriptNotice">Use a path on the computer running Hostish. Python, batch, PowerShell, Node.js, shell scripts, JARs, and executable files are supported.</div></section>${scripts.length?`<div class="scriptList">${scripts.map(scriptCard).join('')}</div>`:'<section class="card"><h2>No scripts added</h2><p>Enter a file path above to get started.</p></section>'}`;
const add=document.getElementById('scriptAddPath');const path=document.getElementById('scriptNewPath');const addScript=async()=>{if(!path.value.trim()){path.focus();toast('Enter a file path first');return}add.disabled=true;try{const q=await scriptRequest({action:'add_path',path:path.value.trim(),autostart:document.getElementById('scriptNewAutostart').checked,start_now:true});if(q.ok)await renderScripts()}catch(e){toast(e.message)}finally{if(add.isConnected)add.disabled=false}};
add.onclick=addScript;path.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();addScript()}};
document.querySelectorAll('[data-script-action]').forEach(btn=>btn.onclick=async()=>{const name=btn.dataset.script,action=btn.dataset.scriptAction;if(action==='remove'&&!confirm(`Remove ${name} from Scripts?${scripts.find(x=>x.name===name)?.running?' This will also stop it.':''}`))return;btn.disabled=true;try{const q=await scriptRequest({name,action});if(q.ok)await renderScripts()}catch(e){toast(e.message)}finally{if(btn.isConnected)btn.disabled=false}});
document.querySelectorAll('[data-script-auto]').forEach(toggle=>toggle.onchange=async()=>{toggle.disabled=true;try{const q=await scriptRequest({name:toggle.dataset.scriptAuto,action:'set_autostart',autostart:toggle.checked});if(!q.ok)toggle.checked=!toggle.checked}catch(e){toggle.checked=!toggle.checked;toast(e.message)}finally{toggle.disabled=false}});
document.querySelectorAll('[data-script-edit]').forEach(btn=>btn.onclick=()=>{const editor=document.querySelectorAll('[data-script-editor]');editor.forEach(x=>x.classList.add('hidden'));const target=[...editor].find(x=>x.dataset.scriptEditor===btn.dataset.scriptEdit);target.classList.remove('hidden');target.querySelector('input').focus()});
document.querySelectorAll('[data-script-cancel]').forEach(btn=>btn.onclick=()=>{const target=[...document.querySelectorAll('[data-script-editor]')].find(x=>x.dataset.scriptEditor===btn.dataset.scriptCancel);target.classList.add('hidden')});
document.querySelectorAll('[data-script-save]').forEach(btn=>btn.onclick=async()=>{const name=btn.dataset.scriptSave;const input=[...document.querySelectorAll('[data-script-path]')].find(x=>x.dataset.scriptPath===name);btn.disabled=true;try{const q=await scriptRequest({name,action:'edit_path',path:input.value.trim()});if(q.ok)await renderScripts()}catch(e){toast(e.message)}finally{if(btn.isConnected)btn.disabled=false}});
document.querySelectorAll('[data-script-path]').forEach(input=>input.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();const btn=[...document.querySelectorAll('[data-script-save]')].find(x=>x.dataset.scriptSave===input.dataset.scriptPath);btn.click()}else if(e.key==='Escape'){const btn=[...document.querySelectorAll('[data-script-cancel]')].find(x=>x.dataset.scriptCancel===input.dataset.scriptPath);btn.click()}})
}
function renderLogs(){page.innerHTML=`<section class="hero"><h2>Live log</h2><p class="subtitle">Filter the unified log without losing output from the other agents.</p></section><section class="card"><div class="nav" id="logTabs"></div><div id="logBox"></div></section>`;renderLogTabs();LOG_SEQ=0;pollLogs()}
function renderLogTabs(){const opts=[['all','All'],['web','Website'],['smtp','Mail'],['minecraft','Minecraft'],['agent','Manager']];logTabs.innerHTML=opts.map(o=>`<a href="#" class="${LOG_FILTER===o[0]?'active':''}" data-log="${o[0]}">${o[1]}</a>`).join('');document.querySelectorAll('[data-log]').forEach(a=>a.onclick=e=>{e.preventDefault();LOG_FILTER=a.dataset.log;LOG_SEQ=0;logBox.innerHTML='';renderLogTabs();pollLogs()})}
async function pollLogs(){if(PAGE!=='logs'||!document.getElementById('logBox'))return;const r=await api('/api/logs?since='+LOG_SEQ);const box=logBox,atBottom=box.scrollTop+box.clientHeight>=box.scrollHeight-24;(r.lines||[]).forEach(line=>{LOG_SEQ=Math.max(LOG_SEQ,line.seq);const svc=String(line.service||'');if(LOG_FILTER!=='all'&&!(LOG_FILTER==='minecraft'?svc.startsWith('minecraft'):svc===LOG_FILTER))return;const div=document.createElement('div');div.className='logline';div.innerHTML=`<span class="t">${h(line.time)}</span> <span class="s ${h(svc)}">${h(svc)}</span> ${h(line.text)}`;box.appendChild(div)});while(box.childElementCount>900)box.removeChild(box.firstChild);if(atBottom)box.scrollTop=box.scrollHeight}
function renderSettings(){const fields=[...MANAGER_FIELDS,...DIAG_FIELDS,...RAW_COMPLEX_FIELDS];page.innerHTML=`${group('Global account & manager',MANAGER_FIELDS,'The global account is used by website hosting and SMTP. Minecraft accounts are managed separately on the Minecraft page.')}<details class="webDisclosure"><summary><span>Diagnostics<small>Logging and health checks</small></span></summary><div class="webDisclosureBody">${group('Diagnostics',DIAG_FIELDS)}</div></details><details class="webDisclosure"><summary><span>Advanced configuration<small>Raw JSON, paths, and overrides</small></span></summary><div class="webDisclosureBody">${group('Raw configuration',RAW_COMPLEX_FIELDS)}</div></details>${saveBar(fields)}`;bindSave()}
function renderUpdate(){const fields=UPDATE_FIELDS;const u=STATE.update||{};page.innerHTML=`<section class="hero"><div class="sectionHead"><div><h2>Agent update</h2><p>Current version: <b>v${h(u.current)}</b>${u.newest?' · newest seen: v'+h(u.newest):''}</p></div><div class="actions"><button class="btn gray" id="updateCheck">Check now</button><button class="btn" id="updateApply">Install update</button></div></div>${u.error?`<div class="note"><span class="dangerText">${h(u.error)}</span></div>`:''}</section>${group('Update security & retention',UPDATE_FIELDS,'Every update must match the published SHA-256 file. HTTPS enforcement, redirect policy, an optional extra SHA-256 pin, and optional Ed25519 signatures are also supported.')}${saveBar(fields)}`;bindSave();updateCheck.onclick=async()=>{const r=await api('/api/update',{method:'POST',body:JSON.stringify({action:'check'})});toast(r.message);await refreshState();renderUpdate()};updateApply.onclick=async()=>{if(!confirm('Install the latest agent and restart this process?'))return;const r=await api('/api/update',{method:'POST',body:JSON.stringify({action:'apply'})});toast(r.message)}}
async function renderCurrent(){header();if(PAGE==='overview')renderOverview();else if(PAGE==='website')renderWebsite();else if(PAGE==='mail')await renderMail();else if(PAGE==='minecraft')await renderMinecraft();else if(PAGE==='scripts')await renderScripts();else if(PAGE==='logs')renderLogs();else if(PAGE==='settings')renderSettings();else if(PAGE==='update')renderUpdate()}
window.addEventListener('beforeunload',event=>{if(PAGE==='website'&&WEB_DIRTY){event.preventDefault();event.returnValue=''}});
async function boot(){nav();await refreshState();showApp();await renderCurrent();setInterval(async()=>{try{await refreshState();if(PAGE==='overview')renderOverview();else if(PAGE==='scripts')updateScriptStatuses(STATE.scripts||[])}catch(e){}},2500);setInterval(()=>pollLogs().catch(()=>{}),1500)}
(async()=>{try{STATE=await api('/api/state');await boot()}catch(e){showLock()}})();
</script>
</body>
</html>
"""


WEBMAIL_APP_HTML = r"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hostish Webmail</title>
<style>
:root{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color-scheme:dark}*{box-sizing:border-box}body{margin:0;background:#0d1117;color:#e6edf3;min-height:100vh}button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!important}#login{min-height:100vh;display:grid;place-items:center;padding:24px}.card{width:min(420px,100%);background:#161b22;border:1px solid #30363d;border-radius:14px;padding:24px}h1,h2{margin-top:0}label{display:block;margin:14px 0 6px;color:#8b949e;font-size:13px}input,textarea{width:100%;background:#0d1117;border:1px solid #30363d;color:#e6edf3;border-radius:8px;padding:10px 12px}textarea{min-height:220px;resize:vertical}.primary{background:#238636;color:white;border:1px solid #2ea043;border-radius:8px;padding:9px 14px}.secondary{background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:8px;padding:9px 14px}.danger{background:#da3633;color:white;border:1px solid #f85149;border-radius:8px;padding:8px 12px}.error{color:#ff7b72;white-space:pre-wrap;margin-top:12px}.small{font-size:12px;color:#8b949e}#app{height:100vh;display:grid;grid-template-columns:220px 360px 1fr}.sidebar{border-right:1px solid #30363d;background:#161b22;padding:16px;overflow:auto}.brand{font-weight:700;font-size:20px;margin:5px 0 20px}.navbtn{display:block;width:100%;text-align:left;background:transparent;border:0;color:#c9d1d9;padding:10px;border-radius:8px;margin:4px 0}.navbtn:hover,.navbtn.active{background:#21262d}.bottom{margin-top:24px;border-top:1px solid #30363d;padding-top:14px}.list{border-right:1px solid #30363d;overflow:auto}.listhead{position:sticky;top:0;background:#0d1117;border-bottom:1px solid #30363d;padding:14px;z-index:2}.message-row{padding:13px 14px;border-bottom:1px solid #21262d;cursor:pointer}.message-row:hover{background:#161b22}.message-row.unread{font-weight:700}.sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meta{font-size:12px;color:#8b949e;margin-top:4px}.reader{overflow:auto;padding:24px}.reader-head{border-bottom:1px solid #30363d;padding-bottom:16px;margin-bottom:18px}.reader-body{white-space:pre-wrap;line-height:1.55;word-break:break-word}iframe.mailhtml{width:100%;min-height:500px;background:white;border:1px solid #30363d;border-radius:8px}.attach{display:inline-block;margin:6px 6px 0 0;padding:7px 10px;background:#21262d;border-radius:7px;color:#58a6ff;text-decoration:none}.modal{position:fixed;inset:0;background:#000a;display:grid;place-items:center;padding:20px;z-index:10}.modalbox{width:min(700px,100%);max-height:90vh;overflow:auto;background:#161b22;border:1px solid #30363d;border-radius:14px;padding:22px}.row{display:flex;gap:10px;align-items:center}.row>*{flex:1}.userrow{display:flex;gap:10px;align-items:center;border-bottom:1px solid #30363d;padding:10px 0}.grow{flex:1}@media(max-width:900px){#app{grid-template-columns:170px 1fr}.reader{position:fixed;inset:0 0 0 170px;background:#0d1117;z-index:3}.reader.mobile-hidden{display:none}}@media(max-width:600px){#app{grid-template-columns:76px 1fr}.sidebar{padding:10px 6px}.brand{font-size:12px;text-align:center}.navbtn{font-size:11px;padding:9px 4px;text-align:center}.reader{left:76px;padding:14px}.small{font-size:11px}}
</style></head><body>
<section id="login"><div class="card"><h1>Hostish Webmail</h1><div class="small">Sign in to your local mailbox.</div><label>Username</label><input id="loginUser" autocomplete="username"><label>Password</label><input id="loginPass" type="password" autocomplete="current-password"><button class="primary" style="width:100%;margin-top:16px" onclick="login()">Sign in</button><div id="loginError" class="error"></div></div></section>
<section id="app" class="hidden"><aside class="sidebar"><div class="brand">Webmail</div><button class="primary" style="width:100%;margin-bottom:14px" onclick="openCompose()">Compose</button><button id="inboxBtn" class="navbtn active" onclick="loadFolder('inbox')">Inbox</button><button id="sentBtn" class="navbtn" onclick="loadFolder('sent')">Sent</button><button id="usersBtn" class="navbtn hidden" onclick="openUsers()">Users</button><div class="bottom"><div id="me" class="small"></div><button class="navbtn" onclick="openPassword()">Password</button><button class="navbtn" onclick="logout()">Sign out</button></div></aside><main class="list"><div class="listhead"><strong id="folderTitle">Inbox</strong></div><div id="messageList"></div></main><article id="reader" class="reader mobile-hidden"><div class="small">Select a message.</div></article></section>
<div id="composeModal" class="modal hidden"><div class="modalbox"><h2>New message</h2><label>To</label><input id="composeTo" placeholder="person@example.com"><label>Cc</label><input id="composeCc"><label>Subject</label><input id="composeSubject"><label>Message</label><textarea id="composeBody"></textarea><div class="row" style="margin-top:14px"><button class="secondary" onclick="closeModal('composeModal')">Cancel</button><button class="primary" onclick="sendMessage()">Send</button></div><div id="composeError" class="error"></div></div></div>
<div id="usersModal" class="modal hidden"><div class="modalbox"><h2>Mailbox users</h2><div class="row"><input id="newUsername" placeholder="username"><input id="newPassword" type="password" placeholder="password (8+ chars)"></div><label style="display:flex;gap:8px;align-items:center"><input id="newAdmin" type="checkbox" style="width:auto"> Administrator</label><button class="primary" onclick="addUser()">Add user</button><div id="userError" class="error"></div><div id="userList" style="margin-top:18px"></div><button class="secondary" style="margin-top:16px" onclick="closeModal('usersModal')">Close</button></div></div>
<div id="passwordModal" class="modal hidden"><div class="modalbox" style="max-width:460px"><h2>Change password</h2><label>Current password</label><input id="oldPassword" type="password"><label>New password</label><input id="newPassword2" type="password"><div class="row" style="margin-top:14px"><button class="secondary" onclick="closeModal('passwordModal')">Cancel</button><button class="primary" onclick="changePassword()">Change</button></div><div id="passwordError" class="error"></div></div></div>
<script>
let token=localStorage.getItem('hostishWebmailToken')||'',currentFolder='inbox',me=null;
async function api(path,options={}){options.headers=Object.assign({},options.headers||{});if(token)options.headers.Authorization='Bearer '+token;if(options.body&&typeof options.body!=='string'){options.headers['Content-Type']='application/json';options.body=JSON.stringify(options.body)}const r=await fetch(path,options),text=await r.text();let data;try{data=text?JSON.parse(text):{}}catch{data=text}if(!r.ok){if(r.status===401&&path!='/api/mail/login'){token='';localStorage.removeItem('hostishWebmailToken');showLogin()}throw new Error(typeof data==='string'?data:(data.error||r.statusText))}return data}
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c]))}
function showLogin(){document.getElementById('login').classList.remove('hidden');document.getElementById('app').classList.add('hidden')}
async function init(){if(!token){showLogin();return}try{me=await api('/api/mail/me');document.getElementById('login').classList.add('hidden');document.getElementById('app').classList.remove('hidden');document.getElementById('me').textContent=me.email;document.getElementById('usersBtn').classList.toggle('hidden',!me.admin);await loadFolder('inbox')}catch{showLogin()}}
async function login(){const e=document.getElementById('loginError');e.textContent='';try{const d=await api('/api/mail/login',{method:'POST',body:{username:loginUser.value,password:loginPass.value}});token=d.token;localStorage.setItem('hostishWebmailToken',token);loginPass.value='';await init()}catch(x){e.textContent=x.message}}
async function logout(){try{await api('/api/mail/logout',{method:'POST'})}catch{}token='';localStorage.removeItem('hostishWebmailToken');showLogin()}
async function loadFolder(folder){currentFolder=folder;folderTitle.textContent=folder==='inbox'?'Inbox':'Sent';inboxBtn.classList.toggle('active',folder==='inbox');sentBtn.classList.toggle('active',folder==='sent');reader.innerHTML='<div class="small">Select a message.</div>';reader.classList.add('mobile-hidden');await refreshListSilently()}
async function refreshListSilently(){const items=await api('/api/mail/messages?folder='+encodeURIComponent(currentFolder));messageList.innerHTML='';if(!items.length){messageList.innerHTML='<div class="small" style="padding:16px">No messages.</div>';return}for(const m of items){const d=document.createElement('div');d.className='message-row'+(!m.read?' unread':'');const who=currentFolder==='inbox'?m.from:m.to;d.innerHTML=`<div class="sub">${esc(who||'(unknown)')}</div><div class="sub">${esc(m.subject||'(no subject)')}</div><div class="meta">${esc(m.date||m.received_at||'')}</div>`;d.onclick=()=>openMessage(m.id);messageList.appendChild(d)}}
async function openMessage(id){const m=await api('/api/mail/message/'+encodeURIComponent(id));reader.classList.remove('mobile-hidden');const body=m.html?'<iframe id="mailFrame" class="mailhtml" sandbox=""></iframe>':`<div class="reader-body">${esc(m.text||'')}</div>`;let at='';for(const a of(m.attachments||[]))at+=`<a class="attach" href="/api/mail/attachment/${encodeURIComponent(m.id)}/${encodeURIComponent(a.filename)}" onclick="return attachmentClick(event,this)">${esc(a.filename)} (${Math.ceil(a.size/1024)} KB)</a>`;reader.innerHTML=`<div class="reader-head"><button class="secondary" onclick="reader.classList.add('mobile-hidden')" style="float:right">Back</button><h2>${esc(m.subject)}</h2><div><b>From:</b> ${esc(m.from)}</div><div><b>To:</b> ${esc(m.to)}</div>${m.cc?`<div><b>Cc:</b> ${esc(m.cc)}</div>`:''}<div class="small">${esc(m.date||m.received_at)}</div>${at}<div style="margin-top:12px"><button class="danger" onclick="deleteMessage('${esc(m.id)}')">Delete</button></div></div>${body}`;if(m.html)mailFrame.srcdoc=m.html;await refreshListSilently()}
async function deleteMessage(id){if(!confirm('Delete this message?'))return;await api('/api/mail/message/'+encodeURIComponent(id),{method:'DELETE'});reader.innerHTML='<div class="small">Message deleted.</div>';reader.classList.add('mobile-hidden');await loadFolder(currentFolder)}
function openCompose(){composeError.textContent='';composeModal.classList.remove('hidden')}function openPassword(){passwordError.textContent='';passwordModal.classList.remove('hidden')}function closeModal(id){document.getElementById(id).classList.add('hidden')}
async function sendMessage(){composeError.textContent='Sending...';try{await api('/api/mail/send',{method:'POST',body:{to:composeTo.value,cc:composeCc.value,subject:composeSubject.value,body:composeBody.value}});composeTo.value=composeCc.value=composeSubject.value=composeBody.value='';closeModal('composeModal');if(currentFolder==='sent')await loadFolder('sent')}catch(x){composeError.textContent=x.message}}
async function openUsers(){userError.textContent='';usersModal.classList.remove('hidden');await refreshUsers()}async function refreshUsers(){const users=await api('/api/mail/users');userList.innerHTML='';for(const u of users){const d=document.createElement('div');d.className='userrow';d.innerHTML=`<div class="grow"><b>${esc(u.username)}</b><div class="small">${esc(u.email)}${u.admin?' · admin':''}</div></div>${u.username!==me.username?`<button class="danger" onclick="deleteUser('${esc(u.username)}')">Delete</button>`:''}`;userList.appendChild(d)}}
async function addUser(){userError.textContent='';try{await api('/api/mail/users',{method:'POST',body:{username:newUsername.value,password:newPassword.value,admin:newAdmin.checked}});newUsername.value=newPassword.value='';newAdmin.checked=false;await refreshUsers()}catch(x){userError.textContent=x.message}}async function deleteUser(u){if(!confirm('Remove account '+u+'? Mail files will be kept on disk.'))return;try{await api('/api/mail/users/'+encodeURIComponent(u),{method:'DELETE'});await refreshUsers()}catch(x){userError.textContent=x.message}}
async function changePassword(){passwordError.textContent='';try{await api('/api/mail/change-password',{method:'POST',body:{old_password:oldPassword.value,new_password:newPassword2.value}});oldPassword.value=newPassword2.value='';closeModal('passwordModal');alert('Password changed.')}catch(x){passwordError.textContent=x.message}}
async function attachmentClick(ev,a){ev.preventDefault();try{const r=await fetch(a.href,{headers:{Authorization:'Bearer '+token}});if(!r.ok)throw new Error(await r.text());const blob=await r.blob(),url=URL.createObjectURL(blob),tmp=document.createElement('a');tmp.href=url;tmp.download=decodeURIComponent(a.href.split('/').pop());tmp.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}catch(x){alert(x.message)}return false}
loginPass.addEventListener('keydown',e=>{if(e.key==='Enter')login()});setInterval(()=>{if(token&&!document.hidden)refreshListSilently().catch(()=>{})},15000);init();
</script></body></html>
"""


def _panel_authorized(headers: dict) -> bool:
    expected = str(CONTROL_PANEL_PASSWORD or "")
    if not expected:
        return True
    provided = str(headers.get("x-hostish-auth", ""))
    return secrets.compare_digest(provided, expected)


def _public_settings() -> dict:
    values = current_settings()
    for name in SECRET_SETTINGS:
        values[name] = "" if not values.get(name) else "********"
    # Never leak nested Minecraft passwords through the generic settings API.
    values.pop("MINECRAFT_INSTANCES", None)
    return values


def _uptime_text() -> str:
    seconds = int(time.time() - START_TS)
    if seconds < 60:
        return f"{seconds}s"
    if seconds < 3600:
        return f"{seconds // 60}m"
    if seconds < 86400:
        return f"{seconds // 3600}h {seconds % 3600 // 60}m"
    return f"{seconds // 86400}d {seconds % 86400 // 3600}h"


async def _panel_mail_client_action(payload: Optional[dict] = None) -> dict:
    payload = payload or {}
    action = str(payload.get("action", "source") or "source").strip().lower()
    if action == "export":
        ok, message, path = await asyncio.to_thread(
            _export_mail_client_template, bool(payload.get("overwrite", False)))
        return {"ok": ok, "message": message, "template_path": path}
    source, custom, path = await asyncio.to_thread(_mail_client_source)
    return {
        "ok": True,
        "html": source,
        "using_custom": custom,
        "template_path": path,
        "filename": "hostish-mail.html",
    }


def _panel_state() -> dict:
    services = {}
    for name in ("web", "smtp"):
        state = SERVICE_STATE.get(name, {})
        services[name] = {
            "status": state.get("status", "stopped"),
            "detail": state.get("detail", ""),
            "running": bool(SUPERVISORS.get(name) and SUPERVISORS[name].running),
        }
    mc_manager = MINECRAFT_APP_MANAGER.status()
    services["minecraft"] = {
        "status": mc_manager.get("status", "stopped"),
        "detail": mc_manager.get("detail", ""),
        "running": bool(mc_manager.get("running")),
    }
    combined_stats = dict(STATS)
    combined_stats["minecraft"] = {}
    return {
        "version": version,
        "uptime": _uptime_text(),
        "settings": _public_settings(),
        "stats": combined_stats,
        "web_bytes": fmt_bytes(STATS["web"]["bytes"]),
        "services": services,
        "minecraft_instances": [],
        "minecraft_manager": mc_manager,
        "scripts": [runner.status() for runner in MANAGED_SCRIPT_RUNNERS.values()],
        "webmail": {
            "enabled": WEBMAIL_ENABLED,
            "domain": _webmail_domain() if WEBMAIL_ENABLED else "",
            "user_count": len(webmail_get_users()) if WEBMAIL_ENABLED else 0,
            "url": "/webmail",
            "portable_gateway": "/.hostish/mail/",
            "template_path": _mail_client_template_path(),
        },
        "smtp_custom": {
            "enabled": bool(LINKED_CUSTOM_DOMAIN_MAIL_ENABLED),
            "domains": list(getattr(_SMTP_AGENT_INSTANCE, "custom_mail_domains", []) or []),
            "dns": dict(getattr(_SMTP_AGENT_INSTANCE, "custom_domain_dns", {}) or {}),
        },
        "update": {
            "current": version,
            "newest": _version_cache.get("newest_version"),
            "error": _version_cache.get("error"),
        },
    }


async def _panel_service_action(payload: dict) -> dict:
    service = str(payload.get("service", ""))
    action = str(payload.get("action", ""))

    if service == "minecraft":
        if action not in {"start", "stop", "restart"}:
            return {"ok": False, "message": f"Unknown action {action!r}"}
        return await _panel_minecraft_manager_action({"action": action})

    if service == "all":
        changed = []
        for name, enabled in (("web", WEB_ENABLED or WEBMAIL_ENABLED), ("smtp", SMTP_ENABLED or WEBMAIL_ENABLED)):
            supervisor = SUPERVISORS.get(name)
            if supervisor is None:
                continue
            if action == "stop":
                if await supervisor.stop():
                    changed.append(name)
            elif action == "start":
                if enabled and supervisor.start():
                    changed.append(name)
            elif action == "restart":
                if enabled:
                    await supervisor.restart()
                    changed.append(name)
            else:
                return {"ok": False, "message": f"Unknown action {action!r}"}
        mc_result = await _panel_minecraft_manager_action({"action": action})
        if mc_result.get("ok"):
            changed.append("minecraft-manager")
        return {"ok": True, "message": f"{action}: {', '.join(changed) if changed else 'nothing to do'}"}

    if service not in {"web", "smtp"}:
        return {"ok": False, "message": f"Unknown service {service!r}"}
    supervisor = SUPERVISORS.get(service)
    if supervisor is None:
        return {"ok": False, "message": f"Service {service!r} is unavailable"}
    if action == "stop":
        changed = await supervisor.stop()
    elif action == "start":
        changed = supervisor.start()
    elif action == "restart":
        await supervisor.restart()
        changed = True
    else:
        return {"ok": False, "message": f"Unknown action {action!r}"}
    return {"ok": True, "message": f"{action}: {service}" if changed else "Nothing to do"}


async def _panel_save_settings(payload: dict) -> dict:
    incoming = payload.get("settings", {})
    if not isinstance(incoming, dict):
        return {"ok": False, "message": "Invalid settings payload"}

    # Minecraft instances are edited through their dedicated API so nested
    # passwords never pass through the generic settings form.
    incoming.pop("MINECRAFT_INSTANCES", None)

    # Blank or masked secrets mean "leave this one alone".
    for name in SECRET_SETTINGS:
        value = incoming.get(name)
        if value in (None, "", "********"):
            incoming.pop(name, None)

    previous_values = {name: globals().get(name) for name in incoming if name in EDITABLE_SETTINGS}
    applied = apply_settings(incoming)
    if payload.get("restart_service") == "web":
        errors = [item for item in validate_configuration()
                  if str(item).startswith("[web]") or
                  (str(item).startswith("[account]") and (WEB_ENABLED or WEBMAIL_ENABLED))]
        if errors:
            apply_settings(previous_values)
            return {"ok": False, "message": "Cannot apply website settings: " + "; ".join(errors[:3])}
    stored = load_config_file()
    stored.update({name: globals().get(name) for name in applied})
    try:
        save_config_file(stored)
    except Exception as error:
        if payload.get("restart_service") == "web":
            apply_settings(previous_values)
        return {"ok": False, "message": f"Could not write {CONFIG_FILE}: {error}"}

    log(f"Settings updated from the control panel ({len(applied)} values)", "agent")
    merge_legacy_auto_start_scripts()
    sync_managed_script_runners()

    if payload.get("restart_service") == "web":
        supervisor = SUPERVISORS.get("web")
        if supervisor is None:
            return {"ok": True, "message": "Settings saved; start the agent to apply hosting changes"}
        await supervisor.stop()
        if WEB_ENABLED or WEBMAIL_ENABLED:
            supervisor.start()
        return {"ok": True, "message": "Website settings saved and applied"}

    if payload.get("restart"):
        for supervisor in SUPERVISORS.values():
            await supervisor.stop()
        await MINECRAFT_APP_MANAGER.restart()
        if WEB_ENABLED:
            SUPERVISORS["web"].start()
        if SMTP_ENABLED or WEBMAIL_ENABLED:
            SUPERVISORS["smtp"].start()
        return {"ok": True, "message": "Saved and services restarted"}

    return {"ok": True, "message": f"Saved {len(applied)} settings"}


def persist_script_entries(entries: list[dict]) -> None:
    """Keep the Scripts page's file paths as the only editable source of truth.

    Old multiline paths are folded into MANAGED_SCRIPTS on the first edit,
    without dropping still-supported older custom commands.
    """
    stored = load_config_file()
    stored.update({
        "MANAGED_SCRIPTS": entries,
        "AUTO_START_PYTHON_SCRIPTS": [],
        "AUTO_START_PYTHON_SCRIPTS_ENABLED": False,
    })
    save_config_file(stored)
    apply_settings({
        "MANAGED_SCRIPTS": entries,
        "AUTO_START_PYTHON_SCRIPTS": [],
        "AUTO_START_PYTHON_SCRIPTS_ENABLED": False,
    })
    sync_managed_script_runners()


def managed_script_path_entries() -> list[dict]:
    # merge_legacy_auto_start_scripts has already run at startup or settings-save.
    # Normalize old source markers so subsequent merges do not remove these paths.
    entries = []
    for entry in (MANAGED_SCRIPTS or []):
        if not isinstance(entry, dict):
            continue
        item = dict(entry)
        if item.get("_source") == "auto_python_path":
            item["_source"] = "file_path"
        entries.append(item)
    return entries


async def _panel_scripts_action(payload: dict) -> dict:
    name = str(payload.get("name", "")).strip()
    action = str(payload.get("action", ""))

    if action == "add":
        # Backward compatibility for old API consumers. The panel itself only
        # provides a file-path input and never exposes command editing.
        entry = {"name": str(payload.get("name", "")).strip(),
                 "command": str(payload.get("command", "")).strip(),
                 "cwd": str(payload.get("cwd", "")).strip(),
                 "autostart": bool(payload.get("autostart"))}
        if not entry["name"] or not entry["command"]:
            return {"ok": False, "message": "A name and command are required"}
        entries = [item for item in managed_script_path_entries()
                   if str(item.get("name", "")) != entry["name"]]
        entries.append(entry)
        try:
            persist_script_entries(entries)
        except OSError as error:
            return {"ok": False, "message": f"Could not save script: {error}"}
        return {"ok": True, "message": f"Added {entry['name']}"}

    if action in {"add_path", "edit_path", "set_autostart", "remove"}:
        entries = managed_script_path_entries()
        chosen = next((x for x in entries if str(x.get("name", "")) == name), None)
        if action == "add_path":
            path = normalized_script_file_path(payload.get("path", ""))
            error = validate_script_file(path)
            if error:
                return {"ok": False, "message": error}
            if any(os.path.normcase(normalized_script_file_path(x.get("path"))) == os.path.normcase(path)
                   for x in entries if x.get("path")):
                return {"ok": False, "message": "This file is already in your scripts"}
            base = os.path.splitext(os.path.basename(path))[0] or "script"
            existing_names = {str(x.get("name", "")) for x in entries}
            name = base
            number = 2
            while name in existing_names:
                name = f"{base}-{number}"
                number += 1
            entries.append({"name": name, "path": path, "command": "", "cwd": "",
                            "autostart": bool(payload.get("autostart", True)),
                            "_source": "file_path"})
            try:
                persist_script_entries(entries)
            except OSError as error:
                return {"ok": False, "message": f"Could not save script: {error}"}
            log(f"Script added: {name}", "agent")
            runner = MANAGED_SCRIPT_RUNNERS[name]
            if payload.get("start_now", True):
                ok, message = await runner.start()
                if not ok:
                    return {"ok": True, "message": f"Added {name}, but could not start: {message}"}
                return {"ok": True, "message": f"Added and started {name}"}
            return {"ok": True, "message": f"Added {name}"}

        if chosen is None:
            return {"ok": False, "message": "Script not found. Refresh the page and try again."}

        runner = MANAGED_SCRIPT_RUNNERS.get(name)
        if action == "edit_path":
            if not chosen.get("path"):
                return {"ok": False, "message": "This older custom command has no file path to edit"}
            path = normalized_script_file_path(payload.get("path", ""))
            error = validate_script_file(path)
            if error:
                return {"ok": False, "message": error}
            if any(x is not chosen and x.get("path") and
                   os.path.normcase(normalized_script_file_path(x["path"])) == os.path.normcase(path)
                   for x in entries):
                return {"ok": False, "message": "This file is already in your scripts"}
            if os.path.normcase(path) == os.path.normcase(normalized_script_file_path(chosen["path"])):
                return {"ok": True, "message": "File path is unchanged"}
            chosen.update({"path": path, "command": "", "cwd": "", "_source": "file_path"})
        elif action == "set_autostart":
            if "autostart" not in payload or not isinstance(payload["autostart"], bool):
                return {"ok": False, "message": "Invalid start-on-launch setting"}
            chosen["autostart"] = payload["autostart"]
        elif action == "remove":
            entries.remove(chosen)

        was_running = bool(runner and runner.running)
        if action == "remove" and was_running and runner:
            await runner.stop()
        try:
            persist_script_entries(entries)
        except OSError as error:
            return {"ok": False, "message": f"Could not save script changes: {error}"}
        if action == "remove":
            # Registry synchronization schedules a stop for removed processes.
            return {"ok": True, "message": f"Removed {name}"}
        if action == "edit_path":
            if was_running and runner:
                ok, message = await runner.restart()
                return {"ok": ok, "message": f"Changed path and restarted {name}" if ok else message}
            return {"ok": True, "message": f"Updated path for {name}"}
        return {"ok": True, "message": "Start on launch enabled" if chosen["autostart"]
                else "Start on launch disabled"}

    # Older custom commands remain runnable, but the UI creates new scripts
    # exclusively by file path (no shell-command input or advanced form).
    if action == "restart_autostart":
        merge_legacy_auto_start_scripts()
        sync_managed_script_runners()
        restarted = 0
        for runner in list(MANAGED_SCRIPT_RUNNERS.values()):
            if runner.autostart:
                await runner.restart()
                restarted += 1
        return {"ok": True, "message": f"Restarted {restarted} autostart script(s)"}

    runner = MANAGED_SCRIPT_RUNNERS.get(name)
    if runner is None:
        return {"ok": False, "message": f"Unknown script {name!r}"}

    if action == "start":
        ok, message = await runner.start()
    elif action == "stop":
        ok, message = await runner.stop()
    elif action == "restart":
        ok, message = await runner.restart()
    else:
        return {"ok": False, "message": f"Unknown action {action!r}"}
    return {"ok": ok, "message": message}


async def _panel_webmail_users_action(method: str, target_username: str, payload: dict) -> dict:
    if not WEBMAIL_ENABLED:
        return {"ok": False, "message": "Enable webmail first"}

    if method == "GET":
        users = webmail_get_users()
        return {"ok": True, "users": [
            {"username": u, "email": _webmail_email_for(u), "admin": bool(i.get("admin")),
             "created": i.get("created", "")} for u, i in sorted(users.items())]}

    if method == "POST":
        username = _webmail_normalize_username(str(payload.get("username", "")))
        password = str(payload.get("password", ""))
        if not _WEBMAIL_USERNAME_RE.fullmatch(username):
            return {"ok": False, "message": "Invalid username"}
        if len(password) < 8:
            return {"ok": False, "message": "Password must be at least 8 characters"}
        users = webmail_get_users()
        if username in users:
            return {"ok": False, "message": "User already exists"}
        users[username] = {"password_hash": _webmail_hash_password(password),
                           "admin": bool(payload.get("admin", False)),
                           "created": datetime.now().isoformat()}
        await asyncio.to_thread(_webmail_save_users, users)
        await asyncio.to_thread(_webmail_mailbox_dir, username)
        log(f"Webmail: account created for {username} (via panel)", "mail")
        return {"ok": True, "message": f"Created {username}@{_webmail_domain()}"}

    if method == "DELETE":
        users = webmail_get_users()
        if target_username not in users:
            return {"ok": False, "message": "User not found"}
        if users[target_username].get("admin") and sum(1 for u in users.values() if u.get("admin")) <= 1:
            return {"ok": False, "message": "Cannot delete the last administrator"}
        users.pop(target_username)
        await asyncio.to_thread(_webmail_save_users, users)
        log(f"Webmail: account removed for {target_username} (via panel)", "mail")
        return {"ok": True, "message": f"Removed {target_username}"}

    return {"ok": False, "message": "Unsupported"}


async def _panel_update_action(action: str) -> dict:
    if not SELF_UPDATE_ENABLED:
        return {"ok": False, "message": "Self-update is disabled in Manager settings"}
    if action == "check":
        newest = await run_in_thread(fetch_latest_agent_version, True)
        if newest is None:
            return {"ok": False, "message": _version_cache.get("error") or "Could not check for updates"}
        if newest <= version:
            return {"ok": True, "message": "Already up to date", "current": version, "newest": newest}
        return {"ok": True, "message": f"Update available: v{newest}", "current": version, "newest": newest}

    if action == "apply":
        try:
            newest = await run_in_thread(fetch_latest_agent_version, True)
            result = await run_in_thread(install_latest_agent_update, newest)
        except Exception as error:
            return {"ok": False, "message": f"Update failed: {type(error).__name__}: {error}"}
        log(f"Updated to v{result['downloaded_version']}; restarting...", "agent")
        asyncio.get_event_loop().call_later(
            0.3, lambda: restart_into_updated_agent(result["script_path"]))
        return {"ok": True, "message": f"Installed v{result['downloaded_version']}; restarting now"}

    return {"ok": False, "message": f"Unknown action {action!r}"}


async def _proxy_minecraft_request(
    reader: asyncio.StreamReader,
    writer: asyncio.StreamWriter,
    request_line: bytes,
    raw_header_lines: list,
    headers: dict,
) -> None:
    """Stream one browser request to the in-process Minecraft Flask server.

    This deliberately does not buffer the request body, so the supplied
    manager's multi-gigabyte world/mod uploads retain their original behavior.
    """
    manager = MINECRAFT_APP_MANAGER
    if not manager.running:
        ok, message = await manager.start()
        if not ok:
            body = (
                "<!doctype html><meta charset='utf-8'><title>Minecraft Manager</title>"
                "<body style='font-family:system-ui;background:#313338;color:#fff;padding:32px'>"
                "<h1>Minecraft Manager could not start</h1><pre style='white-space:pre-wrap'>"
                + str(message).replace("&", "&amp;").replace("<", "&lt;")
                + "</pre><p><a style='color:#8ea1e1' href='/overview'>Back to Hostish Unified</a></p></body>"
            ).encode("utf-8")
            writer.write(
                b"HTTP/1.1 503 Service Unavailable\r\n"
                b"Content-Type: text/html; charset=utf-8\r\n"
                + f"Content-Length: {len(body)}\r\n".encode("ascii")
                + b"Connection: close\r\n\r\n" + body
            )
            await writer.drain()
            return

    try:
        upstream_reader, upstream_writer = await asyncio.wait_for(
            asyncio.open_connection("127.0.0.1", int(manager.active_port)),
            timeout=5.0,
        )
    except Exception as error:
        body = f"Minecraft manager proxy connection failed: {error}".encode("utf-8", "replace")
        writer.write(
            b"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain; charset=utf-8\r\n"
            + f"Content-Length: {len(body)}\r\n".encode("ascii")
            + b"Connection: close\r\n\r\n" + body
        )
        await writer.drain()
        return

    try:
        # Forward the browser-visible /minecraft path unchanged. Standard
        # DispatcherMiddleware performs the WSGI mount and gives Flask the
        # correct SCRIPT_NAME, so url_for/forms/redirects/static all agree.
        upstream_writer.write(request_line)
        saw_connection = False
        for raw in raw_header_lines:
            if raw.lower().startswith(b"connection:"):
                upstream_writer.write(b"Connection: close\r\n")
                saw_connection = True
            else:
                upstream_writer.write(raw)
        if not saw_connection:
            upstream_writer.write(b"Connection: close\r\n")
        upstream_writer.write(b"\r\n")
        await upstream_writer.drain()

        transfer_encoding = str(headers.get("transfer-encoding", "")).lower()
        if "chunked" in transfer_encoding:
            while True:
                size_line = await asyncio.wait_for(reader.readline(), timeout=300.0)
                if not size_line:
                    break
                upstream_writer.write(size_line)
                await upstream_writer.drain()
                size_text = size_line.split(b";", 1)[0].strip()
                try:
                    size = int(size_text, 16)
                except ValueError:
                    break
                if size == 0:
                    # Forward trailing headers and final blank line.
                    while True:
                        trailer = await asyncio.wait_for(reader.readline(), timeout=300.0)
                        upstream_writer.write(trailer)
                        await upstream_writer.drain()
                        if trailer in (b"\r\n", b"\n", b""):
                            break
                    break
                remaining = size + 2  # chunk bytes plus CRLF
                while remaining > 0:
                    chunk = await asyncio.wait_for(
                        reader.read(min(1024 * 1024, remaining)), timeout=300.0)
                    if not chunk:
                        remaining = 0
                        break
                    upstream_writer.write(chunk)
                    await upstream_writer.drain()
                    remaining -= len(chunk)
        else:
            remaining = int(headers.get("content-length", "0") or 0)
            while remaining > 0:
                chunk = await asyncio.wait_for(
                    reader.read(min(1024 * 1024, remaining)), timeout=300.0)
                if not chunk:
                    break
                upstream_writer.write(chunk)
                await upstream_writer.drain()
                remaining -= len(chunk)

        # Stream the complete HTTP response back to the browser.
        while True:
            chunk = await asyncio.wait_for(upstream_reader.read(1024 * 1024), timeout=600.0)
            if not chunk:
                break
            writer.write(chunk)
            await writer.drain()
    finally:
        try:
            upstream_writer.close()
            await upstream_writer.wait_closed()
        except Exception:
            pass


async def handle_panel_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
    async def respond(status: int, body: bytes, content_type: str = "application/json",
                      extra_headers: Optional[dict] = None) -> None:
        header_lines = [
            f"HTTP/1.1 {status} OK",
            f"Content-Type: {content_type}",
            f"Content-Length: {len(body)}",
            "Cache-Control: no-store",
            "X-Content-Type-Options: nosniff",
            "Connection: close",
        ]
        for key, value in (extra_headers or {}).items():
            header_lines.append(f"{key}: {value}")
        writer.write(("\r\n".join(header_lines) + "\r\n\r\n").encode("ascii", "replace") + body)
        await writer.drain()

    try:
        request_line = await asyncio.wait_for(reader.readline(), timeout=20)
        if not request_line:
            return
        parts = request_line.decode("latin-1").strip().split(" ")
        if len(parts) < 2:
            return
        method, target = parts[0].upper(), parts[1]

        headers = {}
        raw_header_lines = []
        while True:
            line = await asyncio.wait_for(reader.readline(), timeout=20)
            if not line or line in (b"\r\n", b"\n"):
                break
            raw_header_lines.append(line)
            if b":" in line:
                key, value = line.decode("latin-1").split(":", 1)
                headers[key.strip().lower()] = value.strip()

        path = urllib.parse.urlparse(target)
        route = path.path
        query = {k: v[0] for k, v in urllib.parse.parse_qs(path.query).items()}

        # Minecraft Manager v4 is mounted directly into the unified site.
        # Forward before reading the body so large world/mod uploads stream.
        if route == "/minecraft" or route.startswith("/minecraft/"):
            await _proxy_minecraft_request(
                reader, writer, request_line, raw_header_lines, headers)
            return

        # Compatibility for stale v10/v11 pages/bookmarks that escaped the mount.
        # 308 preserves POST bodies/methods for server controls and uploads.
        mc_root_alias = (
            route == "/servers" or route.startswith("/servers/") or
            route.startswith("/static/") or route.startswith("/api/versions/") or
            route.startswith("/api/servers/") or route in {"/setup", "/login", "/logout"} or
            route == "/server-dash" or route.startswith("/server-dash/")
        )
        referer = str(headers.get("referer", ""))
        if route == "/settings" and "/minecraft" in referer:
            mc_root_alias = True
        if mc_root_alias:
            location = "/minecraft" + route
            if path.query:
                location += "?" + path.query
            await respond(308, b"", "text/plain; charset=utf-8", {"Location": location})
            return

        body = b""
        length = int(headers.get("content-length", "0") or 0)
        if 0 < length <= 25 * 1024 * 1024:
            body = await asyncio.wait_for(reader.readexactly(length), timeout=30)

        panel_pages = {"/", "/index.html", "/overview", "/website", "/mail",
                       "/scripts", "/logs", "/settings", "/update"}
        if route in panel_pages:
            await respond(200, CONTROL_PANEL_HTML.encode("utf-8"), "text/html; charset=utf-8")
            return

        if route in ("/webmail", "/webmail/"):
            await respond(200, WEBMAIL_APP_HTML.encode("utf-8"), "text/html; charset=utf-8")
            return

        # The webmail API has its own Bearer-token login and permits CORS so
        # the downloaded single-file client can run from file:// or another host.
        if route.startswith("/api/mail/"):
            if method == "OPTIONS":
                await respond(204, b"", "text/plain; charset=utf-8", _mail_cors_headers())
                return
            payload = None
            if body:
                try:
                    payload = json.loads(body.decode("utf-8"))
                except Exception:
                    payload = None
            status, response_body, content_type, extra_headers = await webmail_api(
                method, route, headers, query, payload)
            cors_headers = _mail_cors_headers(extra_headers)
            if isinstance(response_body, (bytes, bytearray)):
                await respond(status, bytes(response_body), content_type, cors_headers)
            else:
                await respond(status, _json(response_body).encode("utf-8"), "application/json", cors_headers)
            return

        if not route.startswith("/api/"):
            await respond(404, b'{"error":"not found"}')
            return

        if not _panel_authorized(headers):
            await respond(401, b'{"error":"panel authentication required"}',
                          extra_headers={"X-Hostish-Panel-Auth": "required"})
            return

        payload = {}
        if body:
            try:
                payload = json.loads(body.decode("utf-8"))
            except Exception:
                await respond(400, b'{"error":"invalid JSON"}')
                return

        if route == "/api/state":
            await respond(200, _json(_panel_state()).encode("utf-8"))
        elif route == "/api/mail-client" and method in {"GET", "POST"}:
            await respond(200, _json(await _panel_mail_client_action(payload)).encode("utf-8"))
        elif route == "/api/logs":
            try:
                since = int(query.get("since", "0"))
            except ValueError:
                since = 0
            lines = [line for line in list(LOG_LINES) if line["seq"] > since]
            await respond(200, _json({"lines": lines[-300:]}).encode("utf-8"))
        elif route == "/api/settings" and method == "GET":
            await respond(200, _json({"settings": _public_settings()}).encode("utf-8"))
        elif route == "/api/settings" and method == "POST":
            await respond(200, _json(await _panel_save_settings(payload)).encode("utf-8"))
        elif route == "/api/service" and method == "POST":
            await respond(200, _json(await _panel_service_action(payload)).encode("utf-8"))
        elif route == "/api/minecraft" and method in {"GET", "POST"}:
            if method == "GET":
                payload = {"action": "list"}
            await respond(200, _json(await _panel_minecraft_action(payload)).encode("utf-8"))
        elif route == "/api/minecraft-manager" and method in {"GET", "POST"}:
            if method == "GET":
                payload = {"action": "status"}
            await respond(200, _json(await _panel_minecraft_manager_action(payload)).encode("utf-8"))
        elif route == "/api/scripts" and method == "GET":
            await respond(200, _json({"scripts": [r.status() for r in MANAGED_SCRIPT_RUNNERS.values()]})
                         .encode("utf-8"))
        elif route == "/api/scripts" and method == "POST":
            await respond(200, _json(await _panel_scripts_action(payload)).encode("utf-8"))
        elif route == "/api/webmail/users" and method in {"GET", "POST"}:
            await respond(200, _json(await _panel_webmail_users_action(method, "", payload)).encode("utf-8"))
        elif route.startswith("/api/webmail/users/") and method == "DELETE":
            target_username = _webmail_normalize_username(route.rsplit("/", 1)[-1])
            await respond(200, _json(await _panel_webmail_users_action("DELETE", target_username, payload))
                         .encode("utf-8"))
        elif route == "/api/update" and method == "POST":
            await respond(200, _json(await _panel_update_action(str(payload.get("action", "check"))))
                         .encode("utf-8"))
        else:
            await respond(404, b'{"error":"not found"}')

    except (asyncio.IncompleteReadError, asyncio.TimeoutError, ConnectionError):
        pass
    except Exception as error:
        log_repeatable_error("panel", f"Control panel error: {type(error).__name__}: {error}")
    finally:
        try:
            writer.close()
            await writer.wait_closed()
        except Exception:
            pass


async def start_control_panel():
    if not CONTROL_PANEL_ENABLED:
        return None

    if not _is_loopback_host(CONTROL_PANEL_HOST) and not CONTROL_PANEL_PASSWORD:
        log("[WARN] Control panel not started: set CONTROL_PANEL_PASSWORD before binding "
            "it to a non-loopback address.")
        return None

    try:
        server = await asyncio.start_server(
            handle_panel_client, CONTROL_PANEL_HOST, int(CONTROL_PANEL_PORT))
    except Exception as error:
        log(f"[WARN] Control panel could not start on port {CONTROL_PANEL_PORT}: {error}")
        return None

    url = f"http://{CONTROL_PANEL_HOST}:{int(CONTROL_PANEL_PORT)}/"
    log(f"Control panel ready: {url}")

    if CONTROL_PANEL_OPEN_BROWSER:
        try:
            import webbrowser
            threading.Thread(target=webbrowser.open, args=(url,), daemon=True).start()
        except Exception:
            pass

    return server


# ============================================================
# STARTUP
# ============================================================

def validate_configuration() -> list:
    errors = []

    global_account_needed = bool(WEB_ENABLED or SMTP_ENABLED or WEBMAIL_ENABLED)
    if global_account_needed:
        if not USERNAME or USERNAME == "your_username":
            errors.append("[account] Set USERNAME for website/SMTP hosting.")
        if not PASSWORD or PASSWORD == "your_password":
            errors.append("[account] Set PASSWORD for website/SMTP hosting.")

    if WEB_ENABLED:
        mode = str(MODE or "").strip().lower()
        if mode not in {"static", "proxy", "hybrid"}:
            errors.append(f"[web] MODE={MODE!r} is invalid; use static, proxy, or hybrid")
        if mode in {"static", "hybrid"} and not os.path.isdir(os.path.expanduser(WEBSITE_DIR)):
            errors.append(f"[web] WEBSITE_DIR does not exist: {WEBSITE_DIR!r}")
        if str(STATIC_SECURITY_MODE).lower() not in {"safe", "custom", "unrestricted"}:
            errors.append("[web] STATIC_SECURITY_MODE must be safe, custom, or unrestricted")
        if str(UPSTREAM_NETWORK_POLICY).lower() not in {"loopback", "private", "any"}:
            errors.append("[web] UPSTREAM_NETWORK_POLICY must be loopback, private, or any")
        try:
            if int(TUNNEL_PROTOCOL) not in {1, 2, 3}:
                raise ValueError
        except Exception:
            errors.append("[web] TUNNEL_PROTOCOL must be 1, 2, or 3")
        if mode in {"proxy", "hybrid"}:
            targets = [("UPSTREAM_BASE", UPSTREAM_BASE)] + [
                (f"UPSTREAM_ROUTES[{prefix!r}]", target)
                for prefix, target in (UPSTREAM_ROUTES or {}).items()]
            for label, target in targets:
                try:
                    _scheme, host, _port, _path = parse_upstream_base(str(target))
                    if not _upstream_host_allowed(host):
                        errors.append(f"[web] {label} {target!r} is blocked by "
                                      f"UPSTREAM_NETWORK_POLICY={UPSTREAM_NETWORK_POLICY!r}")
                    panel_host = str(CONTROL_PANEL_HOST or "").strip().lower()
                    panel_covers_loopback = _is_loopback_host(panel_host) or panel_host in {"0.0.0.0", "::", "[::]"}
                    if (CONTROL_PANEL_ENABLED and panel_covers_loopback
                            and _is_loopback_host(host) and int(_port) == int(CONTROL_PANEL_PORT)):
                        errors.append(
                            f"[web] {label} points back to the Hostish control panel on port "
                            f"{CONTROL_PANEL_PORT}; choose a different upstream port to avoid a proxy loop")
                except Exception as error:
                    errors.append(f"[web] {label}={target!r} is invalid: {error}")

    if (SMTP_ENABLED or WEBMAIL_ENABLED) and str(LOCAL_DELIVERY_MODE).lower() not in {"plain", "starttls", "ssl"}:
        errors.append("[smtp] LOCAL_DELIVERY_MODE must be plain, starttls, or ssl")
    if OUTBOUND_RELAY_ENABLED and not _is_loopback_host(LOCAL_RELAY_BIND_HOST):
        if not LOCAL_RELAY_USERNAME or not LOCAL_RELAY_PASSWORD:
            errors.append("[smtp] Relay authentication is required when the local relay binds beyond loopback")
    if CONTROL_PANEL_ENABLED and OUTBOUND_RELAY_ENABLED:
        panel_host = str(CONTROL_PANEL_HOST or "").strip().lower()
        relay_host = str(LOCAL_RELAY_BIND_HOST or "").strip().lower()
        panel_covers_loopback = _is_loopback_host(panel_host) or panel_host in {"0.0.0.0", "::", "[::]"}
        relay_covers_loopback = _is_loopback_host(relay_host) or relay_host in {"0.0.0.0", "::", "[::]"}
        if panel_covers_loopback and relay_covers_loopback and int(LOCAL_RELAY_BIND_PORT) == int(CONTROL_PANEL_PORT):
            errors.append(
                f"[smtp] LOCAL_RELAY_BIND_PORT conflicts with the control panel on port {CONTROL_PANEL_PORT}")

    mc_missing = _minecraft_manager_missing_dependencies()
    if mc_missing:
        errors.append("[minecraft] Missing manager dependencies: " + ", ".join(mc_missing))
    mc_cfg = _minecraft_manager_read_config()
    try:
        mc_port = int(mc_cfg.get("dashboard_port", 6868) or 6868)
        if not 1 <= mc_port <= 65535:
            raise ValueError
    except Exception:
        errors.append("[minecraft] Minecraft manager dashboard_port must be 1-65535")

    if CONTROL_PANEL_ENABLED:
        try:
            port = int(CONTROL_PANEL_PORT)
            if not 1 <= port <= 65535:
                raise ValueError
        except Exception:
            errors.append("[manager] CONTROL_PANEL_PORT must be 1-65535")
        if not _is_loopback_host(CONTROL_PANEL_HOST) and not CONTROL_PANEL_PASSWORD:
            errors.append("[manager] CONTROL_PANEL_PASSWORD is required when binding beyond loopback")

    return errors


def print_startup_banner() -> None:
    art = (
        " _   _           _   _     _\n"
        "| | | | ___  ___| |_(_)___| |__\n"
        "| |_| |/ _ \\/ __| __| / __| '_ \\\n"
        "|  _  | (_) \\__ \\ |_| \\__ \\ | | |\n"
        "|_| |_|\\___/|___/\\__|_|___/_| |_|"
    )
    for line in art.splitlines():
        log(line)
    mc_cfg = _minecraft_manager_read_config()
    log(f" - Unified agent   : v{version}")
    log(f" - Global account  : {USERNAME if USERNAME != 'your_username' else '(not configured)'}")
    log(f" - Web hosting     : {'ON (' + str(MODE) + ')' if WEB_ENABLED else 'OFF'}")
    log(f" - Mail (SMTP)     : {'ON' if (SMTP_ENABLED or WEBMAIL_ENABLED) else 'OFF'}")
    log(f" - Minecraft       : bundled Hostish MC Manager v{MINECRAFT_MANAGER_BUNDLE_VERSION} "
        f"on {mc_cfg.get('dashboard_host', '127.0.0.1')}:{mc_cfg.get('dashboard_port', 6868)}")
    log(f" - Control panel   : "
        f"{'http://' + str(CONTROL_PANEL_HOST) + ':' + str(CONTROL_PANEL_PORT) + '/' if CONTROL_PANEL_ENABLED else 'OFF'}")
    if WEB_ENABLED and USERNAME and USERNAME != "your_username":
        log(f" - Public site URL : https://{USERNAME}.hostish.site/")
    log("-" * 72)


async def main() -> None:
    global SUPERVISORS, MINECRAFT_INSTANCES, MINECRAFT_ENABLED

    stored = load_config_file()
    if stored:
        apply_settings(stored)
        # One-time transparent migration: old plaintext config passwords become
        # encrypted the next time this version starts successfully.
        try:
            save_config_file(stored)
        except Exception as error:
            print(f"[WARN] Could not migrate {CONFIG_FILE} secrets to encrypted storage: {error}")
        print(f"[INFO] Loaded settings from {CONFIG_FILE}")
    else:
        apply_settings({})

    print_startup_banner()
    errors = validate_configuration()
    for error in errors:
        log(f"[CONFIG] {error}")
    if errors:
        log("Valid services can still start; fix affected items from the relevant control-panel page.")

    if WEBMAIL_ENABLED:
        webmail_ensure_first_admin()

    if IP_PROTECTION_ENABLED:
        public_ip = await run_in_thread(fetch_public_ip, True)
        log(f"IP protection initialized{' (' + public_ip + ')' if public_ip else ' (could not fetch yet)'}")

    if SELF_UPDATE_ENABLED and SELF_UPDATE_CHECK_ON_STARTUP:
        newest = await run_in_thread(fetch_latest_agent_version, True)
        if newest is not None and newest > version:
            log(f"A newer agent is available: v{newest} (running v{version}).")
            action = str(SELF_UPDATE_NONINTERACTIVE_ACTION or "continue").strip().lower()
            if action == "update":
                try:
                    result = await run_in_thread(install_latest_agent_update, newest)
                    log(f"Installed v{result['downloaded_version']}; restarting now.")
                    restart_into_updated_agent(result["script_path"])
                    return
                except Exception as error:
                    log(f"Automatic update failed: {type(error).__name__}: {error}")
            elif action == "exit":
                log("SELF_UPDATE_NONINTERACTIVE_ACTION=exit; stopping because an update is available.")
                return
            else:
                log("Open the Update page in the control panel to review/install it.")

    SUPERVISORS = build_supervisors()
    await MINECRAFT_APP_MANAGER.start()
    panel = await start_control_panel()

    def has_error(prefixes):
        return any(any(str(error).startswith(prefix) for prefix in prefixes) for error in errors)

    web_ok = not has_error(("[account]", "[web]"))
    smtp_ok = not has_error(("[account]", "[smtp]"))

    if WEB_ENABLED or WEBMAIL_ENABLED:
        if web_ok:
            SUPERVISORS["web"].start()
            if WEBMAIL_ENABLED and not WEB_ENABLED:
                log("Web tunnel is running in mail-gateway-only mode for /.hostish/mail/.", "mail")
        else:
            log("Website/mail HTTP gateway is paused because its configuration has errors.")

    if SMTP_ENABLED or WEBMAIL_ENABLED:
        if smtp_ok:
            SUPERVISORS["smtp"].start()
        else:
            log("SMTP tunnel is paused because its configuration has errors.")

    if not (WEB_ENABLED or SMTP_ENABLED or WEBMAIL_ENABLED):
        log("Website/mail tunnels are disabled. Minecraft Manager is still available locally.")

    await start_autostart_scripts()
    background_tasks = [
        asyncio.create_task(periodic_summary_loop()),
        asyncio.create_task(idle_status_checker_loop()),
    ]

    try:
        while True:
            await asyncio.sleep(3600)
    finally:
        for task in background_tasks:
            task.cancel()
        if background_tasks:
            await asyncio.gather(*background_tasks, return_exceptions=True)
        for supervisor in SUPERVISORS.values():
            await supervisor.stop()
        await MINECRAFT_APP_MANAGER.stop()
        await stop_all_managed_scripts()
        if panel is not None:
            panel.close()
            try:
                await panel.wait_closed()
            except Exception:
                pass


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n[INFO] Stopped by user")
    except Exception:
        traceback.print_exc()
