#!/bin/bash # DisplayHUB Controller Installer — writes installation.json where server.js expects it set -euo pipefail REMOTE_BASE="${REMOTE_BASE:-https://displayhub.us/clients/api/install-controller-roku}" PRODUCT_NAME="${PRODUCT_NAME:-DisplayHUB}" API_BASE_URL="${API_BASE_URL:-https://displayhub.us/clients/api}" ORIGINAL_USER=${SUDO_USER:-$(logname 2>/dev/null || echo "pi")} RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' log(){ echo -e "${GREEN}[$(date +'%H:%M:%S')]${NC} $*"; } warn(){ echo -e "${YELLOW}[WARN]${NC} $*"; } err(){ echo -e "${RED}[ERROR]${NC} $*"; exit 1; } [ "$EUID" -eq 0 ] || err "Run as root (sudo)." echo; echo " ${PRODUCT_NAME} Controller Installer"; echo mkdir -p /tmp; chattr -i /tmp 2>/dev/null || true; chown root:root /tmp || true; chmod 1777 /tmp || true log "Installing prerequisites..." apt-get update -qq 2>&1 | grep -v "^Reading" || true DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl jq ca-certificates unzip avahi-daemon avahi-utils libcap2-bin || true id -u displayhub >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin -d /nonexistent displayhub || true if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then log "Installing Node.js (v18)..." curl -fsSL https://deb.nodesource.com/setup_18.x | bash - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nodejs || err "Failed to install Node.js" fi INSTALL_DIR="/opt/displayhub-controller" APP_DIR="$INSTALL_DIR/app" LOG_DIR="/var/log/displayhub-controller" ETC_DIR="/etc/displayhub-controller" UNIT="/etc/systemd/system/displayhub-controller.service" log "Preparing filesystem layout..." mkdir -p "$INSTALL_DIR" "$APP_DIR" "$LOG_DIR" "$ETC_DIR" "$APP_DIR/config" "/etc/displayhub" touch "$LOG_DIR/server.log" "$LOG_DIR/error.log" chown -R displayhub:displayhub "$INSTALL_DIR" "$LOG_DIR" "$ETC_DIR" 2>/dev/null || true chmod 755 "$INSTALL_DIR" || true; chmod 664 "$LOG_DIR/"*.log || true REMOTE_VERSION="$(curl -fsSL "${REMOTE_BASE}/version.txt" | tr -d '\r\n' || echo "1.0.0-controller")" DEVICE_ID="$(grep -m1 -i '^Serial' /proc/cpuinfo 2>/dev/null | awk '{print $3}' | sha256sum | awk '{print $1}' | cut -c1-16 || echo unknown)" IP_ADDR="$(hostname -I 2>/dev/null | awk '{print $1}' || echo 127.0.0.1)" ROKU_APP_ID="${ROKU_APP_ID:-dev}" PRODUCT_SLUG="$(echo "$PRODUCT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/-\+/-/g; s/^-//; s/-$//' | cut -c1-63)" [ -z "$PRODUCT_SLUG" ] && PRODUCT_SLUG="displayhub" DESIRED_HOST="$PRODUCT_SLUG" CUR_HOST="$(hostnamectl --static 2>/dev/null || hostname)" if [ "$DESIRED_HOST" != "$CUR_HOST" ]; then log "Setting hostname -> ${DESIRED_HOST}" hostnamectl set-hostname "$DESIRED_HOST" || err "Failed to set hostname" if grep -qE '^127\.0\.1\.1\s' /etc/hosts; then sed -i "s/^127\.0\.1\.1.*/127.0.1.1\t${DESIRED_HOST}/" /etc/hosts else echo -e "127.0.1.1\t${DESIRED_HOST}" >> /etc/hosts fi systemctl enable avahi-daemon >/dev/null 2>&1 || true systemctl restart avahi-daemon || true fi log "Fetching manifest (files.json)..." MAN="$(mktemp)" # Always append a timestamp to bypass CDN / curl caching curl -fsSL "${REMOTE_BASE}/files.json?d=$(date +%s)" -o "$MAN" || err "Couldn't download files.json" install_one() { local src="$1" dest="$2" mode="$3" owner="$4" group="$5" mkdir -p "$(dirname "$dest")" local tmp="$(mktemp)" local bust="$(date +%s)" local sep='?' [[ "$src" == *'?'* ]] && sep='&' # Ensure each fetch is unique even if multiple files share name curl -fsSL "${REMOTE_BASE}/${src}${sep}d=${bust}" -o "$tmp" || err "Failed to fetch ${src}" install -m "$mode" -o "$owner" -g "$group" "$tmp" "$dest" } log "Stopping controller (if running)..." systemctl stop displayhub-controller.service 2>/dev/null || true # Clean out any stale public copies so browser never serves old bundle rm -rf "$APP_DIR/public" "$APP_DIR/public_old" "$APP_DIR/public-backup" 2>/dev/null || true log "Syncing files from server..." len=$(jq 'length' "$MAN") for ((i=0; i/dev/null 2>&1) || (cd "$APP_DIR" && npm install --only=prod) # Ensure axios/xml2js are present even if package.json didn’t include them (cd "$APP_DIR" && npm ls axios >/dev/null 2>&1) || (cd "$APP_DIR" && npm install --no-save axios) (cd "$APP_DIR" && npm ls xml2js >/dev/null 2>&1) || (cd "$APP_DIR" && npm install --no-save xml2js) chown -R displayhub:displayhub "$APP_DIR" || true fi HTTP_PORT=80 if ! setcap 'cap_net_bind_service=+ep' /usr/bin/node 2>/dev/null; then warn "setcap failed; using port 3000" HTTP_PORT=3000 fi # WRITE THE FILES server.js EXPECTS (installation.json) CFG_ETC_ROKU="/etc/displayhub/installation.json" CFG_LOCAL_INSTALL="$APP_DIR/config/installation.json" CONFIG_PAYLOAD="$(cat < "$CFG_ETC_ROKU" chown displayhub:displayhub "$CFG_ETC_ROKU" || true chmod 664 "$CFG_ETC_ROKU" || true log "Writing ${CFG_LOCAL_INSTALL}" echo "${CONFIG_PAYLOAD}" > "$CFG_LOCAL_INSTALL" chown displayhub:displayhub "$CFG_LOCAL_INSTALL" || true chmod 664 "$CFG_LOCAL_INSTALL" || true # Unit with preflight check for installation.json at either location UNIT="/etc/systemd/system/displayhub-controller.service" cat > "$UNIT" </dev/null systemctl restart displayhub-controller.service || true sleep 2 log "Health check..." if ! systemctl is-active --quiet displayhub-controller.service; then journalctl -u displayhub-controller -n 120 --no-pager || true err "Service failed to start." fi URL="http://127.0.0.1:${HTTP_PORT}/" if ! curl -fsS --max-time 4 "$URL" >/dev/null; then journalctl -u displayhub-controller -n 120 --no-pager || true err "HTTP probe failed at ${URL}" fi # --------------------------------------------- # Compute local access URL # --------------------------------------------- LOCAL_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" HOSTNAME_URL="$(hostnamectl --static 2>/dev/null || hostname)" if [[ -n "$HOSTNAME_URL" ]]; then # Prefer mDNS-style .local hostname if available URL="http://${HOSTNAME_URL}.local/" else URL="http://${LOCAL_IP:-127.0.0.1}/" fi echo echo "=========================================" echo " Installation Complete" echo "=========================================" echo " Product: $PRODUCT_NAME Controller" echo " Version: $REMOTE_VERSION" echo " Hostname: $(hostnamectl --static 2>/dev/null || hostname)" echo " Web UI: ${URL}" echo "=========================================" echo