Files
pymc_console-dist/manage.sh
GitHub Actions Bot 561cb8e452 Sync build v0.9.170
Automated sync from private repository.
Commit: 01f98cc4202f952932a0d3db2640d5fe266669a7
2026-01-21 23:12:37 +00:00

657 lines
27 KiB
Bash
Executable File

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════════════════
# pyMC Console - Dashboard Overlay Manager
# ═══════════════════════════════════════════════════════════════════════════════
#
# ARCHITECTURE: Thin wrapper around pyMC_Repeater's native installer.
# We don't duplicate upstream's functionality - we extend it with our dashboard.
#
# WHAT WE DO:
# • Clone/update pyMC_Repeater (to access their manage.sh)
# • Call upstream's manage.sh for install/upgrade/uninstall
# • Overlay our React dashboard after upstream completes
# • Respect user's UI preference (don't force our dashboard on upgrades)
#
# WHAT UPSTREAM DOES:
# • User creation, directories, dependencies
# • pip install (pymc_repeater, pymc_core)
# • Service file, systemd management
# • Radio/GPIO configuration
# • Config file management
#
# DASHBOARD OVERLAY RULES:
# • Fresh install → Set web.web_path to our dashboard
# • Upgrade → Update files only, preserve user's web_path choice
#
# ═══════════════════════════════════════════════════════════════════════════════
set -e
# ─────────────────────────────────────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLONE_DIR="$(dirname "$SCRIPT_DIR")/pyMC_Repeater"
INSTALL_DIR="/opt/pymc_repeater"
CONSOLE_DIR="/opt/pymc_console"
UI_DIR="$CONSOLE_DIR/web/html"
CONFIG_DIR="/etc/pymc_repeater"
SERVICE_NAME="pymc-repeater"
DEFAULT_BRANCH="dev"
UI_REPO="dmduran12/pymc_console-dist"
UI_RELEASE_URL="https://github.com/${UI_REPO}/releases"
# ─────────────────────────────────────────────────────────────────────────────
# Terminal Output
# ─────────────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
print_step() { echo -e "\n${BOLD}${CYAN}[$1/$2]${NC} ${BOLD}$3${NC}"; }
print_success() { echo -e " ${GREEN}${NC} $1"; }
print_error() { echo -e " ${RED}${NC} ${RED}$1${NC}"; }
print_info() { echo -e " ${CYAN}${NC} $1"; }
print_warning() { echo -e " ${YELLOW}${NC} $1"; }
print_banner() {
clear
echo ""
echo -e "${BOLD}${CYAN}pyMC Console${NC}"
echo -e "${DIM}React Dashboard for pyMC_Repeater${NC}"
echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# TUI Setup (whiptail/dialog)
# ─────────────────────────────────────────────────────────────────────────────
setup_dialog() {
if command -v whiptail &>/dev/null; then
DIALOG="whiptail"
elif command -v dialog &>/dev/null; then
DIALOG="dialog"
else
echo "Installing whiptail..."
apt-get update -qq && apt-get install -y whiptail
DIALOG="whiptail"
fi
}
show_info() { $DIALOG --backtitle "pyMC Console" --title "$1" --msgbox "$2" 12 60; }
show_error() { $DIALOG --backtitle "pyMC Console" --title "Error" --msgbox "$1" 10 60; }
ask_yes_no() { $DIALOG --backtitle "pyMC Console" --title "$1" --yesno "$2" 12 60; }
# ─────────────────────────────────────────────────────────────────────────────
# Status Helpers
# ─────────────────────────────────────────────────────────────────────────────
is_installed() { [[ -d "$INSTALL_DIR" ]] && [[ -f "$INSTALL_DIR/pyproject.toml" ]]; }
service_running() { systemctl is-active "$SERVICE_NAME" &>/dev/null; }
get_version() {
pip3 show pymc-repeater 2>/dev/null | grep "^Version:" | awk '{print $2}' || echo "unknown"
}
get_console_version() {
[[ -f "$UI_DIR/VERSION" ]] && cat "$UI_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' && return
echo "unknown"
}
get_core_version() {
pip3 show pymc-core 2>/dev/null | grep "^Version:" | awk '{print $2}' || echo "unknown"
}
get_repeater_version() {
pip3 show pymc-repeater 2>/dev/null | grep "^Version:" | awk '{print $2}' || echo "unknown"
}
print_version_summary() {
local core_ver=$(get_core_version)
local rep_ver=$(get_repeater_version)
local ui_ver=$(get_console_version)
echo ""
echo -e " ${DIM}Versions:${NC}"
echo -e " pyMC Core: ${CYAN}$core_ver${NC}"
echo -e " pyMC Repeater: ${CYAN}$rep_ver${NC}"
echo -e " pyMC Console: ${CYAN}v$ui_ver${NC}"
}
# ─────────────────────────────────────────────────────────────────────────────
# Git Operations
# ─────────────────────────────────────────────────────────────────────────────
clone_upstream() {
local branch="$1"
git config --global --add safe.directory "$CLONE_DIR" 2>/dev/null || true
if [[ -d "$CLONE_DIR/.git" ]]; then
print_info "Updating existing clone..."
cd "$CLONE_DIR"
git fetch origin --prune
git reset --hard HEAD
git checkout "$branch" 2>/dev/null || git checkout -b "$branch" "origin/$branch"
git reset --hard "origin/$branch"
else
print_info "Cloning pyMC_Repeater@$branch..."
rm -rf "$CLONE_DIR"
git clone -b "$branch" "https://github.com/rightup/pyMC_Repeater.git" "$CLONE_DIR"
fi
# Show what we got
cd "$CLONE_DIR"
local commit=$(git rev-parse --short HEAD)
local date=$(git log -1 --format=%cd --date=short)
print_success "Source: $branch @ $commit ($date)"
}
# ─────────────────────────────────────────────────────────────────────────────
# Upstream Installer Wrapper
# ─────────────────────────────────────────────────────────────────────────────
run_upstream() {
local action="$1"
local upstream_script="$CLONE_DIR/manage.sh"
if [[ ! -f "$upstream_script" ]]; then
print_error "Upstream manage.sh not found"
return 1
fi
echo ""
echo -e "${DIM}─────────────────────────────────────────────────────────${NC}"
echo -e "${BOLD}Running pyMC_Repeater $action...${NC}"
echo -e "${DIM}─────────────────────────────────────────────────────────${NC}"
echo ""
(cd "$CLONE_DIR" && bash "$upstream_script" "$action")
local exit_code=$?
echo ""
echo -e "${DIM}─────────────────────────────────────────────────────────${NC}"
if [[ $exit_code -eq 0 ]]; then
print_success "pyMC_Repeater $action completed"
return 0
else
print_error "pyMC_Repeater $action failed"
return 1
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Dashboard Installation
# ─────────────────────────────────────────────────────────────────────────────
install_dashboard() {
local config_file="$CONFIG_DIR/config.yaml"
local temp_file="/tmp/pymc-ui-$$.tar.gz"
local is_fresh_install=true
# Detect fresh install vs upgrade
if [[ -d "$CONSOLE_DIR" ]]; then
is_fresh_install=false
fi
# Download dashboard
print_info "Downloading dashboard..."
if ! curl -fsSL -o "$temp_file" "${UI_RELEASE_URL}/latest/download/pymc-ui-latest.tar.gz"; then
print_error "Download failed"
rm -f "$temp_file"
return 1
fi
# Clean and extract
rm -rf "$UI_DIR"
mkdir -p "$UI_DIR"
tar -xzf "$temp_file" -C "$UI_DIR"
rm -f "$temp_file"
# Set permissions
chown -R repeater:repeater "$CONSOLE_DIR" 2>/dev/null || true
# Configure web_path (fresh install only)
if [[ -f "$config_file" ]] && command -v yq &>/dev/null; then
# Ensure web section exists
yq -i '.web //= {}' "$config_file" 2>/dev/null || true
if [[ "$is_fresh_install" == true ]]; then
yq -i ".web.web_path = \"$UI_DIR\"" "$config_file"
print_success "Dashboard installed (web_path configured)"
else
print_success "Dashboard updated (web_path preserved)"
fi
else
print_warning "Could not configure web_path - set manually in $config_file"
fi
local size=$(du -sh "$UI_DIR" 2>/dev/null | cut -f1)
print_info "Size: $size"
}
# ─────────────────────────────────────────────────────────────────────────────
# Install
# ─────────────────────────────────────────────────────────────────────────────
do_install() {
if [[ "$EUID" -ne 0 ]]; then
show_error "Installation requires root.\n\nRun: sudo $0 install"
return 1
fi
# Check if pyMC_Repeater already installed
local repeater_exists=false
is_installed && repeater_exists=true
# Installation type selection (interactive only)
local install_type="${1:-}"
if [[ -z "$install_type" ]]; then
if [[ "$repeater_exists" == true ]]; then
# Repeater exists - only offer console
install_type="console"
show_info "Existing Installation" "pyMC_Repeater detected.\n\nInstalling Console dashboard only."
else
# Fresh system - offer choice
install_type=$($DIALOG --backtitle "pyMC Console" --title "Installation Type" --menu \
"\nSelect what to install:" 14 55 2 \
"full" "Full Stack (pyMC_Repeater + Console)" \
"console" "Console Only (dashboard for existing install)" \
3>&1 1>&2 2>&3) || return 0
fi
fi
case "$install_type" in
full) do_install_full ;;
console) do_install_console ;;
*) show_error "Unknown install type: $install_type"; return 1 ;;
esac
}
do_install_full() {
if is_installed; then
show_error "pyMC_Repeater already installed.\n\nUse 'upgrade' or choose Console-only install."
return 1
fi
# Branch selection
local branch
branch=$($DIALOG --backtitle "pyMC Console" --title "Select Branch" --menu \
"\nSelect pyMC_Repeater branch:" 14 50 3 \
"dev" "Development (recommended)" \
"main" "Stable release" \
"custom" "Enter custom branch" 3>&1 1>&2 2>&3) || return 0
if [[ "$branch" == "custom" ]]; then
branch=$($DIALOG --backtitle "pyMC Console" --inputbox "Branch name:" 8 40 "dev" 3>&1 1>&2 2>&3) || return 0
fi
print_banner
echo -e " ${DIM}Mode: Full Stack${NC}"
echo -e " ${DIM}Branch: $branch${NC}"
# Step 1: Clone upstream
print_step 1 3 "Preparing pyMC_Repeater"
clone_upstream "$branch"
# Step 2: Run upstream installer
print_step 2 3 "Installing pyMC_Repeater"
run_upstream "install" || return 1
# Step 3: Overlay dashboard
print_step 3 3 "Installing dashboard"
install_dashboard
# Done
local ip=$(hostname -I 2>/dev/null | awk '{print $1}')
echo ""
echo -e "${GREEN}${BOLD}Installation Complete!${NC}"
print_version_summary
echo ""
echo -e " Dashboard: ${CYAN}http://$ip:8000/${NC}"
echo ""
echo -e " ${DIM}Configure radio: cd $CLONE_DIR && sudo ./manage.sh${NC}"
echo ""
}
do_install_console() {
if ! is_installed; then
show_error "pyMC_Repeater not found.\n\nUse Full Stack install or install pyMC_Repeater first."
return 1
fi
if [[ -d "$UI_DIR" ]]; then
if ! ask_yes_no "Console Exists" "Console dashboard already installed.\n\nReinstall?"; then
return 0
fi
fi
print_banner
echo -e " ${DIM}Mode: Console Only${NC}"
# Single step: Install dashboard
print_step 1 1 "Installing dashboard"
install_dashboard
# Done
local ip=$(hostname -I 2>/dev/null | awk '{print $1}')
echo ""
echo -e "${GREEN}${BOLD}Console Installed!${NC}"
echo ""
echo -e " ${DIM}Versions:${NC}"
echo -e " pyMC Console: ${CYAN}v$(get_console_version)${NC}"
echo ""
echo -e " Dashboard: ${CYAN}http://$ip:8000/${NC}"
echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Upgrade
# ─────────────────────────────────────────────────────────────────────────────
do_upgrade() {
if ! is_installed; then
show_error "Not installed. Use 'install' first."
return 1
fi
if [[ "$EUID" -ne 0 ]]; then
show_error "Upgrade requires root.\n\nRun: sudo $0 upgrade"
return 1
fi
# Self-update pymc_console repo
if [[ -d "$SCRIPT_DIR/.git" ]]; then
print_info "Checking for pymc_console updates..."
cd "$SCRIPT_DIR"
git config --global --add safe.directory "$SCRIPT_DIR" 2>/dev/null || true
git fetch origin 2>/dev/null || true
local local_hash=$(git rev-parse HEAD 2>/dev/null)
local remote_hash=$(git rev-parse origin/main 2>/dev/null)
if [[ -n "$remote_hash" && "$local_hash" != "$remote_hash" ]]; then
if git pull --ff-only 2>/dev/null || git reset --hard origin/main 2>/dev/null; then
print_success "pymc_console updated - restarting..."
exec "$SCRIPT_DIR/manage.sh" upgrade
fi
fi
fi
# Capture current versions before upgrade
local core_before=$(get_core_version)
local rep_before=$(get_repeater_version)
local ui_before=$(get_console_version)
# Get current branch
local branch="$DEFAULT_BRANCH"
if [[ -d "$CLONE_DIR/.git" ]]; then
branch=$(cd "$CLONE_DIR" && git rev-parse --abbrev-ref HEAD 2>/dev/null) || branch="$DEFAULT_BRANCH"
fi
# Upgrade type selection
local upgrade_type
upgrade_type=$($DIALOG --backtitle "pyMC Console" --title "Upgrade" --menu \
"\nWhat would you like to upgrade?" 12 55 2 \
"console" "Console only" \
"full" "Full Stack (Repeater + Console)" \
3>&1 1>&2 2>&3) || return 0
print_banner
echo -e " ${DIM}Mode: $([[ "$upgrade_type" == "full" ]] && echo "Full Stack" || echo "Console Only")${NC}"
if [[ "$upgrade_type" == "full" ]]; then
# Full stack: 3 steps
print_step 1 3 "Updating pyMC_Repeater source"
clone_upstream "$branch"
print_step 2 3 "Upgrading pyMC_Repeater"
run_upstream "upgrade" || return 1
print_step 3 3 "Updating dashboard"
install_dashboard
else
# Console only: 1 step
print_step 1 1 "Updating dashboard"
install_dashboard
fi
# Done
local ip=$(hostname -I 2>/dev/null | awk '{print $1}')
local core_after=$(get_core_version)
local rep_after=$(get_repeater_version)
local ui_after=$(get_console_version)
echo ""
echo -e "${GREEN}${BOLD}Upgrade Complete!${NC}"
echo ""
echo -e " ${DIM}Versions:${NC}"
[[ "$core_before" != "$core_after" ]] \
&& echo -e " pyMC Core: ${DIM}$core_before${NC}${CYAN}$core_after${NC}" \
|| echo -e " pyMC Core: ${CYAN}$core_after${NC}"
[[ "$rep_before" != "$rep_after" ]] \
&& echo -e " pyMC Repeater: ${DIM}$rep_before${NC}${CYAN}$rep_after${NC}" \
|| echo -e " pyMC Repeater: ${CYAN}$rep_after${NC}"
[[ "$ui_before" != "$ui_after" ]] \
&& echo -e " pyMC Console: ${DIM}v$ui_before${NC}${CYAN}v$ui_after${NC}" \
|| echo -e " pyMC Console: ${CYAN}v$ui_after${NC}"
echo ""
echo -e " Dashboard: ${CYAN}http://$ip:8000/${NC}"
echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Uninstall
# ─────────────────────────────────────────────────────────────────────────────
do_uninstall() {
# Check if there's anything to uninstall
local has_repeater=false
local has_console=false
local has_clone=false
local has_self=true # Script is always running from somewhere
is_installed && has_repeater=true
[[ -d "$CONSOLE_DIR" ]] && has_console=true
[[ -d "$CLONE_DIR" ]] && has_clone=true
# Debug: show what we found
print_banner
echo -e " ${DIM}Detected components:${NC}"
echo -e " Repeater: $([[ "$has_repeater" == true ]] && echo "${GREEN}found${NC}" || echo "${DIM}not found${NC}")"
echo -e " Console: $([[ "$has_console" == true ]] && echo "${GREEN}found${NC} ($CONSOLE_DIR)" || echo "${DIM}not found${NC}")"
echo -e " Clone: $([[ "$has_clone" == true ]] && echo "${GREEN}found${NC} ($CLONE_DIR)" || echo "${DIM}not found${NC}")"
echo -e " This repo: ${GREEN}$SCRIPT_DIR${NC}"
echo ""
if [[ "$EUID" -ne 0 ]]; then
show_error "Uninstall requires root.\n\nRun: sudo $0 uninstall"
return 1
fi
# Build description of what will be removed
local will_remove=""
[[ "$has_repeater" == true ]] && will_remove+="• pyMC_Repeater\n"
[[ "$has_console" == true ]] && will_remove+="• Console dashboard ($CONSOLE_DIR)\n"
[[ "$has_clone" == true ]] && will_remove+="• pyMC_Repeater clone ($CLONE_DIR)\n"
will_remove+="• pymc_console repo ($SCRIPT_DIR)"
if ! ask_yes_no "Confirm Uninstall" "\nThis will remove:\n${will_remove}\n\nContinue?"; then
return 0
fi
print_banner
# Calculate steps: repeater(optional) + console + clone(optional) + self
local step=1
local total=1 # self is always counted
[[ "$has_repeater" == true ]] && ((total++))
[[ "$has_console" == true ]] && ((total++))
[[ "$has_clone" == true ]] && ((total++))
# Step: Run upstream uninstall (if Repeater installed)
if [[ "$has_repeater" == true ]]; then
print_step $step $total "Removing pyMC_Repeater"
if [[ -f "$CLONE_DIR/manage.sh" ]]; then
# Run upstream in completely separate bash process
# Use script -q to capture and isolate the entire session
bash -c 'cd "'"$CLONE_DIR"'" && bash manage.sh uninstall' </dev/tty || true
else
# Manual cleanup if upstream not available
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
rm -f /etc/systemd/system/pymc-repeater.service
systemctl daemon-reload
rm -rf "$INSTALL_DIR" "$CONFIG_DIR" /var/log/pymc_repeater
pip3 uninstall -y pymc_repeater pymc_core 2>/dev/null || true
userdel repeater 2>/dev/null || true
fi
print_success "pyMC_Repeater removed"
((step++))
fi
# Step: Remove dashboard overlay (if exists)
if [[ "$has_console" == true ]]; then
print_step $step $total "Removing dashboard"
rm -rf "$CONSOLE_DIR"
print_success "Dashboard removed ($CONSOLE_DIR)"
((step++))
fi
# Step: Remove clone
if [[ "$has_clone" == true ]]; then
print_step $step $total "Removing pyMC_Repeater clone"
rm -rf "$CLONE_DIR"
print_success "Clone removed ($CLONE_DIR)"
((step++))
fi
# Step: Remove pymc_console repo itself (scheduled for after script exits)
print_step $step $total "Removing pymc_console repo"
echo -e " ${YELLOW}Will remove $SCRIPT_DIR after script exits${NC}"
trap "rm -rf '$SCRIPT_DIR'" EXIT
print_success "Scheduled for removal"
echo ""
echo -e "${GREEN}${BOLD}Uninstall Complete${NC}"
echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Service Control (simple wrappers)
# ─────────────────────────────────────────────────────────────────────────────
do_start() {
[[ "$EUID" -ne 0 ]] && { echo "Run as root: sudo $0 start"; return 1; }
systemctl start "$SERVICE_NAME"
sleep 1
service_running && echo "✓ Service started" || echo "✗ Service failed to start"
}
do_stop() {
[[ "$EUID" -ne 0 ]] && { echo "Run as root: sudo $0 stop"; return 1; }
systemctl stop "$SERVICE_NAME"
echo "✓ Service stopped"
}
do_restart() {
[[ "$EUID" -ne 0 ]] && { echo "Run as root: sudo $0 restart"; return 1; }
systemctl restart "$SERVICE_NAME"
sleep 1
service_running && echo "✓ Service restarted" || echo "✗ Service failed to start"
}
do_status() {
if is_installed; then
echo "pyMC_Repeater: $(get_version)"
echo "Console: v$(get_console_version)"
echo "Service: $(service_running && echo "running" || echo "stopped")"
else
echo "Not installed"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Main Menu
# ─────────────────────────────────────────────────────────────────────────────
show_menu() {
local status="Not installed"
is_installed && status=$(service_running && echo "Running" || echo "Stopped")
local choice=$($DIALOG --backtitle "pyMC Console" --title "Main Menu" --menu \
"\nStatus: $status\n\nSelect action:" 16 50 6 \
"install" "Fresh installation" \
"upgrade" "Upgrade existing" \
"uninstall" "Remove everything" \
"status" "Show versions" \
"logs" "View live logs" \
"exit" "Exit" 3>&1 1>&2 2>&3) || return 1
case "$choice" in
install) do_install ;;
upgrade) do_upgrade ;;
uninstall) do_uninstall ;;
status) clear; do_status; read -p "Press Enter..." ;;
logs) clear; journalctl -u "$SERVICE_NAME" -f ;;
exit) exit 0 ;;
esac
}
# ─────────────────────────────────────────────────────────────────────────────
# CLI Entry Point
# ─────────────────────────────────────────────────────────────────────────────
show_help() {
cat << EOF
pyMC Console - Dashboard Overlay Manager
Usage: $0 [command]
Commands:
install [full|console] Fresh installation (default: interactive)
full - pyMC_Repeater + Console dashboard
console - Dashboard only (existing Repeater)
upgrade Upgrade existing installation
uninstall Remove everything
start Start service
stop Stop service
restart Restart service
status Show versions and status
Run without arguments for interactive menu.
Radio/GPIO configuration: cd $CLONE_DIR && sudo ./manage.sh
EOF
}
case "${1:-}" in
-h|--help) show_help ;;
install) setup_dialog; do_install "${2:-}" ;;
upgrade) setup_dialog; do_upgrade ;;
uninstall) setup_dialog; do_uninstall ;;
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
"")
setup_dialog
while true; do show_menu || break; done
;;
*)
echo "Unknown command: $1"
show_help
exit 1
;;
esac