Files
pymc_console-dist/manage.sh
GitHub Actions Bot 0fb09907f1 Sync build v0.9.303
Automated sync from private repository.
Commit: fa42e00b651e5a5c5ebab7249ed7582220b9b3c6
2026-03-03 01:28:13 +00:00

663 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 (allow non-interactive via arg or env)
local upgrade_type="${1:-${UPGRADE_TYPE:-}}"
if [[ -z "$upgrade_type" ]]; then
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
fi
if [[ "$upgrade_type" != "console" && "$upgrade_type" != "full" ]]; then
show_error "Unknown upgrade type: $upgrade_type"
return 1
fi
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 [full|console] Upgrade existing installation (non-interactive)
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 "${2:-}" ;;
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