Files
Atomcms-edit/update-Nitrov3.sh
T

576 lines
22 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# =============================================================================
# EPIC WEB CONTROL — Enterprise Update Script
# Compatible with Laravel 13 + Nitro V3 + MariaDB
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- Load .env ---------------------------------------------------------------
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
. "$SCRIPT_DIR/.env"
set +a
fi
# --- Logging -----------------------------------------------------------------
LOG_DIR="${NITRO_LOG_DIR:-$SCRIPT_DIR/storage/logs}"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/update-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1
STEP_NUM=0
STEP_TOTAL=8
START_TIME=$(date +%s)
step() {
STEP_NUM=$((STEP_NUM + 1))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ Step %d/%d — %-49s║\n" "$STEP_NUM" "$STEP_TOTAL" "$1"
echo "╚══════════════════════════════════════════════════════════════╝"
}
info() { echo " --> $1"; }
ok() { echo " [OK] $1"; }
warn() { echo " [WARN] $1"; }
fail() { echo " [FAIL] $1"; }
die() { echo ""; echo "=== ❌ $1 ==="; cleanup_notify_failure "$1"; exit 1; }
# --- Notifications -----------------------------------------------------------
NOTIFY_URL="${NITRO_NOTIFY_URL:-}"
NOTIFY_FAILED=false
notify() {
local status="$1"
local message="$2"
[ -z "$NOTIFY_URL" ] && return 0
local elapsed=$(( $(date +%s) - START_TIME ))
local elapsed_fmt
elapsed_fmt=$(printf '%dm %ds' $((elapsed/60)) $((elapsed%60)))
curl -s -o /dev/null -X POST "$NOTIFY_URL" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"[Nitro Update] $status\",
\"attachments\": [{
\"color\": \"$([ "$status" = "SUCCESS" ] && echo \"good\" || echo \"danger\")\",
\"fields\": [
{\"title\": \"Status\", \"value\": \"$message\", \"short\": true},
{\"title\": \"Duration\", \"value\": \"$elapsed_fmt\", \"short\": true},
{\"title\": \"Server\", \"value\": \"$(hostname)\", \"short\": true},
{\"title\": \"Log\", \"value\": \"$LOG_FILE\", \"short\": false}
]
}]
}" 2>/dev/null || true
}
cleanup_notify_failure() {
$NOTIFY_FAILED && return
NOTIFY_FAILED=true
notify "FAILED" "$1"
}
# --- Rollback support --------------------------------------------------------
LAST_BACKUP=""
ROLLBACK_NEEDED=false
rollback_db() {
[ -z "$LAST_BACKUP" ] && { warn "No backup file known — cannot rollback DB."; return 1; }
[ ! -f "$LAST_BACKUP" ] && { warn "Backup file not found: $LAST_BACKUP"; return 1; }
info "Rolling back database from: $LAST_BACKUP"
mariadb $MYSQL_CRED "$DB_NAME" < "$LAST_BACKUP" && ok "Database restored." || warn "Rollback failed."
}
cleanup_on_exit() {
local exit_code=$?
if [ $exit_code -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then
echo ""
echo "=== ⚠️ Update failed — initiating rollback ==="
rollback_db
fi
[ $exit_code -ne 0 ] && cleanup_notify_failure "Script exited with code $exit_code"
}
trap cleanup_on_exit EXIT
trap 'echo ""; die "Interrupted by user"' SIGINT SIGTERM
# --- Pre-flight: interactive prompts (must be before unbuffer) ---------------
# Site URL
if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then
NITRO_SITE_URL="$APP_URL"
info "Using APP_URL from .env: $NITRO_SITE_URL"
fi
if [ -z "${NITRO_SITE_URL:-}" ]; then
read -r -p " Enter your site URL (e.g. https://example.com): " NITRO_SITE_URL
NITRO_SITE_URL="${NITRO_SITE_URL%/}"
[ -z "$NITRO_SITE_URL" ] && die "NITRO_SITE_URL is required"
export NITRO_SITE_URL
fi
# Branch
if [ -z "${NITRO_BRANCH:-}" ]; then
read -r -p " Enter branch (main/dev) [main]: " NITRO_BRANCH
NITRO_BRANCH="${NITRO_BRANCH:-main}"
case "$NITRO_BRANCH" in main|dev) ;; *) die "Invalid branch '$NITRO_BRANCH'. Use main or dev." ;; esac
export NITRO_BRANCH
fi
# Dry-run mode
DRY_RUN=false
if [ "${1:-}" = "--dry-run" ] || [ "${1:-}" = "-n" ]; then
DRY_RUN=true
echo ""
info "DRY-RUN mode: no changes will be made"
fi
# --- Unbuffer re-exec --------------------------------------------------------
if [ -z "${_UNBUFFERED:-}" ] && command -v unbuffer &> /dev/null; then
export _UNBUFFERED=1
exec unbuffer bash "$0" "$@"
fi
# --- Single-instance lock ----------------------------------------------------
LOCKFILE="/tmp/$(basename "$0").lock"
exec 200>"$LOCKFILE"
flock -n 200 || die "Another instance is already running"
# --- Configuration -----------------------------------------------------------
DB_NAME="${NITRO_DB_NAME:-habbo}"
DB_HOST="${NITRO_DB_HOST:-127.0.0.1}"
DB_PORT="${NITRO_DB_PORT:-3306}"
DB_USER="${NITRO_DB_USER:-root}"
DB_PASS="${NITRO_DB_PASS:-}"
EMULATOR_SERVICE="${NITRO_EMULATOR_SERVICE:-emulator}"
EMULATOR_DIR="${NITRO_EMULATOR_PATH:-/var/www/emulator}"
SQL_DIR="${NITRO_SQL_DIR:-$EMULATOR_DIR/Database Updates}"
GAMEDATA_CONF_DIR="${NITRO_GAMEDATA_DIR:-/var/www/Gamedata/config}"
NITRO_SRC_DIR="${NITRO_CLIENT_DIR:-/var/www/Nitro-V3/public/configuration}"
BACKUP_DIR="${NITRO_BACKUP_DIR:-$EMULATOR_DIR/Database Updates/backups}"
NITRO_CLIENT="${NITRO_CLIENT_SRC:-/var/www/Nitro-V3}"
NITRO_RENDERER="${NITRO_RENDERER_SRC:-/var/www/Nitro_Render_V3}"
NITRO_BRANCH="${NITRO_BRANCH:-main}"
MIN_DISK_GB="${NITRO_MIN_DISK_GB:-5}"
HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}"
HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}"
# Derive ws/wss protocol and domain
case "$NITRO_SITE_URL" in
https://*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL#https://}" ;;
http://*) NITRO_WS_PROTO="ws://" ; NITRO_DOMAIN="${NITRO_SITE_URL#http://}" ;;
*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="$NITRO_SITE_URL" ;;
esac
# Critical URLs (override via NITRO_* env vars)
NITRO_IMAGE_LIBRARY_URL="${NITRO_IMAGE_LIBRARY_URL:-$NITRO_SITE_URL/gamedata/c_images/}"
NITRO_HOF_FURNITURE_URL="${NITRO_HOF_FURNITURE_URL:-$NITRO_SITE_URL/gamedata/icons}"
NITRO_API_URL="${NITRO_API_URL:-$NITRO_SITE_URL}"
NITRO_SOCKET_URL="${NITRO_SOCKET_URL:-${NITRO_WS_PROTO}ws.${NITRO_DOMAIN}}"
NITRO_CAMERA_URL="${NITRO_CAMERA_URL:-$NITRO_SITE_URL/camera/photo/}"
NITRO_GAMEDATA_URL="${NITRO_GAMEDATA_URL:-$NITRO_SITE_URL/gamedata}"
NITRO_ASSET_URL="${NITRO_ASSET_URL:-$NITRO_SITE_URL/gamedata/bundled}"
NITRO_FURNI_ASSET_ICON_URL="${NITRO_FURNI_ASSET_ICON_URL:-$NITRO_SITE_URL/gamedata/icons/%libname%%param%_icon.png}"
# MySQL credentials
MYSQL_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=OFF"
[ -n "$DB_PASS" ] && export MYSQL_PWD="$DB_PASS"
# =============================================================================
# PRE-FLIGHT CHECKS
# =============================================================================
step "Pre-flight Checks"
info "Checking required commands..."
for cmd in git mariadb mariadb-dump mvn node python3 yarn; do
command -v "$cmd" &>/dev/null || die "Required command not found: $cmd"
done
ok "All required commands available"
info "Checking directories..."
REQUIRED_DIRS=(
"$EMULATOR_DIR/Emulator"
"$NITRO_RENDERER"
"$NITRO_CLIENT"
"$NITRO_SRC_DIR"
)
for dir in "${REQUIRED_DIRS[@]}"; do
[ -d "$dir" ] || die "Required directory not found: $dir"
ok "Found: $dir"
done
info "Checking disk space..."
AVAIL_KB=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1)
AVAIL_GB=$(( AVAIL_KB / 1024 / 1024 ))
[ "$AVAIL_GB" -lt "$MIN_DISK_GB" ] && die "Only ${AVAIL_GB}GB free on $EMULATOR_DIR (min ${MIN_DISK_GB}GB required)"
ok "${AVAIL_GB}GB disk space available"
info "Checking database connection..."
mariadb $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "Cannot connect to database $DB_NAME on $DB_HOST:$DB_PORT"
ok "Database connection OK"
if [ "$DRY_RUN" = true ]; then
echo ""
warn "DRY-RUN — skipping actual update"
echo " Site: $NITRO_SITE_URL"
echo " Branch: $NITRO_BRANCH"
echo " Emulator: $EMULATOR_DIR/Emulator"
echo " Nitro-V3: $NITRO_CLIENT"
echo " Renderer: $NITRO_RENDERER"
echo ""
info "Dry-run complete. Pass --dry-run or -n to preview."
exit 0
fi
echo ""
info "Site: $NITRO_SITE_URL | Branch: $NITRO_BRANCH"
info "Log: $LOG_FILE"
# =============================================================================
# HELPERS
# =============================================================================
clean_node_modules() {
rm -rf node_modules 2>/dev/null || sudo rm -rf node_modules 2>/dev/null || true
if [ "$(stat -c '%U' .)" != "$(whoami)" ] && command -v sudo &> /dev/null; then
sudo chown -R "$(whoami)":"$(whoami)" .
fi
}
git_update() {
local repo="$1"
local branch="$2"
cd "$repo"
git stash --include-untracked || true
local old_head
old_head=$(git rev-parse HEAD)
git checkout "$branch"
git pull
[ "$(git rev-parse HEAD)" != "$old_head" ]
}
HAD_UPDATES=false
NITRO_BUILT=false
# =============================================================================
# 1. UPDATE & BUILD EMULATOR
# =============================================================================
step "Update & Build Emulator"
if git_update "$EMULATOR_DIR/Emulator" "$NITRO_BRANCH"; then
info "New commits detected"
HAD_UPDATES=true
ROLLBACK_NEEDED=true
# Database backup
info "Creating database backup..."
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE"; then
LAST_BACKUP="$BACKUP_FILE"
BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0)
[ "$BK_SIZE" -lt 1024 ] && { rm -f "$BACKUP_FILE"; die "Backup is too small (${BK_SIZE}B) — possible corruption, aborting"; }
ok "Backup saved: $(numfmt --to=iec "$BK_SIZE")$BACKUP_FILE"
else
die "Database backup failed"
fi
# SQL imports
info "Checking for new SQL files..."
if [ -d "$SQL_DIR" ]; then
SQL_COUNT=0
while IFS= read -r -d '' sql_file; do
info "Importing: $(basename "$sql_file")"
mariadb $MYSQL_CRED --force "$DB_NAME" < "$sql_file" || warn "Import failed for $(basename "$sql_file")"
SQL_COUNT=$((SQL_COUNT + 1))
done < <(find "$SQL_DIR" -name '*.sql' -mmin -10 -not -path "$BACKUP_DIR/*" -print0 2>/dev/null)
[ "$SQL_COUNT" -gt 0 ] && ok "$SQL_COUNT SQL file(s) imported" || info "No new SQL files found"
else
info "SQL directory not found, skipping"
fi
# Maven build
info "Building emulator (mvn package)..."
cd "$EMULATOR_DIR/Emulator"
mvn package -q || die "Maven build failed"
ok "Build complete"
JAR_FILE=$(find target -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs basename)
[ -z "$JAR_FILE" ] && die "No jar file found in target/"
ok "Jar: $JAR_FILE"
info "Updating emulator launch script..."
cd target/
cat << EOF > emulator
#!/bin/sh
file_name_emulator=emulator.log
current_time=\$(date "+%H%M_%d-%m-%Y")
file_name=\$file_name_emulator.\$current_time
mv /var/log/emu/emulator.log /var/log/emu/\$file_name
java -Dfile.encoding=UTF8 -Xmx2G -jar $JAR_FILE
EOF
chmod +x emulator
ok "Launch script updated"
ROLLBACK_NEEDED=false
else
info "Already up to date, skipping"
fi
# =============================================================================
# 2. UPDATE NITRO_RENDER_V3
# =============================================================================
step "Update Nitro_Render_V3"
if git_update "$NITRO_RENDERER" "$NITRO_BRANCH"; then
info "New commits detected"
HAD_UPDATES=true
yarn install --frozen-lockfile || { info "Retrying with clean node_modules..."; clean_node_modules && yarn install; } || die "yarn install failed for Nitro_Render_V3"
ok "Dependencies installed"
else
info "Already up to date, skipping"
fi
# =============================================================================
# 3. UPDATE & BUILD NITRO-V3
# =============================================================================
step "Update & Build Nitro-V3"
if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then
info "New commits detected"
HAD_UPDATES=true
NITRO_BUILT=true
yarn install --frozen-lockfile || { info "Retrying with clean node_modules..."; clean_node_modules && yarn install; } || die "yarn install failed for Nitro-V3"
ok "Dependencies installed"
info "Building Nitro-V3 (yarn build)..."
yarn build || die "yarn build failed"
ok "Build complete"
else
info "Already up to date, skipping build"
fi
# custom-themes/index.json
mkdir -p "$NITRO_CLIENT/dist/custom-themes"
if [ ! -f "$NITRO_CLIENT/dist/custom-themes/index.json" ]; then
echo '{"themes":[]}' > "$NITRO_CLIENT/dist/custom-themes/index.json"
ok "Created custom-themes/index.json"
else
ok "custom-themes/index.json exists"
fi
# =============================================================================
# 4. SYNC CONFIGS
# =============================================================================
step "Sync Configurations"
mkdir -p "$GAMEDATA_CONF_DIR"
MERGE_SCRIPT="$(dirname "$0")/scripts/merge-config.cjs"
NITRO_DIST_CONFIG_DIR="$NITRO_CLIENT/dist/configuration"
# Make dirs writable
for dir in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do
if [ -d "$dir" ] && [ ! -w "$dir" ] && command -v sudo &> /dev/null; then
sudo chown -R "$(whoami)":"$(whoami)" "$dir" 2>/dev/null || true
fi
done
EXAMPLE_COUNT=0
for example_file in "$NITRO_SRC_DIR"/*.example; do
[ -f "$example_file" ] || continue
base=$(basename "$example_file")
target_name="${base%.example}"
case "$target_name" in *.*) ;; *) target_name="$target_name.json" ;; esac
node "$MERGE_SCRIPT" "$example_file" "$NITRO_SRC_DIR/$target_name" 2>/dev/null
node "$MERGE_SCRIPT" "$example_file" "$GAMEDATA_CONF_DIR/$target_name" 2>/dev/null
if [ -d "$NITRO_DIST_CONFIG_DIR" ]; then
cp "$NITRO_SRC_DIR/$target_name" "$NITRO_DIST_CONFIG_DIR/$target_name" 2>/dev/null || true
fi
EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1))
done
ok "$EXAMPLE_COUNT .example file(s) processed"
# Force critical URLs
info "Applying critical config URLs..."
FORCE_CONFIG_SCRIPT=$(cat << 'PYEOF'
import json, sys, os
paths = [
os.path.join(os.environ.get('NITRO_SRC_DIR', ''), 'renderer-config.json'),
os.path.join(os.environ.get('NITRO_DIST_CONFIG_DIR', ''), 'renderer-config.json'),
os.path.join(os.environ.get('NITRO_SRC_DIR', ''), 'ui-config.json'),
os.path.join(os.environ.get('NITRO_DIST_CONFIG_DIR', ''), 'ui-config.json'),
]
for path in paths:
if not os.path.exists(path):
continue
with open(path, 'r') as f:
data = json.load(f)
for key in ['image.library.url', 'hof.furni.url', 'gamedata.url', 'asset.url']:
env_val = os.environ.get(f'NITRO_{key.upper().replace(".", "_")}')
if env_val:
data[key] = env_val
for key in ['api.url', 'socket.url', 'furni.asset.icon.url']:
if key in data:
env_val = os.environ.get(f'NITRO_{key.upper().replace(".", "_")}')
if env_val:
data[key] = env_val
if 'gamedata.url' in data:
data['radio.url'] = data['gamedata.url'] + '/config/radio-stations.json5?t=%timestamp%'
data['soundboard.url'] = data['gamedata.url'] + '/config/soundboard-sounds.json5?t=%timestamp%'
if 'show.google.ads' in data:
data['show.google.ads'] = False
with open(path, 'w') as f:
json.dump(data, f, indent=(4 if 'dist' not in path else None), separators=(',', ':') if 'dist' in path else (',', ': '))
print(f' [OK] Fixed: {os.path.basename(path)}')
print(' [OK] All config URLs synced')
PYEOF
)
cd "$NITRO_CLIENT" && NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$NITRO_DIST_CONFIG_DIR" \
NITRO_IMAGE_LIBRARY_URL="$NITRO_IMAGE_LIBRARY_URL" NITRO_HOF_FURNITURE_URL="$NITRO_HOF_FURNITURE_URL" \
NITRO_API_URL="$NITRO_API_URL" NITRO_SOCKET_URL="$NITRO_SOCKET_URL" \
NITRO_GAMEDATA_URL="$NITRO_GAMEDATA_URL" NITRO_ASSET_URL="$NITRO_ASSET_URL" \
NITRO_FURNI_ASSET_ICON_URL="$NITRO_FURNI_ASSET_ICON_URL" \
python3 -c "$FORCE_CONFIG_SCRIPT"
# =============================================================================
# 5. CLEANUP
# =============================================================================
step "Cleanup"
info "Removing logs older than 14 days..."
find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null || true
info "Managing backups (keeping max 5)..."
if [ -d "$BACKUP_DIR" ]; then
find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true
fi
if [ "$HAD_UPDATES" = true ]; then
info "Cleaning Yarn cache..."
yarn cache clean 2>/dev/null || true
info "Removing old .jar files..."
find "$EMULATOR_DIR/Emulator/target" -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +2 | sed 's/^[0-9.]* //' | xargs -r rm -f || true
fi
ok "Cleanup complete"
# =============================================================================
# 6. PERMISSIONS
# =============================================================================
step "Set Permissions"
if command -v sudo &> /dev/null; then
for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do
if [ -d "$dir" ]; then
sudo chown -R www-data:www-data "$dir" 2>/dev/null || warn "Could not chown $dir"
fi
done
ok "Permissions set to www-data:www-data"
else
warn "sudo not available, skipping chown"
fi
# =============================================================================
# 7. RESTART SERVICES
# =============================================================================
step "Restart Services"
RESTART_OK=false
if [ "$HAD_UPDATES" = true ]; then
if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
if command -v sudo &> /dev/null; then
info "Restarting $EMULATOR_SERVICE..."
sudo systemctl restart "$EMULATOR_SERVICE" || die "Failed to restart $EMULATOR_SERVICE"
RESTART_OK=true
fi
elif command -v pm2 &> /dev/null; then
info "Restarting PM2 processes..."
pm2 restart all || warn "PM2 restart had issues"
RESTART_OK=true
else
warn "No systemd or PM2 found — restart manually"
fi
if [ "$RESTART_OK" = true ]; then
ok "Services restarted"
fi
else
info "No updates applied, no restart needed"
fi
# =============================================================================
# 8. HEALTH CHECK & VALIDATION
# =============================================================================
step "Health Check & Validation"
ERRORS=0
# Build output check
if [ "$NITRO_BUILT" = true ]; then
if [ -d "$NITRO_CLIENT/dist/assets" ]; then
ASSET_COUNT=$(find "$NITRO_CLIENT/dist/assets" -type f 2>/dev/null | wc -l)
ok "Nitro-V3 built: $ASSET_COUNT assets in dist/assets"
else
fail "Nitro-V3 build assets missing!"
ERRORS=$((ERRORS + 1))
fi
else
info "Skipping build check (no new commits)"
fi
# Health check with retries for emulator
if [ "$HAD_UPDATES" = true ] && systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
info "Waiting for $EMULATOR_SERVICE to become active..."
HEALTHY=false
for i in $(seq 1 "$HEALTH_RETRIES"); do
sleep "$HEALTH_INTERVAL"
STATUS=$(systemctl is-active "$EMULATOR_SERVICE" 2>/dev/null || echo "unknown")
if [ "$STATUS" = "active" ]; then
HEALTHY=true
break
fi
info " Attempt $i/$HEALTH_RETRIES — status: $STATUS"
done
if [ "$HEALTHY" = true ]; then
ok "$EMULATOR_SERVICE is active ($(( i * HEALTH_INTERVAL ))s to start)"
else
fail "$EMULATOR_SERVICE did not become active after ${HEALTH_RETRIES} retries"
ERRORS=$((ERRORS + 1))
fi
fi
# Permission audit
for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do
if [ -d "$dir" ]; then
OWNER=$(stat -c '%U:%G' "$dir" 2>/dev/null || echo "unknown")
if [ "$OWNER" = "www-data:www-data" ] || [ "$(id -u)" -ne 0 ]; then
ok "$(basename "$dir") ($OWNER)"
else
warn "$(basename "$dir") owner is $OWNER (should be www-data:www-data)"
sudo chown -R www-data:www-data "$dir" 2>/dev/null || true
fi
fi
done
# =============================================================================
# SUMMARY
# =============================================================================
ELAPSED=$(( $(date +%s) - START_TIME ))
ELAPSED_FMT=$(printf '%dm %ds' $((ELAPSED/60)) $((ELAPSED%60)))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
if [ "$ERRORS" -eq 0 ]; then
echo "║ ✅ UPDATE SUCCESSFULLY COMPLETED ║"
else
echo "║ ⚠️ UPDATE COMPLETED WITH $ERRORS WARNING(S) ║"
fi
echo "╠══════════════════════════════════════════════════════════════╣"
printf "║ Duration: %-46s║\n" "$ELAPSED_FMT"
printf "║ Updates: %-46s║\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")"
printf "║ Nitro build: %-46s║\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")"
printf "║ Log file: %-46s║\n" "$LOG_FILE"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
notify "SUCCESS" "Completed in $ELAPSED_FMT (${ERRORS} warnings)"