#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="${script_dir}" default_external_auth_repo_url="https://gitea.qortal.link/crowetic/Qortal-External-Auth.git" source "${repo_root}/scripts/lib-compose-project.sh" env_file="${repo_root}/.env.devprod" topology="auto" host_broker_dir="/opt/qortal-broker" host_broker_dir_explicit="0" host_force_refresh_contexts="0" host_finish_nextcloud_upgrade="0" host_rebuild_qortal="0" resolve_cli_path() { local raw="${1:-}" if [[ -z "${raw}" ]]; then printf '%s' "" elif [[ "${raw}" == /* ]]; then printf '%s' "${raw}" else printf '%s' "${repo_root}/${raw}" fi } delegate_host_hybrid_recreate() { local update_args=(--broker-dir "${host_broker_dir}") if [[ "${host_force_refresh_contexts}" == "1" ]]; then update_args+=(--force-refresh-contexts) fi if [[ "${host_finish_nextcloud_upgrade}" == "1" ]]; then update_args+=(--finish-host-nextcloud-upgrade) fi echo "Detected host-hybrid NuQloud install." echo "Delegating full repair/update to scripts/update-host-nextcloud-hybrid.sh." "${repo_root}/scripts/update-host-nextcloud-hybrid.sh" "${update_args[@]}" if [[ "${host_rebuild_qortal}" == "1" && -x "${repo_root}/scripts/restart-host-nextcloud-hybrid.sh" ]]; then echo "Recreating host-hybrid services with qortal_node rebuild..." "${repo_root}/scripts/restart-host-nextcloud-hybrid.sh" --broker-dir "${host_broker_dir}" --qortal fi exit 0 } repo_devprod_install_present() { [[ -f "${env_file}" ]] || return 1 [[ -f "${repo_root}/docker-compose.devprod.yml" || -f "${repo_root}/docker-compose.devprod.nossl.yml" ]] } host_hybrid_install_present() { [[ -f "${host_broker_dir}/.env" && -f "${host_broker_dir}/docker-compose.yml" ]] } auto_select_host_broker_dir() { local candidate if [[ "${host_broker_dir_explicit}" == "1" ]]; then return 0 fi for candidate in \ "/opt/qortal-broker" \ "${repo_root}/qortal-broker" \ "${repo_root}/broker"; do if [[ -f "${candidate}/.env" && -f "${candidate}/docker-compose.yml" ]]; then host_broker_dir="${candidate}" return 0 fi done } resolve_auto_topology() { local repo_present="0" local host_present="0" local choice="" auto_select_host_broker_dir repo_devprod_install_present && repo_present="1" host_hybrid_install_present && host_present="1" if [[ "${host_broker_dir_explicit}" == "1" && "${host_present}" == "1" ]]; then delegate_host_hybrid_recreate fi if [[ "${repo_present}" == "1" && "${host_present}" == "0" ]]; then echo "Detected repo devprod NuQloud install." return 0 fi if [[ "${repo_present}" == "0" && "${host_present}" == "1" ]]; then delegate_host_hybrid_recreate fi if [[ "${repo_present}" == "1" && "${host_present}" == "1" ]]; then echo "Detected more than one NuQloud install shape:" echo " 1) repo devprod container stack (${env_file})" echo " 2) host-hybrid broker stack (${host_broker_dir})" if [[ -t 0 ]]; then read -r -p "Which install should recreate-devprod repair? [1/2] (default: 1): " choice case "${choice}" in 2) delegate_host_hybrid_recreate ;; ""|1) echo "Using repo devprod container stack." return 0 ;; *) echo "Invalid selection: ${choice}" exit 1 ;; esac fi echo "Non-interactive shell detected; using repo devprod container stack." return 0 fi } topology_next="0" broker_dir_next="0" for arg in "$@"; do if [[ "${topology_next}" == "1" ]]; then topology="${arg}" topology_next="0" continue fi if [[ "${broker_dir_next}" == "1" ]]; then host_broker_dir="${arg}" host_broker_dir_explicit="1" broker_dir_next="0" continue fi case "${arg}" in --topology) topology_next="1" ;; --topology=*) topology="${arg#*=}" ;; --host-hybrid) topology="host-hybrid" ;; --repo-devprod) topology="repo-devprod" ;; --broker-dir) broker_dir_next="1" ;; --broker-dir=*) host_broker_dir="${arg#*=}" host_broker_dir_explicit="1" ;; --force-refresh-contexts) host_force_refresh_contexts="1" ;; --finish-host-nextcloud-upgrade) host_finish_nextcloud_upgrade="1" ;; --qortal|--full|--qortal=*|--qortal-branch|--qortal-branch=*) host_rebuild_qortal="1" ;; esac done if [[ "${topology_next}" == "1" ]]; then echo "--topology requires a value" exit 1 fi if [[ "${broker_dir_next}" == "1" ]]; then echo "--broker-dir requires a value" exit 1 fi if [[ "${topology}" != "auto" && "${topology}" != "repo-devprod" && "${topology}" != "host-hybrid" ]]; then echo "Invalid topology: ${topology}" exit 1 fi host_broker_dir="$(resolve_cli_path "${host_broker_dir}")" if [[ "${topology}" == "host-hybrid" ]]; then delegate_host_hybrid_recreate fi if [[ "${topology}" == "auto" ]]; then resolve_auto_topology fi if [[ ! -f "${env_file}" ]]; then echo "Missing ${env_file}. Run ./start-devprod.sh first." echo "No host-hybrid broker stack was found at ${host_broker_dir} either." echo "For host-hybrid installs, run: ./recreate-devprod.sh --host-hybrid --broker-dir " exit 1 fi if ! export_compose_project_name_from_env "${env_file}"; then warn_missing_compose_project_name "${env_file}" fi if [[ -x "${repo_root}/scripts/ensure-env-keys.sh" ]]; then "${repo_root}/scripts/ensure-env-keys.sh" \ --env-file "${env_file}" \ --template-file "${repo_root}/.env.example" fi read_kv() { local key="$1" local line line="$(grep -m1 -E "^${key}=" "${env_file}" || true)" if [[ -z "${line}" ]]; then return 1 fi echo "${line#*=}" } set_kv() { local key="$1" local value="$2" local tmp_file="" tmp_file="$(mktemp)" awk -v key="${key}" -v value="${value}" ' BEGIN { found = 0 } $0 ~ "^" key "=" { print key "=" value found = 1 next } { print } END { if (!found) { print key "=" value } } ' "${env_file}" > "${tmp_file}" mv "${tmp_file}" "${env_file}" } nextcloud_image_major() { local image="$1" local tag="${image##*:}" if [[ "${tag}" =~ ^([0-9]+)([.-]|$) ]]; then echo "${BASH_REMATCH[1]}" return 0 fi return 1 } nextcloud_version_major() { local version="$1" if [[ "${version}" =~ ^([0-9]+)([.-]|$) ]]; then echo "${BASH_REMATCH[1]}" return 0 fi return 1 } local_nextcloud_config_version() { local config_php="${repo_root}/nextcloud/html/config/config.php" if [[ ! -f "${config_php}" ]]; then return 1 fi sed -n "s/.*['\"]version['\"][[:space:]]*=>[[:space:]]*['\"]\\([^'\"]*\\)['\"].*/\\1/p" "${config_php}" | head -n1 } nextcloud_image_from_version() { local version="$1" local major="" local minor="" local patch="" IFS=. read -r major minor patch _rest <<< "${version}" if [[ -z "${major}" || -z "${minor}" || -z "${patch}" ]]; then return 1 fi echo "nextcloud:${major}.${minor}.${patch}-apache" } running_app_image() { local container_id="" container_id="$(docker compose -f "${compose_file}" --env-file "${env_file}" ps -q app 2>/dev/null | head -n1 || true)" if [[ -z "${container_id}" ]]; then return 1 fi docker inspect --format '{{.Config.Image}}' "${container_id}" 2>/dev/null } preserve_running_nextcloud_image_if_newer() { local configured_image="" local configured_major="" local installed_image="" local installed_major="" local installed_version="" local running_image="" local running_major="" if [[ -n "${nextcloud_image_override}" ]]; then return 0 fi configured_image="$(read_kv "NEXTCLOUD_IMAGE" || true)" configured_image="${configured_image:-nextcloud:34-apache}" configured_major="$(nextcloud_image_major "${configured_image}" || true)" installed_version="$(local_nextcloud_config_version || true)" if [[ -n "${installed_version}" ]]; then installed_major="$(nextcloud_version_major "${installed_version}" || true)" installed_image="$(nextcloud_image_from_version "${installed_version}" || true)" if [[ -n "${configured_major}" && -n "${installed_major}" && -n "${installed_image}" ]] && (( installed_major > configured_major )); then export NEXTCLOUD_IMAGE="${installed_image}" set_kv "NEXTCLOUD_IMAGE" "${installed_image}" echo "Detected installed Nextcloud version ${installed_version} is newer than configured image ${configured_image}." echo "Preserving installed version and updating ${env_file}: NEXTCLOUD_IMAGE=${installed_image}" return 0 fi fi running_image="$(running_app_image || true)" if [[ -z "${running_image}" ]]; then return 0 fi running_major="$(nextcloud_image_major "${running_image}" || true)" if [[ -z "${configured_major}" || -z "${running_major}" ]]; then return 0 fi if (( running_major > configured_major )); then export NEXTCLOUD_IMAGE="${running_image}" set_kv "NEXTCLOUD_IMAGE" "${running_image}" echo "Detected running Nextcloud image ${running_image} is newer than configured ${configured_image}." echo "Preserving running image and updating ${env_file}: NEXTCLOUD_IMAGE=${running_image}" fi } resolve_path_from_env() { local key="$1" local default_path="$2" local value value="$(read_kv "${key}" || true)" value="${value:-${default_path}}" if [[ "${value}" == /* ]]; then echo "${value}" else echo "${repo_root}/${value}" fi } ensure_docker_access() { local docker_path="" docker_path="$(type -P docker || true)" if [[ -z "${docker_path}" ]]; then echo "docker is required" exit 1 fi if "${docker_path}" info >/dev/null 2>&1; then DOCKER_CMD=("${docker_path}") DOCKER_REQUIRES_SUDO="0" return 0 fi if ! command -v sudo >/dev/null 2>&1; then echo "docker is installed but this user cannot access it, and sudo is unavailable." exit 1 fi if sudo -n "${docker_path}" info >/dev/null 2>&1; then DOCKER_CMD=(sudo "${docker_path}") DOCKER_REQUIRES_SUDO="1" echo "Using sudo for docker commands." return 0 fi echo "docker requires elevated privileges on this machine." echo "You may be prompted for your sudo password." if sudo "${docker_path}" info >/dev/null 2>&1; then DOCKER_CMD=(sudo "${docker_path}") DOCKER_REQUIRES_SUDO="1" echo "Using sudo for docker commands." return 0 fi echo "Unable to access docker, even with sudo." exit 1 } docker() { "${DOCKER_CMD[@]}" "$@" } ensure_context_repo() { local label="$1" local target_path="$2" local repo_url="$3" if [[ -d "${target_path}/.git" ]]; then echo "Using existing ${label} repo at ${target_path}" return 0 fi if [[ -e "${target_path}" && ! -d "${target_path}/.git" ]]; then echo "${label} context exists but is not a git repo: ${target_path}" echo "Set a valid path in ${env_file} or remove this path." exit 1 fi echo "Cloning ${label} repo into ${target_path}" mkdir -p "$(dirname "${target_path}")" if ! git clone --depth 1 "${repo_url}" "${target_path}"; then echo "Failed to clone ${label} from ${repo_url}" echo "Set the context manually in ${env_file} and rerun." exit 1 fi } refresh_context_repo() { local label="$1" local target_path="$2" local branch="" if [[ ! -d "${target_path}/.git" ]]; then echo "Cannot refresh ${label}; not a git repo: ${target_path}" exit 1 fi if ! command -v git >/dev/null 2>&1; then echo "git is required to refresh ${label}." exit 1 fi if [[ -n "$(git -C "${target_path}" status --porcelain)" ]]; then echo "${label} checkout has local changes; refusing to auto-pull: ${target_path}" echo "Commit/stash those changes or update EXTERNAL_AUTH_CONTEXT to a clean checkout." exit 1 fi branch="$(git -C "${target_path}" branch --show-current)" if [[ -z "${branch}" ]]; then branch="$(git -C "${target_path}" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##' || true)" fi if [[ -z "${branch}" ]]; then branch="main" fi echo "Refreshing ${label} repo at ${target_path} (${branch})..." git -C "${target_path}" fetch origin if git -C "${target_path}" show-ref --verify --quiet "refs/heads/${branch}"; then git -C "${target_path}" checkout "${branch}" else git -C "${target_path}" checkout -B "${branch}" "origin/${branch}" fi git -C "${target_path}" pull --ff-only origin "${branch}" } prompt_qortal_branch_if_needed() { local use_specific_branch="" local requested_branch="" if [[ "${recreate_qortal}" != "1" || -n "${qortal_branch}" || ! -t 0 ]]; then return 0 fi read -r -p "Use a specific Qortal Core branch for this recreate? (y/N): " use_specific_branch if [[ ! "${use_specific_branch}" =~ ^[Yy]$ ]]; then return 0 fi while true; do read -r -p "Qortal branch name: " requested_branch requested_branch="${requested_branch//[$'\r\n']}" if [[ -z "${requested_branch}" ]]; then echo "Branch name cannot be empty." continue fi if git check-ref-format --branch "${requested_branch}" >/dev/null 2>&1; then qortal_branch="${requested_branch}" echo "Using Qortal branch override for this run: ${qortal_branch}" return 0 fi echo "Invalid Qortal branch name: ${requested_branch}" done } cleanup_nextcloud_defaults() { local defaults_config_status=0 local firstrun_disable_status=0 local errexit_was_set="0" [[ $- == *e* ]] && errexit_was_set="1" set +e "${repo_root}/scripts/configure-nextcloud-defaults.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" \ --restore-core-skeleton >/dev/null 2>&1 defaults_config_status=$? docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ app:disable firstrunwizard >/dev/null 2>&1 firstrun_disable_status=$? if [[ "${errexit_was_set}" == "1" ]]; then set -e fi if [[ "${defaults_config_status}" -ne 0 ]]; then echo "Warning: could not configure Nextcloud default onboarding/skeleton settings." fi if [[ "${firstrun_disable_status}" -ne 0 ]]; then echo "Warning: could not disable the Nextcloud First Run Wizard app." fi } occ_has_command() { local command_name="${1:-}" if [[ -z "${command_name}" ]]; then return 1 fi docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ list 2>/dev/null \ | grep -qE "^[[:space:]]+${command_name}([[:space:]]|$)" } occ_exec_quiet_retry() { local rc=1 local max_attempts="${OCC_RETRY_ATTEMPTS:-6}" local retry_sleep="${OCC_RETRY_SLEEP_SECONDS:-4}" local attempt if [[ $# -eq 0 ]]; then return 1 fi for attempt in $(seq 1 "${max_attempts}"); do docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "$@" >/dev/null 2>&1 rc=$? if [[ "${rc}" -eq 0 ]]; then return 0 fi sleep "${retry_sleep}" done return "${rc}" } verify_cron_container_user() { local cron_uid cron_uid="$(docker compose -f "${compose_file}" --env-file "${env_file}" exec -T cron id -u 2>/dev/null | tr -d '\r[:space:]' || true)" if [[ "${cron_uid}" == "33" ]]; then echo "Cron container user verified: 33 (www-data)." return 0 fi echo "Warning: cron container is running as UID ${cron_uid:-unknown}, expected 33 (www-data)." echo "Recreate cron container to apply compose user settings:" echo " docker compose -f ${compose_file} --env-file ${env_file} up -d --force-recreate --no-deps cron" return 1 } service_in_resolved_config() { local needle="${1:-}" local svc if [[ -z "${needle}" ]]; then return 1 fi for svc in "${services[@]:-}"; do if [[ "${svc}" == "${needle}" ]]; then return 0 fi done return 1 } occ_retoggle_app() { local app_name="${1:-}" local force_enable="${2:-0}" if [[ -z "${app_name}" ]]; then return 1 fi # Ignore disable failures (already disabled / transient OCC issues), then enforce enable. occ_exec_quiet_retry app:disable "${app_name}" >/dev/null 2>&1 || true if [[ "${force_enable}" == "1" ]]; then occ_exec_quiet_retry app:enable --force "${app_name}" else occ_exec_quiet_retry app:enable "${app_name}" fi } run_quiet_step() { local label="${1:-}" shift || true local start_ts end_ts rc start_ts="$(date +%s)" echo " ${label}..." "$@" >/dev/null 2>&1 rc=$? end_ts="$(date +%s)" echo " ${label}: status=${rc} elapsed=$((end_ts - start_ts))s" return "${rc}" } mode="ssl" extauth="0" recreate_qortal="0" qortal_branch="" full_rebuild="0" nextcloud_image_override="" nextcloud_image_next="0" qortal_branch_next="0" for arg in "$@"; do if [[ "${nextcloud_image_next}" == "1" ]]; then nextcloud_image_override="${arg}" nextcloud_image_next="0" continue fi if [[ "${qortal_branch_next}" == "1" ]]; then qortal_branch="${arg}" recreate_qortal="1" qortal_branch_next="0" continue fi case "${arg}" in --nossl) mode="nossl" ;; --ssl) mode="ssl" ;; --extauth) extauth="1" ;; --qortal) recreate_qortal="1" ;; --qortal=*) recreate_qortal="1" qortal_branch="${arg#*=}" ;; --qortal-branch) recreate_qortal="1" qortal_branch_next="1" ;; --qortal-branch=*) recreate_qortal="1" qortal_branch="${arg#*=}" ;; --full) full_rebuild="1" recreate_qortal="1" ;; --nc34) nextcloud_image_override="nextcloud:34-apache" ;; --nc33) nextcloud_image_override="nextcloud:33-apache" ;; --nextcloud-image=*) nextcloud_image_override="${arg#*=}" ;; --nextcloud-image) nextcloud_image_next="1" ;; esac done if [[ "${nextcloud_image_next}" == "1" ]]; then echo "--nextcloud-image requires a value" exit 1 fi if [[ "${qortal_branch_next}" == "1" ]]; then echo "--qortal-branch requires a value" exit 1 fi if [[ -n "${qortal_branch}" ]] && ! git check-ref-format --branch "${qortal_branch}" >/dev/null 2>&1; then echo "Invalid Qortal branch name: ${qortal_branch}" exit 1 fi if [[ "${mode}" != "nossl" && "${mode}" != "ssl" ]]; then mode="ssl" fi if [[ -n "${nextcloud_image_override}" ]]; then export NEXTCLOUD_IMAGE="${nextcloud_image_override}" echo "Overriding NEXTCLOUD_IMAGE for this run: ${NEXTCLOUD_IMAGE}" fi if [[ "${mode}" == "ssl" && ! "$*" =~ --ssl && ! "$*" =~ --nossl ]]; then read -r -p "Use no-SSL compose (external proxy)? (y/N): " use_nossl if [[ "${use_nossl}" == "y" || "${use_nossl}" == "Y" ]]; then mode="nossl" fi fi compose_file="${repo_root}/docker-compose.devprod.yml" if [[ "${mode}" == "nossl" ]]; then compose_file="${repo_root}/docker-compose.devprod.nossl.yml" fi ensure_docker_access preserve_running_nextcloud_image_if_newer if [[ -x "${repo_root}/scripts/check-nextcloud-image-update.sh" ]]; then "${repo_root}/scripts/check-nextcloud-image-update.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" || true fi qortal_context="$(grep -E '^QORTAL_NODE_CONTEXT=' "${env_file}" | tail -n1 | cut -d= -f2- || true)" qortal_dockerfile="$(grep -E '^QORTAL_NODE_DOCKERFILE=' "${env_file}" | tail -n1 | cut -d= -f2- || true)" echo "Qortal build source: context=${qortal_context:-../qortal} dockerfile=${qortal_dockerfile:-Dockerfile}" if [[ -n "${qortal_branch}" ]]; then echo "Qortal branch override for this run: ${qortal_branch}" fi prompt_qortal_branch_if_needed nextcloud_custom_apps_path="$(resolve_path_from_env "NEXTCLOUD_CUSTOM_APPS_PATH" "./nextcloud/custom_apps")" mkdir -p "${nextcloud_custom_apps_path}" "${repo_root}/qortal/data" if [[ "${nextcloud_custom_apps_path}" == *"/custom_apps_appstore" ]]; then echo "Using app-store mimic custom apps path: ${nextcloud_custom_apps_path}" echo "Note: app code is copied into this path; source edits require sync during recreate." fi if [[ -x "${repo_root}/scripts/ensure-user-oidc-app.sh" ]]; then NEXTCLOUD_CUSTOM_APPS_PATH="${nextcloud_custom_apps_path}" "${repo_root}/scripts/ensure-user-oidc-app.sh" fi if [[ -x "${repo_root}/scripts/ensure-qortal-integration-app.sh" ]]; then NEXTCLOUD_CUSTOM_APPS_PATH="${nextcloud_custom_apps_path}" "${repo_root}/scripts/ensure-qortal-integration-app.sh" fi if [[ -x "${repo_root}/scripts/ensure-qortal-settings.sh" ]]; then "${repo_root}/scripts/ensure-qortal-settings.sh" fi if [[ -x "${repo_root}/scripts/ensure-qortal-start-args.sh" ]]; then "${repo_root}/scripts/ensure-qortal-start-args.sh" "${env_file}" fi if [[ -x "${repo_root}/scripts/select-qortal-p2p-port.sh" ]]; then "${repo_root}/scripts/select-qortal-p2p-port.sh" "${env_file}" fi if [[ -f "${repo_root}/scripts/ensure-broker-internal-token.sh" ]]; then bash "${repo_root}/scripts/ensure-broker-internal-token.sh" "${env_file}" fi if [[ -x "${repo_root}/scripts/ensure-oidc-signing-key.sh" ]]; then "${repo_root}/scripts/ensure-oidc-signing-key.sh" "${env_file}" fi broker_internal_api_token="$(read_kv "BROKER_INTERNAL_API_TOKEN" || true)" if [[ -z "${broker_internal_api_token}" ]]; then echo "BROKER_INTERNAL_API_TOKEN is missing in ${env_file}" echo "Run: bash scripts/ensure-broker-internal-token.sh ${env_file}" exit 1 fi export BROKER_INTERNAL_API_TOKEN="${broker_internal_api_token}" broker_cors_allowed_origins="$(read_kv "BROKER_CORS_ALLOWED_ORIGINS" || true)" export BROKER_CORS_ALLOWED_ORIGINS="${broker_cors_allowed_origins:-}" echo "Broker auth env loaded from ${env_file}: token_set=yes cors_origins=${broker_cors_allowed_origins:-}" current_profiles="" if command -v rg >/dev/null 2>&1; then profiles_line="$(rg -m1 '^COMPOSE_PROFILES=' "${env_file}" || true)" else profiles_line="$(grep -m1 -E '^COMPOSE_PROFILES=' "${env_file}" || true)" fi if [[ -n "${profiles_line}" ]]; then current_profiles="${profiles_line#COMPOSE_PROFILES=}" fi external_auth_base_url="$(read_kv "QORTAL_EXTERNAL_AUTH_BASE_URL" || true)" if [[ "${extauth}" == "1" ]]; then if [[ -z "${current_profiles}" ]]; then current_profiles="external-auth" elif [[ ",${current_profiles}," != *",external-auth,"* ]]; then current_profiles="${current_profiles},external-auth" fi elif [[ "${external_auth_base_url}" == *"external_auth"* ]]; then if [[ -z "${current_profiles}" ]]; then current_profiles="external-auth" elif [[ ",${current_profiles}," != *",external-auth,"* ]]; then current_profiles="${current_profiles},external-auth" fi fi if [[ -n "${current_profiles}" ]]; then export COMPOSE_PROFILES="${current_profiles}" else unset COMPOSE_PROFILES || true fi echo "Using compose profiles: ${COMPOSE_PROFILES:-}" if [[ -x "${repo_root}/scripts/select-host-service-ports.sh" ]]; then "${repo_root}/scripts/select-host-service-ports.sh" \ --env-file "${env_file}" \ --mode "${mode}" \ --compose-file "${compose_file}" \ --skip-when-running fi if [[ -x "${repo_root}/scripts/sync-public-env-urls.sh" ]]; then "${repo_root}/scripts/sync-public-env-urls.sh" \ --env-file "${env_file}" \ --mode "${mode}" fi manage_external_auth="0" if [[ ",${COMPOSE_PROFILES:-}," == *",external-auth,"* || "${extauth}" == "1" ]]; then manage_external_auth="1" fi external_auth_context_path="" external_auth_repo_url="" if [[ "${manage_external_auth}" == "1" ]]; then external_auth_context_path="$(resolve_path_from_env "EXTERNAL_AUTH_CONTEXT" "../Qortal-External-Auth")" external_auth_repo_url="${EXTERNAL_AUTH_GIT_URL:-${default_external_auth_repo_url}}" ensure_context_repo "Qortal External Auth" "${external_auth_context_path}" "${external_auth_repo_url}" if [[ "${extauth}" == "1" ]]; then refresh_context_repo "Qortal External Auth" "${external_auth_context_path}" fi fi office_profile_enabled="0" talk_profile_enabled="0" turn_profile_enabled="0" recording_profile_enabled="0" if [[ ",${COMPOSE_PROFILES:-}," == *",office,"* ]]; then office_profile_enabled="1" fi if [[ ",${COMPOSE_PROFILES:-}," == *",talk,"* ]]; then talk_profile_enabled="1" fi if [[ ",${COMPOSE_PROFILES:-}," == *",turn,"* ]]; then turn_profile_enabled="1" talk_profile_enabled="1" fi if [[ ",${COMPOSE_PROFILES:-}," == *",janus,"* ]]; then talk_profile_enabled="1" fi if [[ ",${COMPOSE_PROFILES:-}," == *",recording,"* ]]; then recording_profile_enabled="1" talk_profile_enabled="1" fi echo "Recreating containers using ${compose_file}..." mapfile -t services < <(docker compose -f "${compose_file}" --env-file "${env_file}" config --services) compose_profile_mismatch=0 missing_profile_services=() if [[ "${office_profile_enabled}" == "1" ]] && ! service_in_resolved_config "collabora"; then compose_profile_mismatch=1 missing_profile_services+=("collabora (required by profile: office)") fi if [[ "${talk_profile_enabled}" == "1" ]] && ! service_in_resolved_config "talk_hpb"; then compose_profile_mismatch=1 missing_profile_services+=("talk_hpb (required by profile: talk/turn/janus)") fi if [[ "${recording_profile_enabled}" == "1" ]] && ! service_in_resolved_config "talk_recording"; then compose_profile_mismatch=1 missing_profile_services+=("talk_recording (required by profile: recording)") fi if [[ "${manage_external_auth}" == "1" ]] && ! service_in_resolved_config "external_auth"; then compose_profile_mismatch=1 missing_profile_services+=("external_auth (required by bundled external-auth mode)") fi if [[ "${compose_profile_mismatch}" == "1" ]]; then echo "Error: Compose profile/service mismatch detected before recreate." echo "Compose file: ${compose_file}" echo "Env file: ${env_file}" echo "Requested profiles: ${COMPOSE_PROFILES:-}" echo "Resolved services: ${services[*]:-}" echo "Missing expected services:" for item in "${missing_profile_services[@]}"; do echo " - ${item}" done echo echo "This usually means the compose file does not match the branch/version you expect," echo "or you're recreating from a clone with stale compose files." echo "Verify:" echo " git status -sb" echo " git branch -vv" echo " git rev-parse --short HEAD" echo " docker compose -f ${compose_file} --env-file ${env_file} config --services" exit 1 fi target_services=() for svc in "${services[@]}"; do if [[ "${recreate_qortal}" != "1" && "${svc}" == "qortal_node" ]]; then continue fi if [[ "${manage_external_auth}" == "1" && "${svc}" == "external_auth" ]]; then # Handle external_auth in a separate pass so --force-recreate does not # implicitly recreate qortal_node through depends_on. continue fi target_services+=("${svc}") done if [[ "${#target_services[@]}" -eq 0 ]]; then echo "No services selected for recreation." exit 1 fi echo "Recreate target services: ${target_services[*]}" if [[ "${manage_external_auth}" == "1" && "${recreate_qortal}" != "1" ]]; then echo "external_auth will be recreated separately with --no-deps; qortal_node will remain running." fi if [[ "${recreate_qortal}" == "1" ]]; then echo "Including qortal_node in force-recreate." if [[ "${full_rebuild}" == "1" || -n "${qortal_branch}" ]]; then qortal_context_path="$(resolve_path_from_env "QORTAL_NODE_CONTEXT" "../qortal")" if [[ -x "${repo_root}/scripts/ensure-qortal-core-repo.sh" ]]; then ensure_qortal_args=( --target-path "${qortal_context_path}" --refresh ) if [[ -n "${qortal_branch}" ]]; then ensure_qortal_args+=(--branch "${qortal_branch}") fi "${repo_root}/scripts/ensure-qortal-core-repo.sh" "${ensure_qortal_args[@]}" else echo "Warning: scripts/ensure-qortal-core-repo.sh not found; cannot auto-refresh Qortal source from upstream." fi fi if [[ "${full_rebuild}" != "1" ]]; then echo "Building qortal_node with --no-cache to ensure Dockerfile/entrypoint changes are applied..." docker compose -f "${compose_file}" --env-file "${env_file}" build --no-cache qortal_node fi else echo "Excluding qortal_node from force-recreate (use --qortal to include it)." fi if [[ "${full_rebuild}" == "1" ]]; then echo "Full rebuild enabled: rebuilding all buildable services with --no-cache..." docker compose -f "${compose_file}" --env-file "${env_file}" build --no-cache fi compose_up_args=(-d --force-recreate) if [[ "${full_rebuild}" != "1" ]]; then compose_up_args+=(--build) fi docker compose -f "${compose_file}" --env-file "${env_file}" up "${compose_up_args[@]}" "${target_services[@]}" if [[ "${manage_external_auth}" == "1" ]]; then echo "Rebuilding and recreating external_auth..." if [[ "${full_rebuild}" != "1" ]]; then external_auth_build_args=() if [[ "${extauth}" == "1" ]]; then external_auth_build_args+=(--no-cache) fi docker compose -f "${compose_file}" --env-file "${env_file}" build "${external_auth_build_args[@]}" external_auth fi extauth_up_args=(-d --force-recreate) if [[ "${full_rebuild}" != "1" ]]; then extauth_up_args+=(--build) fi if [[ "${recreate_qortal}" == "1" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" up "${extauth_up_args[@]}" external_auth else docker compose -f "${compose_file}" --env-file "${env_file}" up "${extauth_up_args[@]}" --no-deps external_auth fi fi if [[ "${manage_external_auth}" == "1" && -x "${repo_root}/scripts/finish-initial-setup.sh" ]]; then echo "Verifying bundled external-auth registration and Qortal node API key..." "${repo_root}/scripts/finish-initial-setup.sh" \ --mode "${mode}" \ --env-file "${env_file}" \ --skip-occ fi docker compose -f "${compose_file}" --env-file "${env_file}" ps verify_cron_container_user || true if [[ -x "${repo_root}/scripts/ensure-nextcloud-apps-writable.sh" ]]; then if ! "${repo_root}/scripts/ensure-nextcloud-apps-writable.sh" --mode "${mode}" --env-file "${env_file}"; then echo "Warning: could not guarantee Nextcloud apps directory writeability." echo "Apps page and app install/update operations may fail until fixed." fi fi occ_ready=0 occ_ready_retries="${OCC_READY_RETRIES:-36}" occ_ready_sleep_seconds="${OCC_READY_SLEEP_SECONDS:-5}" occ_wait_start_ts="$(date +%s)" echo "Waiting for Nextcloud OCC readiness... (timeout=$((occ_ready_retries * occ_ready_sleep_seconds))s)" for _ in $(seq 1 "${occ_ready_retries}"); do if docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ status >/dev/null 2>&1; then occ_ready=1 break fi sleep "${occ_ready_sleep_seconds}" done occ_wait_end_ts="$(date +%s)" echo "Waiting for Nextcloud OCC readiness: status=${occ_ready} elapsed=$((occ_wait_end_ts - occ_wait_start_ts))s" if [[ "${occ_ready}" != "1" ]]; then echo "Warning: Nextcloud OCC did not become ready after recreate within $((occ_ready_retries * occ_ready_sleep_seconds))s." if [[ -x "${repo_root}/scripts/diagnose-nextcloud-occ-readiness.sh" ]]; then echo "Collecting OCC readiness diagnostics..." "${repo_root}/scripts/diagnose-nextcloud-occ-readiness.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" || true fi echo "Run manually once app is healthy:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" echo " ./scripts/diagnose-nextcloud-occ-readiness.sh --compose-file ${compose_file} --env-file ${env_file}" echo " ./scripts/ensure-nextcloud-url-config.sh --compose-file ${compose_file} --env-file ${env_file}" echo " ./scripts/ensure-nextcloud-service-auth.sh --compose-file ${compose_file} --env-file ${env_file} --repair" echo " ./scripts/ensure-nextcloud-default-app-bundle.sh --compose-file ${compose_file} --env-file ${env_file}" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force user_oidc" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force notify_push" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable qortal_integration" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ db:add-missing-indices" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:repair" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:repair --include-expensive" echo "Done." exit 0 fi occ_status_json="$(docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ status --output=json 2>/dev/null || true)" if [[ -n "${occ_status_json}" ]]; then if echo "${occ_status_json}" | grep -q '"needsDbUpgrade":[[:space:]]*true'; then echo "Warning: Nextcloud still requires a database upgrade after recreate." echo "Stop here and finish the upgrade before running the broader repair pass." echo "Run:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" echo "Then rerun:" echo " ./recreate-devprod.sh --${mode}" exit 1 fi if echo "${occ_status_json}" | grep -q '"maintenance":[[:space:]]*true'; then echo "Warning: Nextcloud is still in maintenance mode after recreate." echo "Run:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:mode --off" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" echo "Then rerun:" echo " ./recreate-devprod.sh --${mode}" exit 1 fi fi if [[ "${manage_external_auth}" == "1" && -x "${repo_root}/scripts/sync-bundled-runtime-env-from-nextcloud.sh" ]]; then run_quiet_step "Syncing bundled runtime credentials" \ "${repo_root}/scripts/sync-bundled-runtime-env-from-nextcloud.sh" \ --mode "${mode}" \ --env-file "${env_file}" \ --restart-services if [[ "$?" -ne 0 ]]; then echo "Warning: bundled external-auth credentials are still invalid after recreate." echo "Run manually:" echo " ./scripts/finish-initial-setup.sh --mode ${mode} --env-file ${env_file} --force-register --force-api-key" fi fi echo "Refreshing Nextcloud app registration..." cleanup_nextcloud_defaults oidc_issuer="$(read_kv "OIDC_ISSUER" || true)" oidc_client_id="$(read_kv "OIDC_CLIENT_ID" || true)" oidc_client_secret="$(read_kv "OIDC_CLIENT_SECRET" || true)" oidc_client_id="${oidc_client_id:-nextcloud-local}" oidc_provider_config_status=0 oidc_provider_skipped=0 oidc_discovery_url="" if [[ -n "${oidc_issuer}" ]]; then oidc_discovery_url="${oidc_issuer%/}/.well-known/openid-configuration" fi office_install_status=0 office_enable_status=0 office_wopi_status=0 office_public_wopi_status=0 talk_install_status=0 talk_enable_status=0 talk_signaling_add_status=0 talk_signaling_skipped=0 talk_stun_add_status=0 talk_stun_skipped=0 talk_turn_add_status=0 talk_turn_skipped=0 talk_recording_add_status=0 talk_recording_skipped=0 talk_recording_command_missing=0 talk_namespace_missing=0 talk_command_prefix="" talk_recording_prefix="" qortal_sync_verify_status=0 qortal_sync_verify_skipped=0 backgroundjobs_mode_status=0 notify_push_enable_status=0 notify_push_install_status=0 notify_push_verify_status=0 repair_expensive_status=0 user_oidc_invite_patch_status=0 user_oidc_invite_patch_skipped=1 talk_signaling_prune_status=0 talk_signaling_multi_count=0 office_verify_status=0 office_verify_skipped=1 talk_verify_status=0 talk_verify_skipped=1 public_env_resync_status=0 nextcloud_url_verify_status=0 custom_pwa_push_status=0 custom_pwa_push_skipped=1 qortal_runtime_sync_status=0 qortal_runtime_sync_skipped=1 set +e nextcloud_url_sync_status=0 if [[ -x "${repo_root}/scripts/ensure-nextcloud-url-config.sh" ]]; then run_quiet_step "Syncing Nextcloud URL config" \ "${repo_root}/scripts/ensure-nextcloud-url-config.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" nextcloud_url_sync_status=$? fi nextcloud_service_auth_status=0 if [[ -x "${repo_root}/scripts/ensure-nextcloud-service-auth.sh" ]]; then run_quiet_step "Checking broker service auth" \ "${repo_root}/scripts/ensure-nextcloud-service-auth.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" \ --repair nextcloud_service_auth_status=$? fi run_quiet_step "Setting background job mode" occ_exec_quiet_retry config:system:set backgroundjobs_mode --value=cron backgroundjobs_mode_status=$? run_quiet_step "Enabling user_oidc" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ app:enable --force user_oidc enable_oidc_status=$? install_oidc_status=0 if [[ "${enable_oidc_status}" -ne 0 ]]; then run_quiet_step "Installing user_oidc" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ app:install user_oidc install_oidc_status=$? run_quiet_step "Re-enabling user_oidc" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ app:enable --force user_oidc enable_oidc_status=$? fi if [[ -x "${repo_root}/scripts/patch-user-oidc-invite-flow.sh" ]]; then run_quiet_step "Checking user_oidc invite bridge" \ env NEXTCLOUD_CUSTOM_APPS_PATH="${nextcloud_custom_apps_path}" \ "${repo_root}/scripts/patch-user-oidc-invite-flow.sh" user_oidc_invite_patch_status=$? user_oidc_invite_patch_skipped=0 fi run_quiet_step "Enabling notify_push" occ_exec_quiet_retry app:enable --force notify_push notify_push_enable_status=$? if [[ "${notify_push_enable_status}" -ne 0 ]]; then run_quiet_step "Installing notify_push" occ_exec_quiet_retry app:install notify_push notify_push_install_status=$? run_quiet_step "Re-enabling notify_push" occ_exec_quiet_retry app:enable --force notify_push notify_push_enable_status=$? fi docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ app:list >/tmp/occ_app_list_notify_push.txt 2>/dev/null if grep -qE '^[[:space:]]+- notify_push([[:space:]:]|$)' /tmp/occ_app_list_notify_push.txt 2>/dev/null; then notify_push_verify_status=0 else notify_push_verify_status=1 fi rm -f /tmp/occ_app_list_notify_push.txt >/dev/null 2>&1 || true docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ user_oidc:provider --help >/dev/null 2>&1 verify_oidc_status=$? if [[ -n "${oidc_discovery_url}" && -n "${oidc_client_secret}" ]]; then run_quiet_step "Configuring user_oidc provider" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ user_oidc:provider qortal \ -c "${oidc_client_id}" \ -s "${oidc_client_secret}" \ -d "${oidc_discovery_url}" \ --scope="openid profile email" \ --mapping-uid=sub \ --mapping-display-name=name \ --mapping-email=email oidc_provider_config_status=$? else oidc_provider_skipped=1 fi oidc_login_label_status=0 sovereign_mode="$(docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ config:app:get qortal_integration sc_mode 2>/dev/null | tr -d '\r' || true)" if [[ "${sovereign_mode}" == "full_qortal" ]]; then run_quiet_step "Clearing user_oidc login label" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ config:system:delete user_oidc login_label oidc_login_label_status=$? else run_quiet_step "Setting user_oidc login label" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ config:system:set user_oidc login_label --value="NuQloud Login" oidc_login_label_status=$? fi if [[ "${office_profile_enabled}" == "1" ]]; then echo " Configuring Office integration..." collabora_public_url="$(read_kv "COLLABORA_PUBLIC_URL" || true)" occ_exec_quiet_retry app:enable --force richdocuments office_enable_status=$? if [[ "${office_enable_status}" -ne 0 ]]; then occ_exec_quiet_retry app:install richdocuments office_install_status=$? occ_exec_quiet_retry app:enable --force richdocuments office_enable_status=$? fi if [[ -n "${collabora_public_url}" ]]; then occ_exec_quiet_retry config:app:set richdocuments wopi_url --value="${collabora_public_url}" office_wopi_status=$? occ_exec_quiet_retry config:app:set richdocuments public_wopi_url --value="${collabora_public_url}" office_public_wopi_status=$? else office_wopi_status=1 fi fi if [[ "${talk_profile_enabled}" == "1" ]]; then echo " Configuring Talk integration..." talk_signaling_public_url="$(read_kv "TALK_SIGNALING_PUBLIC_URL" || true)" talk_signaling_secret="$(read_kv "TALK_SIGNALING_SECRET" || true)" talk_stun_server="$(read_kv "TALK_STUN_SERVER" || true)" talk_turn_server="$(read_kv "TALK_TURN_SERVER" || true)" talk_domain="$(read_kv "SIGNALING_DOMAIN" || true)" if [[ -z "${talk_domain}" ]]; then talk_domain="$(read_kv "TALK_DOMAIN" || true)" fi talk_turn_host="$(read_kv "TALK_TURN_HOST" || true)" if [[ -z "${talk_turn_host}" ]]; then talk_turn_host="$(read_kv "NEXTCLOUD_DOMAIN" || true)" fi talk_turn_port="$(read_kv "TALK_TURN_PORT" || true)" talk_turn_port="${talk_turn_port:-3478}" talk_turn_secret="$(read_kv "TALK_TURN_SECRET" || true)" if [[ -z "${talk_turn_server}" ]]; then talk_turn_server="${talk_turn_host}:${talk_turn_port}" fi talk_recording_url="$(read_kv "TALK_RECORDING_URL" || true)" talk_recording_url="${talk_recording_url:-http://talk_recording:1234}" talk_recording_secret="$(read_kv "TALK_RECORDING_SHARED_SECRET" || true)" occ_exec_quiet_retry app:enable --force spreed talk_enable_status=$? if [[ "${talk_enable_status}" -ne 0 ]]; then occ_exec_quiet_retry app:install spreed talk_install_status=$? occ_exec_quiet_retry app:enable --force spreed talk_enable_status=$? fi if occ_has_command "talk:signaling:list"; then talk_command_prefix="talk" elif occ_has_command "spreed:signaling:list"; then talk_command_prefix="spreed" fi if occ_has_command "talk:recording:add"; then talk_recording_prefix="talk" elif occ_has_command "spreed:recording:add"; then talk_recording_prefix="spreed" fi if [[ -z "${talk_command_prefix}" ]]; then talk_namespace_missing=1 talk_signaling_skipped=1 talk_stun_skipped=1 if [[ "${turn_profile_enabled}" == "1" ]]; then talk_turn_skipped=1 fi else if [[ -n "${talk_signaling_public_url}" && -n "${talk_signaling_secret}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:signaling:list" >/tmp/occ_talk_signaling_list.txt 2>/dev/null if grep -Fq "${talk_signaling_public_url}" /tmp/occ_talk_signaling_list.txt 2>/dev/null; then talk_signaling_add_status=0 else docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:signaling:add" "${talk_signaling_public_url}" "${talk_signaling_secret}" >/dev/null 2>&1 talk_signaling_add_status=$? if [[ "${talk_signaling_add_status}" -ne 0 ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:signaling:list" >/tmp/occ_talk_signaling_list_retry.txt 2>/dev/null if grep -Fq "${talk_signaling_public_url}" /tmp/occ_talk_signaling_list_retry.txt 2>/dev/null; then talk_signaling_add_status=0 fi rm -f /tmp/occ_talk_signaling_list_retry.txt >/dev/null 2>&1 || true fi fi rm -f /tmp/occ_talk_signaling_list.txt >/dev/null 2>&1 || true if occ_has_command "${talk_command_prefix}:signaling:delete"; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:signaling:list" >/tmp/occ_talk_signaling_list_prune.txt 2>/dev/null mapfile -t talk_signaling_delete_ids < <( awk -v keep="${talk_signaling_public_url}" ' /https?:\/\// { if (keep != "" && index($0, keep) > 0) { next; } if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) { print m[1]; next; } if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) { print m[1]; next; } } ' /tmp/occ_talk_signaling_list_prune.txt 2>/dev/null | sort -u ) for signaling_id in "${talk_signaling_delete_ids[@]}"; do if [[ -n "${signaling_id}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:signaling:delete" "${signaling_id}" >/dev/null 2>&1 || true fi done rm -f /tmp/occ_talk_signaling_list_prune.txt >/dev/null 2>&1 || true fi docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:signaling:list" >/tmp/occ_talk_signaling_list_final.txt 2>/dev/null talk_signaling_multi_count="$( grep -Eo 'https?://[^[:space:]|]+' /tmp/occ_talk_signaling_list_final.txt 2>/dev/null | sort -u | wc -l | tr -d '[:space:]' )" rm -f /tmp/occ_talk_signaling_list_final.txt >/dev/null 2>&1 || true if [[ -n "${talk_signaling_multi_count}" && "${talk_signaling_multi_count}" -gt 1 ]]; then talk_signaling_prune_status=1 else talk_signaling_prune_status=0 fi else talk_signaling_skipped=1 fi if [[ -n "${talk_stun_server}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:stun:list" >/tmp/occ_talk_stun_list.txt 2>/dev/null if grep -Fq "${talk_stun_server}" /tmp/occ_talk_stun_list.txt 2>/dev/null; then talk_stun_add_status=0 else docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:stun:add" "${talk_stun_server}" >/dev/null 2>&1 talk_stun_add_status=$? if [[ "${talk_stun_add_status}" -ne 0 ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:stun:list" >/tmp/occ_talk_stun_list_retry.txt 2>/dev/null if grep -Fq "${talk_stun_server}" /tmp/occ_talk_stun_list_retry.txt 2>/dev/null; then talk_stun_add_status=0 fi rm -f /tmp/occ_talk_stun_list_retry.txt >/dev/null 2>&1 || true fi fi if occ_has_command "${talk_command_prefix}:stun:delete"; then mapfile -t talk_stun_delete_ids < <( awk -v keep="${talk_stun_server}" ' { if (keep != "" && index($0, keep) > 0) { next; } if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) { print m[1]; next; } if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) { print m[1]; next; } } ' /tmp/occ_talk_stun_list.txt 2>/dev/null | sort -u ) for stun_id in "${talk_stun_delete_ids[@]}"; do if [[ -n "${stun_id}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:stun:delete" "${stun_id}" >/dev/null 2>&1 || true fi done fi rm -f /tmp/occ_talk_stun_list.txt >/dev/null 2>&1 || true else talk_stun_skipped=1 fi if [[ "${turn_profile_enabled}" == "1" && -n "${talk_turn_secret}" && -n "${talk_domain}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:turn:list" >/tmp/occ_talk_turn_list.txt 2>/dev/null if grep -Fq "${talk_turn_server}" /tmp/occ_talk_turn_list.txt 2>/dev/null; then talk_turn_add_status=0 else docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:turn:add" turn "${talk_turn_server}" udp,tcp --secret "${talk_turn_secret}" >/dev/null 2>&1 talk_turn_add_status=$? if [[ "${talk_turn_add_status}" -ne 0 ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:turn:list" >/tmp/occ_talk_turn_list_retry.txt 2>/dev/null if grep -Fq "${talk_turn_server}" /tmp/occ_talk_turn_list_retry.txt 2>/dev/null; then talk_turn_add_status=0 fi rm -f /tmp/occ_talk_turn_list_retry.txt >/dev/null 2>&1 || true fi fi if occ_has_command "${talk_command_prefix}:turn:delete"; then mapfile -t talk_turn_delete_ids < <( awk -v keep="${talk_turn_server}" ' { if (keep != "" && index($0, keep) > 0) { next; } if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) { print m[1]; next; } if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) { print m[1]; next; } } ' /tmp/occ_talk_turn_list.txt 2>/dev/null | sort -u ) for turn_id in "${talk_turn_delete_ids[@]}"; do if [[ -n "${turn_id}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_command_prefix}:turn:delete" "${turn_id}" >/dev/null 2>&1 || true fi done fi rm -f /tmp/occ_talk_turn_list.txt >/dev/null 2>&1 || true elif [[ "${turn_profile_enabled}" == "1" ]]; then talk_turn_skipped=1 fi fi if [[ "${recording_profile_enabled}" == "1" && -n "${talk_recording_url}" && -n "${talk_recording_secret}" ]]; then if [[ -n "${talk_recording_prefix}" ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_recording_prefix}:recording:add" "${talk_recording_url}" "${talk_recording_secret}" >/dev/null 2>&1 talk_recording_add_status=$? if [[ "${talk_recording_add_status}" -ne 0 ]]; then docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ "${talk_recording_prefix}:recording:set" "${talk_recording_url}" "${talk_recording_secret}" >/dev/null 2>&1 talk_recording_add_status=$? fi else talk_recording_command_missing=1 talk_recording_skipped=1 fi elif [[ "${recording_profile_enabled}" == "1" ]]; then talk_recording_skipped=1 fi fi echo " Refreshing qortal custom app enable state..." occ_retoggle_app qortal_integration 1 enable_qortal_status=$? for custom_app in custom_pwa chd_admin qortal_files_bridge qortal_talk_bridge nuqloud_scrum; do occ_retoggle_app "${custom_app}" 1 || true done echo "Rebuilding Nextcloud app cache..." if [[ -x "${repo_root}/scripts/clear-nextcloud-appdata-assets.sh" ]]; then echo " Clearing Nextcloud appdata asset caches..." "${repo_root}/scripts/clear-nextcloud-appdata-assets.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" >/dev/null 2>&1 || true fi default_app_bundle_status=0 if [[ -x "${repo_root}/scripts/ensure-nextcloud-default-app-bundle.sh" ]]; then run_quiet_step "Applying default app bundle" \ "${repo_root}/scripts/ensure-nextcloud-default-app-bundle.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" default_app_bundle_status=$? fi run_quiet_step "Running maintenance:repair" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T --user www-data app php occ maintenance:repair repair_status=$? run_quiet_step "Running maintenance:repair --include-expensive" occ_exec_quiet_retry maintenance:repair --include-expensive repair_expensive_status=$? # Re-assert app enables after repair/cache refresh. Some instances briefly fail earlier # during startup and can appear disabled after recreate. if [[ "${office_profile_enabled}" == "1" ]]; then occ_retoggle_app richdocuments 1 if [[ $? -ne 0 ]]; then occ_exec_quiet_retry app:install richdocuments office_install_status=$? occ_exec_quiet_retry app:enable --force richdocuments office_enable_status=$? else office_enable_status=0 fi fi if [[ "${talk_profile_enabled}" == "1" ]]; then occ_retoggle_app spreed 1 if [[ $? -ne 0 ]]; then occ_exec_quiet_retry app:install spreed talk_install_status=$? occ_exec_quiet_retry app:enable --force spreed talk_enable_status=$? else talk_enable_status=0 fi fi # One more frontend cache clear after final app toggles to reduce stale app menu/app-page UI state. if [[ -x "${repo_root}/scripts/clear-nextcloud-appdata-assets.sh" ]]; then "${repo_root}/scripts/clear-nextcloud-appdata-assets.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" >/dev/null 2>&1 || true fi if [[ -x "${repo_root}/scripts/sync-public-env-urls.sh" ]]; then run_quiet_step "Re-syncing public env URLs" \ "${repo_root}/scripts/sync-public-env-urls.sh" \ --env-file "${env_file}" \ --mode "${mode}" public_env_resync_status=$? fi if [[ -x "${repo_root}/scripts/ensure-nextcloud-url-config.sh" ]]; then run_quiet_step "Re-validating Nextcloud URL config" \ "${repo_root}/scripts/ensure-nextcloud-url-config.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" nextcloud_url_verify_status=$? fi if [[ -x "${repo_root}/scripts/ensure-custom-pwa-push-config.sh" ]]; then run_quiet_step "Ensuring CustomPWA push config" \ "${repo_root}/scripts/ensure-custom-pwa-push-config.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" custom_pwa_push_status=$? custom_pwa_push_skipped=0 fi if [[ -x "${repo_root}/scripts/ensure-qortal-integration-runtime-config.sh" ]]; then qortal_runtime_log="$(mktemp)" "${repo_root}/scripts/ensure-qortal-integration-runtime-config.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" >"${qortal_runtime_log}" 2>&1 qortal_runtime_sync_status=$? if [[ "${qortal_runtime_sync_status}" -ne 0 && -s "${qortal_runtime_log}" ]]; then echo "Qortal runtime sync details:" sed 's/^/ /' "${qortal_runtime_log}" fi rm -f "${qortal_runtime_log}" >/dev/null 2>&1 || true qortal_runtime_sync_skipped=0 fi if [[ "${office_profile_enabled}" == "1" && -x "${repo_root}/scripts/ensure-nextcloud-office-config.sh" ]]; then office_verify_log="$(mktemp)" echo " Validating Office/Collabora configuration..." office_verify_start_ts="$(date +%s)" "${repo_root}/scripts/ensure-nextcloud-office-config.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" >"${office_verify_log}" 2>&1 office_verify_status=$? office_verify_end_ts="$(date +%s)" echo " Validating Office/Collabora configuration: status=${office_verify_status} elapsed=$((office_verify_end_ts - office_verify_start_ts))s" if [[ "${office_verify_status}" -ne 0 && -s "${office_verify_log}" ]]; then sed 's/^/ /' "${office_verify_log}" fi rm -f "${office_verify_log}" >/dev/null 2>&1 || true office_verify_skipped=0 fi if [[ ( "${talk_profile_enabled}" == "1" || "${recording_profile_enabled}" == "1" ) && -x "${repo_root}/scripts/ensure-nextcloud-talk-config.sh" ]]; then talk_verify_log="$(mktemp)" echo " Validating Talk backend configuration..." talk_verify_start_ts="$(date +%s)" "${repo_root}/scripts/ensure-nextcloud-talk-config.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" >"${talk_verify_log}" 2>&1 talk_verify_status=$? talk_verify_end_ts="$(date +%s)" echo " Validating Talk backend configuration: status=${talk_verify_status} elapsed=$((talk_verify_end_ts - talk_verify_start_ts))s" if [[ "${talk_verify_status}" -ne 0 && -s "${talk_verify_log}" ]]; then sed 's/^/ /' "${talk_verify_log}" fi rm -f "${talk_verify_log}" >/dev/null 2>&1 || true talk_verify_skipped=0 fi if [[ -x "${repo_root}/scripts/verify-qortal-integration-sync.sh" ]]; then echo "Verifying qortal_integration app sync..." "${repo_root}/scripts/verify-qortal-integration-sync.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" \ --repair qortal_sync_verify_status=$? else qortal_sync_verify_skipped=1 fi set -e occ_failure=0 if [[ "${nextcloud_url_sync_status}" -ne 0 || "${nextcloud_service_auth_status}" -ne 0 || "${enable_oidc_status}" -ne 0 || "${verify_oidc_status}" -ne 0 || "${oidc_provider_config_status}" -ne 0 || "${oidc_login_label_status}" -ne 0 || "${enable_qortal_status}" -ne 0 || "${notify_push_enable_status}" -ne 0 || "${notify_push_verify_status}" -ne 0 || "${default_app_bundle_status}" -ne 0 || "${repair_status}" -ne 0 || "${repair_expensive_status}" -ne 0 || "${public_env_resync_status}" -ne 0 || "${nextcloud_url_verify_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${backgroundjobs_mode_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${qortal_sync_verify_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${user_oidc_invite_patch_skipped}" -eq 0 && "${user_oidc_invite_patch_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${custom_pwa_push_skipped}" -eq 0 && "${custom_pwa_push_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${qortal_runtime_sync_skipped}" -eq 0 && "${qortal_runtime_sync_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${office_verify_skipped}" -eq 0 ]]; then if [[ "${office_verify_status}" -ne 0 ]]; then occ_failure=1 fi else if [[ "${office_profile_enabled}" == "1" && ( "${office_enable_status}" -ne 0 || "${office_wopi_status}" -ne 0 || "${office_public_wopi_status}" -ne 0 ) ]]; then occ_failure=1 fi fi if [[ "${talk_verify_skipped}" -eq 0 ]]; then if [[ "${talk_verify_status}" -ne 0 ]]; then occ_failure=1 fi else if [[ "${talk_profile_enabled}" == "1" && ( "${talk_enable_status}" -ne 0 || "${talk_signaling_add_status}" -ne 0 || "${talk_stun_add_status}" -ne 0 ) ]]; then occ_failure=1 fi if [[ "${talk_signaling_prune_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${turn_profile_enabled}" == "1" && "${talk_turn_add_status}" -ne 0 ]]; then occ_failure=1 fi if [[ "${recording_profile_enabled}" == "1" && "${talk_recording_add_status}" -ne 0 && "${talk_recording_command_missing}" -ne 1 ]]; then occ_failure=1 fi fi if [[ "${occ_failure}" == "1" ]]; then echo "Warning: one or more OCC refresh steps failed." echo "Step status:" echo " nextcloud_url_sync_status=${nextcloud_url_sync_status}" echo " public_env_resync_status=${public_env_resync_status}" echo " nextcloud_url_verify_status=${nextcloud_url_verify_status}" echo " nextcloud_service_auth_status=${nextcloud_service_auth_status}" echo " backgroundjobs_mode_status=${backgroundjobs_mode_status}" echo " enable_oidc_status=${enable_oidc_status}" echo " verify_oidc_status=${verify_oidc_status}" if [[ "${user_oidc_invite_patch_skipped}" -eq 0 ]]; then echo " user_oidc_invite_patch_status=${user_oidc_invite_patch_status}" fi echo " oidc_provider_config_status=${oidc_provider_config_status}" echo " oidc_login_label_status=${oidc_login_label_status}" echo " enable_qortal_status=${enable_qortal_status}" echo " notify_push_enable_status=${notify_push_enable_status}" echo " notify_push_verify_status=${notify_push_verify_status}" echo " default_app_bundle_status=${default_app_bundle_status}" echo " repair_status=${repair_status}" echo " repair_expensive_status=${repair_expensive_status}" echo " qortal_sync_verify_status=${qortal_sync_verify_status}" if [[ "${custom_pwa_push_skipped}" -eq 0 ]]; then echo " custom_pwa_push_status=${custom_pwa_push_status}" fi if [[ "${qortal_runtime_sync_skipped}" -eq 0 ]]; then echo " qortal_runtime_sync_status=${qortal_runtime_sync_status}" fi if [[ "${office_profile_enabled}" == "1" ]]; then echo " office_enable_status=${office_enable_status}" echo " office_wopi_status=${office_wopi_status}" echo " office_public_wopi_status=${office_public_wopi_status}" fi if [[ "${office_verify_skipped}" -eq 0 ]]; then echo " office_verify_status=${office_verify_status}" fi if [[ "${talk_profile_enabled}" == "1" ]]; then echo " talk_enable_status=${talk_enable_status}" echo " talk_signaling_add_status=${talk_signaling_add_status}" echo " talk_signaling_prune_status=${talk_signaling_prune_status}" echo " talk_signaling_multi_count=${talk_signaling_multi_count}" echo " talk_stun_add_status=${talk_stun_add_status}" fi if [[ "${turn_profile_enabled}" == "1" ]]; then echo " talk_turn_add_status=${talk_turn_add_status}" fi if [[ "${recording_profile_enabled}" == "1" ]]; then echo " talk_recording_add_status=${talk_recording_add_status}" echo " talk_recording_command_missing=${talk_recording_command_missing}" fi if [[ "${talk_verify_skipped}" -eq 0 ]]; then echo " talk_verify_status=${talk_verify_status}" fi echo "Run manually:" echo " ./scripts/sync-public-env-urls.sh --env-file ${env_file} --mode ${mode}" echo " ./scripts/ensure-nextcloud-url-config.sh --compose-file ${compose_file} --env-file ${env_file}" echo " ./scripts/ensure-nextcloud-service-auth.sh --compose-file ${compose_file} --env-file ${env_file} --repair" echo " ./scripts/ensure-custom-pwa-push-config.sh --compose-file ${compose_file} --env-file ${env_file}" echo " ./scripts/ensure-qortal-integration-runtime-config.sh --compose-file ${compose_file} --env-file ${env_file}" echo " ./scripts/ensure-nextcloud-default-app-bundle.sh --compose-file ${compose_file} --env-file ${env_file}" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ config:system:set backgroundjobs_mode --value=cron" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:install user_oidc" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force user_oidc" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:install notify_push" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force notify_push" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force nuqloud_scrum" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ user_oidc:provider --help" if [[ -n "${oidc_discovery_url}" ]]; then echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ user_oidc:provider qortal -c ${oidc_client_id} -s '' -d ${oidc_discovery_url} --scope='openid profile email' --mapping-uid=sub --mapping-display-name=name --mapping-email=email" fi echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ config:system:set user_oidc login_label --value='NuQloud Login'" if [[ "${office_profile_enabled}" == "1" ]]; then echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force richdocuments" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ config:app:set richdocuments wopi_url --value='$(read_kv "COLLABORA_PUBLIC_URL" || true)'" fi if [[ "${talk_profile_enabled}" == "1" ]]; then talk_cmd="${talk_command_prefix:-talk}" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force spreed" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ ${talk_cmd}:signaling:add '$(read_kv "TALK_SIGNALING_PUBLIC_URL" || true)' ''" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ ${talk_cmd}:signaling:list" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ ${talk_cmd}:stun:add '$(read_kv "TALK_STUN_SERVER" || true)'" fi if [[ "${turn_profile_enabled}" == "1" ]]; then talk_cmd="${talk_command_prefix:-talk}" turn_domain="$(read_kv "SIGNALING_DOMAIN" || true)" if [[ -z "${turn_domain}" ]]; then turn_domain="$(read_kv "TALK_DOMAIN" || true)" fi turn_port="$(read_kv "TALK_TURN_PORT" || true)" turn_port="${turn_port:-3478}" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ ${talk_cmd}:turn:add turn '${turn_domain}:${turn_port}' udp,tcp --secret ''" fi if [[ "${recording_profile_enabled}" == "1" ]]; then recording_cmd="${talk_recording_prefix:-${talk_command_prefix:-talk}}" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ ${recording_cmd}:recording:add '$(read_kv "TALK_RECORDING_URL" || true)' ''" fi if [[ -x "${repo_root}/scripts/ensure-nextcloud-talk-config.sh" ]]; then echo " ./scripts/ensure-nextcloud-talk-config.sh --compose-file ${compose_file} --env-file ${env_file}" fi if [[ -x "${repo_root}/scripts/ensure-nextcloud-office-config.sh" ]]; then echo " ./scripts/ensure-nextcloud-office-config.sh --compose-file ${compose_file} --env-file ${env_file}" fi echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ db:add-missing-indices" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:disable qortal_integration" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable qortal_integration" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ app:enable --force nuqloud_scrum" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:repair" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:repair --include-expensive" echo " ./scripts/verify-qortal-integration-sync.sh --compose-file ${compose_file} --env-file ${env_file} --repair" if [[ "${install_oidc_status}" -ne 0 && "${enable_oidc_status}" -ne 0 ]]; then echo " Note: app:install user_oidc fallback failed; check network/appstore access or custom_apps mount." fi if [[ "${oidc_provider_skipped}" -eq 1 ]]; then echo " Note: provider auto-config skipped because OIDC_ISSUER or OIDC_CLIENT_SECRET is empty in ${env_file}." fi if [[ "${office_verify_skipped}" -eq 0 && "${office_verify_status}" -ne 0 ]]; then echo " Note: final Office validation failed; rerun ./scripts/ensure-nextcloud-office-config.sh directly for detailed diagnostics." fi if [[ "${talk_verify_skipped}" -eq 1 ]]; then if [[ "${talk_signaling_skipped}" -eq 1 ]]; then echo " Note: Talk signaling auto-config skipped because TALK_SIGNALING_PUBLIC_URL or TALK_SIGNALING_SECRET is empty." fi if [[ "${talk_signaling_prune_status}" -ne 0 ]]; then echo " Note: multiple signaling backends are still configured (count=${talk_signaling_multi_count}); remove extra entries in Talk admin or via ${talk_command_prefix:-talk}:signaling:delete." fi if [[ "${talk_stun_skipped}" -eq 1 ]]; then echo " Note: Talk STUN auto-config skipped because TALK_STUN_SERVER is empty." fi if [[ "${talk_turn_skipped}" -eq 1 ]]; then echo " Note: TURN auto-config skipped because TALK_TURN_SECRET or SIGNALING_DOMAIN/TALK_TURN_PORT is empty." fi if [[ "${talk_namespace_missing}" -eq 1 ]]; then echo " Note: Talk OCC namespace was not found (expected talk:* or spreed:*). Verify Nextcloud Talk app activation." fi if [[ "${talk_recording_command_missing}" -eq 1 ]]; then echo " Note: Talk recording OCC command not available (checked talk:* and spreed:*); configure recording backend URL/secret in Talk admin settings." elif [[ "${talk_recording_skipped}" -eq 1 ]]; then echo " Note: Talk recording auto-config skipped because TALK_RECORDING_URL or TALK_RECORDING_SHARED_SECRET is empty." fi elif [[ "${talk_verify_status}" -ne 0 ]]; then echo " Note: final Talk validation failed; rerun ./scripts/ensure-nextcloud-talk-config.sh directly for detailed backend diagnostics." fi if [[ "${qortal_sync_verify_skipped}" -eq 1 ]]; then echo " Note: qortal app sync verification was skipped because scripts/verify-qortal-integration-sync.sh is missing." fi fi echo "Done."