cyberia/research/events/launch.sh

#!/usr/bin/env bash

# Cyber Valley Tickets - Development Environment Launcher
# This script sets up the complete development environment using tmux

set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color

# Configuration
SESSION_NAME="cyber-valley-dev"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRODUCTION_FRONTEND=false

# Load environment variables from .env if it exists
if span> ; then
    source "$SCRIPT_DIR/.env"
fi

# Parse command line arguments
STOP_MODE=false
while span> -gt 0 ; do
    case $1 in
        --production-frontend)
            PRODUCTION_FRONTEND=true
            shift
            ;;
        --stop)
            STOP_MODE=true
            shift
            ;;
        *)
            echo "Unknown option: $1"
            echo "Usage: $0 [--production-frontend] [--stop]"
            exit 1
            ;;
    esac
done

# Logging functions with aligned columns
log_info() {
    printf "${BLUE}%-50s${NC} ${CYAN}[%s]${NC}\n" "$1" "$2"
}

log_success() {
    printf "${GREEN}%-50s${NC} ${GREEN}[%s]${NC}\n" "$1" "$2"
}

log_warning() {
    printf "${YELLOW}%-50s${NC} ${YELLOW}[%s]${NC}\n" "$1" "$2"
}

log_error() {
    printf "${RED}%-50s${NC} ${RED}[%s]${NC}\n" "$1" "$2"
}

log_section() {
    echo -e "\n${BOLD}${CYAN}=== $1 ===${NC}\n"
}

# Check if tmux is available, install if possible
check_and_install_tmux() {
    if command -v tmux &> /dev/null; then
        return 0
    fi

    log_warning "tmux not found" "checking"

    # Detect package manager and attempt installation
    if command -v apt &> /dev/null; then
        log_info "Installing tmux via apt" "installing"
        sudo apt update && sudo apt install -y tmux
    elif command -v xbps-install &> /dev/null; then
        log_info "Installing tmux via xbps-install" "installing"
        sudo xbps-install -y tmux
    elif command -v brew &> /dev/null; then
        log_info "Installing tmux via brew" "installing"
        brew install tmux
    else
        log_error "Could not install tmux automatically" "failed"
        echo "Please install tmux manually using your package manager:"
        echo "  - Debian/Ubuntu: sudo apt install tmux"
        echo "  - Void Linux:    sudo xbps-install tmux"
        echo "  - macOS:         brew install tmux"
        exit 1
    fi

    if command -v tmux &> /dev/null; then
        log_success "tmux installed successfully" "done"
    else
        log_error "tmux installation failed" "failed"
        exit 1
    fi
}

# Validate required directories exist
validate_environment() {
    local missing_dirs=()
    for dir in backend client ethereum; do
        if span> ; then
            missing_dirs+=("$dir")
        fi
    done

    if [[ ${#missing_dirs[@]} -gt 0 ]]; then
        log_error "Missing required directories: ${missing_dirs[*]}" "failed"
        exit 1
    fi
}

# Helper: Create tmux window with logging
create_tmux_window() {
    local window_name="$1"
    local log_file="$2"

    tmux new-window -t "$SESSION_NAME" -n "$window_name" -c "$SCRIPT_DIR"
    if span> ; then
        rm -f "$log_file"
        tmux pipe-pane -t "$SESSION_NAME:$window_name" "cat >> $log_file"
    fi
}

# Helper: Run command in buf window and wait for completion with exit code check
run_buf_command() {
  local cmd=$1
  local done=/tmp/done.

$$
  local exit_code_file=/tmp/exit_code.$$


  rm -f "$done" "$exit_code_file"
  tmux send-keys -t "$SESSION_NAME:buf" \
       "$cmd; echo \$? > '$exit_code_file'; touch '$done'" C-m
  until  -f $done ; do sleep 0.1; done
  local exit_code=$(cat "$exit_code_file" 2>/dev/null || echo "1")
  rm -f "$done" "$exit_code_file"
  if  "$exit_code" -ne 0 ; then
    return 1
  fi
  return 0
}

# Helper: Wait for service to start by monitoring log
wait_for_service() {
    local log_file="$1"
    local pattern="$2"
    local service_name="$3"

    # First check if pattern already exists in log (service started quickly)
    if grep -q "$pattern" "$log_file" 2>/dev/null; then
        return 0
    fi

    # Otherwise wait for new lines
    if timeout 60 tail -f "$log_file" | grep -q "$pattern"; then
        return 0
    else
        log_error "$service_name failed to start" "timeout"
        exit 1
    fi
}

# Helper: Restart a service (kill window and recreate)
restart_service() {
    local service_name="$1"
    local window_name="$2"
    local log_file="$3"
    local make_command="$4"

    log_info "Restarting $service_name" "restarting"
    tmux kill-window -t "$SESSION_NAME:$window_name"
    create_tmux_window "$window_name" "$log_file"
    tmux send-keys -t "$SESSION_NAME:$window_name" "$make_command" Enter
}

# Helper: Stop all services and cleanup
stop_services() {
    log_section "Stopping Services"

    log_info "Stopping tmux session" "stopping"
    if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
        tmux kill-session -t "$SESSION_NAME"
        log_success "Tmux session stopped" "done"
    else
        log_warning "No tmux session found" "skipped"
    fi

    log_info "Stopping podman containers" "stopping"
    for name in "ganache" "ipfs" "valkey"; do
        podman ps -a \
            | grep "cvland-$name" \
            | awk '{print $1}' \
            | xargs -r -I{} sh -c 'podman stop {} || true; podman rm {} || true' 2>/dev/null &
    done
    wait
    log_success "Podman containers stopped" "done"

    log_info "Cleaning up temporary files" "cleaning"
    rm -f /tmp/done.* /tmp/exit_code.* /tmp/contract_vars.txt
    rm -f /tmp/backend.log /tmp/frontend.log /tmp/indexer.log /tmp/telegram-bot.log
    log_success "Temporary files cleaned" "done"

    log_section "Stop Complete"
    echo -e "${GREEN}${BOLD}All services stopped!${NC}"
}

# ============================================================================
# MAIN SCRIPT
# ============================================================================

# Handle stop mode
if  "$STOP_MODE" == true ; then
    stop_services
    exit 0
fi

# Check prerequisites
check_and_install_tmux
validate_environment

# ============================================================================
log_section "Environment Setup"

log_info "Cleaning database" "starting"
rm -f backend/db.sqlite3
log_success "Database cleaned" "done"

log_info "Stopping containers" "starting"
for name in "ganache" "ipfs" "valkey"; do
    podman ps -a \
	| grep "cvland-$name" \
	| awk '{print $1}' \
	| xargs -r -I{} sh -c 'podman stop {} || true; podman rm {} || true' 2>/dev/null &
done
wait
log_success "Containers stopped" "done"

log_info "Installing dependencies" "starting"
make install 2>&1 >/dev/null
log_success "Dependencies installed" "done"

log_info "Creating tmux session: $SESSION_NAME" "starting"
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
    log_info "Killing previous tmux session" "cleaning"
    tmux kill-session -t "$SESSION_NAME"
fi
tmux new-session -d -s "$SESSION_NAME" -c "$SCRIPT_DIR"
log_success "Tmux session created" "done"

# Create window for running sequential commands
tmux new-window -t "$SESSION_NAME" -n "buf" -c "$SCRIPT_DIR"

# ============================================================================
log_section "Starting Services"

log_info "Starting Ganache blockchain" "starting"
if ! run_buf_command "make -C ethereum/ ganache"; then
    log_error "Ganache blockchain failed to start" "failed"
    exit 1
fi
log_success "Ganache blockchain ready" "started"

# Update .env to use local Ganache for this session
log_info "Updating blockchain connection to local Ganache" "updating"
# Get the actual Ganache port (default to 8545 if not set)
GANACHE_PORT=${GANACHE_PORT:-8545}
# Set environment variables for current session
export WS_ETH_NODE_HOST=ws://127.0.0.1:${GANACHE_PORT}
# Update .env file for child processes (backend/indexer) to ensure they talk to
# the local Ganache instance. We intentionally avoid overwriting PUBLIC_* URLs
# when building the production frontend, because those should point to the
# public reverse-proxy path (e.g. https://<domain>/ganache) for browser clients.
sed -i "s|^export WS_ETH_NODE_HOST=.*|export WS_ETH_NODE_HOST=ws://127.0.0.1:${GANACHE_PORT}|" .env
sed -i "s|^export HTTP_ETH_NODE_HOST=.*|export HTTP_ETH_NODE_HOST=http://127.0.0.1:${GANACHE_PORT}|" .env 2>/dev/null || echo "export HTTP_ETH_NODE_HOST=http://127.0.0.1:${GANACHE_PORT}" >> .env
if  "$PRODUCTION_FRONTEND" == false ; then
    sed -i "s|^export PUBLIC_HTTP_ETH_NODE_HOST=.*|export PUBLIC_HTTP_ETH_NODE_HOST=http://127.0.0.1:${GANACHE_PORT}|" .env
    sed -i "s|^export PUBLIC_WS_ETH_NODE_HOST=.*|export PUBLIC_WS_ETH_NODE_HOST=ws://127.0.0.1:${GANACHE_PORT}|" .env 2>/dev/null || echo "export PUBLIC_WS_ETH_NODE_HOST=ws://127.0.0.1:${GANACHE_PORT}" >> .env
else
    log_info "Keeping PUBLIC_* blockchain endpoints unchanged (production frontend)" "skipped"
fi
log_success "Blockchain connection updated to local Ganache (port ${GANACHE_PORT})" "done"

log_info "Starting Django backend server" "starting"
create_tmux_window "backend" "/tmp/backend.log"
tmux send-keys -t "$SESSION_NAME:backend" "make -C backend/ run" Enter

if  "$PRODUCTION_FRONTEND" == false ; then
    log_info "Starting Vite frontend server" "starting"
    create_tmux_window "frontend" "/tmp/frontend.log"
    tmux send-keys -t "$SESSION_NAME:frontend" "FRONTEND_MODE=dev make -C client/ run" Enter
fi

log_info "Waiting for services to initialize" "waiting"
wait_for_service "/tmp/backend.log" "Starting development server at" "Backend"
log_success "Django backend server ready" "started"
if  "$PRODUCTION_FRONTEND" == false ; then
    wait_for_service "/tmp/frontend.log" "VITE" "Frontend"
    log_success "Vite frontend server ready" "started"
fi

# ============================================================================
log_section "Database Configuration"

log_info "Seeding database with initial data" "starting"
if ! run_buf_command "make -C backend/ seed-db"; then
    log_error "Database seeding failed" "failed"
    exit 1
fi
log_success "Database seeding completed" "done"

log_info "Synchronizing geodata" "starting"
if ! run_buf_command "make -C backend/ sync-geodata"; then
    log_error "Geodata synchronization failed" "failed"
    exit 1
fi
log_success "Geodata synchronization completed" "done"

# ============================================================================
log_section "Smart Contract Deployment"

log_info "Deploying contracts and seeding on-chain entities" "starting"
if ! run_buf_command "make -C ethereum/ deploy-dev"; then
    log_error "Deployment and seeding failed" "failed"
    echo -e "${RED}Check the buf window for error details:${NC}"
    echo -e "  ${CYAN}tmux attach -t $SESSION_NAME:buf${NC}"
    exit 1
fi
log_success "Contracts deployed and entities created" "done"

log_info "Updating contract addresses in .env" "starting"
# Extract contract addresses from deployment output
# Capture pane output and join wrapped lines (-J flag)
# Lines may be wrapped due to terminal width, so we need to join them
tmux capture-pane -J -pt "$SESSION_NAME:buf" | grep '^export' > /tmp/contract_vars.txt

# Verify contract addresses were captured
if contract_vars.txt ; then
    log_error "No contract addresses found in deployment output" "failed"
    echo -e "${RED}Check the buf window for deployment errors:${NC}"
    echo -e "  ${CYAN}tmux attach -t $SESSION_NAME:buf${NC}"
    exit 1
fi

# Update .env file with new contract addresses
python3 <<'PY'
import sys
from pathlib import Path

try:
    contract_vars = Path("/tmp/contract_vars.txt").read_text().splitlines()
    if not contract_vars:
        print("ERROR: No contract addresses found", file=sys.stderr)
        sys.exit(1)
    
    splitted = []
    for line in contract_vars:
        if "=" in line:
            parts = line.rstrip().split("=", 1)
            if len(parts) == 2:
                var_name, var_value = parts
                # Validate that this looks like an Ethereum address
                if var_value.startswith("0x") and len(var_value) >= 42:
                    splitted.append(parts)
                else:
                    print(f"WARNING: Skipping invalid address: {var_name}={var_value}", file=sys.stderr)
    
    if not splitted:
        print("ERROR: Could not parse valid contract addresses", file=sys.stderr)
        sys.exit(1)
    
    env_file = Path(".env")
    if not env_file.exists():
        print("ERROR: .env file not found", file=sys.stderr)
        sys.exit(1)
    
    # Replace existing values, but also append missing vars so fresh .env files
    # (without contract placeholders) still work.
    updated = []
    replaced = set()
    for line in env_file.read_text().splitlines():
        for replacement in splitted:
            if line.startswith(f"{replacement[0]}="):
                updated.append("=".join(replacement))
                replaced.add(replacement[0])
                break
        else:
            updated.append(line.rstrip())

    for var_name, var_value in splitted:
        if var_name not in replaced:
            updated.append(f"{var_name}={var_value}")
    
    env_file.write_text("\n".join(updated))
    print(f"Processed {len(splitted)} contract addresses ({len(replaced)} replaced, {len(splitted) - len(replaced)} appended)")
except Exception as e:
    print(f"ERROR: {e}", file=sys.stderr)
    sys.exit(1)
PY

if  $? -ne 0 ; then
    log_error "Failed to update contract addresses in .env" "failed"
    exit 1
fi

rm -f /tmp/contract_vars.txt
log_success "Contract addresses updated" "done"

# Re-source .env so subsequent direct manage.py invocations (not via make)
# see the newly updated contract addresses.
set -a
source "$SCRIPT_DIR/.env"
set +a

# ============================================================================
log_section "Starting Indexer & Restarting Services"

log_info "Waiting for IPFS to be ready" "waiting"
until curl -s http://127.0.0.1:5001/api/v0/version >/dev/null 2>&1; do
    sleep 1
done
log_success "IPFS is ready" "ready"

# Reset indexer state to ensure it processes all historical events
# Events are created during deploy-dev at past blocks due to time manipulation
log_info "Resetting indexer state for full re-index" "resetting"
cd backend
.venv/bin/python manage.py shell -c "
from cyber_valley.indexer.models import LastProcessedBlock, LogProcessingError
# Force the daemon indexer to start from block 0 on this relaunch.
# (Deleting the row would also work, but being explicit is clearer and survives
# any future changes to the indexer defaulting logic.)
LastProcessedBlock.objects.update_or_create(id=1, defaults={\"block_number\": 0})
LogProcessingError.objects.all().delete()
print('Indexer state reset (last_processed_block=0)')
"
cd ..
log_success "Indexer state reset" "done"

# Run oneshot indexer first to build DB deterministically (no websocket listener).
# This prevents startup races where realtime events could be enqueued before
# their dependencies are indexed.
log_info "Running oneshot indexer to index all historical events" "indexing"
if ! run_buf_command "make -C backend/ run-indexer-oneshot"; then
    log_warning "Oneshot indexer had some errors (may be expected for duplicate events)" "warning"
fi
log_success "Oneshot indexer completed" "done"

# The oneshot run advances LastProcessedBlock to the latest block. Force it back to 0
# so the daemon indexer replays from genesis as requested.
log_info "Forcing indexer last processed block back to 0" "resetting"
cd backend
.venv/bin/python manage.py shell -c "
from cyber_valley.indexer.models import LastProcessedBlock
LastProcessedBlock.objects.update_or_create(id=1, defaults={\"block_number\": 0})
print('LastProcessedBlock set to 0')
"
cd ..
log_success "Indexer last processed block forced to 0" "done"

log_info "Starting blockchain indexer" "starting"
create_tmux_window "indexer" "/tmp/indexer.log"
tmux send-keys -t "$SESSION_NAME:indexer" "make -C backend/ run-indexer-from-zero" Enter
sleep 2
if ! tmux list-windows -t "$SESSION_NAME" | grep -q "indexer"; then
    log_error "Failed to create indexer window" "failed"
    exit 1
fi
log_success "Blockchain indexer started" "started"

log_info "Starting telegram bot" "starting"
if  "${RUN_TELEGRAM_BOT:-0}" != "1" ; then
    log_warning "Skipping telegram bot (set RUN_TELEGRAM_BOT=1 to enable)" "skipped"
elif  -z "${TELEGRAM_BOT_API_TOKEN:-}" ; then
    log_warning "Skipping telegram bot (TELEGRAM_BOT_API_TOKEN not set)" "skipped"
else
    create_tmux_window "telegram-bot" "/tmp/telegram-bot.log"
    tmux send-keys -t "$SESSION_NAME:telegram-bot" "make -C backend/ run-telegram-bot" Enter
    sleep 2
    if ! tmux list-windows -t "$SESSION_NAME" | grep -q "telegram-bot"; then
        log_error "Failed to create telegram-bot window" "failed"
        exit 1
    fi
    log_success "Telegram bot started" "started"
fi

restart_service "Backend" "backend" "/tmp/backend.log" "make -C backend/ run"

if  "$PRODUCTION_FRONTEND" == true ; then
    log_info "Starting frontend build" "starting"
    if ! run_buf_command "make -C client/ build"; then
        log_error "Frontend build failed" "failed"
        exit 1
    fi
    log_success "Frontend build" "done"
else
    restart_service "Frontend" "frontend" "/tmp/frontend.log" "FRONTEND_MODE=dev make -C client/ run"
fi

# Note: Tickets are now minted during deploy-dev phase
# No need to wait for indexing before minting - contract is source of truth

log_info "Waiting for services to restart" "waiting"
wait_for_service "/tmp/backend.log" "Starting development server at" "Backend"
log_success "Django backend server ready" "restarted"
if  "$PRODUCTION_FRONTEND" == false ; then
    wait_for_service "/tmp/frontend.log" "VITE" "Frontend"
    log_success "Vite frontend server ready" "restarted"
fi

# ============================================================================
log_section "Startup Complete"
echo -e "${GREEN}${BOLD}All services are running!${NC}"
echo -e "Attach to session: ${CYAN}tmux attach -t $SESSION_NAME${NC}"

Graph