#!/bin/bash
# Last-Updated: 2026-08-05 17:00 EDT
# Created: 2026-07 (tlb-2.0+ packaging / updater)
# Authors: Paul Aidukas KN2R, Logan Crook K8LRC
#
# update-tlbweb.sh - update an existing LinkBox web monitor + tlb-admin install
#
# Keeps your current IP, reflector name, AllStar node, and log paths.
# Public monitor is permanently exp.php (not web-root index.php).
# Does NOT write index.html / index.htm into the web root.
# Does NOT reset tlb-admin/.htpasswd.
# Does NOT overwrite existing tlb-admin/.htaccess.
# Does NOT touch /home/thelinkbox/scripts or tlb.conf EventScript=
#   (operator manages EventScripts / restart scripts manually).
# Preserves existing $tlbrestartscript in tlb-admin across template rewrite.
#
# Package URL (IP first — some networks block crabdance.com DDNS):
#   http://45.32.193.131/tlb-2+/
#   Alternate: http://linkbox.k8lrc.crabdance.com/tlb-2+/
#
# Preferred one-package update (Paul-style tgz — same idea as supermonASL3_latest_update):
#   wget 'http://45.32.193.131/update-tlbweb-latest'
#   chmod +x update-tlbweb-latest
#   sudo ./update-tlbweb-latest
#
# Or update-tlbweb.sh only (fetches the rest automatically):
#   wget 'http://45.32.193.131/tlb-2+/update-tlbweb.sh'
#   chmod +x update-tlbweb.sh
#   sudo ./update-tlbweb.sh
#
# Run from /root or anywhere. Installer scripts do NOT go in /var/www/html.
# They update the live site files IN /var/www/html.
#
# Options:
#   --with-tlbevent    Accepted for compatibility (no longer updates EventScripts)
#   --monitor NAME     Accepted for compatibility; public monitor is always exp.php
#   -y, --yes          Non-interactive (auto-download, no confirm prompt)

UPDATE_VERSION=32

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGE_URL="${PACKAGE_URL:-http://45.32.193.131/tlb-2+}"
INSTALL_LIB="${SCRIPT_DIR}/install-tlbweb.sh"

ensure_install_lib() {
    if command -v wget >/dev/null 2>&1; then
        printf 'update-tlbweb: Refreshing install-tlbweb.sh from %s/\n' "${PACKAGE_URL%/}"
        if wget --show-progress -O "${INSTALL_LIB}.tmp" "${PACKAGE_URL%/}/install-tlbweb.sh"; then
            mv -f "${INSTALL_LIB}.tmp" "${INSTALL_LIB}"
            chmod +x "${INSTALL_LIB}"
            return 0
        fi
        rm -f "${INSTALL_LIB}.tmp"
        printf 'update-tlbweb: WARNING: could not download install-tlbweb.sh - using local copy\n' >&2
    fi

    if [[ -f "${INSTALL_LIB}" ]]; then
        return 0
    fi

    printf 'update-tlbweb: ERROR: install-tlbweb.sh not found in %s\n' "${SCRIPT_DIR}" >&2
    printf 'These are installer scripts - keep them in %s (NOT /var/www/html).\n' "${SCRIPT_DIR}" >&2
    printf 'Download install-tlbweb.sh with:\n' >&2
    printf "  wget '%s/install-tlbweb.sh' -O %s/install-tlbweb.sh\n" "${PACKAGE_URL%/}" "${SCRIPT_DIR}" >&2
    exit 1
}

ensure_install_lib

# shellcheck source=install-tlbweb.sh
source "${INSTALL_LIB}"

if ! declare -F refresh_update_package >/dev/null 2>&1; then
    printf 'update-tlbweb: ERROR: install-tlbweb.sh is too old (missing refresh_update_package).\n' >&2
    printf 'Upload the latest install-tlbweb.sh to %s/ then re-run:\n' "${PACKAGE_URL%/}" >&2
    printf "  wget '%s/install-tlbweb.sh' -O %s/install-tlbweb.sh\n" "${PACKAGE_URL%/}" "${INSTALL_LIB}" >&2
    exit 1
fi

ulog() {
    printf 'update-tlbweb: %s\n' "$*"
}

update_usage() {
    cat <<'EOF'
Usage: update-tlbweb.sh [options]

Updates an existing LinkBox install under /var/www/html:
  - Public monitor (official: exp.php; admin is tlb-admin/index.php)
  - tlb-common.inc
  - tlb-admin/index.php

Does NOT overwrite / touch:
  - tlb-admin/.htaccess (if present) or .htpasswd
  - /home/thelinkbox/scripts/* (EventScripts, restart-tlb*, etc.)
  - tlb.conf EventScript=
  - WEB_ROOT/index.html (no longer shipped)

Leave EventScript / restart scripts / .htaccess to the operator — update manually
from the package samples if you want newer copies.

Reads your current settings from the installed files and re-applies them
to the latest web templates from the package.

Run this script from /root (or any directory). It updates files IN /var/www/html.
Do not copy update-tlbweb.sh into /var/www/html.

Options:
  --with-tlbevent    Accepted for compatibility (does not update EventScripts)
  --monitor NAME     Ignored / forced to exp.php (public monitor is permanent)
  -y, --yes          Non-interactive (auto-download; prompts if config missing)
  -h, --help         Show this help

Environment (optional overrides):
  SITE_TYPE          irlp | echolink | link | repeater
  REFLECTOR_NAME     e.g. exp0093, *CONF*, K8LRC-L, K8LRC-R
  USES_IRLP          1/0 — this box uses IRLP
  USES_ECHOLINK      1/0 — this box uses EchoLink
  SERVER_IP          e.g. 45.77.112.131

Updates the public monitor as exp.php only (Paul KN2R permanent naming).
Does not refresh a legacy web-root index.php monitor if present.
Deploys tlb-admin/index.php if missing.

Requires install-tlbweb.sh in the same directory (auto-downloaded if missing).
EOF
}

ensure_update_prereqs() {
    ulog "Installer scripts directory: ${SCRIPT_DIR}"
    ulog "Live site directory:         ${WEB_ROOT}"
    echo
}

trim_value() {
    local v="$1"
    v="${v#"${v%%[![:space:]]*}"}"
    v="${v%"${v##*[![:space:]]}"}"
    printf '%s' "${v}"
}

read_php_string_var() {
    local file="$1"
    local var="$2"
    local line=""

    [[ -f "${file}" ]] || return 0
    line="$(grep -E "^[[:space:]]*\\$${var}[[:space:]]*=" "${file}" 2>/dev/null | head -1 || true)"
    [[ -n "${line}" ]] || return 0
    sed -n 's/.*= "\([^"]*\)".*/\1/p' <<< "${line}" | head -1
}

detect_installed_monitor() {
    local monitors
    monitors="$(find_linkbox_monitors)"
    [[ -n "${monitors}" ]] || return 1
    choose_primary_monitor ${monitors}
}

resolve_monitor_for_update() {
    # Permanent: public monitor is always exp.php.
    PUBLIC_MONITOR_SCRIPT="exp.php"
    local monitor_path="${WEB_ROOT}/exp.php"

    if is_linkbox_monitor_file "${monitor_path}" 2>/dev/null || [[ -f "${monitor_path}" ]]; then
        ulog "Using public monitor: exp.php"
        printf '%s' "${monitor_path}"
        return 0
    fi

    if [[ -f "${WEB_ROOT}/tlb-admin/index.php" ]]; then
        ulog "No exp.php yet - will deploy public monitor: exp.php"
        printf '%s' "${monitor_path}"
        return 0
    fi

    # Last resort: allow update if a legacy index.php monitor exists (settings only),
    # but still deploy/refresh exp.php as the public monitor.
    if is_linkbox_monitor_file "${WEB_ROOT}/index.php" 2>/dev/null; then
        ulog "Legacy ${WEB_ROOT}/index.php found - will deploy exp.php as public monitor (index.php not updated)"
        printf '%s' "${monitor_path}"
        return 0
    fi

    return 1
}

read_public_monitor_script_from_admin() {
    local admin="${WEB_ROOT}/tlb-admin/index.php"
    local tmp=""

    [[ -f "${admin}" ]] || return 1
    tmp="$(sed -n 's|.*href="\.\./\([^"]*\)".*Back to public.*|\1|p' "${admin}" 2>/dev/null | head -1)"
    tmp="$(trim_value "${tmp}")"
    # Always exp.php — ignore legacy admin "Back to public" links to index.php
    if [[ "${tmp}" == "index.php" || "${tmp}" == "exp.php" ]]; then
        PUBLIC_MONITOR_SCRIPT="exp.php"
        return 0
    fi
    return 1
}

read_deployed_config_from_admin() {
    local admin="${WEB_ROOT}/tlb-admin/index.php"
    local tmp=""

    [[ -f "${admin}" ]] || return 1

    read_public_monitor_script_from_admin || true

    if [[ -z "${REFLECTOR_NAME:-}" ]]; then
        tmp="$(sed -n 's/.*IRLP \([^< ]*\) Reflector.*/\1/p' "${admin}" | head -1)"
        REFLECTOR_NAME="$(trim_value "${tmp}")"
        if [[ -z "${REFLECTOR_NAME}" ]]; then
            tmp="$(sed -n 's/.*LinkBox Manager 2+ IRLP \([^< ]*\) Reflector.*/\1/p' "${admin}" | head -1)"
            REFLECTOR_NAME="$(trim_value "${tmp}")"
        fi
    fi

    if [[ -z "${SERVER_IP:-}" ]] || ! is_valid_host_or_ip "${SERVER_IP}"; then
        tmp="$(sed -n 's/.*Administration &mdash; \([^<]*\)<.*/\1/p' "${admin}" | head -1)"
        SERVER_IP="$(trim_value "${tmp}")"
    fi

    if [[ -z "${DOMAIN_OR_IP:-}" ]]; then
        tmp="$(grep "HTTP_HOST" "${admin}" 2>/dev/null | head -1 || true)"
        tmp="$(sed -n "s/.*?? '\\([^']*\\)'.*/\1/p" <<< "${tmp}" | head -1)"
        tmp="$(trim_value "${tmp}")"
        if [[ -n "${tmp}" ]] && is_valid_host_or_ip "${tmp}"; then
            DOMAIN_OR_IP="${tmp}"
        fi
    fi

    if [[ -z "${ALLSTAR_NODE:-}" ]]; then
        ALLSTAR_NODE="$(extract_allstar_node_from_file "${admin}" || true)"
        ALLSTAR_NODE="$(trim_value "${ALLSTAR_NODE}")"
    fi
    if [[ -z "${ALLSTAR_NODE:-}" ]]; then
        ALLSTAR_NODE="$(find_allstar_node_from_webroot || true)"
        ALLSTAR_NODE="$(trim_value "${ALLSTAR_NODE:-}")"
    fi

    if [[ -z "${TLBCMD_PORT:-}" ]]; then
        TLBCMD_PORT="$(read_php_string_var "${admin}" "tlbcmdport")"
    fi
    if [[ -z "${TLB_LOG_PATH:-}" ]]; then
        TLB_LOG_PATH="$(read_php_string_var "${admin}" "tlblogpath")"
    fi
    if [[ -z "${TLB_MESSAGES_PATH:-}" ]]; then
        TLB_MESSAGES_PATH="$(read_php_string_var "${admin}" "tlbmessagespath")"
    fi
}

is_linkbox_monitor_file() {
    local path="$1"
    [[ -f "${path}" ]] || return 1
    grep -qE 'exp_render_live_panel|tlb-common\.inc|LinkBox Monitor|LinkBox Manager|tlb-admin/|tlbcmdport' "${path}" 2>/dev/null
}

find_linkbox_monitors() {
    # Public monitor is permanently exp.php only.
    if is_linkbox_monitor_file "${WEB_ROOT}/exp.php" || [[ -f "${WEB_ROOT}/tlb-admin/index.php" ]]; then
        printf '%s\n' "exp.php"
    fi
}

read_monitor_script_name() {
    local monitor="$1"
    local tmp=""

    [[ -f "${monitor}" ]] || return 1

    tmp="$(sed -n "s/.*MONITOR_SCRIPT = '\\([^']*\\)'.*/\\1/p" "${monitor}" | head -1)"
    if [[ -n "${tmp}" ]]; then
        printf '%s' "${tmp}"
        return 0
    fi
    tmp="$(sed -n "s/.*fetch('\\([^']*\\)?partial=1'.*/\\1/p" "${monitor}" | head -1)"
    if [[ -n "${tmp}" ]]; then
        printf '%s' "${tmp}"
        return 0
    fi
    printf '%s' "$(basename "${monitor}")"
}

choose_primary_monitor() {
    # Public monitor is permanently exp.php.
    PUBLIC_MONITOR_SCRIPT="exp.php"
    printf '%s' "${WEB_ROOT}/exp.php"
}

read_reflector_name_from_file() {
    local file="$1"
    local tmp=""

    [[ -f "${file}" ]] || return 1

    tmp="$(sed -n 's/.*LinkBox Monitor 2+ IRLP \([^< ]*\) Reflector.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Manager 2+ IRLP \([^< ]*\) Reflector.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Monitor 2+ EchoLink \([^< ]*\) Conference.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Manager 2+ EchoLink \([^< ]*\) Conference.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Monitor 2+ EchoLink \([^< ]*\) Link.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Manager 2+ EchoLink \([^< ]*\) Link.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Monitor 2+ EchoLink \([^< ]*\) Repeater.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*LinkBox Manager 2+ EchoLink \([^< ]*\) Repeater.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*IRLP \([^< ]*\) Reflector.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*\(exp[0-9][0-9]*\) Monitor.*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(sed -n 's/.*IRLP[[:space:]]\+\(exp[0-9][0-9]*\).*/\1/p' "${file}" | head -1)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(grep -oE '\*[A-Za-z0-9*]+\*' "${file}" 2>/dev/null | head -1 || true)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    tmp="$(grep -oE 'exp[0-9]{4}' "${file}" 2>/dev/null | head -1 || true)"
    REFLECTOR_NAME="$(trim_value "${tmp}")"
    [[ -n "${REFLECTOR_NAME}" ]] && return 0

    REFLECTOR_NAME=""
    return 1
}

read_server_ip_from_file() {
    local file="$1"
    local tmp=""

    [[ -f "${file}" ]] || return 1

    tmp="$(sed -n 's/.*IP Address:&nbsp; \([^<]*\).*/\1/p' "${file}" | head -1)"
    SERVER_IP="$(trim_value "${tmp}")"
    if is_valid_host_or_ip "${SERVER_IP}"; then
        return 0
    fi

    tmp="$(sed -n 's/.*IP Address:[[:space:]]*\([^<]*\).*/\1/p' "${file}" | head -1)"
    SERVER_IP="$(trim_value "${tmp}")"
    if is_valid_host_or_ip "${SERVER_IP}"; then
        return 0
    fi

    SERVER_IP=""
    return 1
}

ensure_update_config() {
    local monitor="$1"

    if [[ -z "${SITE_TYPE:-}" ]]; then
        read_site_type_from_file "${monitor}" || true
    fi
    if [[ -z "${SITE_TYPE:-}" ]]; then
        read_site_type_from_file "${WEB_ROOT}/tlb-admin/index.php" || true
    fi

    if [[ -z "${REFLECTOR_NAME:-}" ]]; then
        read_reflector_name_from_file "${monitor}" || true
    fi

    # Name is authoritative: *OHIO* => EchoLink Conference, CALL-L => Link, exp0092 => IRLP Reflector.
    # This corrects pages previously saved as "IRLP *OHIO* Reflector".
    if [[ -n "${REFLECTOR_NAME:-}" ]] && force_site_type_from_name; then
        :
    elif [[ -z "${SITE_TYPE:-}" || -z "${REFLECTOR_NAME:-}" ]]; then
        # Ask site type + name when interactive, or require env with --yes.
        if [[ "${ASSUME_YES}" -eq 1 ]]; then
            if [[ -z "${SITE_TYPE:-}" ]]; then
                SITE_TYPE="$(detect_site_type_guess)"
            fi
            [[ -n "${REFLECTOR_NAME:-}" ]] || die "Could not read name from ${monitor}. Set REFLECTOR_NAME='*OHIO*' or exp0092 or CALL-L"
            if ! force_site_type_from_name; then
                [[ -n "${SITE_TYPE:-}" ]] || die "Could not detect SITE_TYPE. Set SITE_TYPE=echolink REFLECTOR_NAME='*OHIO*'"
                set_site_type_labels
            fi
        else
            prompt_site_type_and_name
        fi
    else
        set_site_type_labels
        force_site_type_from_name || true
    fi

    # Always confirm networks on interactive update (or honor env with --yes).
    if declare -F prompt_network_usage >/dev/null 2>&1; then
        prompt_network_usage
    fi

    if ! is_valid_host_or_ip "${SERVER_IP:-}"; then
        read_server_ip_from_file "${monitor}" || true
    fi
    if ! is_valid_host_or_ip "${SERVER_IP:-}"; then
        SERVER_IP="$(detect_public_ip)"
    fi
    if ! is_valid_host_or_ip "${SERVER_IP:-}"; then
        if [[ "${ASSUME_YES}" -eq 1 ]]; then
            die "Could not read server IP from ${monitor}. Set SERVER_IP= and re-run with --yes"
        fi
        prompt_host_or_ip SERVER_IP "Server public IP address" "${SERVER_IP}"
    fi

    [[ -n "${DOMAIN_OR_IP}" ]] || DOMAIN_OR_IP="${SERVER_IP}"

    if [[ -z "${ALLSTAR_NODE:-}" ]]; then
        ALLSTAR_NODE="$(find_allstar_node_from_webroot || true)"
        ALLSTAR_NODE="$(trim_value "${ALLSTAR_NODE:-}")"
    fi
    if [[ -z "${ALLSTAR_NODE:-}" ]]; then
        if [[ "${ASSUME_YES}" -ne 1 ]]; then
            prompt ALLSTAR_NODE "AllStar hub node (e.g. 41001; blank = omit AllStar link)" ""
            ALLSTAR_NODE="$(trim_value "${ALLSTAR_NODE:-}")"
        fi
    fi
}

read_deployed_config() {
    local monitor="$1"
    local tmp=""

    # Defaults under set -u (exp.php may not exist yet on first deploy)
    SERVER_IP="${SERVER_IP:-}"
    DOMAIN_OR_IP="${DOMAIN_OR_IP:-}"
    REFLECTOR_NAME="${REFLECTOR_NAME:-}"
    ALLSTAR_NODE="${ALLSTAR_NODE:-}"
    SUPERMON_URL="${SUPERMON_URL:-}"
    TLBCMD_PORT="${TLBCMD_PORT:-}"
    TLB_LOG_PATH="${TLB_LOG_PATH:-}"
    TLB_MESSAGES_PATH="${TLB_MESSAGES_PATH:-}"

    if [[ -f "${monitor}" ]]; then
        if [[ -z "${PUBLIC_MONITOR_SCRIPT:-}" ]]; then
            PUBLIC_MONITOR_SCRIPT="$(basename "${monitor}")"
        fi

        tmp="$(read_monitor_script_name "${monitor}")"
        if [[ -n "${tmp}" ]]; then
            PUBLIC_MONITOR_SCRIPT="${tmp}"
        fi

        tmp="$(sed -n 's/.*IP Address:&nbsp; \([^<]*\).*/\1/p' "${monitor}" | head -1)"
        SERVER_IP="$(trim_value "${tmp}")"
        if ! is_valid_host_or_ip "${SERVER_IP}"; then
            read_server_ip_from_file "${monitor}" || true
        fi

        read_reflector_name_from_file "${monitor}" || true
        if [[ -z "${REFLECTOR_NAME:-}" ]]; then
            tmp="$(sed -n 's/.*IRLP \([^<]*\) Reflector.*/\1/p' "${monitor}" | head -1)"
            REFLECTOR_NAME="$(trim_value "${tmp}")"
        fi

        tmp="$(extract_allstar_node_from_file "${monitor}" || true)"
        ALLSTAR_NODE="$(trim_value "${tmp}")"
        if [[ -z "${ALLSTAR_NODE:-}" ]]; then
            ALLSTAR_NODE="$(find_allstar_node_from_webroot || true)"
            ALLSTAR_NODE="$(trim_value "${ALLSTAR_NODE:-}")"
        fi
        tmp="$(extract_supermon_url_from_file "${monitor}" || true)"
        if [[ -n "${tmp}" ]]; then
            SUPERMON_URL="$(normalize_supermon_url "${tmp}")"
        elif [[ -n "${ALLSTAR_NODE}" ]]; then
            SUPERMON_URL="$(normalize_supermon_url "${ALLSTAR_NODE}")"
        fi

        TLBCMD_PORT="$(read_php_string_var "${monitor}" "tlbcmdport")"
        TLB_LOG_PATH="$(read_php_string_var "${monitor}" "tlblogpath")"
        TLB_MESSAGES_PATH="$(read_php_string_var "${monitor}" "tlbmessagespath")"
    elif [[ -z "${PUBLIC_MONITOR_SCRIPT:-}" ]]; then
        PUBLIC_MONITOR_SCRIPT="exp.php"
    fi

    # If deploying new exp.php, pull settings from legacy index.php or tlb-admin
    if [[ -z "${SERVER_IP}" ]] && [[ -f "${WEB_ROOT}/index.php" ]]; then
        tmp="$(sed -n 's/.*IP Address:&nbsp; \([^<]*\).*/\1/p' "${WEB_ROOT}/index.php" | head -1)"
        SERVER_IP="$(trim_value "${tmp}")"
        if ! is_valid_host_or_ip "${SERVER_IP}"; then
            read_server_ip_from_file "${WEB_ROOT}/index.php" || true
        fi
        if [[ -z "${REFLECTOR_NAME}" ]]; then
            read_reflector_name_from_file "${WEB_ROOT}/index.php" || true
        fi
        if [[ -z "${ALLSTAR_NODE}" ]]; then
            tmp="$(extract_allstar_node_from_file "${WEB_ROOT}/index.php" || true)"
            ALLSTAR_NODE="$(trim_value "${tmp}")"
        fi
        if [[ -z "${SUPERMON_URL:-}" ]]; then
            tmp="$(extract_supermon_url_from_file "${WEB_ROOT}/index.php" || true)"
            [[ -n "${tmp}" ]] && SUPERMON_URL="$(normalize_supermon_url "${tmp}")"
        fi
    fi

    DOMAIN_OR_IP="${DOMAIN_OR_IP:-${SERVER_IP:-}}"
    read_deployed_config_from_admin || true
    [[ -n "${DOMAIN_OR_IP:-}" ]] || DOMAIN_OR_IP="${SERVER_IP:-}"
    # Permanent naming — never leave public monitor as web-root index.php
    PUBLIC_MONITOR_SCRIPT="exp.php"
}

print_update_plan() {
    local monitors plan_monitors
    monitors="$(find_linkbox_monitors | tr '\n' ' ')"
    plan_monitors="${monitors}"
    if [[ -n "${PUBLIC_MONITOR_SCRIPT:-}" ]] && [[ "${plan_monitors}" != *"${PUBLIC_MONITOR_SCRIPT}"* ]]; then
        plan_monitors="${plan_monitors} ${PUBLIC_MONITOR_SCRIPT} (deploy)"
    fi
    plan_monitors="$(trim_value "${plan_monitors}")"
    local admin_note="tlb-admin/index.php"
    if [[ ! -f "${WEB_ROOT}/tlb-admin/index.php" ]]; then
        admin_note="tlb-admin/index.php (will install)"
    fi

    cat <<EOF
Current settings (will be preserved):
  Primary monitor: ${WEB_ROOT}/${PUBLIC_MONITOR_SCRIPT}
  Site type:       ${LINK_TYPE:-?} ${REFLECTOR_NAME} ${SITE_KIND:-?}
  Networks:        IRLP=$([ "${USES_IRLP:-0}" = 1 ] && echo yes || echo no), EchoLink=$([ "${USES_ECHOLINK:-0}" = 1 ] && echo yes || echo no)
  Server IP:       ${SERVER_IP:-<detect/prompt>}
  Domain/IP:       ${DOMAIN_OR_IP:-<detect/prompt>}
  Supermon URL:    ${SUPERMON_URL:-<omit>}
  AllStar node:    ${ALLSTAR_NODE:-<none>}
  tlbcmd port:     ${TLBCMD_PORT:-<default>}
  tlb.log:         ${TLB_LOG_PATH:-<default>}
  messages log:    ${TLB_MESSAGES_PATH:-<default>}

Will update in ${WEB_ROOT}:
  Public monitor file(s): ${plan_monitors:-${PUBLIC_MONITOR_SCRIPT}}
  tlb-common.inc, ${admin_note}

Will NOT touch (operator-managed — update never installs these):
  tlb-admin/.htaccess (kept if present)
  tlb-admin/.htpasswd
  tlbevent-conference.sh / tlbevent-nopolicy.sh / tlbevent.sh
  restart-tlb-conference / restart-tlb-nopolicy / restart-tlb
  tlb.conf EventScript=
  ${WEB_ROOT}/index.html

Will remove: ${WEB_ROOT}/index.php if it is a legacy LinkBox public monitor (backed up first)
EOF
}

verify_monitor_update() {
    local path="$1"
    if grep -q 'href="tlb-admin/"' "${path}" 2>/dev/null; then
        ulog "Verified: ${path} links to tlb-admin/ (Apache auth)"
        return 0
    fi
    ulog "WARNING: ${path} may not link to tlb-admin/ (check template)"
    return 1
}

update_all_monitor_files() {
    local monitors=()
    local script monitor_path

    UPDATE_PRESERVE_AUTH=1
    UPDATE_ALL_MONITORS=1
    # Never touch /home/thelinkbox (tlb.conf / EventScripts / restart) during web update.
    SKIP_OPERATOR_BACKUP_COLLAPSE=1
    PUBLIC_MONITOR_SCRIPT="exp.php"
    monitors=("exp.php")
    ulog "Public monitor update target: exp.php only"
    ulog "Will not stop/restart TheLinkBox (tlb) — web files only"

    install_web_files

    # install_web_files already removes legacy web-root index.php; ensure it here too
    remove_legacy_webroot_public_monitor

    for script in "${monitors[@]}"; do
        monitor_path="${WEB_ROOT}/${script}"
        verify_monitor_update "${monitor_path}" || true
    done

    verify_admin_monitor_link
}

# True if a tlb process is running (name or common binary paths).
tlb_is_running() {
    if command -v pidof >/dev/null 2>&1; then
        pidof tlb >/dev/null 2>&1 && return 0
    fi
    pgrep -x tlb >/dev/null 2>&1 && return 0
    pgrep -f '/usr/local/libexec/tlb( |$)' >/dev/null 2>&1 && return 0
    pgrep -f '/usr/local/sbin/tlb( |$)' >/dev/null 2>&1 && return 0
    pgrep -f '/usr/sbin/tlb( |$)' >/dev/null 2>&1 && return 0
    return 1
}

# Web update must never leave TheLinkBox down. If tlb was up before update and
# is down after, try to bring it back (systemd first, then common binary paths).
ensure_tlb_still_running_after_update() {
    local was_running="${1:-0}"

    if tlb_is_running; then
        ulog "TheLinkBox (tlb) is running — OK"
        return 0
    fi

    if [[ "${was_running}" != "1" ]]; then
        ulog "NOTE: tlb was not running before this update (left alone)"
        return 0
    fi

    ulog "WARNING: tlb was running before update but is not running now"
    ulog "Attempting to start tlb again (web update should not stop it)..."

    if command -v systemctl >/dev/null 2>&1; then
        if systemctl start tlb >/dev/null 2>&1; then
            sleep 1
            if tlb_is_running; then
                ulog "Recovered: systemctl start tlb"
                return 0
            fi
        fi
    fi

    local bin conf="/home/thelinkbox/tlb.conf"
    for bin in /usr/local/libexec/tlb /usr/local/sbin/tlb /usr/sbin/tlb /usr/bin/tlb; do
        if [[ -x "${bin}" && -f "${conf}" ]]; then
            ( cd /home/thelinkbox && "${bin}" -d -f "${conf}" >/dev/null 2>&1 & ) || true
            sleep 1
            if tlb_is_running; then
                ulog "Recovered: started ${bin}"
                return 0
            fi
        fi
    done

    ulog "ERROR: could not auto-restart tlb — start it manually, e.g.:"
    ulog "  systemctl start tlb"
    ulog "  OR: /home/thelinkbox/scripts/restart-tlb"
    return 1
}

verify_admin_monitor_link() {
    local admin="${WEB_ROOT}/tlb-admin/index.php"
    local script="${PUBLIC_MONITOR_SCRIPT:-exp.php}"

    [[ -f "${admin}" ]] || return 0

    if grep -q "href=\"\.\./${script}\"" "${admin}" 2>/dev/null; then
        ulog "Verified: tlb-admin back link points to ${script}"
        return 0
    fi
    ulog "WARNING: tlb-admin may not link to public monitor ${script} (re-run update)"
    return 1
}

update_print_summary() {
    local script="${PUBLIC_MONITOR_SCRIPT:-exp.php}"
    local pub="${DOMAIN_OR_IP:-${SERVER_IP}}"

    cat <<EOF

Updated web files in ${WEB_ROOT}:
  ${WEB_ROOT}/${script}
  ${WEB_ROOT}/tlb-common.inc
  ${WEB_ROOT}/tlb-admin/index.php

Left alone (edit manually if needed):
  ${WEB_ROOT}/index.html (and other operator landing pages)
  tlb-admin/.htaccess / .htpasswd
  /home/thelinkbox/scripts/  (EventScripts + restart-tlb*)
  tlb.conf EventScript=
  tlb-admin \$tlbrestartscript (preserved if already set)

Package samples (copy by hand if you want newer EventScripts):
  tlbevent-conference.sh     # WITH policy
  tlbevent-nopolicy.sh       # NO policy (preferred name)
  tlbevent.sh                # NO policy (legacy name)

Public monitor: http://${pub}/${script}
Admin + logs:   http://${pub}/tlb-admin/  (Apache login required)

Quick verify on this server:
  grep -c 'href="tlb-admin/"' ${WEB_ROOT}/${script}
  grep 'Back to public' ${WEB_ROOT}/tlb-admin/index.php

Package download URL (note tlb- prefix):
  http://45.32.193.131/tlb-2+/
EOF
}

update_main() {
    local monitor_path=""
    local answer=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --with-tlbevent) shift ;; # compatibility; EventScripts no longer updated
            --monitor)
                PUBLIC_MONITOR_SCRIPT="exp.php"
                if [[ -n "${2:-}" && "${2}" != "exp.php" ]]; then
                    ulog "NOTE: --monitor ${2} ignored — public monitor is permanently exp.php"
                fi
                shift 2
                ;;
            -y|--yes) ASSUME_YES=1; shift ;;
            -h|--help) update_usage; exit 0 ;;
            *) die "Unknown option: $1 (use --help)" ;;
        esac
    done

    if [[ "$(id -u)" -ne 0 ]]; then
        die "Run as root (sudo ./update-tlbweb.sh)"
    fi

    [[ -d "${WEB_ROOT}" ]] || die "WEB_ROOT does not exist: ${WEB_ROOT}"

    echo "LinkBox web updater v${UPDATE_VERSION} (package: ${SCRIPT_DIR})"
    ulog "IRLP / EchoLink / -L / -R — see SETUP-INSTRUCTIONS.txt"
    ensure_update_prereqs

    ASSUME_YES="${ASSUME_YES:-0}"
    ensure_package
    refresh_update_package
    validate_public_monitor_template
    echo

    monitor_path="$(resolve_monitor_for_update)" || die "No LinkBox install found in ${WEB_ROOT} (need exp.php or tlb-admin)"

    if [[ ! -f "${WEB_ROOT}/tlb-admin/index.php" ]]; then
        ulog "tlb-admin/index.php not found - will install from template"
    fi

    read_deployed_config "${monitor_path}"
    # Fill any still-empty settings from tlb.ini (does not override live values)
    if declare -F load_tlb_ini >/dev/null 2>&1; then
        load_tlb_ini
    fi
    ensure_update_config "${monitor_path}"

    if declare -F write_tlb_ini >/dev/null 2>&1; then
        write_tlb_ini
    fi

    print_update_plan
    echo

    if [[ "${ASSUME_YES}" -ne 1 ]]; then
        prompt answer "Proceed with update? (Y/n)" "Y"
        [[ "${answer}" =~ ^[Nn] ]] && die "Update cancelled"
    fi

    TLBCMD_PORT_ARG="$(normalize_tlbcmd_port "${TLBCMD_PORT}")"

    # Snapshot tlb state — update must never kill TheLinkBox; recover if it vanishes.
    local tlb_was_running=0
    if tlb_is_running; then
        tlb_was_running=1
        ulog "TheLinkBox (tlb) is running before update"
    else
        ulog "TheLinkBox (tlb) is not running before update (will not start it unless it was up)"
    fi

    ulog "Updating web files only (exp.php, tlb-common.inc, tlb-admin/index.php)..."
    ulog "Not touching /home/thelinkbox or stopping tlb"
    update_all_monitor_files

    ulog "Leaving alone — operator-managed (update never installs/overwrites these):"
    ulog "  EventScripts: tlbevent-conference.sh, tlbevent-nopolicy.sh, tlbevent.sh"
    ulog "  Restart:      restart-tlb-conference, restart-tlb-nopolicy, restart-tlb"
    ulog "  tlb.conf EventScript=, tlb-admin/.htaccess / .htpasswd"
    ulog "(Fresh install only offers EventScripts. Copy from package by hand if you want newer ones.)"

    ensure_tlb_still_running_after_update "${tlb_was_running}" || true

    echo
    ulog "Update complete."
    update_print_summary
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    update_main "$@"
fi
