Files

1746 lines
68 KiB
Bash
Executable File

#!/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 <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:-<empty>}"
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:-<none>}"
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:-<none>}"
echo "Resolved services: ${services[*]:-<none>}"
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 '<client-secret>' -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)' '<signaling-secret>'"
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 '<turn-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)' '<recording-secret>'"
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."