#!/bin/sh
#
# IRL Dashboard — installer for the GL.iNet Mudi 7 (GL-E5800) and other OpenWRT routers
# https://streamhelpers.com/irl-dashboard/
#
# Installs the dashboard as /streaming/ on the router's existing web server, and
# (on the Mudi 7 / aarch64) the SRTLA modem-bonding sender.
#
# Run it straight from the router's terminal:
#     wget -qO- https://streamhelpers.com/irl-dashboard/install.sh | sh
#
# Safe to re-run: it upgrades the app files and never overwrites your settings
# (they live in /etc/irl-dashboard/, outside the web root).
#
# Provided free and AS-IS, with no warranty and no support.
# https://streamhelpers.com/legal.html

set -e

APPDIR="/www/streaming"
BASE="https://streamhelpers.com/irl-dashboard"
SRC="$BASE/irl-dashboard.tar.gz"
BINBASE="$BASE/bin"
SETTINGS_DIR="/etc/irl-dashboard"
FPM_SOCK="/var/run/php8-fpm.sock"

say()  { echo "  $*"; }
step() { echo; echo "==> $*"; }
die()  { echo; echo "!! $*" >&2; exit 1; }

# Append a cron line once (idempotent).
add_cron() {
    EXIST=$(crontab -l 2>/dev/null || true)
    echo "$EXIST" | grep -qF "$1" && return 0
    { [ -n "$EXIST" ] && echo "$EXIST"; echo "$1"; } | crontab -
}

remove_cron() {
    EXIST=$(crontab -l 2>/dev/null || true)
    printf '%s\n' "$EXIST" | grep -vF "$1" | crontab -
}

# ============================================================================
#  --check : say what is working and what is not, in plain English
# ----------------------------------------------------------------------------
#  Run after installing, or any time video is not arriving. It walks the same
#  path the stream takes and reports each step separately, because from the
#  outside every failure looks identical: no video.
# ============================================================================
CHK_OK=0; CHK_WARN=0; CHK_FAIL=0
ok()   { echo "  [ OK ]  $1";                  CHK_OK=$((CHK_OK+1)); }
warn() { echo "  [WARN]  $1"; [ -n "$2" ] && echo "          -> $2"; CHK_WARN=$((CHK_WARN+1)); }
bad()  { echo "  [FAIL]  $1"; [ -n "$2" ] && echo "          -> $2"; CHK_FAIL=$((CHK_FAIL+1)); }

# Read one value out of settings.json without needing a JSON parser.
setting() { sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$SETTINGS_DIR/settings.json" 2>/dev/null | head -1; }

# HTTP helpers. BusyBox wget cannot POST or set headers, so prefer curl and
# only fall back for plain GETs.
http_get() {  # url [bearer]
    if command -v curl >/dev/null 2>&1; then
        [ -n "$2" ] && curl -s -m 10 -H "Authorization: Bearer $2" "$1" || curl -s -m 10 "$1"
    else
        wget -qO- --timeout=10 "$1" 2>/dev/null
    fi
}
http_login() {  # base user pass  -> access token
    command -v curl >/dev/null 2>&1 || return 0
    curl -s -m 10 -X POST -H 'Content-Type: application/json' \
         -d "{\"username\":\"$2\",\"password\":\"$3\"}" "$1/api/login" 2>/dev/null \
    | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p'
}

do_check() {
    echo
    echo "============================================"
    echo " IRL Dashboard — checking your setup"
    echo "============================================"

    echo; echo "The dashboard itself"
    if [ -f "$APPDIR/index.php" ]; then ok "app installed in $APPDIR"
    else bad "the app is missing from $APPDIR" "re-run this installer without --check"; fi

    if pgrep -f php.*fpm >/dev/null 2>&1 || [ -S "$FPM_SOCK" ]; then ok "php-fpm is running"
    else bad "php-fpm is not running" "/etc/init.d/php8-fpm start"; fi

    if grep -q 'location /streaming' /etc/nginx/conf.d/*.conf 2>/dev/null; then ok "nginx serves /streaming/"
    else bad "nginx has no /streaming block" "re-run this installer — a firmware upgrade wipes it"; fi

    IP=$(uci -q get network.lan.ipaddr 2>/dev/null || echo "your-router")
    if http_get "http://127.0.0.1/streaming/index.php" | grep -qi "html"; then
        ok "the dashboard answers at http://$IP/streaming/"
    else warn "could not load the dashboard over http" "open http://$IP/streaming/ in a browser and see what it says"; fi

    if [ -f "$SETTINGS_DIR/settings.json" ]; then ok "your settings are saved (survives firmware upgrades)"
    else warn "nothing configured yet" "open the dashboard — it starts on the Settings screen"; fi

    echo; echo "Background helpers"
    for j in "gps-data.php" "net-logger.php" "youtube-chat-worker.php" "modem-control.php"; do
        if crontab -l 2>/dev/null | grep -q "$j"; then ok "cron runs $j"
        else warn "$j is not in cron" "re-run this installer"; fi
    done
    if /etc/init.d/irl-gps-nmea enabled 2>/dev/null && pidof gps-nmea-watchdog.sh >/dev/null 2>&1; then
        ok "GPS reader service is running"
    else
        warn "GPS reader service is not running" "re-run this installer"
    fi
    if [ -f /etc/sudoers.d/irl-dashboard ]; then ok "privileged readouts permitted"
    else warn "no sudoers entry" "modem, bonding and Bluetooth controls will not work"; fi

    echo; echo "Bonding (SRTLA)"
    if [ -x /usr/bin/srtla_send ]; then ok "srtla_send installed ($(srtla_send -v 2>/dev/null || echo version unknown))"
    else warn "srtla_send is not installed" "bonding needs a Mudi 7 (aarch64); other routers skip it"; fi

    RXHOST=""; RXPORT=""; SRTIN=""
    if [ -f "$SETTINGS_DIR/srtla.conf" ]; then
        . "$SETTINGS_DIR/srtla.conf" 2>/dev/null || true
        RXHOST="$SRTLA_HOST"; RXPORT="$SRTLA_PORT"; SRTIN="$SRT_LISTEN_PORT"
    fi
    if [ -n "$RXHOST" ]; then ok "receiver configured: $RXHOST:$RXPORT"
    else warn "no receiver set" "Bonding tab -> Settings -> Receiver host"; fi

    if pidof srtla_send >/dev/null 2>&1; then
        ok "bonding is running (encoder should send SRT to $IP:${SRTIN:-5000})"
        LOG=/tmp/srtla_send.log
        if [ -f "$LOG" ]; then
            if tail -40 "$LOG" 2>/dev/null | grep -q "registered\|established"; then
                ok "the receiver is answering"
                # Reconnect churn with no media is normal: srtla ignores keepalives
                # when deciding a link is alive, so an idle bond always cycles.
                # A bond carrying video goes quiet, so only mention it if the log
                # is still being written to right now.
                if [ -n "$(find "$LOG" -mmin -1 2>/dev/null)" ] && tail -6 "$LOG" 2>/dev/null | grep -q "connection failed"; then
                    echo "          (it reconnects every few seconds while no video is flowing — that is normal,"
                    echo "           and stops on its own once your encoder is actually sending)"
                fi
            else
                bad "no answer from the receiver" "check srtla_rec is running and UDP $RXPORT is forwarded to it"
            fi
        fi
    else
        warn "bonding is not running" "press Start bonding in the Bonding tab"
    fi

    echo; echo "Where the video ends up"
    RS=$(setting rs_base)
    if [ -z "$RS" ]; then
        warn "no streaming server configured" "Settings -> Restreamer, so this check can see whether video arrives"
    else
        RSU=$(setting rs_user); RSP=$(setting rs_pass)
        TOK=$(http_login "$RS" "$RSU" "$RSP")
        if [ -z "$TOK" ]; then
            bad "cannot log in to $RS" "check the address, username and password under Settings -> Restreamer"
        else
            ok "logged in to your streaming server"
            ING=$(http_get "$RS/api/v3/process" "$TOK" \
                  | tr ',' '\n' | grep -oE 'restreamer-ui:ingest:[a-f0-9-]+' | grep -v _snapshot | head -1 | sed 's/.*://')
            if [ -z "$ING" ]; then
                warn "no ingest channel found" "create one in Restreamer — it is what your encoder publishes into"
            else
                ok "publish this stream ID from your encoder: $ING.stream,mode:publish"
                ST=$(http_get "$RS/api/v3/process/restreamer-ui:ingest:$ING?filter=state" "$TOK")
                if echo "$ST" | grep -q '"exec":"running"'; then
                    ok "VIDEO IS ARRIVING at your streaming server"
                else
                    bad "no video arriving yet" "start the SRT target on your encoder, and make sure its Stream ID is exactly the line above"
                fi
            fi
        fi
    fi

    echo
    echo "============================================"
    echo " $CHK_OK ok, $CHK_WARN warnings, $CHK_FAIL problems"
    echo "============================================"
    [ "$CHK_FAIL" -gt 0 ] && return 1
    return 0
}

case "${1:-}" in
    --check|check|-c) set +e; do_check; exit $? ;;
esac

echo
echo "============================================"
echo " IRL Dashboard installer"
echo "============================================"

# ---------------------------------------------------------------- checks ----
[ "$(id -u)" = "0" ] || die "Run this as root (you should already be, on OpenWRT)."
command -v opkg >/dev/null 2>&1 || die "opkg not found. This installer is for OpenWRT / GL.iNet routers."

FREE_KB=$(df -k /overlay 2>/dev/null | awk 'NR==2{print $4}' || echo 0)
[ -z "$FREE_KB" ] && FREE_KB=0
if [ "$FREE_KB" -gt 0 ] && [ "$FREE_KB" -lt 8000 ]; then
    say "WARNING: only ${FREE_KB}KB free on /overlay. You need roughly 8MB."
    say "Continuing anyway — if opkg fails, free some space and re-run."
fi

# ------------------------------------------------------------- packages ----
step "Installing packages (this is the slow part)"
opkg update >/dev/null 2>&1 || die "opkg update failed. Is the router online?"

# nginx is deliberately NOT in this list. It ships with the router firmware and
# serves the admin UI, and the vendor build carries modules the generic feed
# package does not (GL.iNet compiles in lua, which gl.conf depends on).
# Installing "nginx" here would swap that build out and kill the admin UI.
command -v nginx >/dev/null 2>&1 \
    || die "nginx not found. This installer uses the router's own nginx - the one serving its admin UI."

# No php8-mod-json here: JSON stopped being an optional extension in PHP 8.0
# and is always compiled into core, so no such package exists. It is checked
# for below rather than assumed.
for p in php8 php8-fpm php8-cgi php8-mod-curl php8-mod-filter \
         php8-mod-mbstring php8-mod-ctype php8-mod-session php8-mod-tokenizer; do
    if opkg list-installed 2>/dev/null | grep -q "^$p "; then
        say "$p already installed"
    else
        say "installing $p"
        opkg install "$p" >/dev/null 2>&1 || say "  (skipped $p — not in this firmware's feed)"
    fi
done

# USB-serial bridge drivers so USB GNSS pucks that aren't native-USB u-blox still
# enumerate (cp210x and ch34x pucks work well; ftdi too). Best-effort — skipped if a
# driver isn't in this firmware's feed. NOTE: Prolific PL2303 *GC* pucks (e.g. GlobalSat
# BU-353N5) enumerate but pass no data on these kernels — prefer native-USB u-blox.
for p in kmod-usb-serial kmod-usb-serial-cp210x kmod-usb-serial-ch341 kmod-usb-serial-ftdi kmod-usb-serial-pl2303; do
    opkg list-installed 2>/dev/null | grep -q "^$p " || opkg install "$p" >/dev/null 2>&1 || true
done
# gpsd: the robust GPS reader. It drives ANY receiver (baud probing, u-blox binary,
# SiRF, and finicky Prolific PL2303 pucks like the GlobalSat BU-353N5 that a raw
# serial read can't coax data out of); gps-nmea.php parses the NMEA it relays and
# falls back to raw serial if gpsd isn't present. Best-effort.
for p in gpsd gpsd-clients; do
    opkg list-installed 2>/dev/null | grep -q "^$p " || opkg install "$p" >/dev/null 2>&1 || true
done
# The dashboard's GPS worker starts gpsd itself (dynamically, against whatever USB
# receiver is present, with -n -b). So disable OpenWrt's own gpsd service — otherwise
# it would start a second gpsd from /etc/config/gpsd and the two fight over the port.
if [ -f /etc/init.d/gpsd ]; then
    /etc/init.d/gpsd disable 2>/dev/null || true
    /etc/init.d/gpsd stop 2>/dev/null || true
fi

# The dashboard is unusable without JSON. Confirm it rather than assume it.
PHPBIN=""
for b in php-cgi php-fcgi php8-cgi php-cli php8 php; do
    command -v "$b" >/dev/null 2>&1 && { PHPBIN=$(command -v "$b"); break; }
done
if [ -n "$PHPBIN" ]; then
    if "$PHPBIN" -m 2>/dev/null | grep -qi '^json$'; then
        say "PHP JSON support: present"
    else
        say "WARNING: could not confirm PHP JSON support via $PHPBIN."
        say "         The dashboard needs it. If pages come up blank, that is why."
    fi
else
    say "note: no PHP binary on PATH to query - skipping the JSON check"
fi

# ----------------------------------------------------------- app files ----
step "Downloading the dashboard"
TMP="/tmp/irl-dashboard.tar.gz"
rm -f "$TMP"
# Append a cache-buster: CDNs happily serve a stale tarball for hours, which
# silently installs old code and is very hard to spot. Fall back to the plain
# URL if the query string upsets anything, and show wget's own error either
# way - a swallowed error message tells you nothing.
DL_OK=""
for url in "$SRC?v=$(date +%s)" "$SRC"; do
    if ERR=$(wget -O "$TMP" "$url" 2>&1); then
        DL_OK=1
        break
    fi
    say "download attempt failed: $url"
    echo "$ERR" | sed 's/^/      /'
done

if [ -z "$DL_OK" ]; then
    echo >&2
    echo "!! Check the router is online:  ping -c2 streamhelpers.com" >&2
    die "Download failed: $SRC"
fi

# A CDN challenge page or a 404 arrives as a perfectly readable file that
# simply is not an archive. Catch it here, not as a confusing tar error.
[ -s "$TMP" ] || die "The server returned an empty file."
if ! gzip -t "$TMP" 2>/dev/null; then
    say "what came back was not a gzip archive - it starts with:"
    head -c 200 "$TMP" | sed 's/^/      /'
    echo
    die "Download was not a valid archive (an error or challenge page?)."
fi
mkdir -p "$APPDIR"

tar -xzf "$TMP" -C "$APPDIR" || die "Could not extract the archive."
rm -f "$TMP"

# Refuse to run on a stale archive. A CDN can serve an old tarball for hours,
# which used to install superseded code silently - by far the most confusing
# failure this installer had. Fail loudly instead.
MISSING=""
for f in index.php config.php auth.php settings.php srtla.php; do
    [ -f "$APPDIR/$f" ] || MISSING="$MISSING $f"
done
if [ -n "$MISSING" ]; then
    echo >&2
    echo "!! The downloaded archive is out of date - it is missing:$MISSING" >&2
    echo "!! Your CDN or proxy is serving an old copy of:" >&2
    echo "!!   $SRC" >&2
    echo "!!" >&2
    echo "!! Purge the cache for /irl-dashboard/* and run this installer again." >&2
    die "Refusing to install a stale version."
fi

# Settings live in $SETTINGS_DIR, outside the web root, so an upgrade never
# touches them and no web server rule is needed to keep them private.
mkdir -p "$SETTINGS_DIR"
if [ -f "$APPDIR/settings.json" ] && [ ! -f "$SETTINGS_DIR/settings.json" ]; then
    mv "$APPDIR/settings.json" "$SETTINGS_DIR/settings.json"
    say "moved your settings out of the web root into $SETTINGS_DIR"
fi
rm -f "$APPDIR/settings.json"

if [ -f "$SETTINGS_DIR/settings.json" ]; then
    say "kept your existing settings"
else
    say "not configured yet - you'll do that in the browser, not a text editor"
fi

# ------------------------------------------------------- writable state ----
step "Setting permissions"
# The dashboard writes small JSON state files next to itself.
for f in button_states.json timer_state.json streaming_bitrate.json active_stream.json; do
    [ -f "$APPDIR/$f" ] || echo '{}' > "$APPDIR/$f"
done
# Created at runtime by the Director Mini login; seed it so it is owned and
# permissioned like the rest rather than by whichever process gets there first.
[ -f "$APPDIR/cookie.txt" ] || : > "$APPDIR/cookie.txt"
FPM_USER=$(awk -F'=' '/^[[:space:]]*user[[:space:]]*=/{gsub(/ /,"",$2); print $2; exit}' \
           /etc/php8-fpm.d/www.conf 2>/dev/null || true)
[ -z "$FPM_USER" ] && FPM_USER="nobody"
say "php-fpm runs as: $FPM_USER"
# The Settings screen writes settings.json here, so the DIRECTORY itself has
# to be writable by php-fpm, not just the files already in it.
chown -R "$FPM_USER" "$APPDIR" 2>/dev/null || true
# These cron jobs run as root and write state files next to the app. Without a
# setgid directory and a group-writable umask, root recreates a file as
# root:root 0644 and php-fpm can never write it again - the dashboard then
# fails silently (stale timer, unrefreshable login cookie) until someone
# notices. Give the directory the web user's group and hand new files that
# group, so either side can still write what the other created.
FPM_GROUP=$(awk -F: -v u="$FPM_USER" '$1==u{print $4}' /etc/passwd 2>/dev/null             | while read -r g; do awk -F: -v g="$g" '$3==g{print $1}' /etc/group; done)
[ -z "$FPM_GROUP" ] && FPM_GROUP="$FPM_USER"
chgrp -R "$FPM_GROUP" "$APPDIR" 2>/dev/null || true
chmod 2775 "$APPDIR"
chmod 664 "$APPDIR"/*.json 2>/dev/null || true
[ -f "$APPDIR/cookie.txt" ] && chmod 664 "$APPDIR/cookie.txt"
say "state files group: $FPM_GROUP (setgid on $APPDIR)"

# settings.json holds API keys and the login hash. It sits outside the web
# root and is readable only by the web user.
chown -R "$FPM_USER" "$SETTINGS_DIR" 2>/dev/null || true
chmod 700 "$SETTINGS_DIR"
[ -f "$SETTINGS_DIR/settings.json" ] && chmod 600 "$SETTINGS_DIR/settings.json"

# --------------------------------------------------- SRTLA modem bonding ----
# The bonding sender (srtla_send) is the one architecture-specific piece. It is
# built for the Mudi 7 (GL-E5800, aarch64). On other hardware we skip it: the
# dashboard still installs and runs, the Bonding tab just shows it as absent.
step "Setting up SRTLA modem bonding"
ARCH=$(uname -m 2>/dev/null || echo unknown)
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
    BOND_OK=1
    for pair in "srtla_send-aarch64:/usr/bin/srtla_send" \
                "daemonize-aarch64:/usr/bin/daemonize" \
                "moblink-streamer-aarch64:/usr/bin/moblink-streamer" \
    "irl-ffmpeg-aarch64:/usr/bin/irl-ffmpeg" \
    "srt-live-transmit-aarch64:/usr/bin/srt-live-transmit" \
                "srtla-bond.sh:/usr/bin/srtla-bond.sh"; do
        src=${pair%%:*}; dst=${pair#*:}
        if wget -qO "${dst}.new" "$BINBASE/${src}?v=$(date +%s)" && [ -s "${dst}.new" ]; then
            mv "${dst}.new" "$dst"; chmod +x "$dst"; say "installed $(basename "$dst")"
        else
            rm -f "${dst}.new"; BOND_OK=""; say "WARNING: could not download $src"
        fi
    done
    if [ -n "$BOND_OK" ]; then
        # Persist enlarged UDP socket buffers so a brief stall doesn't overflow the
        # receive queue and drop packets before srtla_send sees them.
        cat > /etc/sysctl.d/99-srtla.conf <<'EOS'
net.core.rmem_max=16777216
net.core.rmem_default=8388608
net.core.wmem_max=16777216
net.core.wmem_default=4194304
EOS
        sysctl -p /etc/sysctl.d/99-srtla.conf >/dev/null 2>&1 || true
        # Watchdog keeps bonding alive and restarts it after a reboot if you left it on.
        add_cron '* * * * * /usr/bin/srtla-bond.sh watchdog >/dev/null 2>&1'
        say "bonding installed — turn it on in the dashboard's Bonding tab, once you've set your receiver"
    else
        say "bonding download incomplete — re-run the installer once the router is online"
    fi
else
    say "note: the bonding sender is built for the Mudi 7 (aarch64); this router is '$ARCH'."
    say "      skipping it — the dashboard still works, the Bonding tab just shows it as not installed."
fi

# Background helpers: GPS-to-server sender, the connection logger and the
# YouTube chat collector. All are harmless no-ops until you enable the matching
# feature in Settings. Niced so
# they never compete with the realtime bonding sender.
if [ -n "$PHPBIN" ]; then
    add_cron "* * * * * umask 002 && nice -n 19 $PHPBIN -f $APPDIR/gps-data.php cron >/dev/null 2>&1"
    add_cron "* * * * * umask 002 && nice -n 19 $PHPBIN -f $APPDIR/net-logger.php log >/dev/null 2>&1"
    # The chat collector runs for just under a minute and is restarted each
    # minute, so one crash costs 60 seconds and never needs a service manager.
    add_cron "* * * * * umask 002 && nice -n 19 $PHPBIN -f $APPDIR/youtube-chat-worker.php run >/dev/null 2>&1"
    # A USB GNSS receiver (if one is plugged in) is read continuously by this
    # worker, which runs for just under a minute and is restarted each minute.
    # It does nothing when no receiver is present, so it is always safe to add.
    remove_cron "$APPDIR/gps-nmea.php run"
    # If you pin the modem to a band, GL's own cellular manager will quietly put
    # the full band list back some minutes later. This re-applies your choice.
    # It does nothing at all unless a band is actually pinned.
    add_cron "* * * * * umask 002 && nice -n 19 $PHPBIN -f $APPDIR/modem-control.php enforce >/dev/null 2>&1"
fi
/etc/init.d/cron enable  >/dev/null 2>&1 || true
/etc/init.d/cron start   >/dev/null 2>&1 || true

# --------------------------------------------- survive firmware upgrades ----
# A router firmware upgrade replaces /www and any /etc directory it does not
# know about, which would take your API keys and tokens with it. OpenWrt keeps
# whatever is listed in /etc/sysupgrade.conf, so the settings survive and only
# the app itself needs re-installing (re-run this script) after an upgrade.
if [ -d /etc ] && ! grep -qs '^/etc/irl-dashboard/' /etc/sysupgrade.conf 2>/dev/null; then
    step "Keeping your settings across firmware upgrades"
    echo '/etc/irl-dashboard/' >> /etc/sysupgrade.conf
    say "added /etc/irl-dashboard/ to /etc/sysupgrade.conf"
    say "after a firmware upgrade, re-run this installer - your settings will still be there"
fi

# ------------------------------------------------ bluetooth (DJI camera) ----
# OpenWrt has no Bluetooth stack out of the box, so a BLE dongle does nothing
# until these are installed. Only done when an adapter is actually plugged in —
# USB class E0 is "wireless controller", which is what a BLE dongle reports.
BT_PKGS="kmod-bluetooth bluez-libs bluez-utils"
bt_adapter_present() {
    for f in /sys/bus/usb/devices/*/bDeviceClass /sys/bus/usb/devices/*/*/bInterfaceClass; do
        [ -f "$f" ] || continue
        [ "$(cat "$f" 2>/dev/null)" = "e0" ] && return 0
    done
    return 1
}
if command -v hciconfig >/dev/null 2>&1; then
    say "Bluetooth support already installed"
elif bt_adapter_present; then
    step "Installing Bluetooth support for the plugged-in adapter"
    opkg update >/dev/null 2>&1 || true
    if opkg install $BT_PKGS >/dev/null 2>&1; then
        hciconfig hci0 up >/dev/null 2>&1 || true
        say "Bluetooth ready - the DJI tab can now find your camera"
    else
        say "could not install $BT_PKGS - the DJI tab offers a retry button"
    fi
else
    say "no Bluetooth adapter plugged in - skipping the Bluetooth stack"
    say "  (plug one in later and the DJI tab will offer to install it for you)"
fi

# djictl is what actually talks to the camera - a single static binary, no
# runtime of its own. It is a build of xaionaro-go/djictl (CC0 / public domain).
if [ "$ARCH" = "aarch64" ]; then
    step "Installing the DJI camera control tool"
    if wget -qO /usr/bin/djictl.new "$BINBASE/djictl-aarch64?v=$(date +%s)" && [ -s /usr/bin/djictl.new ]; then
        mv /usr/bin/djictl.new /usr/bin/djictl; chmod +x /usr/bin/djictl
        say "installed djictl"
    else
        rm -f /usr/bin/djictl.new
        say "WARNING: could not download djictl - the DJI tab will say so"
    fi
fi

# ------------------------------------- router + modem + bonding readouts ----
# A few binaries need root for their tabs. Each is granted individually, never
# a blanket rule:
#   gl_modem        - band locking, rescan, modem reboot (Modem tab)
#   ubus            - CPU temp / battery, network status (Router + Network tabs)
#   dmesg           - USB power / enumeration warnings (Network tab)
#   srtla-bond.sh   - start/stop bonding, source routing (Bonding tab)
#   uci -q show wireless - your own hotspot names/keys (Network tab). Read-only:
#                     this ONE command is granted, not uci in general.
SUDO_CMDS=""
add_sudo() { [ -x "$1" ] && { [ -n "$SUDO_CMDS" ] && SUDO_CMDS="$SUDO_CMDS, $1" || SUDO_CMDS="$1"; }; }
add_sudo_args() { [ -x "$1" ] && { [ -n "$SUDO_CMDS" ] && SUDO_CMDS="$SUDO_CMDS, $1 $2" || SUDO_CMDS="$1 $2"; }; }
# The TTL helper is a small shell script (any arch): it reads and, on request,
# sets the built-in modem's egress TTL to 65 so cellular traffic looks phone-
# native. Downloaded here so the Network tab's TTL check works.
# dji-golive.sh: robust "make the DJI camera go live" wrapper — resets the BLE
# adapter and auto-retries (a single djictl attempt to a DJI camera is flaky), and
# confirms real video before declaring success. Runs as the web user; it needs no
# sudoers entry of its own since it calls the already-granted djictl/hciconfig/killall.
for h in irl-ttl.sh irl-venue.sh dji-golive.sh irl-repeater-move.sh gps-nmea-watchdog.sh irl-compact-mode.sh; do
    if wget -qO "/usr/bin/$h.new" "$BINBASE/$h?v=$(date +%s)" && [ -s "/usr/bin/$h.new" ]; then
        mv "/usr/bin/$h.new" "/usr/bin/$h"; chmod +x "/usr/bin/$h"
    else
        rm -f "/usr/bin/$h.new"
    fi
done

wget -qO /etc/init.d/irl-gps-nmea "$BINBASE/irl-gps-nmea?v=$(date +%s)" \
    || die "Could not download the GPS reader service."
chmod 0755 /etc/init.d/irl-gps-nmea
/etc/init.d/irl-gps-nmea enable
/etc/init.d/irl-gps-nmea restart

add_sudo /usr/bin/gl_modem
add_sudo /bin/ubus
add_sudo /bin/dmesg
add_sudo /usr/bin/srtla-bond.sh
add_sudo /usr/bin/irl-ttl.sh
add_sudo /usr/bin/irl-venue.sh
add_sudo_args /sbin/uci "-q show wireless"
# Granted whether or not they exist yet: the Bluetooth stack and djictl can be
# installed after this script runs, and a sudoers entry for a missing file is
# harmless. Without this, installing Bluetooth from the dashboard would work but
# the adapter could not then be brought up.
SUDO_CMDS="$SUDO_CMDS, /usr/bin/hciconfig, /usr/bin/djictl"
# djictl ignores SIGTERM and never stops scanning on its own, so the dashboard
# has to kill it - otherwise it keeps the BLE adapter open and every later scan
# fails with "device or resource busy". Only that one process may be killed.
SUDO_CMDS="$SUDO_CMDS, /usr/bin/killall -9 djictl"
SUDO_CMDS="$SUDO_CMDS, /bin/opkg update, /bin/opkg install $BT_PKGS"

if [ -n "$SUDO_CMDS" ]; then
    step "Enabling privileged readouts / controls"
    if ! command -v sudo >/dev/null 2>&1; then
        say "installing sudo (needed for those commands)"
        opkg install sudo >/dev/null 2>&1 || say "  could not install sudo - those tabs will show as unavailable"
    fi
    mkdir -p /etc/sudoers.d
    cat > /etc/sudoers.d/irl-dashboard <<EOF
$FPM_USER ALL=(ALL) NOPASSWD: $SUDO_CMDS
Defaults:$FPM_USER !requiretty
EOF
    chmod 440 /etc/sudoers.d/irl-dashboard
    say "permitted for $FPM_USER: $SUDO_CMDS"
else
    say "none of gl_modem / ubus / dmesg / srtla-bond.sh present - those tabs stay inactive (fine)"
fi

# ------------------------------------------------------------- nginx ------
step "Configuring nginx"
if [ ! -S "$FPM_SOCK" ]; then
    ALT=$(ls /var/run/php*fpm*.sock 2>/dev/null | head -1 || true)
    [ -n "$ALT" ] && FPM_SOCK="$ALT"
fi
say "php-fpm socket: $FPM_SOCK"

# Clear anything an older version of this installer left behind. It used to
# add a second server{} on its own port, which could pass `nginx -t` and then
# fail to BIND that port at startup - taking nginx, and the router's admin
# UI, down with it. We now add a location to the router's existing server
# block instead, on the port it already listens on.
rm -f /etc/nginx/conf.d/irl-dashboard.conf

# Find the file that actually holds the server block (gl.conf on GL.iNet).
GL_CONF=""
for c in /etc/nginx/conf.d/*.conf; do
    [ -f "$c" ] || continue
    if grep -qE '^[[:space:]]*server[[:space:]]*\{' "$c"; then GL_CONF="$c"; break; fi
done
[ -n "$GL_CONF" ] || die "No nginx server block found in /etc/nginx/conf.d/. Nothing was changed."
say "adding a location block to $GL_CONF"

# Keep one pristine copy of the original, made only on the very first run.
BACKUP="$GL_CONF.irl-dashboard.orig"
[ -f "$BACKUP" ] || cp "$GL_CONF" "$BACKUP"

APP_PARENT=$(dirname "$APPDIR")
APP_NAME=$(basename "$APPDIR")

# Use the distro's fastcgi_params when it exists; spell them out when it doesn't.
if [ -f /etc/nginx/fastcgi_params ]; then
    FCGI="            include fastcgi_params;"
else
    FCGI=$(cat <<'EOP'
            fastcgi_param  QUERY_STRING       $query_string;
            fastcgi_param  REQUEST_METHOD     $request_method;
            fastcgi_param  CONTENT_TYPE       $content_type;
            fastcgi_param  CONTENT_LENGTH     $content_length;
            fastcgi_param  REQUEST_URI        $request_uri;
            fastcgi_param  DOCUMENT_URI       $document_uri;
            fastcgi_param  DOCUMENT_ROOT      $document_root;
            fastcgi_param  SERVER_PROTOCOL    $server_protocol;
            fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
            fastcgi_param  SERVER_SOFTWARE    nginx;
            fastcgi_param  REMOTE_ADDR        $remote_addr;
            fastcgi_param  REMOTE_PORT        $remote_port;
            fastcgi_param  SERVER_ADDR        $server_addr;
            fastcgi_param  SERVER_PORT        $server_port;
            fastcgi_param  SERVER_NAME        $server_name;
            fastcgi_param  REDIRECT_STATUS    200;
EOP
)
fi

BLOCKFILE=/tmp/irl-dashboard-block.conf
cat > "$BLOCKFILE" <<EOF

    # >>> irl-dashboard >>>  managed by the installer; edits here are replaced
    location /$APP_NAME/ {
        root $APP_PARENT;
        index index.php index.html index.htm;

        try_files \$uri \$uri/ /$APP_NAME/index.php?\$query_string;

        location ~ ^/$APP_NAME/settings\.json\$ {
            return 404;
        }

        location ~ \.php\$ {
            fastcgi_pass unix:$FPM_SOCK;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $APP_PARENT\$fastcgi_script_name;
            fastcgi_param SCRIPT_NAME \$fastcgi_script_name;
$FCGI
        }
    }
    # <<< irl-dashboard <<<
EOF

# Drop any block we added before, then insert a fresh one right after the
# opening "server {" line. Reading the block from a file (rather than passing
# it through awk -v) keeps backslashes in the regex locations intact.
STRIPPED=/tmp/irl-gl-stripped.conf
MERGED=/tmp/irl-gl-merged.conf
sed '/# >>> irl-dashboard >>>/,/# <<< irl-dashboard <<</d' "$GL_CONF" > "$STRIPPED"

if ! awk -v bf="$BLOCKFILE" '
      !ins && /^[[:space:]]*server[[:space:]]*\{/ {
          print
          while ((getline line < bf) > 0) print line
          close(bf)
          ins = 1
          next
      }
      { print }
      END { if (!ins) exit 1 }
  ' "$STRIPPED" > "$MERGED"; then
    rm -f "$STRIPPED" "$MERGED" "$BLOCKFILE"
    die "Could not find a 'server {' line in $GL_CONF. Nothing was changed."
fi

cp "$MERGED" "$GL_CONF"
rm -f "$STRIPPED" "$MERGED" "$BLOCKFILE"

# ---- validate, then verify nginx actually COMES BACK UP, then commit ------
restore_and_die() {
    cp "$BACKUP" "$GL_CONF"
    /etc/init.d/nginx restart >/dev/null 2>&1 || true
    sleep 2
    if pgrep nginx >/dev/null 2>&1; then
        echo "!! Your original $GL_CONF is restored and nginx is running again." >&2
    else
        echo "!! Your original $GL_CONF is restored, but nginx is still down." >&2
        echo "!! Run: /etc/init.d/nginx restart" >&2
    fi
    die "$1"
}

step "Testing nginx"
if ! NGX_OUT=$(nginx -t 2>&1); then
    echo >&2
    echo "$NGX_OUT" | sed 's/^/    /' >&2
    echo >&2
    restore_and_die "nginx rejected the change."
fi

# --------------------------------------------------------- fpm capacity ----
# OpenWRT ships pm.max_children = 5. The dashboard polls roughly ten endpoints
# every couple of seconds, so if the uplink dies and a handful of those calls
# sit waiting on a timeout, all five workers are held and the WHOLE dashboard
# stops answering - including pages that never touch the network. Give it
# enough workers to stay usable locally when the internet is gone, and cap how
# long any single request may hold one.
FPMC="/etc/php8-fpm.d/www.conf"
if [ -f "$FPMC" ]; then
    CUR=$(awk -F'=' '/^[[:space:]]*pm.max_children/{gsub(/ /,"",$2); print $2; exit}' "$FPMC" 2>/dev/null)
    if [ -n "$CUR" ] && [ "$CUR" -lt 12 ] 2>/dev/null; then
        cp "$FPMC" "$FPMC.irl-dashboard.orig" 2>/dev/null || true
        sed -i 's/^pm.max_children.*/pm.max_children = 12/; s/^pm.start_servers.*/pm.start_servers = 3/; s/^pm.min_spare_servers.*/pm.min_spare_servers = 2/; s/^pm.max_spare_servers.*/pm.max_spare_servers = 6/' "$FPMC"
        say "raised php-fpm workers from $CUR to 12 (was enough to stall the whole UI)"
    fi
    grep -q "^request_terminate_timeout" "$FPMC" || echo "request_terminate_timeout = 30s" >> "$FPMC"
fi

step "Restarting services"
/etc/init.d/php8-fpm restart >/dev/null 2>&1 || say "could not restart php8-fpm"
/etc/init.d/nginx restart >/dev/null 2>&1 || true
sleep 2
# A passing 'nginx -t' does NOT mean nginx will start - a port it cannot bind
# will kill it at runtime. Check for a live process before declaring success.
if ! pgrep nginx >/dev/null 2>&1; then
    restore_and_die "nginx passed its config test but did not come back up."
fi
say "nginx is running"

IP=$(uci get network.lan.ipaddr 2>/dev/null || ip -4 addr show br-lan 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2); print $2; exit}')
[ -z "$IP" ] && IP="YOUR-ROUTER-IP"

echo
echo "============================================"
echo " Installed."
echo "============================================"
echo
echo "  Open this in your browser and set it up there:"
echo
echo "       http://$IP/$(basename "$APPDIR")/"
echo
echo "  The first visit lands on the Settings screen. Every field is optional -"
echo "  set your Router IP, press Save, add the rest whenever you like."
echo "  No SSH and no text editor needed after this point."
echo
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
echo "  Modem bonding: open the Bonding tab, set your SRTLA receiver, press Start."
echo "  Receiver setup:  https://streamhelpers.com/irl-dashboard/receiver/"
echo
fi
echo "  Guide:  https://streamhelpers.com/irl-dashboard/"
echo
echo "  There is no login until you set one under Access in Settings."
echo "  Until then, keep this router off any untrusted network."
echo "  Free and as-is: no warranty, no support. Re-run this script to update."
echo
