Ctrl K
ComunidadGuías…

install_dash_linux.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
set -euo pipefail

# Lifecycle contract: run this installer once on a fresh Dash host. It is not
# a reinstall, repair, rollback, or version-update entrypoint. After the first
# successful installation, every version change uses the dash update subcommand
# directly or through the compatibility wrapper update_dash_linux.sh. Installation and
# update workflows are never run concurrently.

APP="dash"

readonly POSTGRES_VERSION="16.15"
readonly POSTGRES_MAJOR="${POSTGRES_VERSION%%.*}"
readonly TIMESCALEDB_VERSION="2.29.1"
readonly DEFAULT_NODE_OFFLINE_THRESHOLD="17s"

INSTALL_DIR="/opt/Ithiltir-dash"
RELEASES_DIR="${INSTALL_DIR}/releases"
CURRENT_LINK="${INSTALL_DIR}/current"
LAYOUT_MARKER="${INSTALL_DIR}/.release-layout-v1"
BIN_DIR="${INSTALL_DIR}/bin"
BIN_PATH="${BIN_DIR}/dash"

CONFIG_DIR="${INSTALL_DIR}/configs"
CONFIG_EXAMPLE="${CONFIG_DIR}/config.example.yaml"
CONFIG_LOCAL="${CONFIG_DIR}/config.local.yaml"
NOTIFY_CONFIG_KEY="${CONFIG_DIR}/notify-config.key"

SERVICE_FILE="/etc/systemd/system/${APP}.service"
MANUAL_RUN_FILE="${INSTALL_DIR}/run_dash.sh"

REDIS_INSTALL_METHOD="${REDIS_INSTALL_METHOD:-package}"
REDIS_TAKEOVER_CONFIRMED="0"
SERVICE_MANAGER_MODE="${SERVICE_MANAGER_MODE:-auto}"
SERVICE_MANAGER=""
INSTALL_RELEASE=""
INSTALL_RECOVERY_ROOT=""
INSTALL_PREVIOUS_LAYOUT=""
INSTALL_PREVIOUS_TARGET=""
INSTALL_PREVIOUS_MARKER="0"
INSTALL_PREVIOUS_CONFIG="0"
INSTALL_SYSTEMD_WAS_ACTIVE="0"
INSTALL_ROLLBACK_READY="0"
INSTALL_MIGRATION_STARTED="0"

OS_ID=""
OS_VERSION_ID=""
APT_REPO_ID=""
APT_REPO_CODENAME=""
OS_FAMILY=""
PKG_MANAGER=""
PKG_MANAGER_LABEL=""

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

INSTALL_LANG="${INSTALL_LANG:-}"

usage() {
	cat <<EOF
Usage: $0 [--lang zh|en] [--service-manager auto|systemd|none]
EOF
}

parse_args() {
	while [[ $# -gt 0 ]]; do
		case "$1" in
		--lang)
			[[ $# -ge 2 ]] || die "missing value for --lang"
			INSTALL_LANG="$2"
			shift 2
			;;
		--lang=*)
			INSTALL_LANG="${1#--lang=}"
			shift
			;;
		--service-manager)
			[[ $# -ge 2 ]] || die "missing value for --service-manager"
			SERVICE_MANAGER_MODE="$2"
			shift 2
			;;
		--service-manager=*)
			SERVICE_MANAGER_MODE="${1#--service-manager=}"
			shift
			;;
		-h | --help)
			usage
			exit 0
			;;
		*)
			die "unknown argument: $1"
			;;
		esac
	done
}

default_install_lang() {
	case "${LANG:-}" in
	zh* | zh_*) echo "zh" ;;
	*) echo "en" ;;
	esac
}

choose_install_lang() {
	case "${INSTALL_LANG}" in
	zh | en) return 0 ;;
	"") ;;
	*) die "INSTALL_LANG must be zh or en: ${INSTALL_LANG}" ;;
	esac

	local default ans
	default="$(default_install_lang)"
	if [[ ! -t 0 ]]; then
		INSTALL_LANG="$default"
		return 0
	fi

	while true; do
		echo "Select installer language / 选择安装脚本语言:" >&2
		echo "  1) English" >&2
		echo "  2) 中文" >&2
		if [[ "$default" == "zh" ]]; then
			read -r -p "Enter number / 请输入序号 [2] " ans || true
			ans="${ans:-2}"
		else
			read -r -p "Enter number / 请输入序号 [1] " ans || true
			ans="${ans:-1}"
		fi
		case "$ans" in
		1) INSTALL_LANG="en"; return 0 ;;
		2) INSTALL_LANG="zh"; return 0 ;;
		*) echo "Please enter 1 or 2 / 请输入 1 或 2" >&2 ;;
		esac
	done
}

txt() {
	if [[ "${INSTALL_LANG:-zh}" == "en" ]]; then
		printf '%s' "$2"
	else
		printf '%s' "$1"
	fi
}

say() { echo "$(txt "$1" "$2")"; }
say_err() { echo "$(txt "$1" "$2")" >&2; }

need_cmd() { command -v "$1" >/dev/null 2>&1; }

as_root() {
	if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
		"$@"
		return
	fi
	if need_cmd sudo; then
		sudo "$@"
		return
	fi
	say_err "需要 root 权限且未安装 sudo,请以 root 运行" "Root privileges are required and sudo is not installed. Please run as root."
	exit 1
}

die() {
	echo "ERROR: $*" >&2
	exit 1
}

print_config_summary() {
	local dash_ip="$1" listen_port="$2" public_url="$3" db_user="$4" db_pass="$5" db_name="$6" retention_days="$7" redis_addr="$8" redis_password="$9" offline_threshold="${10}" language="${11}" trusted_proxies="${12}"
	local retention_label="default (45 days)"
	if [[ "$retention_days" != "default" ]]; then
		retention_label="${retention_days} days"
	fi

	say "即将写入配置:${CONFIG_LOCAL}" "About to write config: ${CONFIG_LOCAL}"
	echo "  app.dash_ip: ${dash_ip}"
	echo "  app.listen: :${listen_port}"
	echo "  app.public_url: ${public_url}"
	echo "  app.language: ${language}"
	echo "  database.user: ${db_user}"
	say "  database.password: (已隐藏,长度 ${#db_pass})" "  database.password: (hidden, length ${#db_pass})"
	echo "  database.name: ${db_name}"
	echo "  database.metrics_raw_retention_days: 8"
	echo "  database.retention_days: ${retention_label}"
	echo "  app.node_offline_threshold: ${offline_threshold}"
	echo "  redis.addr: ${redis_addr}"
	if [[ -n "$redis_password" ]]; then
		say "  redis.password: (已配置并隐藏)" "  redis.password: (configured and hidden)"
	else
		say "  redis.password: (空)" "  redis.password: (empty)"
	fi
	echo "  http.trusted_proxies: ${trusted_proxies}"
}

as_postgres() {
	if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
		if need_cmd runuser; then
			runuser -u postgres -- "$@"
			return
		fi
		if need_cmd su; then
			su -s /bin/bash postgres -c "$(printf '%q ' "$@")"
			return
		fi
		die "$(txt "缺少 runuser/su,无法以 postgres 用户执行命令" "Missing runuser/su; cannot run commands as postgres")"
	fi

	if need_cmd sudo; then
		sudo -u postgres "$@"
		return
	fi
	die "$(txt "非 root 且未安装 sudo,无法以 postgres 用户执行命令" "Not running as root and sudo is not installed; cannot run commands as postgres")"
}

prompt_yes_no() {
	local prompt="$1" default="${2:-Y}" ans
	while true; do
		local hint="[Y/n]"
		case "${default,,}" in
		y | yes) hint="[Y/n]" ;;
		n | no) hint="[y/N]" ;;
		esac

		read -r -p "${prompt} ${hint} " ans || true
		ans="${ans:-$default}"
		case "${ans,,}" in
		y | yes) return 0 ;;
		n | no) return 1 ;;
		*) say "请输入 y 或 n" "Please enter y or n" ;;
		esac
	done
}

systemd_available() {
	need_cmd systemctl && [[ -d /run/systemd/system ]]
}

detect_service_manager() {
	if systemd_available; then
		printf '%s\n' systemd
		return 0
	fi
	printf '%s\n' none
}

select_service_manager() {
	local detected
	detected="$(detect_service_manager)"
	case "$SERVICE_MANAGER_MODE" in
	auto)
		[[ "$detected" != "none" ]] || die "$(txt "未检测到受支持的服务管理器;如需手动安装,请使用 --service-manager=none" "No supported service manager was detected; use --service-manager=none for a manual installation")"
		SERVICE_MANAGER="$detected"
		;;
	systemd)
		[[ "$detected" == "$SERVICE_MANAGER_MODE" ]] || die "$(txt "请求的服务管理器不可用:${SERVICE_MANAGER_MODE}" "Requested service manager is unavailable: ${SERVICE_MANAGER_MODE}")"
		SERVICE_MANAGER="$SERVICE_MANAGER_MODE"
		;;
	none)
		SERVICE_MANAGER="none"
		;;
	*)
		die "$(txt "无效的服务管理器:${SERVICE_MANAGER_MODE}(支持 auto/systemd/none)" "Invalid service manager: ${SERVICE_MANAGER_MODE} (supported: auto/systemd/none)")"
		;;
	esac
}

ensure_process_control() {
	need_cmd pgrep || die "$(txt "需要 pgrep 检查并停止现有 Dash 进程" "pgrep is required to detect and stop an existing Dash process")"
}

stop_manual_processes() {
	local -a pids=()
	mapfile -t pids < <(pgrep -f -- "$BIN_PATH" 2>/dev/null || true)
	((${#pids[@]} > 0)) || return 0

	say "停止现有手动 Dash 进程" "Stopping existing manually started Dash process"
	as_root kill -TERM "${pids[@]}" >/dev/null 2>&1 || true
	for _ in {1..50}; do
		mapfile -t pids < <(pgrep -f -- "$BIN_PATH" 2>/dev/null || true)
		((${#pids[@]} == 0)) && return 0
		sleep 0.1
	done
	as_root kill -KILL "${pids[@]}" >/dev/null 2>&1 || true
	sleep 0.1
	if pgrep -f -- "$BIN_PATH" >/dev/null 2>&1; then
		die "$(txt "无法停止现有 Dash 进程" "Failed to stop the existing Dash process")"
	fi
}

enable_time_sync() {
	say "启用系统时间同步(NTP,失败不影响安装)" "Enabling system time sync (NTP; non-fatal)"

	if need_cmd timedatectl && as_root timedatectl set-ntp true >/dev/null 2>&1; then
		say "系统时间同步已启用。" "System time sync is enabled."
		return 0
	fi

	local unit
	case "$SERVICE_MANAGER" in
	systemd)
		for unit in systemd-timesyncd.service chronyd.service ntpd.service ntp.service; do
			if as_root systemctl enable --now "$unit" >/dev/null 2>&1; then
				say "系统时间同步服务已启动:${unit}" "System time sync service started: ${unit}"
				return 0
			fi
		done
		;;
	none) ;;
	esac

	say_err "警告:未能自动启用系统时间同步,请手动检查 NTP/chrony/systemd-timesyncd。" "WARNING: Could not enable system time sync automatically; please check NTP/chrony/systemd-timesyncd manually."
	return 0
}

prompt_string() {
	local prompt="$1" default="${2:-}" out
	if [[ -n "$default" ]]; then
		read -r -p "${prompt} [${default}] " out || true
		echo "${out:-$default}"
	else
		read -r -p "${prompt} " out || true
		echo "$out"
	fi
}

prompt_language() {
	local default="${1:-$([[ "${INSTALL_LANG:-zh}" == "en" ]] && echo 1 || echo 2)}" ans
	while true; do
		say_err "请选择默认语言(app.language):" "Select default language (app.language):"
		say_err "  1) English(en)" "  1) English (en)"
		say_err "  2) 中文(zh)" "  2) 中文 (zh)"
		read -r -p "$(txt "请输入序号 [${default}] " "Enter number [${default}] ")" ans || true
		ans="${ans:-$default}"
		case "$ans" in
		1)
			echo "en"
			return 0
			;;
		2)
			echo "zh"
			return 0
			;;
		*) say_err "请输入 1 或 2" "Please enter 1 or 2" ;;
		esac
	done
}

prompt_retention_days() {
	local default="${1:-1}" ans
	while true; do
		say_err "请选择网卡 raw 和服务检查保留时长(database.retention_days):" "Select raw NIC metric and service-check retention (database.retention_days):"
		say_err "如需掌握流量历史或 95 计费历史,建议选择 90 days 或更高。" "Choose 90 days or higher if you need traffic history or 95th percentile billing history."
		say_err "  1) default(45 days)" "  1) default (45 days)"
		echo "  2) 90 days" >&2
		echo "  3) 180 days" >&2
		echo "  4) 365 days" >&2
		read -r -p "$(txt "请输入序号 [${default}] " "Enter number [${default}] ")" ans || true
		ans="${ans:-$default}"
		case "$ans" in
		1)
			echo "default"
			return 0
			;;
		2)
			echo "90"
			return 0
			;;
		3)
			echo "180"
			return 0
			;;
		4)
			echo "365"
			return 0
			;;
		*) say_err "请输入 1、2、3 或 4" "Please enter 1, 2, 3, or 4" ;;
		esac
	done
}

prompt_secret_confirm() {
	local prompt="$1" a b
	while true; do
		IFS= read -r -s -p "${prompt}: " a
		printf '\n' >&2
		IFS= read -r -s -p "$(txt "请再次输入确认: " "Confirm again: ")" b
		printf '\n' >&2
		[[ -n "$a" ]] || {
			say_err "不能为空,请重试" "Cannot be empty, please retry"
			continue
		}
		[[ "$a" == "$b" ]] || {
			say_err "两次输入不一致,请重试" "Passwords do not match, please retry"
			continue
		}
		echo "$a"
		return 0
	done
}

prompt_secret_optional() {
	local prompt="$1" a b
	while true; do
		IFS= read -r -s -p "${prompt}: " a
		printf '\n' >&2
		if [[ -z "$a" ]]; then
			printf '\n'
			return 0
		fi
		if [[ "$a" =~ [[:cntrl:]] ]]; then
			say_err "Redis 密码不能包含控制字符,请重试" "Redis password cannot contain control characters, please retry"
			continue
		fi
		IFS= read -r -s -p "$(txt "请再次输入确认: " "Confirm again: ")" b
		printf '\n' >&2
		[[ "$a" == "$b" ]] || {
			say_err "两次输入不一致,请重试" "Passwords do not match, please retry"
			continue
		}
		printf '%s\n' "$a"
		return 0
	done
}

version_ge() {
	local a="$1" b="$2"
	local IFS=.
	local -a av bv
	read -r -a av <<<"$a"
	read -r -a bv <<<"$b"
	for i in 0 1 2; do
		local ai="${av[i]:-0}" bi="${bv[i]:-0}"
		if ((10#${ai} > 10#${bi})); then return 0; fi
		if ((10#${ai} < 10#${bi})); then return 1; fi
	done
	return 0
}

package_version_matches() {
	local version="${1#*:}" want="$2"
	case "$version" in
	"$want" | "$want"-* | "$want"+* | "$want"~*) return 0 ;;
	*) return 1 ;;
	esac
}

apt_package_version() {
	local package="$1" want="$2" version
	while IFS='|' read -r _ version _; do
		version="$(trim_spaces "$version")"
		if package_version_matches "$version" "$want"; then
			printf '%s\n' "$version"
			return 0
		fi
	done < <(apt-cache madison "$package" 2>/dev/null)
	return 1
}

arch_package_version() {
	local package="$1"
	LC_ALL=C pacman -Si "$package" 2>/dev/null |
		awk -F: '$1 ~ /^Version[[:space:]]*$/ {sub(/^[[:space:]]*/, "", $2); print $2; exit}'
}

require_arch_package_version() {
	local package="$1" want="$2" version
	version="$(arch_package_version "$package" || true)"
	if [[ -z "$version" ]]; then
		die "$(txt "仓库中没有软件包 ${package}" "Package ${package} is unavailable in the configured repository")"
	fi
	package_version_matches "$version" "$want" ||
		die "$(txt "仓库中的 ${package}=${version} 不符合新装锁定版本 ${want};请预装兼容依赖后重试" "Repository package ${package}=${version} does not match the fresh-install pin ${want}; preinstall compatible dependencies and retry")"
}

find_listening_pids_on_port() {
	local port="$1"

	if need_cmd ss; then
		ss -H -ltnp "sport = :${port}" 2>/dev/null | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | sort -u
		return 0
	fi

	if need_cmd lsof; then
		lsof -nP -iTCP:"${port}" -sTCP:LISTEN -t 2>/dev/null | sort -u
		return 0
	fi

	if need_cmd fuser; then
		fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | sed -n 's/^\([0-9][0-9]*\)$/\1/p' | sort -u
		return 0
	fi

	return 0
}

pid_comm() {
	local pid="$1"
	if [[ -r "/proc/${pid}/comm" ]]; then
		tr -d '\n' <"/proc/${pid}/comm"
		return 0
	fi
	if need_cmd ps; then
		ps -p "$pid" -o comm= 2>/dev/null | head -n1 | tr -d '[:space:]'
		return 0
	fi
	return 1
}

pid_looks_like_redis() {
	local pid="$1" comm=""
	comm="$(pid_comm "$pid" 2>/dev/null || true)"
	case "$comm" in
	redis | redis-server | redis-sentinel)
		return 0
		;;
	esac
	if [[ -r "/proc/${pid}/cmdline" ]]; then
		tr '\0' ' ' <"/proc/${pid}/cmdline" | grep -Eq '(^|[ /])(redis|redis-server|redis-sentinel)( |$)'
		return $?
	fi
	return 1
}

port_has_redis_listener() {
	local port="$1"
	local pids="${2:-}"
	local pid
	if [[ -z "$pids" ]]; then
		pids="$(find_listening_pids_on_port "$port")"
	fi
	for pid in $pids; do
		if pid_looks_like_redis "$pid"; then
			return 0
		fi
	done
	return 1
}

confirm_redis_takeover() {
	if [[ "$REDIS_TAKEOVER_CONFIRMED" == "1" ]]; then
		return 0
	fi

	local pids
	pids="$(find_listening_pids_on_port 6379)"
	if [[ ! -e /etc/redis/redis.conf && ! -e /etc/systemd/system/redis-server.service && ! -e /etc/init.d/redis && ! -e /etc/init.d/redis-server && -z "$pids" ]]; then
		REDIS_TAKEOVER_CONFIRMED="1"
		return 0
	fi

	if ! prompt_yes_no "$(txt "安装器将备份并覆盖现有 Redis 配置/服务,并停止 6379 端口上的旧进程。是否继续?" "The installer will back up and replace the existing Redis configuration/service and stop the old process on port 6379. Continue?")" "Y"; then
		return 1
	fi
	REDIS_TAKEOVER_CONFIRMED="1"
}

kill_listeners_on_port() {
	local port="$1"
	local pids
	pids="$(find_listening_pids_on_port "$port")"
	if [[ -z "$pids" ]]; then
		return 0
	fi
	if port_has_redis_listener "$port" "$pids"; then
		say "检测到端口 ${port} 已由 Redis 监听,先停止旧 Redis 进程:${pids}" "Port ${port} is already owned by Redis; stopping old Redis listeners: ${pids}"
	else
		say "检测到端口 ${port} 已被占用,尝试结束占用进程:${pids}" "Port ${port} is in use; terminating listeners: ${pids}"
	fi

	case "$SERVICE_MANAGER" in
	systemd)
		as_root systemctl stop redis-server.service >/dev/null 2>&1 || true
		as_root systemctl stop redis.service >/dev/null 2>&1 || true
		;;
	none) ;;
	esac

	for pid in $pids; do
		as_root kill -TERM "$pid" >/dev/null 2>&1 || true
	done
	sleep 1

	pids="$(find_listening_pids_on_port "$port")"
	if [[ -z "$pids" ]]; then
		return 0
	fi
	for pid in $pids; do
		as_root kill -KILL "$pid" >/dev/null 2>&1 || true
	done
	sleep 1

	pids="$(find_listening_pids_on_port "$port")"
	if [[ -n "$pids" ]]; then
		die "$(txt "无法释放端口 ${port}(仍占用的 PID:${pids})" "Failed to free port ${port} (still listening PIDs: ${pids})")"
	fi
}

sql_escape_literal() {
	local s="$1"
	s="${s//\'/\'\'}"
	printf "%s" "$s"
}

systemd_escape_env_value() {
	local s="$1"
	s="${s//%/%%}"
	s="${s//\\/\\\\}"
	s="${s//\"/\\\"}"
	printf "%s" "$s"
}

shell_quote_arg() {
	local s="$1"
	s="${s//\'/\'\\\'\'}"
	printf "'%s'" "$s"
}

sed_escape_repl() {
	local s="$1"
	s="${s//\\/\\\\}"
	s="${s//&/\\&}"
	s="${s//|/\\|}"
	printf "%s" "$s"
}

write_redis_conf() {
	[[ "$REDIS_TAKEOVER_CONFIRMED" == "1" ]] || die "$(txt "内部错误:覆盖 Redis 配置前未确认" "Internal error: Redis takeover was not confirmed")"
	as_root install -d -m 0755 /etc/redis /var/lib/redis /var/log/redis
	as_root chown -R redis:redis /var/lib/redis /var/log/redis >/dev/null 2>&1 || true

	if [[ -f /etc/redis/redis.conf ]]; then
		local bak="/etc/redis/redis.conf.bak.$(date +%Y%m%d%H%M%S)"
		as_root cp -f /etc/redis/redis.conf "$bak"
	fi

	local supervised="no"
	[[ "$SERVICE_MANAGER" != "systemd" ]] || supervised="systemd"
	as_root bash -c "cat > /etc/redis/redis.conf <<EOF

bind 127.0.0.1 -::1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300

daemonize no
supervised ${supervised}
pidfile /run/redis/redis-server.pid

loglevel notice
logfile \"/var/log/redis/redis.log\"

databases 16
always-show-logo yes

save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis

appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
EOF"
}

detect_os() {
	[[ -r /etc/os-release ]] || die "$(txt "无法读取 /etc/os-release" "Cannot read /etc/os-release")"
	. /etc/os-release

	OS_ID="${ID:-}"
	OS_VERSION_ID="${VERSION_ID:-}"

	case "${OS_ID}" in
	debian)
		local major="${OS_VERSION_ID%%.*}"
		[[ "$major" =~ ^[0-9]+$ ]] || die "$(txt "无法解析 Debian VERSION_ID=${OS_VERSION_ID:-}" "Cannot parse Debian VERSION_ID=${OS_VERSION_ID:-}")"
		((major >= 11)) || die "$(txt "仅支持 Debian 11+,当前 VERSION_ID=${OS_VERSION_ID}" "Only Debian 11+ is supported (current VERSION_ID=${OS_VERSION_ID})")"
		OS_FAMILY="debian"
		APT_REPO_ID="debian"
		APT_REPO_CODENAME="${DEBIAN_CODENAME:-${VERSION_CODENAME:-}}"
		PKG_MANAGER="apt-get"
		PKG_MANAGER_LABEL="apt-get"
		;;
	ubuntu)
		local major="${OS_VERSION_ID%%.*}"
		[[ "$major" =~ ^[0-9]+$ ]] || die "$(txt "无法解析 Ubuntu VERSION_ID=${OS_VERSION_ID:-}" "Cannot parse Ubuntu VERSION_ID=${OS_VERSION_ID:-}")"
		((major >= 22)) || die "$(txt "仅支持 Ubuntu 22+,当前 VERSION_ID=${OS_VERSION_ID}" "Only Ubuntu 22+ is supported (current VERSION_ID=${OS_VERSION_ID})")"
		OS_FAMILY="debian"
		APT_REPO_ID="ubuntu"
		APT_REPO_CODENAME="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}"
		PKG_MANAGER="apt-get"
		PKG_MANAGER_LABEL="apt-get"
		;;
	rhel | rocky | almalinux | ol | centos)
		local major="${OS_VERSION_ID%%.*}"
		[[ "$major" =~ ^[0-9]+$ ]] || die "$(txt "无法解析 ${OS_ID} VERSION_ID=${OS_VERSION_ID:-}" "Cannot parse ${OS_ID} VERSION_ID=${OS_VERSION_ID:-}")"
		((major >= 8)) || die "$(txt "仅支持 RHEL/Rocky/Alma/Oracle/CentOS 8+,当前 VERSION_ID=${OS_VERSION_ID}" "Only RHEL/Rocky/Alma/Oracle/CentOS 8+ is supported (current VERSION_ID=${OS_VERSION_ID})")"
		OS_FAMILY="rhel"
		if need_cmd dnf; then
			PKG_MANAGER="dnf"
		elif need_cmd yum; then
			PKG_MANAGER="yum"
		else
			die "$(txt "未检测到 dnf/yum" "Neither dnf nor yum was found")"
		fi
		PKG_MANAGER_LABEL="${PKG_MANAGER}"
		;;
	fedora)
		local major="${OS_VERSION_ID%%.*}"
		[[ "$major" =~ ^[0-9]+$ ]] || die "$(txt "无法解析 Fedora VERSION_ID=${OS_VERSION_ID:-}" "Cannot parse Fedora VERSION_ID=${OS_VERSION_ID:-}")"
		((major >= 33)) || die "$(txt "仅支持 Fedora 33+,当前 VERSION_ID=${OS_VERSION_ID}" "Only Fedora 33+ is supported (current VERSION_ID=${OS_VERSION_ID})")"
		OS_FAMILY="fedora"
		if need_cmd dnf; then
			PKG_MANAGER="dnf"
		elif need_cmd yum; then
			PKG_MANAGER="yum"
		else
			die "$(txt "未检测到 dnf/yum" "Neither dnf nor yum was found")"
		fi
		PKG_MANAGER_LABEL="${PKG_MANAGER}"
		;;
	arch | manjaro)
		OS_FAMILY="arch"
		PKG_MANAGER="pacman"
		PKG_MANAGER_LABEL="pacman"
		;;
	alpine)
		OS_FAMILY="manual"
		PKG_MANAGER=""
		PKG_MANAGER_LABEL="manual"
		say_err "警告:Alpine 上的 Dash 仅支持显式手动模式;PostgreSQL ${POSTGRES_MAJOR}+、匹配其主版本的 TimescaleDB 和 Redis 必须预先安装并运行;全新环境目标为 PostgreSQL ${POSTGRES_VERSION} / TimescaleDB ${TIMESCALEDB_VERSION}。" "WARNING: Dash supports only explicit manual mode on Alpine; PostgreSQL ${POSTGRES_MAJOR}+, TimescaleDB for that PostgreSQL major, and Redis must already be installed and running; the fresh-environment target is PostgreSQL ${POSTGRES_VERSION} / TimescaleDB ${TIMESCALEDB_VERSION}."
		return 0
		;;
	*)
		case " ${ID_LIKE:-} " in
		*" ubuntu "*)
			OS_FAMILY="debian"
			APT_REPO_ID="ubuntu"
			APT_REPO_CODENAME="${UBUNTU_CODENAME:-}"
			[[ -n "$APT_REPO_CODENAME" ]] || die "$(txt "无法确定 ${OS_ID} 对应的 Ubuntu 基础版本代号(UBUNTU_CODENAME)" "Cannot determine the Ubuntu base codename for ${OS_ID} (UBUNTU_CODENAME)")"
			PKG_MANAGER="apt-get"
			PKG_MANAGER_LABEL="apt-get"
			;;
		*" debian "*)
			OS_FAMILY="debian"
			APT_REPO_ID="debian"
			APT_REPO_CODENAME="${DEBIAN_CODENAME:-}"
			[[ -n "$APT_REPO_CODENAME" ]] || die "$(txt "无法确定 ${OS_ID} 对应的 Debian 基础版本代号(DEBIAN_CODENAME)" "Cannot determine the Debian base codename for ${OS_ID} (DEBIAN_CODENAME)")"
			PKG_MANAGER="apt-get"
			PKG_MANAGER_LABEL="apt-get"
			;;
		*" rhel "* | *" fedora "*)
			OS_FAMILY="rhel"
			if need_cmd dnf; then
				PKG_MANAGER="dnf"
			elif need_cmd yum; then
				PKG_MANAGER="yum"
			else
				die "$(txt "未检测到 dnf/yum" "Neither dnf nor yum was found")"
			fi
			PKG_MANAGER_LABEL="${PKG_MANAGER}"
			;;
		*" arch "*)
			OS_FAMILY="arch"
			PKG_MANAGER="pacman"
			PKG_MANAGER_LABEL="pacman"
			;;
		*)
			if [[ "$SERVICE_MANAGER" == "none" ]]; then
				OS_FAMILY="manual"
				PKG_MANAGER=""
				PKG_MANAGER_LABEL="manual"
				return 0
			fi
			die "$(txt "仅支持 Debian/Ubuntu、RHEL/Rocky/Alma/Oracle/Fedora、Arch/Manjaro;当前系统 ID=${OS_ID:-unknown}" "Only Debian/Ubuntu, RHEL/Rocky/Alma/Oracle/Fedora, and Arch/Manjaro are supported (current ID=${OS_ID:-unknown})")"
			;;
		esac
		;;
	esac
}

pkg_update() {
	case "${PKG_MANAGER}" in
	apt-get)
		as_root apt-get update -y
		;;
	dnf)
		as_root dnf makecache -y
		;;
	yum)
		as_root yum makecache -y
		;;
	pacman)
		as_root pacman -Sy --noconfirm
		;;
	*)
		die "$(txt "不支持的包管理器:${PKG_MANAGER:-unknown}" "Unsupported package manager: ${PKG_MANAGER:-unknown}")"
		;;
	esac
}

pkg_install() {
	case "${PKG_MANAGER}" in
	apt-get)
		as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
		;;
	dnf)
		as_root dnf install -y "$@"
		;;
	yum)
		as_root yum install -y "$@"
		;;
	pacman)
		as_root pacman -S --noconfirm --needed "$@"
		;;
	*)
		die "$(txt "不支持的包管理器:${PKG_MANAGER:-unknown}" "Unsupported package manager: ${PKG_MANAGER:-unknown}")"
		;;
	esac
}

pkg_install_first_available() {
	local pkg
	for pkg in "$@"; do
		if pkg_install "$pkg" >/dev/null 2>&1; then
			printf '%s\n' "$pkg"
			return 0
		fi
	done
	return 1
}

ensure_pkg_prereqs() {
	pkg_update
	case "${OS_FAMILY}" in
	debian)
		pkg_install ca-certificates curl gnupg
		;;
	rhel | fedora)
		pkg_install ca-certificates curl gnupg2
		;;
	arch)
		pkg_install curl gnupg
		;;
	*)
		die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")"
		;;
	esac
}

ensure_postgres_binaries_on_path() {
	local current_major=""
	if need_cmd psql; then
		current_major="$(psql --version 2>/dev/null | sed -nE 's/.* ([0-9]+)(\.[0-9]+)?.*/\1/p')"
	fi

	local dir major best_dir="" best_major=0
	if [[ "$current_major" =~ ^[0-9]+$ ]] && ((current_major >= POSTGRES_MAJOR)); then
		best_major="$current_major"
	fi
	local -a dirs=()
	shopt -s nullglob
	dirs=(/usr/pgsql-*/bin /usr/lib/postgresql/*/bin)
	shopt -u nullglob
	for dir in "${dirs[@]}"; do
		[[ -x "${dir}/psql" ]] || continue
		major="$("${dir}/psql" --version 2>/dev/null | sed -nE 's/.* ([0-9]+)(\.[0-9]+)?.*/\1/p')"
		if [[ "$major" =~ ^[0-9]+$ ]] && ((major >= POSTGRES_MAJOR && major > best_major)); then
			best_dir="$dir"
			best_major="$major"
		fi
	done
	if [[ -n "$best_dir" ]]; then
		export PATH="${best_dir}:${PATH}"
	fi
}

systemd_enable_now_first() {
	local unit
	for unit in "$@"; do
		if as_root systemctl enable --now "$unit" >/dev/null 2>&1; then
			return 0
		fi
	done
	return 1
}

systemd_restart_first() {
	local unit
	for unit in "$@"; do
		if as_root systemctl restart "$unit" >/dev/null 2>&1; then
			return 0
		fi
	done
	return 1
}

enable_postgres_service() {
	local pg_major
	pg_major="$(postgres_major_version)"
	[[ "$pg_major" =~ ^[0-9]+$ ]] || pg_major="$POSTGRES_MAJOR"
	case "$SERVICE_MANAGER" in
	systemd)
		case "${OS_FAMILY}" in
		debian | arch) as_root systemctl enable --now postgresql.service ;;
		rhel | fedora) as_root systemctl enable --now "postgresql-${pg_major}.service" ;;
		*) die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")" ;;
		esac
		;;
	none) return 0 ;;
	esac
}

restart_postgres_service() {
	local pg_major
	pg_major="$(postgres_major_version)"
	[[ "$pg_major" =~ ^[0-9]+$ ]] || die "$(txt "无法确定 PostgreSQL 主版本" "Cannot determine PostgreSQL major version")"
	case "$SERVICE_MANAGER" in
	systemd)
		case "${OS_FAMILY}" in
		debian | arch) as_root systemctl restart postgresql.service ;;
		rhel | fedora) as_root systemctl restart "postgresql-${pg_major}.service" ;;
		*) die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")" ;;
		esac
		;;
	none) return 0 ;;
	esac
}

enable_restart_redis_service() {
	case "$SERVICE_MANAGER" in
	systemd)
		systemd_enable_now_first redis-server.service redis6.service redis.service >/dev/null 2>&1 || true
		systemd_restart_first redis-server.service redis6.service redis.service >/dev/null 2>&1 || true
		;;
	none) return 0 ;;
	esac
}

write_systemd_redis_service() {
	[[ "$REDIS_TAKEOVER_CONFIRMED" == "1" ]] || die "$(txt "内部错误:覆盖 Redis 服务前未确认" "Internal error: Redis takeover was not confirmed")"
	local unit="/etc/systemd/system/redis-server.service"
	as_root install -d -m 0755 /etc/systemd/system
	if [[ -e "$unit" || -L "$unit" ]]; then
		local bak="${unit}.bak.$(date +%Y%m%d%H%M%S)"
		as_root cp -a "$unit" "$bak" >/dev/null 2>&1 || true
		as_root rm -f "$unit"
	fi

	as_root bash -c "cat > ${unit} <<'EOF'
[Unit]
Description=Redis In-Memory Data Store
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
ExecStop=/usr/local/bin/redis-cli shutdown
Restart=always
RestartSec=2
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF"
}

write_redis_service() {
	[[ "$REDIS_TAKEOVER_CONFIRMED" == "1" ]] || die "$(txt "内部错误:覆盖 Redis 服务前未确认" "Internal error: Redis takeover was not confirmed")"
	case "$SERVICE_MANAGER" in
	systemd) write_systemd_redis_service ;;
	none) return 0 ;;
	esac
}

install_repo_key() {
	local url="$1" fingerprints="$2" destination="$3" format="${4:-armored}"
	local tmp actual expected
	tmp="$(mktemp -d)"
	if ! curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fsSL -o "${tmp}/key" "$url"; then
		rm -rf "$tmp"
		die "$(txt "下载仓库签名密钥失败:${url}" "Failed to download repository signing key: ${url}")"
	fi
	if ! actual="$(gpg --batch --with-colons --show-keys "${tmp}/key" 2>/dev/null |
		awk -F: '$1 == "pub" {primary=1; next} primary && $1 == "fpr" {print $10; primary=0}' |
		sort -u)"; then
		rm -rf "$tmp"
		die "$(txt "无法读取仓库签名密钥:${url}" "Failed to inspect repository signing key: ${url}")"
	fi
	expected="$(printf '%s\n' "$fingerprints" | tr ',' '\n' | sort -u)"
	if [[ -z "$actual" || "$actual" != "$expected" ]]; then
		rm -rf "$tmp"
		die "$(txt "仓库签名密钥指纹不匹配:${url}" "Repository signing-key fingerprint mismatch: ${url}")"
	fi

	local source="${tmp}/key"
	if [[ "$format" == "gpg" ]]; then
		if ! gpg --batch --yes --dearmor --output "${tmp}/key.gpg" "${tmp}/key"; then
			rm -rf "$tmp"
			die "$(txt "转换仓库签名密钥失败" "Failed to convert repository signing key")"
		fi
		source="${tmp}/key.gpg"
	fi
	as_root install -D -m 0644 "$source" "$destination"
	rm -rf "$tmp"
}

setup_postgresql_repo() {
	local arch
	arch="$(uname -m)"

	case "${OS_FAMILY}" in
	debian)
		if [[ -z "${APT_REPO_CODENAME}" && "$OS_ID" == "$APT_REPO_ID" ]] && need_cmd lsb_release; then
			APT_REPO_CODENAME="$(lsb_release -cs 2>/dev/null || true)"
		fi
		[[ -n "${APT_REPO_CODENAME}" ]] || die "$(txt "无法确定 Debian/Ubuntu 基础版本代号" "Cannot determine the Debian/Ubuntu base codename")"
		local keyring="/usr/share/keyrings/postgresql-archive-keyring.gpg"
		install_repo_key \
			https://www.postgresql.org/media/keys/ACCC4CF8.asc \
			B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 \
			"$keyring" \
			gpg
		as_root bash -c "cat > /etc/apt/sources.list.d/pgdg.list <<EOF
deb [signed-by=${keyring}] https://apt.postgresql.org/pub/repos/apt ${APT_REPO_CODENAME}-pgdg main
EOF"
		;;
	rhel)
		local major="${OS_VERSION_ID%%.*}"
		pkg_install "https://download.postgresql.org/pub/repos/yum/reporpms/EL-${major}-${arch}/pgdg-redhat-repo-latest.noarch.rpm"
		if [[ "${PKG_MANAGER}" == "dnf" ]]; then
			as_root dnf -qy module disable postgresql >/dev/null 2>&1 || true
		else
			as_root yum -qy module disable postgresql >/dev/null 2>&1 || true
		fi
		;;
	fedora)
		local major="${OS_VERSION_ID%%.*}"
		pkg_install "https://download.postgresql.org/pub/repos/yum/reporpms/F-${major}-${arch}/pgdg-fedora-repo-latest.noarch.rpm"
		if [[ "${PKG_MANAGER}" == "dnf" ]]; then
			as_root dnf -qy module disable postgresql >/dev/null 2>&1 || true
		else
			as_root yum -qy module disable postgresql >/dev/null 2>&1 || true
		fi
		;;
	arch)
		return 0
		;;
	*)
		die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")"
		;;
	esac
}

postgres_cluster_initialized() {
	local pg_version_file
	for pg_version_file in \
		"/var/lib/postgresql/${POSTGRES_MAJOR}/main/PG_VERSION" \
		/var/lib/postgresql/data/PG_VERSION \
		"/var/lib/pgsql/${POSTGRES_MAJOR}/data/PG_VERSION" \
		/var/lib/pgsql/data/PG_VERSION \
		/var/lib/postgres/data/PG_VERSION; do
		if [[ -f "$pg_version_file" ]]; then
			return 0
		fi
	done
	return 1
}

init_postgres_cluster_if_needed() {
	postgres_cluster_initialized && return 0

	ensure_postgres_binaries_on_path
	case "${OS_FAMILY}" in
	debian)
		need_cmd pg_createcluster || die "$(txt "缺少 pg_createcluster,无法初始化 PostgreSQL ${POSTGRES_MAJOR}" "pg_createcluster is required to initialize PostgreSQL ${POSTGRES_MAJOR}")"
		as_root pg_createcluster "$POSTGRES_MAJOR" main
		;;
	rhel | fedora)
		local setup="/usr/pgsql-${POSTGRES_MAJOR}/bin/postgresql-${POSTGRES_MAJOR}-setup"
		[[ -x "$setup" ]] || die "$(txt "缺少 postgresql-${POSTGRES_MAJOR}-setup" "postgresql-${POSTGRES_MAJOR}-setup is missing")"
		as_root "$setup" initdb
		;;
	arch)
		need_cmd initdb || die "$(txt "缺少 initdb" "initdb is missing")"
		as_root install -d -m 0700 -o postgres -g postgres /var/lib/postgres/data
		as_postgres initdb -D /var/lib/postgres/data
		;;
	*)
		die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")"
		;;
	esac
	postgres_cluster_initialized || die "$(txt "PostgreSQL 数据目录初始化失败" "PostgreSQL data directory was not initialized")"
}

install_postgresql16() {
	ensure_pkg_prereqs
	setup_postgresql_repo
	pkg_update

	case "${OS_FAMILY}" in
	debian)
		local server="postgresql-${POSTGRES_MAJOR}" client="postgresql-client-${POSTGRES_MAJOR}"
		local server_version client_version
		server_version="$(apt_package_version "$server" "$POSTGRES_VERSION" || true)"
		client_version="$(apt_package_version "$client" "$POSTGRES_VERSION" || true)"
		[[ -n "$server_version" && -n "$client_version" ]] ||
			die "$(txt "PGDG 仓库没有新装锁定的 PostgreSQL ${POSTGRES_VERSION}" "The PGDG repository does not provide the pinned fresh-install PostgreSQL ${POSTGRES_VERSION}")"
		pkg_install "${server}=${server_version}" "${client}=${client_version}"
		;;
	rhel | fedora)
		pkg_install \
			"postgresql${POSTGRES_MAJOR}-server-${POSTGRES_VERSION}-*" \
			"postgresql${POSTGRES_MAJOR}-${POSTGRES_VERSION}-*"
		;;
	arch)
		require_arch_package_version postgresql "$POSTGRES_VERSION"
		pkg_install postgresql
		;;
	*)
		die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")"
		;;
	esac

	ensure_postgres_binaries_on_path
	init_postgres_cluster_if_needed
	enable_postgres_service

	local installed_version
	installed_version="$(postgres_server_version || true)"
	[[ "$installed_version" == "$POSTGRES_VERSION" ]] ||
		die "$(txt "新装 PostgreSQL 版本校验失败:当前 ${installed_version:-unknown},要求 ${POSTGRES_VERSION}" "Fresh PostgreSQL version check failed: got ${installed_version:-unknown}, require ${POSTGRES_VERSION}")"
}

postgres_server_version() {
	ensure_postgres_binaries_on_path
	need_cmd psql || return 1
	local server_num
	server_num="$(as_postgres psql -d postgres -tAc 'SHOW server_version_num' 2>/dev/null | tr -d '[:space:]')"
	[[ "$server_num" =~ ^[0-9]+$ ]] && ((server_num >= 10000)) || return 1
	printf '%s.%s\n' "$((server_num / 10000))" "$((server_num % 10000))"
}

postgres_major_version() {
	ensure_postgres_binaries_on_path
	if ! need_cmd psql; then
		echo ""
		return 0
	fi
	local server_num v
	server_num="$(as_postgres psql -d postgres -tAc 'SHOW server_version_num' 2>/dev/null | tr -d '[:space:]' || true)"
	if [[ "$server_num" =~ ^[0-9]+$ ]] && ((server_num >= 10000)); then
		echo "$((server_num / 10000))"
		return 0
	fi
	v="$(psql --version 2>/dev/null | awk '{print $3}' || true)"
	echo "${v%%.*}"
}

ensure_postgresql16_and_password() {
	local installed_by_script="0"
	local major
	major="$(postgres_major_version)"

	if [[ -z "$major" ]] || ! [[ "$major" =~ ^[0-9]+$ ]]; then
		if prompt_yes_no "$(txt "未检测到 PostgreSQL,是否安装新装锁定版本 PostgreSQL ${POSTGRES_VERSION}?" "PostgreSQL not detected. Install the pinned fresh-install version ${POSTGRES_VERSION}?")"; then
			install_postgresql16
			installed_by_script="1"
		else
			die "$(txt "未安装 PostgreSQL,无法继续" "PostgreSQL is required")"
		fi
	elif ((major < POSTGRES_MAJOR)); then
		if prompt_yes_no "$(txt "检测到 PostgreSQL ${major},需要 ${POSTGRES_MAJOR}+。是否安装 PostgreSQL ${POSTGRES_VERSION}?" "Detected PostgreSQL ${major}. Need ${POSTGRES_MAJOR}+. Install PostgreSQL ${POSTGRES_VERSION}?")"; then
			install_postgresql16
			installed_by_script="1"
		else
			die "$(txt "PostgreSQL 版本不足,无法继续" "PostgreSQL version too old")"
		fi
	fi

	if [[ "$installed_by_script" == "1" ]]; then
		local pw pw_sql
		pw="$(prompt_secret_confirm "$(txt "请设置 PostgreSQL 管理员(postgres)密码" "Set PostgreSQL admin (postgres) password")")"
		pw_sql="$(sql_escape_literal "$pw")"
		as_postgres psql -v ON_ERROR_STOP=1 -c "ALTER USER postgres WITH PASSWORD '${pw_sql}';" >/dev/null
	fi
}

timescaledb_installed() {
	ensure_postgres_binaries_on_path
	local pg_major
	pg_major="$(postgres_major_version)"
	[[ "$pg_major" =~ ^[0-9]+$ ]] || return 1
	case "${OS_FAMILY}" in
	debian) [[ -f "/usr/share/postgresql/${pg_major}/extension/timescaledb.control" ]] ;;
	rhel | fedora) [[ -f "/usr/pgsql-${pg_major}/share/extension/timescaledb.control" ]] ;;
	arch)
		need_cmd pg_config || return 1
		local sharedir
		sharedir="$(pg_config --sharedir)" || return 1
		[[ -f "${sharedir}/extension/timescaledb.control" ]]
		;;
	manual)
		need_cmd pg_config || return 1
		local sharedir
		sharedir="$(pg_config --sharedir)" || return 1
		[[ -f "${sharedir}/extension/timescaledb.control" ]]
		;;
	*) return 1 ;;
	esac
}

install_timescaledb_for_postgres() {
	ensure_pkg_prereqs
	local pg_major
	pg_major="$(postgres_major_version)"
	[[ "$pg_major" =~ ^[0-9]+$ ]] && ((pg_major >= POSTGRES_MAJOR)) || die "$(txt "无法确定受支持的 PostgreSQL 主版本" "Cannot determine a supported PostgreSQL major version")"

	case "${OS_FAMILY}" in
	debian)
		[[ -n "${APT_REPO_ID}" && -n "${APT_REPO_CODENAME}" ]] || die "$(txt "无法确定 Debian/Ubuntu 基础仓库身份" "Cannot determine the Debian/Ubuntu base repository identity")"
		local keyring="/etc/apt/keyrings/timescale_timescaledb-archive-keyring.gpg"
		install_repo_key \
			https://packagecloud.io/timescale/timescaledb/gpgkey \
			1005FB68604CE9B8F6879CF759F18EDF47F24417,0641009A8366FDE4444A7B62E7391C94080429FF \
			"$keyring" \
			gpg
		as_root bash -c "cat > /etc/apt/sources.list.d/timescale_timescaledb.list <<EOF
deb [signed-by=${keyring}] https://packagecloud.io/timescale/timescaledb/${APT_REPO_ID} ${APT_REPO_CODENAME} main
EOF"
		pkg_update
		local extension="timescaledb-2-postgresql-${pg_major}"
		local loader="timescaledb-2-loader-postgresql-${pg_major}"
		local extension_version loader_version
		extension_version="$(apt_package_version "$extension" "$TIMESCALEDB_VERSION" || true)"
		loader_version="$(apt_package_version "$loader" "$TIMESCALEDB_VERSION" || true)"
		[[ -n "$extension_version" && -n "$loader_version" ]] ||
			die "$(txt "TimescaleDB 仓库没有新装锁定的 TimescaleDB ${TIMESCALEDB_VERSION}(PostgreSQL ${pg_major})" "The TimescaleDB repository does not provide the pinned fresh-install TimescaleDB ${TIMESCALEDB_VERSION} for PostgreSQL ${pg_major}")"
		pkg_install "${extension}=${extension_version}" "${loader}=${loader_version}"
		;;
	rhel | fedora)
		local repo_os="fedora"
		if [[ "${OS_FAMILY}" == "rhel" ]]; then
			repo_os="el"
		fi
		local os_major="${OS_VERSION_ID%%.*}"
		local key="/etc/pki/rpm-gpg/timescale-timescaledb.asc"
		install_repo_key \
			https://packagecloud.io/timescale/timescaledb/gpgkey \
			1005FB68604CE9B8F6879CF759F18EDF47F24417,0641009A8366FDE4444A7B62E7391C94080429FF \
			"$key"
		as_root bash -c "cat > /etc/yum.repos.d/timescale_timescaledb.repo <<EOF
[timescale_timescaledb]
name=timescale_timescaledb
baseurl=https://packagecloud.io/timescale/timescaledb/${repo_os}/${os_major}/\$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=file://${key}
sslverify=1
metadata_expire=300
EOF"
		pkg_update
		pkg_install \
			"timescaledb-2-postgresql-${pg_major}-${TIMESCALEDB_VERSION}-*" \
			"timescaledb-2-loader-postgresql-${pg_major}-${TIMESCALEDB_VERSION}-*"
		;;
	arch)
		require_arch_package_version timescaledb "$TIMESCALEDB_VERSION"
		pkg_install timescaledb
		;;
	*)
		die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")"
		;;
	esac
}

ensure_timescaledb_enabled() {
	local pg_major
	pg_major="$(postgres_major_version)"
	[[ "$pg_major" =~ ^[0-9]+$ ]] || die "$(txt "无法确定 PostgreSQL 主版本" "Cannot determine PostgreSQL major version")"
	if timescaledb_installed; then
		return 0
	fi

	if prompt_yes_no "$(txt "未检测到匹配 PostgreSQL ${pg_major} 的 TimescaleDB,是否安装新装锁定版本 ${TIMESCALEDB_VERSION}?" "TimescaleDB for PostgreSQL ${pg_major} was not detected. Install the pinned fresh-install version ${TIMESCALEDB_VERSION}?")"; then
		install_timescaledb_for_postgres
	else
		die "$(txt "TimescaleDB 未安装,无法继续" "TimescaleDB is required")"
	fi

	if need_cmd timescaledb-tune; then
		as_root timescaledb-tune --quiet --yes
	fi
	restart_postgres_service

	local installed_version
	installed_version="$(as_postgres psql -d postgres -tAc "SELECT default_version FROM pg_available_extensions WHERE name = 'timescaledb'" 2>/dev/null | tr -d '[:space:]' || true)"
	[[ "$installed_version" == "$TIMESCALEDB_VERSION" ]] ||
		die "$(txt "新装 TimescaleDB 版本校验失败:当前 ${installed_version:-unknown},要求 ${TIMESCALEDB_VERSION}" "Fresh TimescaleDB version check failed: got ${installed_version:-unknown}, require ${TIMESCALEDB_VERSION}")"
}

redis_version() {
	local best="" bin v
	local -a bins=()
	if need_cmd redis-server; then
		bins+=("$(command -v redis-server)")
	fi
	bins+=(/usr/local/bin/redis-server /usr/bin/redis-server /bin/redis-server)

	for bin in "${bins[@]}"; do
		[[ -x "$bin" ]] || continue
		v="$("$bin" --version 2>/dev/null | sed -n 's/.*v=\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -n1 || true)"
		[[ -n "$v" ]] || continue
		if [[ -z "$best" ]] || version_ge "$v" "$best"; then
			best="$v"
		fi
	done

	echo "$best"
}

install_redis_via_package_manager() {
	ensure_pkg_prereqs

	case "${OS_FAMILY}" in
	debian)
		pkg_install redis-server || return 1
		;;
	rhel | fedora)
		pkg_install_first_available redis6 redis >/dev/null || return 1
		;;
	arch)
		pkg_install redis || return 1
		;;
	*)
		return 1
		;;
	esac

	if ! id -u redis >/dev/null 2>&1; then
		as_root useradd --system --no-create-home --shell /usr/sbin/nologin redis
	fi
}

install_redis_build_deps() {
	case "${OS_FAMILY}" in
	debian)
		if [[ "$SERVICE_MANAGER" == "systemd" ]]; then
			pkg_install build-essential pkg-config tcl libsystemd-dev
		else
			pkg_install build-essential pkg-config tcl
		fi
		;;
	rhel | fedora)
		if [[ "$SERVICE_MANAGER" == "systemd" ]]; then
			pkg_install gcc make pkgconf-pkg-config tcl systemd-devel
		else
			pkg_install gcc make pkgconf-pkg-config tcl
		fi
		;;
	arch)
		pkg_install base-devel pkgconf tcl
		;;
	*)
		die "$(txt "未知系统族:${OS_FAMILY:-unknown}" "Unknown OS family: ${OS_FAMILY:-unknown}")"
		;;
	esac
}

install_redis_from_source() {
	local ver="${1:-8.2.5}"
	if ! confirm_redis_takeover; then
		return 1
	fi

	ensure_pkg_prereqs
	install_redis_build_deps

	(
		local tmp tgz
		tmp="$(mktemp -d)"
		trap 'rm -rf "$tmp"' EXIT

		tgz="${tmp}/redis-${ver}.tar.gz"
		curl -fsSL -o "$tgz" "https://download.redis.io/releases/redis-${ver}.tar.gz"
		tar -C "$tmp" -xzf "$tgz"
		pushd "${tmp}/redis-${ver}" >/dev/null
		if [[ "$SERVICE_MANAGER" == "systemd" ]]; then
			make USE_SYSTEMD=yes -j"$(nproc)"
		else
			make -j"$(nproc)"
		fi
		as_root make install
		hash -r || true
		popd >/dev/null

		if ! id -u redis >/dev/null 2>&1; then
			as_root useradd --system --no-create-home --shell /usr/sbin/nologin redis
		fi
		write_redis_conf
		write_redis_service
		kill_listeners_on_port 6379

		case "$SERVICE_MANAGER" in
		systemd)
			as_root systemctl daemon-reload
			as_root systemctl enable --now redis-server.service
			;;
		none) ;;
		esac
	)
}

ensure_redis_82plus() {
	local want="8.2.3"
	local v
	local tried_pkg=0

	case "${REDIS_INSTALL_METHOD}" in
	package | apt)
		v="$(redis_version)"
		if [[ -z "$v" ]]; then
			if prompt_yes_no "$(txt "未检测到 Redis,是否先尝试使用系统包管理器安装兼容的 Redis?" "Redis not detected. Try installing a compatible Redis via the system package manager?")"; then
				tried_pkg=1
				if ! install_redis_via_package_manager; then
					say "系统包管理器未提供可直接使用的 redis-server。" "No usable redis-server package was found in the system repositories."
				fi
				v="$(redis_version)"
			else
				say "已跳过系统包管理器安装 Redis。" "Skipped Redis installation via the system package manager."
			fi
		fi

		if [[ -n "$v" ]] && version_ge "$v" "$want"; then
			if confirm_redis_takeover; then
				write_redis_conf
				enable_restart_redis_service
			else
				say "保留现有 Redis ${v} 的配置和服务,不执行覆盖。" "Keeping the existing Redis ${v} configuration and service; no replacement was performed."
			fi
			return 0
		fi

		if [[ -n "$v" ]]; then
			say "检测到 Redis ${v},但需要 >=8.2.3。" "Detected Redis ${v}, but >=8.2.3 is required."
		elif [[ "$tried_pkg" -eq 1 ]]; then
			say "系统包管理器安装后仍未检测到可用的 redis-server。" "A usable redis-server binary is still not available after the package-manager attempt."
		else
			say "未检测到 Redis。" "Redis was not detected."
		fi
		if ! prompt_yes_no "$(txt "是否源码安装/升级 Redis(默认 8.2.5)?" "Install or upgrade Redis from source instead? (default 8.2.5)")"; then
			if [[ -n "$v" ]]; then
				die "$(txt "Redis 版本不足,无法继续" "Redis version is insufficient")"
			fi
			die "$(txt "Redis 未安装,无法继续" "Redis is required")"
		fi
		local target_ver_pkg
		target_ver_pkg="$(prompt_string "$(txt "请输入要源码安装的 Redis 版本" "Redis version to install from source")" "8.2.5")"
		install_redis_from_source "$target_ver_pkg" || die "$(txt "已取消覆盖现有 Redis,无法完成源码安装/升级" "Redis takeover was declined; the source install/upgrade cannot continue")"
		v="$(redis_version)"
		[[ -n "$v" ]] || die "$(txt "Redis 安装失败:未检测到 redis-server" "Redis install failed: redis-server not found")"
		version_ge "$v" "$want" || die "$(txt "Redis 版本仍不足(当前 ${v},需要 >=8.2.3)" "Redis version still too old (current ${v}, need >=8.2.3)")"
		;;
	source)
		v="$(redis_version)"
		if [[ -n "$v" ]] && version_ge "$v" "$want"; then
			return 0
		fi

		if [[ -z "$v" ]]; then
			if ! prompt_yes_no "$(txt "未检测到 Redis,是否源码安装 Redis(默认 8.2.5)?" "Redis not detected. Install Redis (default 8.2.5) from source?")"; then
				die "$(txt "Redis 未安装,无法继续" "Redis is required")"
			fi
		else
			if ! prompt_yes_no "$(txt "检测到 Redis ${v},需要 >=8.2.3。是否源码安装/升级?" "Detected Redis ${v}. Need >=8.2.3. Install/upgrade from source?")"; then
				die "$(txt "Redis 版本不足,无法继续" "Redis version too old")"
			fi
		fi

		local target_ver
		target_ver="$(prompt_string "$(txt "请输入要源码安装的 Redis 版本" "Redis version to install (source build)")" "8.2.5")"
		install_redis_from_source "$target_ver" || die "$(txt "已取消覆盖现有 Redis,无法完成源码安装/升级" "Redis takeover was declined; the source install/upgrade cannot continue")"

		v="$(redis_version)"
		[[ -n "$v" ]] || die "$(txt "Redis 安装失败:未检测到 redis-server" "Redis install failed: redis-server not found")"
		version_ge "$v" "$want" || die "$(txt "Redis 版本仍不足(当前 ${v},需要 >=8.2.3)" "Redis version still too old (current ${v}, need >=8.2.3)")"
		;;
	*)
		die "$(txt "未知 REDIS_INSTALL_METHOD=${REDIS_INSTALL_METHOD}(支持:package/source;apt 仍可作为兼容别名)" "Unknown REDIS_INSTALL_METHOD=${REDIS_INSTALL_METHOD} (supported: package/source; apt remains a compatibility alias)")"
		;;
	esac
}

check_preinstalled_dependencies() {
	local major
	major="$(postgres_major_version)"
	[[ "$major" =~ ^[0-9]+$ ]] && ((major >= POSTGRES_MAJOR)) || die "$(txt "手动依赖模式要求已安装 PostgreSQL ${POSTGRES_MAJOR}+" "Manual dependency mode requires PostgreSQL ${POSTGRES_MAJOR}+")"
	timescaledb_installed || die "$(txt "手动依赖模式要求已安装与当前 PostgreSQL 匹配的 TimescaleDB" "Manual dependency mode requires TimescaleDB for the installed PostgreSQL")"
	as_postgres psql -d postgres -tAc 'SELECT 1' >/dev/null || die "$(txt "无法连接本机 PostgreSQL;请先启动服务" "Cannot connect to local PostgreSQL; start it before continuing")"
}

check_redis_endpoint() {
	local addr="$1" password="${2:-}"
	local checker="${SCRIPT_DIR}/bin/dash"
	[[ -x "$checker" ]] || die "$(txt "安装包缺少可执行的 ${checker}" "The package is missing executable ${checker}")"
	if [[ -z "$password" ]]; then
		"$checker" check-redis --addr "$addr"
		return
	fi

	(
		local password_file
		umask 077
		password_file="$(mktemp -t dash-redis-password-XXXXXX)"
		trap 'rm -f "$password_file"' EXIT
		chmod 0600 "$password_file"
		printf '%s' "$password" >"$password_file"
		"$checker" check-redis --addr "$addr" --password-file "$password_file"
	)
}

validate_ident() {
	local name="$1" what="$2"
	[[ "$name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || die "$(txt "${what} 仅允许字母/数字/下划线,且不能以数字开头:${name}" "${what} must match [a-zA-Z_][a-zA-Z0-9_]* (got: ${name})")"
}

create_db_and_user() {
	local db_user="$1" db_pass="$2" db_name="$3"
	validate_ident "$db_user" "database.user"
	validate_ident "$db_name" "database.name"

	local pass_sql
	pass_sql="$(sql_escape_literal "$db_pass")"

	local role_exists db_exists
	role_exists="$(as_postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${db_user}'" | tr -d '[:space:]')"
	if [[ "$role_exists" != "1" ]]; then
		as_postgres psql -v ON_ERROR_STOP=1 -c "CREATE USER ${db_user} WITH PASSWORD '${pass_sql}';" >/dev/null
	else
		as_postgres psql -v ON_ERROR_STOP=1 -c "ALTER USER ${db_user} WITH PASSWORD '${pass_sql}';" >/dev/null
	fi

	db_exists="$(as_postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${db_name}'" | tr -d '[:space:]')"
	if [[ "$db_exists" != "1" ]]; then
		as_postgres psql -v ON_ERROR_STOP=1 -c "CREATE DATABASE ${db_name} OWNER ${db_user};" >/dev/null
	fi

	as_postgres psql -d "${db_name}" -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS timescaledb;" >/dev/null
}

grant_db_privileges() {
	local db_user="$1" db_name="$2"
	validate_ident "$db_user" "database.user"
	validate_ident "$db_name" "database.name"

	as_postgres psql -d "${db_name}" -v ON_ERROR_STOP=1 <<SQL >/dev/null
GRANT CONNECT ON DATABASE ${db_name} TO ${db_user};
GRANT USAGE ON SCHEMA public TO ${db_user};
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${db_user};
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO ${db_user};
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${db_user};
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO ${db_user};
SQL
}

render_config_local() {
	local dash_ip="$1" listen_port="$2" public_url="$3" db_user="$4" db_pass="$5" db_name="$6"
	local retention_days="${7:-default}"
	local redis_addr="$8"
	local redis_password="$9"
	local offline_threshold="${10:-${DEFAULT_NODE_OFFLINE_THRESHOLD}}"
	local language="${11:-${INSTALL_LANG:-zh}}"
	local trusted_proxies_yaml="${12:-[]}"

	as_root install -d -m 0755 "$CONFIG_DIR"
	[[ -f "$CONFIG_EXAMPLE" ]] || die "$(txt "缺少模板 ${CONFIG_EXAMPLE}(请在安装目录放置 config.example.yaml)" "Missing template ${CONFIG_EXAMPLE}")"

	local listen=":${listen_port}"

	listen="$(one_line "$listen")"
	public_url="$(one_line "$public_url")"
	dash_ip="$(one_line "$dash_ip")"
	db_user="$(one_line "$db_user")"
	db_pass="$(one_line "$db_pass")"
	db_name="$(one_line "$db_name")"
	retention_days="$(one_line "$retention_days")"
	redis_addr="$(one_line "$redis_addr")"
	redis_password="$(one_line "$redis_password")"
	offline_threshold="$(one_line "$offline_threshold")"
	offline_threshold="${offline_threshold:-${DEFAULT_NODE_OFFLINE_THRESHOLD}}"
	language="$(one_line "$language")"
	case "${language}" in
	zh | en) ;;
	*) die "$(txt "app.language 只能是 zh 或 en:${language}" "app.language must be zh or en: ${language}")" ;;
	esac
	case "${retention_days}" in
	default | 90 | 180 | 365) ;;
	*) die "$(txt "database.retention_days 只能是 default/90/180/365:${retention_days}" "database.retention_days must be one of default/90/180/365: ${retention_days}")" ;;
	esac
	trusted_proxies_yaml="$(one_line "$trusted_proxies_yaml")"
	trusted_proxies_yaml="${trusted_proxies_yaml:-[]}"

	local retention_line="# retention_days: 45"
	if [[ "${retention_days}" != "default" ]]; then
		retention_line="retention_days: ${retention_days}"
	fi

	local dash_ip_esc listen_esc public_url_esc db_user_esc db_pass_esc db_name_esc retention_line_esc redis_addr_esc redis_password_esc offline_th_esc language_esc
	dash_ip_esc="$(yaml_dq_escape "$dash_ip")"
	listen_esc="$(yaml_dq_escape "$listen")"
	public_url_esc="$(yaml_dq_escape "$public_url")"
	db_user_esc="$(yaml_dq_escape "$db_user")"
	db_pass_esc="$(yaml_dq_escape "$db_pass")"
	db_name_esc="$(yaml_dq_escape "$db_name")"
	retention_line_esc="$(yaml_dq_escape "$retention_line")"
	redis_addr_esc="$(yaml_dq_escape "$redis_addr")"
	redis_password_esc="$(yaml_dq_escape "$redis_password")"
	offline_th_esc="$(yaml_dq_escape "$offline_threshold")"
	language_esc="$(yaml_dq_escape "$language")"

	local jwt_signing_key jwt_signing_key_esc
	jwt_signing_key="$(set +o pipefail; tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)"
	[[ "${#jwt_signing_key}" -ge 32 ]] || die "JWT signing key generation failed"
	jwt_signing_key_esc="$(yaml_dq_escape "$jwt_signing_key")"

	local tmp
	tmp="$(mktemp -t dash-config-local-XXXXXX)"
	trap "rm -f \"${tmp}\" >/dev/null 2>&1 || true" RETURN

	local placeholder
	local -a required_placeholders=(
		__APP_DASH_IP__
		__APP_LISTEN__
		__APP_PUBLIC_URL__
		__APP_LANGUAGE__
		__APP_NODE_OFFLINE_THRESHOLD__
		__HTTP_TRUSTED_PROXIES__
		__DB_USER__
		__DB_PASS__
		__DB_NAME__
		__DB_RETENTION_DAYS_LINE__
		__REDIS_ADDR__
		__REDIS_PASSWORD__
		__JWT_SIGNING_KEY__
	)
	for placeholder in "${required_placeholders[@]}"; do
		if ! grep -qF "$placeholder" "$CONFIG_EXAMPLE"; then
			die "$(txt "写入配置失败:模板缺少 ${placeholder}(请更新 ${CONFIG_EXAMPLE})" "Failed to write config: template missing ${placeholder} (please update ${CONFIG_EXAMPLE})")"
		fi
	done

	local r_dash_ip r_listen r_public_url r_language r_offline_th r_http_trusted_proxies r_db_user r_db_pass r_db_name r_db_retention_line r_redis_addr r_redis_password r_jwt_signing_key
	r_dash_ip="$(sed_escape_repl "$dash_ip_esc")"
	r_listen="$(sed_escape_repl "$listen_esc")"
	r_public_url="$(sed_escape_repl "$public_url_esc")"
	r_language="$(sed_escape_repl "$language_esc")"
	r_offline_th="$(sed_escape_repl "$offline_th_esc")"
	r_http_trusted_proxies="$(sed_escape_repl "$trusted_proxies_yaml")"
	r_db_user="$(sed_escape_repl "$db_user_esc")"
	r_db_pass="$(sed_escape_repl "$db_pass_esc")"
	r_db_name="$(sed_escape_repl "$db_name_esc")"
	r_db_retention_line="$(sed_escape_repl "$retention_line_esc")"
	r_redis_addr="$(sed_escape_repl "$redis_addr_esc")"
	r_redis_password="$(sed_escape_repl "$redis_password_esc")"
	r_jwt_signing_key="$(sed_escape_repl "$jwt_signing_key_esc")"

	sed \
		-e "s|__APP_DASH_IP__|${r_dash_ip}|g" \
		-e "s|__APP_LISTEN__|${r_listen}|g" \
		-e "s|__APP_PUBLIC_URL__|${r_public_url}|g" \
		-e "s|__APP_LANGUAGE__|${r_language}|g" \
		-e "s|__APP_NODE_OFFLINE_THRESHOLD__|${r_offline_th}|g" \
		-e "s|__HTTP_TRUSTED_PROXIES__|${r_http_trusted_proxies}|g" \
		-e "s|__DB_USER__|${r_db_user}|g" \
		-e "s|__DB_PASS__|${r_db_pass}|g" \
		-e "s|__DB_NAME__|${r_db_name}|g" \
		-e "s|__DB_RETENTION_DAYS_LINE__|${r_db_retention_line}|g" \
		-e "s|__REDIS_ADDR__|${r_redis_addr}|g" \
		-e "s|__REDIS_PASSWORD__|${r_redis_password}|g" \
		-e "s|__JWT_SIGNING_KEY__|${r_jwt_signing_key}|g" \
		"$CONFIG_EXAMPLE" >"$tmp"

	for placeholder in "${required_placeholders[@]}"; do
		if grep -qF "$placeholder" "$tmp"; then
			die "$(txt "写入配置失败:模板占位符 ${placeholder} 未被替换(请确认 config.example.yaml 版本与安装脚本一致)" "Failed to write config: placeholder ${placeholder} was not replaced (template/script mismatch)")"
		fi
	done

	as_root install -o root -g root -m 0600 "$tmp" "$CONFIG_LOCAL"
	say "已写入配置:${CONFIG_LOCAL}" "Wrote config: ${CONFIG_LOCAL}"
}

is_ip_literal() {
	local host="$1"
	if [[ "$host" =~ ^\[[0-9a-fA-F:]+\]$ ]]; then
		return 0
	fi
	[[ "$host" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
}

yaml_dq_escape() {
	local s="$1"
	s="${s//\\/\\\\}"
	s="${s//\"/\\\"}"
	printf "%s" "$s"
}

one_line() {
	local s="$1"
	s="${s//$'\r'/}"
	s="${s//$'\n'/}"
	printf "%s" "$s"
}

trim_spaces() {
	local s="$1"
	s="${s#"${s%%[![:space:]]*}"}"
	s="${s%"${s##*[![:space:]]}"}"
	printf "%s" "$s"
}

admin_password_valid() {
	local LC_ALL=C
	local s="$1"
	[[ -n "$s" ]] || return 1
	[[ "$s" =~ ^[!-~]+$ ]] || return 1
	(( ${#s} >= 8 ))
}

run_db_migrations() {
	local config_path="$1"
	if [[ -x "$BIN_PATH" ]]; then
		as_root env DASH_HOME="$INSTALL_DIR" "$BIN_PATH" migrate -config "$config_path"
		return 0
	fi
	die "$(txt "无法执行 migrate:未找到可执行文件 ${BIN_PATH}" "Cannot run migrate: executable not found: ${BIN_PATH}")"
}

normalize_public_url() {
	local in="$1"

	in="${in#"${in%%[![:space:]]*}"}"
	in="${in%"${in##*[![:space:]]}"}"

	if [[ -z "$in" ]]; then
		echo ""
		return 0
	fi

	if [[ "$in" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
		echo "$in"
		return 0
	fi

	local hostport="${in%%/*}"
	local ip_probe="$hostport"
	if [[ "$hostport" == \[* ]]; then
		ip_probe="${hostport%%]*}"
		ip_probe="${ip_probe}]"
	elif [[ "$hostport" == *:*:* && "$hostport" =~ ^[0-9a-fA-F:]+$ ]]; then
		in="[${hostport}]${in:${#hostport}}"
		ip_probe="[${hostport}]"
	else
		ip_probe="${hostport%%:*}"
	fi

	local scheme="http"
	if ! is_ip_literal "$ip_probe"; then
		scheme="https"
	fi

	local out="${scheme}://${in}"
	if [[ "$in" != */* ]]; then
		out="${out}/"
	fi
	echo "$out"
}

warn_if_domain_public_url() {
	local public_url="$1"
	local hostport="${public_url#*://}"
	hostport="${hostport%%/*}"

	local host="$hostport"
	if [[ "$hostport" == \[* ]]; then
		host="${hostport%%]*}"
		host="${host}]"
	else
		host="${hostport%%:*}"
	fi
	[[ -n "$host" ]] || return 0
	if ! is_ip_literal "$host"; then
		say "提示:public_url 看起来是域名(${host}),请确保已配置前置 Web 服务器(如 Nginx/Caddy)做反代与 HTTPS。" "NOTE: public_url looks like a domain (${host}). You likely need a reverse proxy (Nginx/Caddy) for HTTPS and forwarding."
	fi
}

require_root_public_url() {
	local public_url="$1"
	if [[ ! "$public_url" =~ ^([hH][tT][tT][pP]|[hH][tT][tT][pP][sS]):// ]]; then
		die "$(txt "public_url 只支持 HTTP 或 HTTPS" "public_url supports only HTTP or HTTPS")"
	fi
	local rest="${public_url#*://}"
	[[ -n "$rest" ]] || die "$(txt "public_url 缺少主机名" "public_url is missing a host")"
	[[ "$rest" != *"@"* ]] || die "$(txt "public_url 不支持用户信息" "public_url must not include user information")"
	[[ "$rest" != *"?"* ]] || die "$(txt "public_url 不支持查询参数" "public_url must not include a query")"
	[[ "$rest" != *"#"* ]] || die "$(txt "public_url 不支持片段" "public_url must not include a fragment")"

	local hostport="${rest%%/*}"
	local path_part="${rest:${#hostport}}"
	[[ -n "$hostport" ]] || die "$(txt "public_url 缺少主机名" "public_url is missing a host")"
	local host="$hostport"
	if [[ "$hostport" == \[* ]]; then
		[[ "$hostport" == *"]"* ]] || die "$(txt "public_url 的 IPv6 主机格式无效" "public_url has an invalid IPv6 host")"
		host="${hostport#\[}"
		host="${host%%]*}"
	else
		host="${hostport%%:*}"
	fi
	[[ -n "$host" ]] || die "$(txt "public_url 缺少主机名" "public_url is missing a host")"
	[[ -z "$path_part" || "$path_part" == "/" ]] || die "$(txt "public_url 不支持路径前缀(${path_part})。请改为根路径 URL,例如 http://127.0.0.1:8080/ 或 https://dash.example.com/" "public_url does not support path prefixes (${path_part}). Use a root URL such as http://127.0.0.1:8080/ or https://dash.example.com/")"
}

stage_app_files_from_cwd() {
	# Full installation always consumes the files beside this script. A release
	# here is only the local immutable directory selected by current.
	need_cmd mv || die "$(txt "缺少命令:mv" "Missing command: mv")"
	mv --version 2>/dev/null | grep -q 'GNU coreutils' || die "$(txt "安装器需要 GNU coreutils 的 mv 以保证原子切换" "The installer requires GNU coreutils mv for atomic cutover")"
	[[ -f "${SCRIPT_DIR}/bin/dash" ]] || die "$(txt "未找到可执行文件 ${SCRIPT_DIR}/bin/dash" "Missing executable: ${SCRIPT_DIR}/bin/dash")"
	[[ -d "${SCRIPT_DIR}/dist" && -d "${SCRIPT_DIR}/deploy" && -d "${SCRIPT_DIR}/configs" ]] || die "$(txt "安装包缺少 dist、deploy 或 configs 目录" "Package is missing the dist, deploy, or configs directory")"

	local stage
	stage="$(as_root mktemp -d "${INSTALL_DIR}.stage.XXXXXX")"
	as_root chown "$(id -u):$(id -g)" "$stage"
	if ! as_root cp -a \
		"${SCRIPT_DIR}/bin" \
		"${SCRIPT_DIR}/configs" \
		"${SCRIPT_DIR}/dist" \
		"${SCRIPT_DIR}/deploy" \
		"${SCRIPT_DIR}/install_dash_linux.sh" \
		"${SCRIPT_DIR}/update_dash_linux.sh" \
		"${SCRIPT_DIR}/release.env" \
		"$stage/"; then
		as_root rm -rf "$stage"
		die "$(txt "暂存安装包失败" "Failed to stage package files")"
	fi
	[[ -f "${stage}/bin/dash" ]] || {
		as_root rm -rf "$stage"
		die "$(txt "暂存后未找到 Dash 二进制" "Dash binary is missing from the staged package")"
	}
	as_root chmod 0755 "${stage}/bin/dash"
	if ! as_root "${stage}/bin/dash" --version >/dev/null 2>&1; then
		as_root rm -rf "$stage"
		die "$(txt "Dash 二进制无法在当前系统运行;Alpine 需要静态或 musl 兼容产物" "The Dash binary cannot run on this system; Alpine requires a static or musl-compatible artifact")"
	fi
	printf '%s\n' "$stage"
}

prepare_staged_release() {
	local stage="$1" version release
	version="$(as_root "${stage}/bin/dash" --version)" || return
	version="${version//$'\r'/}"
	version="${version//$'\n'/}"
	[[ "$version" =~ ^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$ ]] || return 1
	need_cmd mv || return 1
	mv --version 2>/dev/null | grep -q 'GNU coreutils' || return 1

	release="${RELEASES_DIR}/${version}-$(date -u +%Y%m%dT%H%M%SZ)-$$"
	as_root install -d -m 0755 "$INSTALL_DIR" "$RELEASES_DIR" "${INSTALL_DIR}/configs" || return
	[[ ! -e "$release" ]] || return 1
	as_root mv "$stage" "$release" || return
	if ! as_root chown -R root:root "$release" ||
		! as_root chmod 0755 "${release}/bin/dash" ||
		! as_root find "$release" -maxdepth 1 -type f -name '*.sh' -exec chmod 0755 {} +; then
		as_root rm -rf "$release"
		return 1
	fi
	printf '%s\n' "$release"
}

switch_current_target() {
	local target="$1" link
	[[ ! -e "$CURRENT_LINK" || -L "$CURRENT_LINK" ]] || return 1
	link="${INSTALL_DIR}/.current.$$.$RANDOM"
	as_root rm -f "$link" || return
	as_root ln -s "$target" "$link" || return
	as_root mv -Tf "$link" "$CURRENT_LINK" || {
		as_root rm -f "$link"
		return 1
	}
}

switch_current_release() {
	local release="$1"
	[[ -d "$release" ]] || return 1
	switch_current_target "releases/$(basename "$release")"
}

install_compat_alias() {
	local path="$1" target="$2" link
	link="${path}.new.$$.$RANDOM"
	as_root rm -f "$link" || return
	as_root ln -s "$target" "$link" || return
	if [[ -d "$path" && ! -L "$path" ]]; then
		if ! as_root rm -rf "$path"; then
			as_root rm -f "$link"
			return 1
		fi
	fi
	as_root mv -Tf "$link" "$path" || {
		as_root rm -f "$link"
		return 1
	}
}

install_compat_aliases() {
	as_root install -d -m 0755 "${INSTALL_DIR}/configs" || return

	# Compatibility bridge for the legacy flat layout. Remove these aliases
	# after the next breaking-release migration window.
	install_compat_alias "${INSTALL_DIR}/bin" "current/bin" || return
	install_compat_alias "${INSTALL_DIR}/dist" "current/dist" || return
	install_compat_alias "${INSTALL_DIR}/deploy" "current/deploy" || return
	install_compat_alias "${INSTALL_DIR}/install_dash_linux.sh" "current/install_dash_linux.sh" || return
	install_compat_alias "${INSTALL_DIR}/update_dash_linux.sh" "current/update_dash_linux.sh" || return
	install_compat_alias "${INSTALL_DIR}/release.env" "current/release.env" || return

	install_compat_alias "${INSTALL_DIR}/configs/config.example.yaml" "../current/configs/config.example.yaml" || return
}

backup_legacy_install() {
	local backup_dir="$1" rel
	as_root install -d -m 0700 "$backup_dir" || return
	for rel in bin configs dist deploy install_dash_linux.sh update_dash_linux.sh release.env; do
		[[ -e "${INSTALL_DIR}/${rel}" || -L "${INSTALL_DIR}/${rel}" ]] || continue
		as_root cp -a "${INSTALL_DIR}/${rel}" "$backup_dir/" || return
	done
}

restore_legacy_install() {
	local backup_dir="$1"
	[[ -d "$backup_dir" ]] || return 1
	as_root install -d -m 0755 "$INSTALL_DIR" || return
	as_root rm -rf \
		"${INSTALL_DIR}/bin" \
		"${INSTALL_DIR}/configs" \
		"${INSTALL_DIR}/dist" \
		"${INSTALL_DIR}/deploy" \
		"$CURRENT_LINK" || return
	as_root rm -f \
		"${INSTALL_DIR}/install_dash_linux.sh" \
		"${INSTALL_DIR}/update_dash_linux.sh" \
		"${INSTALL_DIR}/release.env" \
		"$LAYOUT_MARKER" || return
	as_root cp -a "${backup_dir}/." "$INSTALL_DIR/" || return
	tighten_sensitive_file_permissions || return
}

legacy_install_present() {
	local rel
	for rel in bin/dash dist deploy install_dash_linux.sh update_dash_linux.sh release.env configs/config.local.yaml configs/config.yaml configs/config.example.yaml; do
		[[ -e "${INSTALL_DIR}/${rel}" || -L "${INSTALL_DIR}/${rel}" ]] && return 0
	done
	return 1
}

prepare_install_recovery() {
	[[ ! -e "$CURRENT_LINK" || -L "$CURRENT_LINK" ]] || return 1
	INSTALL_RECOVERY_ROOT="$(as_root mktemp -d "${INSTALL_DIR}.recovery.XXXXXX")" || return
	as_root chown "$(id -u):$(id -g)" "$INSTALL_RECOVERY_ROOT" || return
	INSTALL_PREVIOUS_LAYOUT="fresh"
	INSTALL_PREVIOUS_TARGET=""
	INSTALL_PREVIOUS_MARKER="0"
	INSTALL_PREVIOUS_CONFIG="0"
	INSTALL_SYSTEMD_WAS_ACTIVE="0"

	if [[ -L "$CURRENT_LINK" ]]; then
		INSTALL_PREVIOUS_LAYOUT="release"
		INSTALL_PREVIOUS_TARGET="$(readlink "$CURRENT_LINK")"
		[[ -f "$LAYOUT_MARKER" ]] && INSTALL_PREVIOUS_MARKER="1"
		if [[ -f "$CONFIG_LOCAL" ]]; then
			as_root cp -a "$CONFIG_LOCAL" "${INSTALL_RECOVERY_ROOT}/config.local.yaml" || return
			INSTALL_PREVIOUS_CONFIG="1"
		fi
	elif legacy_install_present; then
		INSTALL_PREVIOUS_LAYOUT="legacy"
		backup_legacy_install "${INSTALL_RECOVERY_ROOT}/legacy" || return
	else
		as_root install -d -m 0700 "${INSTALL_RECOVERY_ROOT}/legacy" || return
	fi

	if systemd_available && [[ -f "$SERVICE_FILE" ]] && as_root systemctl is-active --quiet "${APP}.service"; then
		INSTALL_SYSTEMD_WAS_ACTIVE="1"
	fi
	INSTALL_ROLLBACK_READY="1"
}

restore_release_install() {
	[[ -n "$INSTALL_PREVIOUS_TARGET" ]] || return 1
	switch_current_target "$INSTALL_PREVIOUS_TARGET" || return
	install_compat_aliases || return
	if [[ "$INSTALL_PREVIOUS_MARKER" == "1" ]]; then
		as_root touch "$LAYOUT_MARKER" || return
	else
		as_root rm -f "$LAYOUT_MARKER" || return
	fi
	if [[ "$INSTALL_PREVIOUS_CONFIG" == "1" ]]; then
		as_root install -o root -g root -m 0600 "${INSTALL_RECOVERY_ROOT}/config.local.yaml" "$CONFIG_LOCAL" || return
	else
		as_root rm -f "$CONFIG_LOCAL" || return
	fi
}

rollback_install() {
	local restored="true"
	say_err "安装提交前失败,正在恢复原文件入口和服务。" "Installation failed before commit; restoring the previous file entrypoint and service."
	case "$INSTALL_PREVIOUS_LAYOUT" in
		release) restore_release_install || restored="false" ;;
		legacy|fresh) restore_legacy_install "${INSTALL_RECOVERY_ROOT}/legacy" || restored="false" ;;
		*) restored="false" ;;
	esac
	if [[ "$restored" == "true" && -n "$INSTALL_RELEASE" ]] && ! install_release_is_current; then
		as_root rm -rf "$INSTALL_RELEASE" || restored="false"
		[[ "$restored" != "true" ]] || INSTALL_RELEASE=""
	fi
	if [[ "$restored" == "true" && "$INSTALL_SYSTEMD_WAS_ACTIVE" == "1" ]]; then
		as_root systemctl start "${APP}.service" || restored="false"
	fi
	if [[ "$restored" == "true" ]]; then
		as_root rm -rf "$INSTALL_RECOVERY_ROOT"
		INSTALL_RECOVERY_ROOT=""
		return 0
	fi
	return 1
}

activate_release() {
	local release="$1"
	switch_current_release "$release" || return
	install_compat_aliases || return
	as_root touch "$LAYOUT_MARKER" || return
}

prune_inactive_releases() {
	[[ -L "$CURRENT_LINK" && -d "$RELEASES_DIR" ]] || return 0
	local current entry entries
	current="$(readlink "$CURRENT_LINK")"
	current="${current##*/}"
	entries="$(mktemp)" || return
	if ! find "$RELEASES_DIR" -mindepth 1 -maxdepth 1 -type d -print0 >"$entries"; then
		rm -f "$entries"
		return 1
	fi
	while IFS= read -r -d '' entry; do
		[[ "$(basename "$entry")" == "$current" ]] && continue
		if ! as_root rm -rf "$entry"; then
			rm -f "$entries"
			return 1
		fi
	done <"$entries"
	rm -f "$entries" || return
}

prepare_staged_app_files() {
	local stage="$1" release=""
	[[ -d "$stage" && -x "${stage}/bin/dash" ]] || die "$(txt "暂存安装包无效" "Invalid staged package")"
	INSTALL_RELEASE=""
	if ! release="$(prepare_staged_release "$stage")"; then
		as_root rm -rf "$stage"
		die "$(txt "准备不可变 release 目录失败;线上文件和进程未改动" "Failed to prepare the immutable release directory; live files and processes were not changed")"
	fi
	INSTALL_RELEASE="$release"
}

install_staged_app_files() {
	[[ -n "$INSTALL_RELEASE" && -d "$INSTALL_RELEASE" ]] || die "$(txt "尚未准备可切换的 release" "No prepared release is available for cutover")"
	activate_release "$INSTALL_RELEASE" || die "$(txt "强制覆盖 Dash 文件入口失败" "Failed to replace the managed Dash file entrypoints")"

	[[ -x "$BIN_PATH" ]] || die "$(txt "安装后未找到可执行文件 ${BIN_PATH}" "Missing executable after install: ${BIN_PATH}")"
}

tighten_sensitive_file_permissions() {
	if [[ -f "$CONFIG_LOCAL" ]]; then
		as_root chown root:root "$CONFIG_LOCAL"
		as_root chmod 0600 "$CONFIG_LOCAL"
	fi
	if [[ -f "$NOTIFY_CONFIG_KEY" ]]; then
		as_root chown root:root "$NOTIFY_CONFIG_KEY"
		as_root chmod 0600 "$NOTIFY_CONFIG_KEY"
	fi
	if [[ -f "$SERVICE_FILE" ]]; then
		as_root chown root:root "$SERVICE_FILE"
		as_root chmod 0600 "$SERVICE_FILE"
	fi
	if [[ -f "$MANUAL_RUN_FILE" ]]; then
		as_root chown root:root "$MANUAL_RUN_FILE"
		as_root chmod 0700 "$MANUAL_RUN_FILE"
	fi
}

write_systemd_service() {
	local admin_password="$1"
	admin_password="$(one_line "$admin_password")"
	local pwd_escaped
	pwd_escaped="$(systemd_escape_env_value "$admin_password")"
	local pg_major pg_units="postgresql.service"
	pg_major="$(postgres_major_version)"
	if [[ "$pg_major" =~ ^[0-9]+$ ]]; then
		pg_units+=" postgresql-${pg_major}.service"
	fi
	local tmp
	tmp="$(mktemp)"

	cat >"$tmp" <<EOF
[Unit]
Description=Dash Server Monitor
After=network-online.target ${pg_units}
Wants=network-online.target
ConditionPathExists=!${INSTALL_DIR}/runtime/dash-update/update.block

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=${INSTALL_DIR}

Environment="DASH_HOME=${INSTALL_DIR}"
Environment="monitor_dash_pwd=${pwd_escaped}"

ExecStart=${BIN_PATH}

Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
	as_root install -m 0600 "$tmp" "$SERVICE_FILE"
	rm -f "$tmp"

	tighten_sensitive_file_permissions
	as_root systemctl daemon-reload
}

write_manual_runner() {
	local admin_password="$1"
	admin_password="$(one_line "$admin_password")"
	local pwd_quoted
	pwd_quoted="$(shell_quote_arg "$admin_password")"
	local tmp
	tmp="$(mktemp)"
	cat >"$tmp" <<EOF
#!/usr/bin/env bash
set -euo pipefail
export DASH_HOME=$(shell_quote_arg "$INSTALL_DIR")
export monitor_dash_pwd=${pwd_quoted}
cd $(shell_quote_arg "$INSTALL_DIR")
exec $(shell_quote_arg "$BIN_PATH")
EOF
	as_root install -m 0700 "$tmp" "$MANUAL_RUN_FILE"
	rm -f "$tmp"
	tighten_sensitive_file_permissions
}

service_install() {
	case "$SERVICE_MANAGER" in
	systemd) write_systemd_service "$1" ;;
	none) write_manual_runner "$1" ;;
	esac
}

service_enable() {
	case "$SERVICE_MANAGER" in
	systemd) as_root systemctl enable "${APP}.service" ;;
	none) return 0 ;;
	esac
}

service_start() {
	case "$SERVICE_MANAGER" in
	systemd) as_root systemctl start "${APP}.service" ;;
	none) return 0 ;;
	esac
}

service_status() {
	case "$SERVICE_MANAGER" in
	systemd) say "状态:systemctl status ${APP}.service;日志:journalctl -u ${APP}.service -f" "Status: systemctl status ${APP}.service; logs: journalctl -u ${APP}.service -f" ;;
	none) say "手动模式未启动服务。运行:${MANUAL_RUN_FILE}" "Manual mode did not start a service. Run: ${MANUAL_RUN_FILE}" ;;
	esac
}

stop_installed_services() {
	if systemd_available && [[ -f "$SERVICE_FILE" ]]; then
		if ! as_root systemctl stop "${APP}.service"; then
			die "$(txt "停止已安装的 ${APP} 服务失败" "Failed to stop the installed ${APP} service")"
		fi
	fi
	stop_manual_processes
}

install_release_is_current() {
	[[ -n "$INSTALL_RELEASE" && -L "$CURRENT_LINK" ]] || return 1
	[[ "$(readlink "$CURRENT_LINK")" == "releases/$(basename "$INSTALL_RELEASE")" ]]
}

finish_dash_install() {
	local status=$?
	trap - EXIT
	set +e
	if ((status != 0)) && [[ "$INSTALL_ROLLBACK_READY" == "1" && "$INSTALL_MIGRATION_STARTED" != "1" ]]; then
		if ! rollback_install; then
			say_err "自动恢复失败;恢复文件保留在 ${INSTALL_RECOVERY_ROOT}" "Automatic recovery failed; recovery files were preserved at ${INSTALL_RECOVERY_ROOT}"
		fi
	elif ((status != 0)) && [[ -n "$INSTALL_RELEASE" ]] && ! install_release_is_current; then
		as_root rm -rf "$INSTALL_RELEASE"
		INSTALL_RELEASE=""
	fi
	if ((status != 0)) && [[ "$INSTALL_ROLLBACK_READY" != "1" && "$INSTALL_MIGRATION_STARTED" != "1" && -n "$INSTALL_RECOVERY_ROOT" ]]; then
		as_root rm -rf "$INSTALL_RECOVERY_ROOT"
		INSTALL_RECOVERY_ROOT=""
	fi
	if ((status != 0)) && [[ "$INSTALL_MIGRATION_STARTED" == "1" && -n "$INSTALL_RECOVERY_ROOT" ]]; then
		say_err "数据库迁移已开始,不能自动恢复旧程序;恢复文件保留在 ${INSTALL_RECOVERY_ROOT}" "Database migration had started, so the old program was not restored; recovery files were preserved at ${INSTALL_RECOVERY_ROOT}"
	fi
	if ((status == 0)) && ! prune_inactive_releases; then
		say_err "安装已完成,但旧 release 清理失败" "Installation completed, but old release cleanup failed"
	fi
	if ((status == 0)) && [[ -n "$INSTALL_RECOVERY_ROOT" ]]; then
		as_root rm -rf "$INSTALL_RECOVERY_ROOT"
		INSTALL_RECOVERY_ROOT=""
	fi
	exit "$status"
}

cleanup_other_service_managers() {
	case "$SERVICE_MANAGER" in
	systemd)
		as_root rm -f "$MANUAL_RUN_FILE"
		;;
	none)
		if systemd_available; then
			as_root systemctl disable --now "${APP}.service" >/dev/null 2>&1 || true
		fi
		as_root rm -f "$SERVICE_FILE"
		;;
	esac
}

main() {
	local stage=""
	select_service_manager
	ensure_process_control

	detect_os

	if [[ "$OS_FAMILY" != "manual" && "$SERVICE_MANAGER" != "none" ]]; then
		if [[ -z "${PKG_MANAGER}" ]] || ! need_cmd "${PKG_MANAGER}"; then
			die "$(txt "未检测到系统包管理器(当前需要 ${PKG_MANAGER_LABEL:-unknown})" "System package manager not found (expected ${PKG_MANAGER_LABEL:-unknown})")"
		fi
	fi

	if [[ "$SERVICE_MANAGER" != "none" ]]; then
		enable_time_sync
	fi

	say "1) 检测并准备数据库依赖(已有兼容安装保持不变;缺失时锁定 PostgreSQL ${POSTGRES_VERSION} / TimescaleDB ${TIMESCALEDB_VERSION};Redis 地址将在配置后校验;模式:${PKG_MANAGER_LABEL})" "1) Checking database dependencies (existing compatible installations stay unchanged; missing dependencies are pinned to PostgreSQL ${POSTGRES_VERSION} / TimescaleDB ${TIMESCALEDB_VERSION}; the configured Redis endpoint is validated next; mode: ${PKG_MANAGER_LABEL})"
	if [[ "$OS_FAMILY" == "manual" || "$SERVICE_MANAGER" == "none" ]]; then
		check_preinstalled_dependencies
	else
		ensure_postgresql16_and_password
		ensure_timescaledb_enabled
		ensure_redis_82plus
	fi

	say "2) 交互式生成配置 ${CONFIG_LOCAL}" "2) Interactive configuration: ${CONFIG_LOCAL}"
	local dash_ip listen_port public_url db_user db_pass db_name retention_days redis_addr redis_password offline_threshold language admin_pwd trusted_proxies_yaml

	while true; do
	while true; do
		dash_ip="$(prompt_string "$(txt "请输入 Dash 服务端 IP(回车查看本机IP:ip addr)" "Dash server IP (press Enter to show local IPs via: ip addr)")")"
		dash_ip="$(trim_spaces "$(one_line "$dash_ip")")"
		if [[ -n "$dash_ip" ]]; then
			break
		fi
		say "未输入 IP,显示本机网络信息(ip addr):" "No IP entered; showing local network info (ip addr):"
		if need_cmd ip; then
			ip addr || true
		else
			say_err "未找到 ip 命令(iproute2),无法显示本机 IP" "ip command not found (iproute2)."
		fi
	done

	listen_port="$(prompt_string "$(txt "请输入监听端口" "Listen port")" "8080")"
	listen_port="${listen_port#:}"
	[[ "$listen_port" =~ ^[0-9]+$ ]] || die "$(txt "监听端口必须是数字:${listen_port}" "Listen port must be numeric (got: ${listen_port})")"
	((10#$listen_port >= 1 && 10#$listen_port <= 65535)) || die "$(txt "监听端口必须在 1 到 65535 之间:${listen_port}" "Listen port must be between 1 and 65535 (got: ${listen_port})")"
	public_url="$(prompt_string "$(txt "请输入 public_url(用于生成安装脚本/外网访问,必须是根路径 URL)" "public_url (external access URL, root URL only)")" "http://127.0.0.1:${listen_port}/")"
	public_url="$(normalize_public_url "$public_url")"
	[[ -n "$public_url" ]] || die "$(txt "public_url 不能为空" "public_url is required")"
	require_root_public_url "$public_url"
	trusted_proxies_yaml="[]"
	if prompt_yes_no "$(txt "是否通过本机反向代理(如同机 Nginx/Caddy/Traefik)对外暴露 Dash?IP 部署请选择否;启用后仅信任来自本机代理的转发头。" "Is Dash exposed through a local reverse proxy on the same host (for example Nginx/Caddy/Traefik)? Choose no for a direct IP deployment; enabling this trusts forwarded headers only from that local proxy.")" "N"; then
		trusted_proxies_yaml='["127.0.0.1/32", "::1/128"]'
	fi
	db_user="$(prompt_string "$(txt "请输入数据库账号(database.user)" "database.user")" "monitor")"
	db_pass="$(prompt_secret_confirm "$(txt "请输入数据库密码(database.password)" "database.password")")"
	db_name="$(prompt_string "$(txt "请输入数据库名(database.name)" "database.name")" "monitor")"
	retention_days="$(prompt_retention_days 1)"
	while true; do
		redis_addr="$(prompt_string "$(txt "请输入 Redis 地址(redis.addr)" "redis.addr")" "127.0.0.1:6379")"
		redis_addr="$(trim_spaces "$(one_line "$redis_addr")")"
		redis_password="$(prompt_secret_optional "$(txt "请输入 Redis 密码(redis.password;无密码直接回车)" "redis.password (press Enter when authentication is disabled)")")"
		if check_redis_endpoint "$redis_addr" "$redis_password"; then
			break
		fi
		say_err "无法使用该 Redis 地址,请确认服务可达、允许 PING/INFO server,且版本不低于 6.2.0。" "Cannot use this Redis endpoint. Ensure it is reachable, permits PING and INFO server, and runs Redis 6.2.0 or newer."
	done
	offline_threshold="$(prompt_string "$(txt "请输入离线判定阈值(app.node_offline_threshold,例如:17s/30s/1m)" "app.node_offline_threshold (e.g. 17s/30s/1m)")" "${DEFAULT_NODE_OFFLINE_THRESHOLD}")"
	language="$(prompt_language)"

	echo ""
	say "2.1) 配置摘要(写入前确认)" "2.1) Configuration summary (confirm before writing)"

	print_config_summary "$dash_ip" "$listen_port" "$public_url" "$db_user" "$db_pass" "$db_name" "$retention_days" "$redis_addr" "$redis_password" "$offline_threshold" "$language" "$trusted_proxies_yaml"
	echo ""
	if prompt_yes_no "$(txt "确认写入以上配置并继续?" "Write the configuration above and continue?")" "Y"; then
		break
	fi
	say "已取消本次配置,请重新填写。" "Configuration was not written; please enter it again."
	echo ""
	done

	say "3) 初始化数据库用户和数据库" "3) Initializing database user and database"
	create_db_and_user "$db_user" "$db_pass" "$db_name"

	say "4) 配置运行方式:${SERVICE_MANAGER}" "4) Configuring runtime mode: ${SERVICE_MANAGER}"
	while true; do
		admin_pwd="$(prompt_secret_confirm "$(txt "请设置 Dash 管理员登录密码(环境变量 monitor_dash_pwd)" "Dash admin password (env monitor_dash_pwd)")")"
		admin_pwd="$(trim_spaces "$admin_pwd")"
		if ! admin_password_valid "$admin_pwd"; then
			say_err "Dash 管理员密码至少需要 8 个字符,仅允许大小写英文、数字和常见符号;会自动忽略输入前后的空格;中间不能包含空格或其他空白字符。" "Dash admin password must contain at least 8 characters using only ASCII letters, digits, and common symbols; leading and trailing spaces are ignored; inner whitespace is not allowed."
			continue
		fi
		break
	done

	say "5) 安装 Dash 文件到 ${INSTALL_DIR}" "5) Installing Dash files into ${INSTALL_DIR}"
	stage="$(stage_app_files_from_cwd)"
	prepare_staged_app_files "$stage"
	prepare_install_recovery || die "$(txt "准备旧安装恢复现场失败;线上文件和进程未改动" "Failed to prepare recovery for the previous installation; live files and processes were not changed")"
	stop_installed_services
	install_staged_app_files
	render_config_local "$dash_ip" "$listen_port" "$public_url" "$db_user" "$db_pass" "$db_name" "$retention_days" "$redis_addr" "$redis_password" "$offline_threshold" "$language" "$trusted_proxies_yaml"
	INSTALL_MIGRATION_STARTED="1"
	run_db_migrations "$CONFIG_LOCAL"
	grant_db_privileges "$db_user" "$db_name"

	cleanup_other_service_managers
	service_install "$admin_pwd"
	if [[ "$SERVICE_MANAGER" != "none" ]]; then
		service_enable
		service_start
	fi

	warn_if_domain_public_url "$public_url"
	service_status
}

parse_args "$@"
choose_install_lang
trap finish_dash_install EXIT
main