forked from hesabix/arc
512 lines
20 KiB
Shell
Executable file
512 lines
20 KiB
Shell
Executable file
#!/usr/bin/env bash
|
||
# Hesabix update script: pull from repo, migrate backend, restart services, rebuild frontend, reload nginx.
|
||
# pip / Flutter mirrors: scripts/mirror_config.sh (saved in ${APP_ROOT}/.deploy_env from deploy).
|
||
# Run via: hesabix -update [-source URL] [-branch NAME]
|
||
# PostgreSQL pgvector: scripts/ensure_pgvector.sh (idempotent, non-fatal) before Alembic migrations.
|
||
# AI voice (local STT/TTS): scripts/ensure_voice_chat.sh — prompts if deps missing (INSTALL_VOICE in .deploy_env).
|
||
# Requires: API_DOMAIN, UI_DOMAIN, BRANCH, REPO_URL in env or in ${APP_ROOT}/.deploy_env
|
||
# Web build API URL: https if /etc/letsencrypt/live/<API_DOMAIN> exists; else http unless API_PUBLIC_SCHEME is set in env (custom TLS).
|
||
set -euo pipefail
|
||
|
||
APP_ROOT="${APP_ROOT:-/opt/hesabix}"
|
||
LOG_FILE="${APP_ROOT}/update.log"
|
||
CHECK_MARK=$'\xE2\x9C\x94'
|
||
CROSS_MARK=$'\xE2\x9D\x8C'
|
||
|
||
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
|
||
log_ok() { echo "${CHECK_MARK} $*" | tee -a "${LOG_FILE}"; }
|
||
log_err() { echo "${CROSS_MARK} $*" >&2; echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >> "${LOG_FILE}"; }
|
||
|
||
UPDATE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=scripts/mirror_config.sh
|
||
if [[ -r "${UPDATE_SCRIPT_DIR}/scripts/mirror_config.sh" ]]; then
|
||
# shellcheck disable=SC1091
|
||
source "${UPDATE_SCRIPT_DIR}/scripts/mirror_config.sh"
|
||
fi
|
||
# shellcheck source=scripts/hesabix_python.sh
|
||
if [[ -r "${UPDATE_SCRIPT_DIR}/scripts/hesabix_python.sh" ]]; then
|
||
# shellcheck disable=SC1091
|
||
source "${UPDATE_SCRIPT_DIR}/scripts/hesabix_python.sh"
|
||
fi
|
||
|
||
# If normal checkout/pull fails (local generated file, merge, diverged branch), realign the clone with remote.
|
||
hesabix_force_sync_origin() {
|
||
log_info "Force sync with origin/${BRANCH}: git reset --hard (local changes and commits in this clone will be discarded)."
|
||
git fetch origin --prune
|
||
if ! git show-ref -q "origin/${BRANCH}"; then
|
||
log_err "origin/${BRANCH} not found after fetch."
|
||
return 1
|
||
fi
|
||
if git show-ref -q "refs/heads/${BRANCH}"; then
|
||
git checkout -f "${BRANCH}"
|
||
else
|
||
git checkout -b "${BRANCH}" "origin/${BRANCH}"
|
||
fi
|
||
if ! git reset --hard "origin/${BRANCH}"; then
|
||
log_err "git reset --hard origin/${BRANCH} failed."
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Same logic as deploy.sh: Flutter PATH for new shells (/etc/profile.d + idempotent line in bash.bashrc)
|
||
persist_flutter_path_in_profile_d() {
|
||
local f="/etc/profile.d/hesabix-flutter.sh"
|
||
if [[ ! -x /opt/flutter/bin/flutter && ! -x /snap/bin/flutter ]]; then
|
||
return 0
|
||
fi
|
||
cat > "${f}" <<'PROFILE'
|
||
# Hesabix: Flutter در PATH برای شِلهای login (deploy.sh / update.sh) — ترجیحاً دستی ویرایش نشود.
|
||
if [ -x /opt/flutter/bin/flutter ]; then
|
||
case ":${PATH}:" in
|
||
*:/opt/flutter/bin:*) ;;
|
||
*) PATH="/opt/flutter/bin${PATH:+:$PATH}"; export PATH ;;
|
||
esac
|
||
fi
|
||
if [ -x /snap/bin/flutter ]; then
|
||
case ":${PATH}:" in
|
||
*:/snap/bin:*) ;;
|
||
*) PATH="/snap/bin${PATH:+:$PATH}"; export PATH ;;
|
||
esac
|
||
fi
|
||
PROFILE
|
||
chmod 644 "${f}" 2>/dev/null || true
|
||
log_ok "Flutter برای شِلهای login در PATH: ${f}"
|
||
|
||
local marker="# hesabix-flutter-PATH (deploy.sh)"
|
||
if [[ -f /etc/bash.bashrc ]] && ! grep -qF "${marker}" /etc/bash.bashrc 2>/dev/null; then
|
||
printf '\n%s\n[ -r /etc/profile.d/hesabix-flutter.sh ] && . /etc/profile.d/hesabix-flutter.sh\n' "${marker}" >> /etc/bash.bashrc
|
||
log_ok "شِل تعاملی bash: منبع ${f} به /etc/bash.bashrc اضافه شد."
|
||
fi
|
||
}
|
||
|
||
configure_pip_hesabix_mirror() {
|
||
if declare -F hesabix_configure_pip_mirror >/dev/null 2>&1; then
|
||
hesabix_configure_pip_mirror
|
||
return 0
|
||
fi
|
||
if ! command -v python3 >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
python3 -m pip config --user set global.index "https://p.mirror.hesabix.ir/simple" 2>/dev/null || true
|
||
python3 -m pip config --user set global.index-url "https://p.mirror.hesabix.ir/simple" 2>/dev/null || true
|
||
python3 -m pip config --user set global.trusted-host "p.mirror.hesabix.ir" 2>/dev/null || true
|
||
log_info "pip user config: Hesabix PyPI (p.mirror.hesabix.ir/simple)"
|
||
}
|
||
|
||
ensure_api_journalctl_env() {
|
||
local dropin_dir="/etc/systemd/system/hesabix-api.service.d"
|
||
local dropin_file="${dropin_dir}/10-journalctl-env.conf"
|
||
mkdir -p "${dropin_dir}"
|
||
cat > "${dropin_file}" <<'EOF'
|
||
[Service]
|
||
Environment=HESABIX_ALLOW_SUDO_JOURNALCTL=1
|
||
EOF
|
||
systemctl daemon-reload
|
||
log_info "Ensured systemd env: HESABIX_ALLOW_SUDO_JOURNALCTL=1 for hesabix-api."
|
||
}
|
||
|
||
if [[ $EUID -ne 0 ]]; then
|
||
log_err "Please run as root (e.g. sudo hesabix -update)"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -d "${APP_ROOT}/app/.git" ]]; then
|
||
log_err "Hesabix app not found at ${APP_ROOT}/app. Run deploy.sh first."
|
||
exit 1
|
||
fi
|
||
|
||
# Load saved deploy config (.deploy_env)
|
||
if [[ -f "${APP_ROOT}/.deploy_env" ]]; then
|
||
set -a
|
||
# shellcheck source=/dev/null
|
||
source "${APP_ROOT}/.deploy_env"
|
||
set +a
|
||
fi
|
||
|
||
for v in API_DOMAIN UI_DOMAIN BRANCH REPO_URL; do
|
||
if [[ -z "${!v:-}" ]]; then
|
||
log_err "Missing $v. Set it in env or in ${APP_ROOT}/.deploy_env"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
if [[ ! -f "${APP_ROOT}/.db_password" ]]; then
|
||
log_err "Missing ${APP_ROOT}/.db_password"
|
||
exit 1
|
||
fi
|
||
DB_PASSWORD=$(cat "${APP_ROOT}/.db_password")
|
||
export DB_PASSWORD
|
||
|
||
echo "==========================================" | tee -a "${LOG_FILE}"
|
||
log_info "Hesabix update started (repo=${REPO_URL}, branch=${BRANCH})"
|
||
echo "==========================================" | tee -a "${LOG_FILE}"
|
||
|
||
# --- 1. Update from repo ---
|
||
log_info "Step 1: Updating source from repository..."
|
||
cd "${APP_ROOT}/app"
|
||
current_remote=$(git remote get-url origin 2>/dev/null || true)
|
||
if [[ "${current_remote}" != "${REPO_URL}" ]]; then
|
||
git remote set-url origin "${REPO_URL}"
|
||
fi
|
||
git fetch origin --prune
|
||
if ! git show-ref -q "origin/${BRANCH}"; then
|
||
log_err "شاخه origin/${BRANCH} روی remote نیست. BRANCH و REPO_URL را در ${APP_ROOT}/.deploy_env بررسی کنید."
|
||
exit 1
|
||
fi
|
||
if git checkout -B "${BRANCH}" "origin/${BRANCH}" && git pull origin "${BRANCH}" --ff-only; then
|
||
:
|
||
else
|
||
log_info "بهروزرسانی معمولی Git ناموفق (مثلاً تغییرات محلی یا همنشانی نشدن شاخه). در حال بازیابی با reset --hard..."
|
||
if ! hesabix_force_sync_origin; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
log_ok "Source updated."
|
||
|
||
# --- 2. Backend: pip, migrations, restart services ---
|
||
log_info "Step 2: Backend – install deps, migrations, restart services..."
|
||
configure_pip_hesabix_mirror
|
||
ensure_api_journalctl_env
|
||
api_dir="${APP_ROOT}/app/hesabixAPI"
|
||
if [[ ! -d "${api_dir}/.venv" ]]; then
|
||
log_err "Backend venv not found. Run full deploy first."
|
||
exit 1
|
||
fi
|
||
cd "${api_dir}"
|
||
if declare -F hesabix_resolve_backend_python >/dev/null 2>&1; then
|
||
backend_python=""
|
||
if ! backend_python=$(hesabix_resolve_backend_python); then
|
||
log_info "Python >= 3.11 not found; installing packages..."
|
||
if ! hesabix_install_backend_python_packages || ! backend_python=$(hesabix_resolve_backend_python); then
|
||
log_err "hesabix-api requires Python >= 3.11."
|
||
exit 1
|
||
fi
|
||
fi
|
||
if ! hesabix_ensure_backend_venv "${api_dir}" "${backend_python}"; then
|
||
log_err "Failed to ensure backend virtualenv with ${backend_python}"
|
||
exit 1
|
||
fi
|
||
if [[ "${HESABIX_VENV_RECREATED:-0}" == "1" ]]; then
|
||
log_info "Backend virtualenv rebuilt with Python >= 3.11."
|
||
fi
|
||
fi
|
||
# shellcheck disable=SC1091
|
||
source .venv/bin/activate
|
||
if declare -F hesabix_apply_pip_mirror_env >/dev/null 2>&1; then
|
||
hesabix_apply_pip_mirror_env
|
||
else
|
||
export PIP_INDEX_URL="${PIP_INDEX_URL:-https://p.mirror.hesabix.ir/simple}"
|
||
export PIP_TRUSTED_HOST="${PIP_TRUSTED_HOST:-p.mirror.hesabix.ir}"
|
||
fi
|
||
log_info "Installing backend deps from PyPI: ${PIP_INDEX_URL}"
|
||
pip install --upgrade pip setuptools wheel -q
|
||
pip install -e . -q
|
||
ensure_voice="${APP_ROOT}/app/scripts/ensure_voice_chat.sh"
|
||
if [[ -f "${ensure_voice}" ]]; then
|
||
chmod +x "${ensure_voice}" 2>/dev/null || true
|
||
log_info "AI voice chat (optional local STT/TTS)..."
|
||
if bash "${ensure_voice}" --update; then
|
||
log_ok "Voice chat prerequisites check completed."
|
||
else
|
||
log_info "Voice chat deps skipped or failed (non-fatal)."
|
||
fi
|
||
fi
|
||
ensure_pgvector="${APP_ROOT}/app/scripts/ensure_pgvector.sh"
|
||
if [[ -f "${ensure_pgvector}" ]]; then
|
||
chmod +x "${ensure_pgvector}" 2>/dev/null || true
|
||
log_info "Ensuring PostgreSQL pgvector package (optional)..."
|
||
if bash "${ensure_pgvector}"; then
|
||
log_ok "pgvector package check completed."
|
||
else
|
||
log_info "pgvector package not installed (non-fatal)."
|
||
fi
|
||
fi
|
||
fixup_db="${APP_ROOT}/app/scripts/hesabix_fixup_db_privileges.sh"
|
||
if [[ -f "${fixup_db}" ]]; then
|
||
chmod +x "${fixup_db}" 2>/dev/null || true
|
||
log_info "Ensuring hesabix owns public schema objects (Alembic/API access)..."
|
||
bash "${fixup_db}"
|
||
fi
|
||
# Ensure alembic_version.version_num is VARCHAR(255) for long revision IDs (fixes StringDataRightTruncation)
|
||
log_info "Ensuring alembic_version schema compatibility..."
|
||
PGPASSWORD="${DB_PASSWORD}" psql -h 127.0.0.1 -U hesabix -d hesabix -tAc "
|
||
DO \$\$
|
||
BEGIN
|
||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='alembic_version') THEN
|
||
ALTER TABLE public.alembic_version ALTER COLUMN version_num TYPE VARCHAR(255);
|
||
ELSE
|
||
CREATE TABLE public.alembic_version (version_num VARCHAR(255) PRIMARY KEY);
|
||
END IF;
|
||
EXCEPTION WHEN OTHERS THEN NULL;
|
||
END \$\$;
|
||
" 2>/dev/null || true
|
||
log_info "Running Alembic migrations..."
|
||
if ! alembic upgrade head; then
|
||
log_err "Migrations failed."
|
||
exit 1
|
||
fi
|
||
log_ok "Migrations done."
|
||
ensure_secrets="${APP_ROOT}/app/scripts/ensure_api_production_secrets.sh"
|
||
if [[ -f "${ensure_secrets}" ]]; then
|
||
chmod +x "${ensure_secrets}" 2>/dev/null || true
|
||
log_info "Ensuring API production secrets in .env..."
|
||
if bash "${ensure_secrets}"; then
|
||
log_ok "API production secrets verified."
|
||
else
|
||
log_err "Failed to ensure API production secrets."
|
||
exit 1
|
||
fi
|
||
fi
|
||
chown -R www-data:www-data "${api_dir}"
|
||
systemctl daemon-reload
|
||
# Softphone Media Edge holds media_hub in-memory (workers=1); restart with API when installed.
|
||
_hesabix_restart_units=(hesabix-api hesabix-rq-worker hesabix-notification-moderation)
|
||
if [[ "$(systemctl show hesabix-api-media.service -p LoadState --value 2>/dev/null)" == "loaded" ]]; then
|
||
_hesabix_restart_units+=(hesabix-api-media)
|
||
fi
|
||
systemctl restart "${_hesabix_restart_units[@]}"
|
||
sleep 3
|
||
for svc in hesabix-api; do
|
||
if ! systemctl is-active --quiet "$svc"; then
|
||
log_err "Service $svc failed to start. Check: journalctl -u $svc"
|
||
exit 1
|
||
fi
|
||
done
|
||
if [[ " ${_hesabix_restart_units[*]} " == *" hesabix-api-media "* ]]; then
|
||
if ! systemctl is-active --quiet hesabix-api-media; then
|
||
log_err "Service hesabix-api-media failed to start. Check: journalctl -u hesabix-api-media"
|
||
exit 1
|
||
fi
|
||
log_ok "hesabix-api-media (Softphone Media Edge) restarted."
|
||
fi
|
||
unset _hesabix_restart_units
|
||
log_ok "Backend services restarted."
|
||
|
||
# --- 3. Flutter: update SDK, build web, deploy (PATH دائمی: /etc/profile.d/hesabix-flutter.sh) ---
|
||
log_info "Step 3: Flutter – update SDK, build web, deploy..."
|
||
export PATH="/opt/flutter/bin:/snap/bin:${PATH:-}"
|
||
ensure_flutter_sdk="${APP_ROOT}/app/scripts/ensure_flutter_sdk_for_update.sh"
|
||
if [[ -f "${ensure_flutter_sdk}" ]]; then
|
||
chmod +x "${ensure_flutter_sdk}" 2>/dev/null || true
|
||
if declare -F hesabix_apply_flutter_mirror_env >/dev/null 2>&1; then
|
||
hesabix_apply_flutter_mirror_env
|
||
else
|
||
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://f.mirror.hesabix.ir/pub}"
|
||
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://f.mirror.hesabix.ir/gcs}"
|
||
fi
|
||
log_info "Ensuring Flutter/Dart SDK (mirror fallbacks; git upgrade only if HESABIX_UPDATE_FLUTTER_SDK=1)..."
|
||
if ! bash "${ensure_flutter_sdk}"; then
|
||
log_err "Flutter/Dart SDK not ready. Set HESABIX_UPDATE_FLUTTER_SDK=0 or fix mirror access, then retry."
|
||
exit 1
|
||
fi
|
||
else
|
||
if [[ -d /opt/flutter ]]; then
|
||
(cd /opt/flutter && git fetch --depth 1 origin stable 2>/dev/null && git reset --hard origin/stable 2>/dev/null) || true
|
||
fi
|
||
fi
|
||
if ! command -v flutter >/dev/null 2>&1; then
|
||
log_err "Flutter not in PATH. Ensure Flutter is installed (e.g. run deploy.sh once)."
|
||
exit 1
|
||
fi
|
||
persist_flutter_path_in_profile_d
|
||
if declare -F hesabix_resolve_flutter_storage_base_url >/dev/null 2>&1; then
|
||
hesabix_resolve_flutter_storage_base_url || true
|
||
elif declare -F hesabix_apply_flutter_mirror_env >/dev/null 2>&1; then
|
||
hesabix_apply_flutter_mirror_env
|
||
else
|
||
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://f.mirror.hesabix.ir/pub}"
|
||
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://f.mirror.hesabix.ir/gcs}"
|
||
fi
|
||
log_info "Flutter pub/storage: PUB_HOSTED_URL=${PUB_HOSTED_URL} FLUTTER_STORAGE_BASE_URL=${FLUTTER_STORAGE_BASE_URL}"
|
||
app_dir="${APP_ROOT}/app"
|
||
build_script="${app_dir}/build_web.sh"
|
||
if [[ ! -f "${build_script}" ]]; then
|
||
log_err "build_web.sh not found: ${build_script}"
|
||
exit 1
|
||
fi
|
||
chmod +x "${build_script}"
|
||
# shellcheck disable=SC1091
|
||
if [[ -r "${app_dir}/scripts/api_public_scheme.sh" ]]; then
|
||
source "${app_dir}/scripts/api_public_scheme.sh"
|
||
fi
|
||
if ! declare -F hesabix_resolve_api_public_scheme >/dev/null 2>&1; then
|
||
hesabix_resolve_api_public_scheme() {
|
||
if [[ -n "${API_DOMAIN:-}" ]] && [[ -d "/etc/letsencrypt/live/${API_DOMAIN}" ]]; then
|
||
printf '%s' "https"; return 0
|
||
fi
|
||
local s="${API_PUBLIC_SCHEME:-}"
|
||
s="${s,,}"
|
||
case "$s" in http|https) printf '%s' "$s"; return 0 ;; esac
|
||
printf '%s' "http"
|
||
}
|
||
fi
|
||
api_scheme="$(hesabix_resolve_api_public_scheme)"
|
||
api_url="${api_scheme}://${API_DOMAIN}"
|
||
cd "${app_dir}"
|
||
if ! env PATH="/opt/flutter/bin:/snap/bin:$PATH" \
|
||
PUB_HOSTED_URL="${PUB_HOSTED_URL:-}" FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-}" \
|
||
SKIP_NGINX_ENSURE=1 \
|
||
bash build_web.sh --mode release --api-base-url "${api_url}" --clean --install-deps; then
|
||
log_err "Frontend build failed."
|
||
exit 1
|
||
fi
|
||
# اطمینان از nginx برای UI در «گام ۴» انجام میشود (یکبار، idempotent)؛ اینجا عمداً SKIP_NGINX_ENSURE تا دوبارهکاری نشود.
|
||
build_output="${app_dir}/hesabixUI/hesabix_ui/build/web"
|
||
if [[ ! -f "${build_output}/index.html" ]]; then
|
||
log_err "Build output missing index.html."
|
||
exit 1
|
||
fi
|
||
mkdir -p "/var/www/${UI_DOMAIN}"
|
||
rsync -a --delete "${build_output}/" "/var/www/${UI_DOMAIN}/"
|
||
chown -R www-data:www-data "/var/www/${UI_DOMAIN}"
|
||
log_ok "Frontend built and deployed to /var/www/${UI_DOMAIN}."
|
||
|
||
# --- 4. Nginx: ensure client_max_body_size 1g for database restore, then reload ---
|
||
log_info "Step 4: Updating Nginx config and reloading..."
|
||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||
hesabix_nginx_bin() {
|
||
if command -v nginx >/dev/null 2>&1; then
|
||
command -v nginx
|
||
return 0
|
||
fi
|
||
if [[ -x /usr/sbin/nginx ]]; then
|
||
echo /usr/sbin/nginx
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
ensure_ai_chat_sse_location() {
|
||
local conf="$1"
|
||
[[ -f "$conf" ]] || return 0
|
||
if grep -q 'location \^~ /api/v1/ai/chat/' "$conf"; then
|
||
return 0
|
||
fi
|
||
python3 - "$conf" <<'PY'
|
||
from pathlib import Path
|
||
import sys
|
||
|
||
path = Path(sys.argv[1])
|
||
text = path.read_text()
|
||
needle = " location /api/ {\n"
|
||
if "location ^~ /api/v1/ai/chat/" in text or needle not in text:
|
||
raise SystemExit(0)
|
||
block = """ # AI chat SSE: stream chunks immediately (no proxy buffering)
|
||
location ^~ /api/v1/ai/chat/ {
|
||
proxy_pass http://127.0.0.1:8000/api/v1/ai/chat/;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 600;
|
||
proxy_connect_timeout 60;
|
||
proxy_send_timeout 600;
|
||
client_max_body_size 1g;
|
||
proxy_buffering off;
|
||
proxy_request_buffering off;
|
||
proxy_cache off;
|
||
gzip off;
|
||
add_header X-Accel-Buffering no always;
|
||
}
|
||
|
||
"""
|
||
path.write_text(text.replace(needle, block + needle, 1))
|
||
PY
|
||
}
|
||
if [[ -f /etc/nginx/sites-available/hesabix-api.conf ]]; then
|
||
if grep -q 'client_max_body_size' /etc/nginx/sites-available/hesabix-api.conf; then
|
||
sed -i 's/client_max_body_size [0-9]*[kmgKMG]*/client_max_body_size 1g/' /etc/nginx/sites-available/hesabix-api.conf
|
||
else
|
||
sed -i '/proxy_send_timeout 300;/a\ client_max_body_size 1g;' /etc/nginx/sites-available/hesabix-api.conf
|
||
fi
|
||
log_info "Ensured client_max_body_size 1g (database restore uploads)."
|
||
fi
|
||
ensure_ai_chat_sse_location /etc/nginx/sites-available/hesabix-api.conf
|
||
ensure_ai_chat_sse_location /etc/nginx/sites-available/hesabix-ui.conf
|
||
log_info "Ensured AI chat SSE nginx location (no proxy buffering)."
|
||
ensure_voice_ws_nginx() {
|
||
local conf="$1"
|
||
[[ -f "$conf" ]] || return 0
|
||
if grep -q 'location /ws/' "$conf"; then
|
||
if ! grep -q 'proxy_read_timeout 86400' "$conf" 2>/dev/null; then
|
||
sed -i '/location \/ws\//,/^[[:space:]]*}/ s/proxy_read_timeout [0-9]*;/proxy_read_timeout 86400;/' "$conf" 2>/dev/null || true
|
||
fi
|
||
return 0
|
||
fi
|
||
python3 - "$conf" <<'PY'
|
||
from pathlib import Path
|
||
import sys
|
||
path = Path(sys.argv[1])
|
||
text = path.read_text()
|
||
if "location /ws/" in text:
|
||
raise SystemExit(0)
|
||
block = """
|
||
# WebSocket (/ws/ai/voice, notifications, ...)
|
||
location /ws/ {
|
||
proxy_pass http://127.0.0.1:8000/ws/;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 86400;
|
||
proxy_send_timeout 86400;
|
||
}
|
||
"""
|
||
needle = " location /api/ {\n"
|
||
if needle not in text:
|
||
raise SystemExit(0)
|
||
path.write_text(text.replace(needle, block + needle, 1))
|
||
PY
|
||
}
|
||
ensure_voice_ws_nginx /etc/nginx/sites-available/hesabix-api.conf
|
||
ensure_voice_ws_nginx /etc/nginx/sites-available/hesabix-ui.conf
|
||
log_info "Ensured WebSocket nginx location (/ws/ai/voice)."
|
||
# UI: version.json، service worker، flutter_bootstrap.js و main.dart.js بدون کش یکسالهٔ immutable
|
||
# اگر هر دو location ورودی JS از قبل باشد، اسکریپت بلافاصله خارج میشود و فایل nginx را دست نمیزند.
|
||
ensure_ui_nginx="${app_dir}/scripts/ensure_nginx_ui_version_probe.sh"
|
||
if [[ -f "${ensure_ui_nginx}" ]]; then
|
||
chmod +x "${ensure_ui_nginx}" 2>/dev/null || true
|
||
log_info "Step 4 (UI): Ensuring nginx cache rules for Flutter web (idempotent)..."
|
||
if bash "${ensure_ui_nginx}"; then
|
||
log_ok "Nginx UI version/SW/entry-JS rules verified or updated."
|
||
else
|
||
log_info "Nginx UI ensure skipped or failed (e.g. no hesabix-ui.conf on this host); non-fatal."
|
||
fi
|
||
else
|
||
log_info "ensure_nginx_ui_version_probe.sh not found at ${ensure_ui_nginx}; skipped."
|
||
fi
|
||
NGINX_BIN="$(hesabix_nginx_bin)" || {
|
||
log_err "nginx not found in PATH or /usr/sbin/nginx"
|
||
exit 1
|
||
}
|
||
nginx_test_out="$("$NGINX_BIN" -t 2>&1)" || nginx_test_rc=$?
|
||
if [[ -n "${nginx_test_out:-}" ]]; then
|
||
echo "${nginx_test_out}" | tee -a "${LOG_FILE}"
|
||
fi
|
||
if [[ "${nginx_test_rc:-0}" -ne 0 ]]; then
|
||
log_err "Nginx config test failed (${NGINX_BIN} -t exit ${nginx_test_rc})."
|
||
exit 1
|
||
fi
|
||
if ! systemctl reload nginx; then
|
||
log_err "systemctl reload nginx failed."
|
||
exit 1
|
||
fi
|
||
log_ok "Nginx reloaded."
|
||
|
||
# --- 5. Optional health check ---
|
||
if command -v curl >/dev/null 2>&1; then
|
||
if curl -sSf --connect-timeout 5 "https://${API_DOMAIN}/api/v1/health" >/dev/null 2>&1 || \
|
||
curl -sSf --connect-timeout 5 "http://127.0.0.1:8000/api/v1/health" >/dev/null 2>&1; then
|
||
log_ok "API health check passed."
|
||
else
|
||
log_info "API health check skipped or failed (non-fatal)."
|
||
fi
|
||
fi
|
||
|
||
echo "==========================================" | tee -a "${LOG_FILE}"
|
||
log_ok "Hesabix update completed."
|
||
echo "==========================================" | tee -a "${LOG_FILE}"
|