Compare commits
6
Commits
369a9e6c19
...
d9ed8c7704
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ed8c7704 | ||
|
|
0ded0c87a1 | ||
|
|
d70c682be2 | ||
|
|
0b3d7d542f | ||
|
|
c1919febc2 | ||
|
|
127b070f7b |
@@ -304,6 +304,9 @@ DOCKER_TIMEOUT=15
|
|||||||
SSH_TIMEOUT=10
|
SSH_TIMEOUT=10
|
||||||
CONTAINER_VERIFY_WAIT=5 # seconds after start before verifying container is up
|
CONTAINER_VERIFY_WAIT=5 # seconds after start before verifying container is up
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: check-interval=${FALLBACK_CHECK_INTERVAL}s handback-strikes=${FALLBACK_HANDBACK_STRIKES} partnership-required=${FALLBACK_PARTNERSHIP_REQUIRED:-true} rsync-enabled=${FALLBACK_RSYNC_ENABLED:-true}"
|
||||||
|
log "$ICON_GEAR Timeouts: docker=${DOCKER_TIMEOUT}s ssh=${SSH_TIMEOUT}s verify-wait=${CONTAINER_VERIFY_WAIT}s"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── STATE FILE HELPERS ────────────────────────────────────────────────────────────────────────
|
# ── STATE FILE HELPERS ────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ if [[ ! -f "$FALLBACK_SCRIPT" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log "fallback.sh found at $FALLBACK_SCRIPT"
|
log "fallback.sh found at $FALLBACK_SCRIPT"
|
||||||
|
log "$ICON_GEAR Config: remote=${REMOTE_SERVER_NAME} (${REMOTE_SERVER}) fallback-script=${FALLBACK_SCRIPT}"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no iptables rules or container changes will be made"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no iptables rules or container changes will be made"
|
||||||
|
|
||||||
|
|||||||
+93
-25
@@ -37,11 +37,13 @@
|
|||||||
# DESIGN PRINCIPLES
|
# DESIGN PRINCIPLES
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
#
|
#
|
||||||
# Remote API Keys Never Stored
|
# Remote API Access — Cache-First, SSH Fallback
|
||||||
# SSHes to each remote node and reads the key directly from that arr's
|
# If conf_sync.sh has populated /tmp/.vv/config/cached/.confs/ and
|
||||||
# config.xml in its appdata directory. Only the API response (JSON) is
|
# load_config.sh has sourced it, HOST*_<ARR>_API_KEY vars are available
|
||||||
# returned — the key never leaves the remote node. Self-maintaining: key
|
# in the environment. Remote functions use them to call the arr API
|
||||||
# regeneration on the remote is picked up automatically next run.
|
# directly over Tailscale (no SSH, no remote shell). If the cached key
|
||||||
|
# is absent (first boot, cache not yet populated) the functions fall back
|
||||||
|
# to SSHing in and reading the key from config.xml on the remote node.
|
||||||
#
|
#
|
||||||
# Blocklist TSV
|
# Blocklist TSV
|
||||||
# ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added
|
# ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added
|
||||||
@@ -186,6 +188,9 @@ if [[ "$ARR_SYNC_ENABLED" != "true" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: api-timeout=${ARR_SYNC_API_TIMEOUT}s connect-timeout=${ARR_SYNC_CONNECT_TIMEOUT}s blocklist=${ARR_SYNC_BLOCKLIST}"
|
||||||
|
log "$ICON_GEAR Ports: lidarr=${ARR_SYNC_LIDARR_PORT} sonarr=${ARR_SYNC_SONARR_PORT} radarr=${ARR_SYNC_RADARR_PORT}"
|
||||||
|
|
||||||
# ── Arr type definitions ───────────────────────────────────────────────────────────────────────
|
# ── Arr type definitions ───────────────────────────────────────────────────────────────────────
|
||||||
# Each arr type maps to its port, API version, endpoint, stable ID field, and display name field
|
# Each arr type maps to its port, API version, endpoint, stable ID field, and display name field
|
||||||
declare -A _PORT=([lidarr]="$ARR_SYNC_LIDARR_PORT" [sonarr]="$ARR_SYNC_SONARR_PORT" [radarr]="$ARR_SYNC_RADARR_PORT")
|
declare -A _PORT=([lidarr]="$ARR_SYNC_LIDARR_PORT" [sonarr]="$ARR_SYNC_SONARR_PORT" [radarr]="$ARR_SYNC_RADARR_PORT")
|
||||||
@@ -308,11 +313,12 @@ _delete_local_item() {
|
|||||||
"${url}/api/${api_ver}/${endpoint}/${internal_id}?deleteFiles=false" 2>/dev/null
|
"${url}/api/${api_ver}/${endpoint}/${internal_id}?deleteFiles=false" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Delete item from remote arr by stable_id via SSH.
|
# Delete item from remote arr by stable_id.
|
||||||
# Outputs: HTTP code on success | "not_found" if item absent | empty on SSH/API failure.
|
# Outputs: HTTP code on success | "not_found" if item absent | empty on failure.
|
||||||
# deleteFiles=false — files become orphans for arr_cleanup to handle with its safety checks.
|
# deleteFiles=false — files become orphans for arr_cleanup to handle with its safety checks.
|
||||||
|
# Args: node_id port api_ver arr_type endpoint id_field id_type stable_id
|
||||||
_delete_remote_item() {
|
_delete_remote_item() {
|
||||||
local node_id="$1" port="$2" api_ver="$3" endpoint="$4" config_xml="$5"
|
local node_id="$1" port="$2" api_ver="$3" arr_type="$4" endpoint="$5"
|
||||||
local id_field="$6" id_type="$7" stable_id="$8"
|
local id_field="$6" id_type="$7" stable_id="$8"
|
||||||
local node_name="${!node_id}"
|
local node_name="${!node_id}"
|
||||||
local node_ip
|
local node_ip
|
||||||
@@ -325,6 +331,21 @@ _delete_remote_item() {
|
|||||||
select_expr=".[] | select(.${id_field} == ${stable_id}) | .id"
|
select_expr=".[] | select(.${id_field} == ${stable_id}) | .id"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
|
||||||
|
if [[ -n "$cached_key" ]]; then
|
||||||
|
local library internal_id
|
||||||
|
library=$(curl -sf --max-time "$ARR_SYNC_API_TIMEOUT" \
|
||||||
|
-H "X-Api-Key: $cached_key" \
|
||||||
|
"http://${node_ip}:${port}/api/${api_ver}/${endpoint}" 2>/dev/null)
|
||||||
|
[[ -z "$library" ]] && return 1
|
||||||
|
internal_id=$(echo "$library" | jq -r "${select_expr}" 2>/dev/null | head -1)
|
||||||
|
[[ -z "$internal_id" ]] && echo "not_found" && return 0
|
||||||
|
curl -sf -o /dev/null -w '%{http_code}' -X DELETE \
|
||||||
|
-H "X-Api-Key: $cached_key" \
|
||||||
|
"http://${node_ip}:${port}/api/${api_ver}/${endpoint}/${internal_id}?deleteFiles=false" 2>/dev/null
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
|
||||||
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
||||||
root@"$node_ip" bash <<REMOTE 2>/dev/null
|
root@"$node_ip" bash <<REMOTE 2>/dev/null
|
||||||
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
||||||
@@ -378,7 +399,6 @@ if [[ -n "$BLOCKLIST_ACTION" ]]; then
|
|||||||
_bl_id_field="${_ID[$BLOCKLIST_ARR]}"
|
_bl_id_field="${_ID[$BLOCKLIST_ARR]}"
|
||||||
_bl_id_type="${_ID_TYPE[$BLOCKLIST_ARR]}"
|
_bl_id_type="${_ID_TYPE[$BLOCKLIST_ARR]}"
|
||||||
_bl_name_field="${_NAME[$BLOCKLIST_ARR]}"
|
_bl_name_field="${_NAME[$BLOCKLIST_ARR]}"
|
||||||
_bl_config_xml="${DOCKER_APPDATA_BASE}/${BLOCKLIST_ARR^}/config.xml"
|
|
||||||
_bl_url="" _bl_key=""
|
_bl_url="" _bl_key=""
|
||||||
case "$BLOCKLIST_ARR" in
|
case "$BLOCKLIST_ARR" in
|
||||||
lidarr) _bl_url="${LIDARR_URL:-}"; _bl_key="${LIDARR_API_KEY:-}" ;;
|
lidarr) _bl_url="${LIDARR_URL:-}"; _bl_key="${LIDARR_API_KEY:-}" ;;
|
||||||
@@ -415,8 +435,8 @@ if [[ -n "$BLOCKLIST_ACTION" ]]; then
|
|||||||
# Remove from all remote arrs
|
# Remove from all remote arrs
|
||||||
for _bl_node_id in "${REMOTE_NODES[@]}"; do
|
for _bl_node_id in "${REMOTE_NODES[@]}"; do
|
||||||
_bl_node_name="${!_bl_node_id}"
|
_bl_node_name="${!_bl_node_id}"
|
||||||
_bl_result=$(_delete_remote_item "$_bl_node_id" "$_bl_port" "$_bl_ver" "$_bl_ep" \
|
_bl_result=$(_delete_remote_item "$_bl_node_id" "$_bl_port" "$_bl_ver" \
|
||||||
"$_bl_config_xml" "$_bl_id_field" "$_bl_id_type" "$BLOCKLIST_ID")
|
"$BLOCKLIST_ARR" "$_bl_ep" "$_bl_id_field" "$_bl_id_type" "$BLOCKLIST_ID")
|
||||||
case "$_bl_result" in
|
case "$_bl_result" in
|
||||||
200) log "Removed from $_bl_node_name ${BLOCKLIST_ARR^}: $_bl_display_name" ;;
|
200) log "Removed from $_bl_node_name ${BLOCKLIST_ARR^}: $_bl_display_name" ;;
|
||||||
not_found) log "Not found on $_bl_node_name ${BLOCKLIST_ARR^} — already removed or not tracked" ;;
|
not_found) log "Not found on $_bl_node_name ${BLOCKLIST_ARR^} — already removed or not tracked" ;;
|
||||||
@@ -478,8 +498,17 @@ _resolve_node_ip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Check if arr is reachable on remote node
|
# Check if arr is reachable on remote node
|
||||||
|
# Args: node_id node_ip port api_ver arr_type
|
||||||
_remote_arr_up() {
|
_remote_arr_up() {
|
||||||
local node_ip="$1" port="$2" api_ver="$3" config_xml="$4"
|
local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5"
|
||||||
|
local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
|
||||||
|
if [[ -n "$cached_key" ]]; then
|
||||||
|
curl -sf --max-time 5 \
|
||||||
|
-H "X-Api-Key: $cached_key" \
|
||||||
|
"http://${node_ip}:${port}/api/${api_ver}/system/status" >/dev/null 2>/dev/null
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
|
||||||
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
||||||
root@"$node_ip" \
|
root@"$node_ip" \
|
||||||
"KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
"KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
||||||
@@ -489,8 +518,17 @@ _remote_arr_up() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Fetch full library from remote arr — returns raw JSON array
|
# Fetch full library from remote arr — returns raw JSON array
|
||||||
|
# Args: node_id node_ip port api_ver arr_type endpoint
|
||||||
_remote_library() {
|
_remote_library() {
|
||||||
local node_ip="$1" port="$2" api_ver="$3" endpoint="$4" config_xml="$5"
|
local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5" endpoint="$6"
|
||||||
|
local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
|
||||||
|
if [[ -n "$cached_key" ]]; then
|
||||||
|
curl -sf --max-time "$ARR_SYNC_API_TIMEOUT" \
|
||||||
|
-H "X-Api-Key: $cached_key" \
|
||||||
|
"http://${node_ip}:${port}/api/${api_ver}/${endpoint}" 2>/dev/null
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
|
||||||
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
||||||
root@"$node_ip" \
|
root@"$node_ip" \
|
||||||
"KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
"KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
||||||
@@ -501,12 +539,31 @@ _remote_library() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Fetch remote arr defaults: qualityProfileId, rootFolderPath, metadataProfileId (Lidarr)
|
# Fetch remote arr defaults: qualityProfileId, rootFolderPath, metadataProfileId (Lidarr)
|
||||||
|
# Args: node_id node_ip port api_ver arr_type
|
||||||
_remote_defaults() {
|
_remote_defaults() {
|
||||||
local node_ip="$1" port="$2" api_ver="$3" arr_type="$4" config_xml="$5"
|
local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5"
|
||||||
|
local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
|
||||||
|
if [[ -n "$cached_key" ]]; then
|
||||||
|
local base_url="http://${node_ip}:${port}/api/${api_ver}"
|
||||||
|
local qp rf mp
|
||||||
|
qp=$(curl -sf -H "X-Api-Key: $cached_key" "${base_url}/qualityprofile" 2>/dev/null | jq '.[0].id // 1')
|
||||||
|
rf=$(curl -sf -H "X-Api-Key: $cached_key" "${base_url}/rootfolder" 2>/dev/null | jq -r '.[0].path // ""')
|
||||||
|
[[ -z "$qp" ]] && return 1
|
||||||
|
if [[ "$arr_type" == "lidarr" ]]; then
|
||||||
|
mp=$(curl -sf -H "X-Api-Key: $cached_key" "${base_url}/metadataprofile" 2>/dev/null | \
|
||||||
|
jq 'map(select(.name == "Standard")) | .[0].id // .[0].id // 1')
|
||||||
|
jq -n --argjson qp "$qp" --arg rf "$rf" --argjson mp "$mp" \
|
||||||
|
'{qualityProfileId: $qp, rootFolderPath: $rf, metadataProfileId: $mp}'
|
||||||
|
else
|
||||||
|
jq -n --argjson qp "$qp" --arg rf "$rf" \
|
||||||
|
'{qualityProfileId: $qp, rootFolderPath: $rf}'
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
|
||||||
local meta_field=""
|
local meta_field=""
|
||||||
[[ "$arr_type" == "lidarr" ]] && \
|
[[ "$arr_type" == "lidarr" ]] && \
|
||||||
meta_field=', metadataProfileId: ($mp | map(select(.name == "Standard")) | .[0].id // .[0].id // 1)'
|
meta_field=', metadataProfileId: ($mp | map(select(.name == "Standard")) | .[0].id // .[0].id // 1)'
|
||||||
|
|
||||||
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
||||||
root@"$node_ip" bash <<REMOTE 2>/dev/null
|
root@"$node_ip" bash <<REMOTE 2>/dev/null
|
||||||
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
||||||
@@ -519,11 +576,23 @@ jq -n --argjson qp "\$QP" --argjson rf "\$RF" --argjson mp "\$MP" \
|
|||||||
REMOTE
|
REMOTE
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add item to remote arr — payload is base64-encoded to avoid SSH quoting issues
|
# Add item to remote arr — payload is base64-encoded to avoid quoting issues
|
||||||
|
# Args: node_id node_ip port api_ver arr_type endpoint encoded_payload
|
||||||
_remote_add() {
|
_remote_add() {
|
||||||
local node_ip="$1" port="$2" api_ver="$3" endpoint="$4" config_xml="$5"
|
local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5" endpoint="$6"
|
||||||
local encoded="$6" # base64-encoded JSON body
|
local encoded="$7"
|
||||||
|
local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
|
||||||
|
if [[ -n "$cached_key" ]]; then
|
||||||
|
local body
|
||||||
|
body=$(printf '%s' "$encoded" | base64 -d)
|
||||||
|
curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "X-Api-Key: $cached_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$body" \
|
||||||
|
"http://${node_ip}:${port}/api/${api_ver}/${endpoint}" 2>/dev/null
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
|
||||||
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
|
||||||
root@"$node_ip" bash <<REMOTE 2>/dev/null
|
root@"$node_ip" bash <<REMOTE 2>/dev/null
|
||||||
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
|
||||||
@@ -644,7 +713,6 @@ _sync_arr() {
|
|||||||
local id_field="${_ID[$arr_type]}"
|
local id_field="${_ID[$arr_type]}"
|
||||||
local name_field="${_NAME[$arr_type]}"
|
local name_field="${_NAME[$arr_type]}"
|
||||||
local id_type="${_ID_TYPE[$arr_type]}"
|
local id_type="${_ID_TYPE[$arr_type]}"
|
||||||
local config_xml="${DOCKER_APPDATA_BASE}/$(echo "${arr_type^}")/config.xml"
|
|
||||||
|
|
||||||
# Resolve local credentials
|
# Resolve local credentials
|
||||||
local local_url local_key
|
local local_url local_key
|
||||||
@@ -698,13 +766,13 @@ _sync_arr() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! _remote_arr_up "$node_ip" "$port" "$api_ver" "$config_xml"; then
|
if ! _remote_arr_up "$node_id" "$node_ip" "$port" "$api_ver" "$arr_type"; then
|
||||||
log "${arr_type^}: not reachable on $node_name — skipping"
|
log "${arr_type^}: not reachable on $node_name — skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local remote_json
|
local remote_json
|
||||||
remote_json=$(_remote_library "$node_ip" "$port" "$api_ver" "$endpoint" "$config_xml")
|
remote_json=$(_remote_library "$node_id" "$node_ip" "$port" "$api_ver" "$arr_type" "$endpoint")
|
||||||
if [[ -z "$remote_json" ]] || ! echo "$remote_json" | jq -e '.' >/dev/null 2>&1; then
|
if [[ -z "$remote_json" ]] || ! echo "$remote_json" | jq -e '.' >/dev/null 2>&1; then
|
||||||
warn "${arr_type^}: could not fetch library from $node_name — skipping"
|
warn "${arr_type^}: could not fetch library from $node_name — skipping"
|
||||||
continue
|
continue
|
||||||
@@ -780,7 +848,7 @@ _sync_arr() {
|
|||||||
|
|
||||||
if [[ "${#to_add_remote[@]}" -gt 0 ]]; then
|
if [[ "${#to_add_remote[@]}" -gt 0 ]]; then
|
||||||
local remote_defs
|
local remote_defs
|
||||||
remote_defs=$(_remote_defaults "$node_ip" "$port" "$api_ver" "$arr_type" "$config_xml")
|
remote_defs=$(_remote_defaults "$node_id" "$node_ip" "$port" "$api_ver" "$arr_type")
|
||||||
if [[ -z "$remote_defs" ]]; then
|
if [[ -z "$remote_defs" ]]; then
|
||||||
warn "${arr_type^}: could not fetch defaults from $node_name — skipping remote adds"
|
warn "${arr_type^}: could not fetch defaults from $node_name — skipping remote adds"
|
||||||
else
|
else
|
||||||
@@ -795,8 +863,8 @@ _sync_arr() {
|
|||||||
else
|
else
|
||||||
local encoded http_code
|
local encoded http_code
|
||||||
encoded=$(printf '%s' "$payload" | base64 -w0)
|
encoded=$(printf '%s' "$payload" | base64 -w0)
|
||||||
http_code=$(_remote_add "$node_ip" "$port" "$api_ver" "$endpoint" \
|
http_code=$(_remote_add "$node_id" "$node_ip" "$port" "$api_ver" \
|
||||||
"$config_xml" "$encoded")
|
"$arr_type" "$endpoint" "$encoded")
|
||||||
if [[ "$http_code" == "201" ]] || [[ "$http_code" == "200" ]]; then
|
if [[ "$http_code" == "201" ]] || [[ "$http_code" == "200" ]]; then
|
||||||
log "Added to $node_name ${arr_type^}: $display_name"
|
log "Added to $node_name ${arr_type^}: $display_name"
|
||||||
(( total_added_remote++ ))
|
(( total_added_remote++ ))
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ platform_require_cmd \
|
|||||||
# Age threshold in seconds
|
# Age threshold in seconds
|
||||||
AGE_THRESHOLD_SECONDS=$(( ARR_IMPORT_RECOVERY_AGE * 3600 ))
|
AGE_THRESHOLD_SECONDS=$(( ARR_IMPORT_RECOVERY_AGE * 3600 ))
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: age-threshold=${ARR_IMPORT_RECOVERY_AGE}hr sonarr-v${SONARR_VERSION_MAJOR} radarr-v${RADARR_VERSION_MAJOR} lidarr-v${LIDARR_VERSION_MAJOR:-?}"
|
||||||
|
|
||||||
TOTAL_ACTIONED=0
|
TOTAL_ACTIONED=0
|
||||||
TOTAL_SKIPPED=0
|
TOTAL_SKIPPED=0
|
||||||
ARR_SUMMARIES=()
|
ARR_SUMMARIES=()
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ if [[ ! -d "$LIDARR_MUSIC_ROOT" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: url=${LIDARR_URL} root=${LIDARR_MUSIC_ROOT}"
|
||||||
echo " $MY_ID ($LOCAL_SERVER_NAME) — $LIDARR_URL"
|
echo " $MY_ID ($LOCAL_SERVER_NAME) — $LIDARR_URL"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ if [[ -z "$LIDARR_URL" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
info "$MY_ID ($LOCAL_SERVER_NAME) — tools OK"
|
info "$MY_ID ($LOCAL_SERVER_NAME) — tools OK"
|
||||||
|
log "$ICON_GEAR Config: url=${LIDARR_URL}"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Status ━━━
|
# ━━━ Status ━━━
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ parse_args "${_FILTERED[@]}"
|
|||||||
SYNC_DAYS="${PLAY_SYNC_DAYS:-90}"
|
SYNC_DAYS="${PLAY_SYNC_DAYS:-90}"
|
||||||
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode,Audio}"
|
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode,Audio}"
|
||||||
[[ "$FULL_SYNC" == true ]] && SYNC_DAYS=0
|
[[ "$FULL_SYNC" == true ]] && SYNC_DAYS=0
|
||||||
|
log "$ICON_GEAR Config: days=${SYNC_DAYS} types=${SYNC_TYPES} remote=${PLAY_SYNC_REMOTE:-true}"
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || { error "jq is required but not installed"; exit 1; }
|
command -v jq >/dev/null 2>&1 || { error "jq is required but not installed"; exit 1; }
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ USER_CAP_PCT="${LIDARR_DISCOVERY_USER_CAP_PCT:-35}"
|
|||||||
REJECT_COOLDOWN="${LIDARR_DISCOVERY_REJECT_COOLDOWN:-30}"
|
REJECT_COOLDOWN="${LIDARR_DISCOVERY_REJECT_COOLDOWN:-30}"
|
||||||
HISTORY_FILE="${LIDARR_DISCOVERY_HISTORY:-${DATA_DIR}/lidarr_discovery_history.db}"
|
HISTORY_FILE="${LIDARR_DISCOVERY_HISTORY:-${DATA_DIR}/lidarr_discovery_history.db}"
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: threshold=${THRESHOLD} lookback=${LOOKBACK_DAYS}d min-plays=${MIN_PLAYS} max-adds=${MAX_ADDS} user-cap=${USER_CAP_PCT}% reject-cooldown=${REJECT_COOLDOWN}d"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added to Lidarr"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added to Lidarr"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ REJECT_COOLDOWN="${RADARR_DISCOVERY_REJECT_COOLDOWN:-60}"
|
|||||||
HISTORY_FILE="${RADARR_DISCOVERY_HISTORY:-${DATA_DIR}/radarr_discovery_history.db}"
|
HISTORY_FILE="${RADARR_DISCOVERY_HISTORY:-${DATA_DIR}/radarr_discovery_history.db}"
|
||||||
SEED_LIBRARIES=("${RADARR_DISCOVERY_SEED_LIBRARIES[@]:-Movies}")
|
SEED_LIBRARIES=("${RADARR_DISCOVERY_SEED_LIBRARIES[@]:-Movies}")
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: threshold=${THRESHOLD} lookback=${LOOKBACK_DAYS}d max-seeds=${MAX_SEEDS} max-adds=${MAX_ADDS} min-votes=${MIN_VOTE_COUNT} min-rating=${MIN_RATING} reject-cooldown=${REJECT_COOLDOWN}d"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no movies will be added to Radarr"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no movies will be added to Radarr"
|
||||||
|
|
||||||
_fmt_rating() { local v="${1:-0}"; echo "${v::-1}.${v: -1}" 2>/dev/null || echo "$v"; }
|
_fmt_rating() { local v="${1:-0}"; echo "${v::-1}.${v: -1}" 2>/dev/null || echo "$v"; }
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ USER_EPISODE_CAP="${SONARR_DISCOVERY_USER_EPISODE_CAP:-8}"
|
|||||||
MONITOR_MODE="${SONARR_DISCOVERY_MONITOR_MODE:-all}"
|
MONITOR_MODE="${SONARR_DISCOVERY_MONITOR_MODE:-all}"
|
||||||
HISTORY_FILE="${SONARR_DISCOVERY_HISTORY:-${DATA_DIR}/sonarr_discovery_history.db}"
|
HISTORY_FILE="${SONARR_DISCOVERY_HISTORY:-${DATA_DIR}/sonarr_discovery_history.db}"
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: threshold=${THRESHOLD} lookback=${LOOKBACK_DAYS}d max-seeds=${MAX_SEEDS} max-adds=${MAX_ADDS} min-votes=${MIN_VOTE_COUNT} min-rating=${MIN_RATING} reject-cooldown=${REJECT_COOLDOWN}d monitor=${MONITOR_MODE}"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no shows will be added to Sonarr"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no shows will be added to Sonarr"
|
||||||
|
|
||||||
_fmt_rating() { local v="${1:-0}"; echo "${v::-1}.${v: -1}" 2>/dev/null || echo "$v"; }
|
_fmt_rating() { local v="${1:-0}"; echo "${v::-1}.${v: -1}" 2>/dev/null || echo "$v"; }
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ if [[ ! -d "$RADARR_MOVIES_ROOT" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: url=${RADARR_URL} root=${RADARR_MOVIES_ROOT}"
|
||||||
echo " $MY_ID ($LOCAL_SERVER_NAME) — $RADARR_URL"
|
echo " $MY_ID ($LOCAL_SERVER_NAME) — $RADARR_URL"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ fi
|
|||||||
|
|
||||||
ADD_EXCLUSION="${RADARR_DROPPED_ADD_EXCLUSION:-true}"
|
ADD_EXCLUSION="${RADARR_DROPPED_ADD_EXCLUSION:-true}"
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: url=${RADARR_URL} add-exclusion=${ADD_EXCLUSION} delete-files=${DELETE_FILES:-false}"
|
||||||
echo " $MY_ID ($LOCAL_SERVER_NAME) — $RADARR_URL"
|
echo " $MY_ID ($LOCAL_SERVER_NAME) — $RADARR_URL"
|
||||||
[[ "$DELETE_FILES" == true ]] && warn "DELETE FILES MODE — files will be removed from disk"
|
[[ "$DELETE_FILES" == true ]] && warn "DELETE FILES MODE — files will be removed from disk"
|
||||||
[[ "$DELETE_FILES" == false ]] && echo " Files: records only (use --delete-files to also remove from disk)"
|
[[ "$DELETE_FILES" == false ]] && echo " Files: records only (use --delete-files to also remove from disk)"
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ if [[ ! -d "$SONARR_TV_ROOT" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: url=${SONARR_URL} root=${SONARR_TV_ROOT}"
|
||||||
echo " $MY_ID ($LOCAL_SERVER_NAME) — $SONARR_URL"
|
echo " $MY_ID ($LOCAL_SERVER_NAME) — $SONARR_URL"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ fi
|
|||||||
|
|
||||||
ADD_EXCLUSION="${SONARR_DROPPED_ADD_EXCLUSION:-true}"
|
ADD_EXCLUSION="${SONARR_DROPPED_ADD_EXCLUSION:-true}"
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: url=${SONARR_URL} add-exclusion=${ADD_EXCLUSION} delete-files=${DELETE_FILES:-false}"
|
||||||
echo " $MY_ID ($LOCAL_SERVER_NAME) — $SONARR_URL"
|
echo " $MY_ID ($LOCAL_SERVER_NAME) — $SONARR_URL"
|
||||||
[[ "$DELETE_FILES" == true ]] && warn "DELETE FILES MODE — files will be removed from disk"
|
[[ "$DELETE_FILES" == true ]] && warn "DELETE FILES MODE — files will be removed from disk"
|
||||||
[[ "$DELETE_FILES" == false ]] && echo " Files: records only (use --delete-files to also remove from disk)"
|
[[ "$DELETE_FILES" == false ]] && echo " Files: records only (use --delete-files to also remove from disk)"
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ if [[ ${#VERIFY_SHARES[@]} -eq 0 ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: sample=${BACKUP_VERIFY_SAMPLE} min-size=${BACKUP_VERIFY_MIN_SIZE} ssh-timeout=${SSH_TIMEOUT}s"
|
||||||
|
log "$ICON_GEAR Remote: $REMOTE_ID ($REMOTE_SERVER_NAME — $REMOTE_SERVER)"
|
||||||
|
log "$ICON_GEAR Shares: ${VERIFY_SHARES[*]}"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — showing sample selection only, no checksums computed"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — showing sample selection only, no checksums computed"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -177,12 +181,14 @@ resolve_remote_ip
|
|||||||
|
|
||||||
# Connectivity — no point making 100+ SSH calls if remote is unreachable
|
# Connectivity — no point making 100+ SSH calls if remote is unreachable
|
||||||
check_connectivity
|
check_connectivity
|
||||||
|
log "Connectivity to $REMOTE_SERVER_NAME ✅"
|
||||||
|
|
||||||
# Version parity — mismatched unRAID could cause md5sum path differences
|
# Version parity — mismatched unRAID could cause md5sum path differences
|
||||||
check_unraid_version_parity || {
|
check_unraid_version_parity || {
|
||||||
warn "Version parity check failed — proceeding with caution"
|
warn "Version parity check failed — proceeding with caution"
|
||||||
warn "Checksum results may be unreliable if md5sum path changed between versions"
|
warn "Checksum results may be unreliable if md5sum path changed between versions"
|
||||||
}
|
}
|
||||||
|
log "Version parity with $REMOTE_SERVER_NAME ✅"
|
||||||
|
|
||||||
# Remote array — if array is down all files appear "missing" = false alarm
|
# Remote array — if array is down all files appear "missing" = false alarm
|
||||||
if ! check_remote_array; then
|
if ! check_remote_array; then
|
||||||
@@ -192,6 +198,7 @@ if ! check_remote_array; then
|
|||||||
"Backup Verify" "warning"
|
"Backup Verify" "warning"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
log "Remote array mounted on $REMOTE_SERVER_NAME ✅"
|
||||||
|
|
||||||
echo "Pre-flight passed ✅"
|
echo "Pre-flight passed ✅"
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ platform_require_cmd \
|
|||||||
# detect_hosts() sets MY_ID for report header
|
# detect_hosts() sets MY_ID for report header
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: log=${BANDWIDTH_LOG} retention=${BANDWIDTH_LOG_RETENTION}d warn=${BANDWIDTH_WARN_GB}GB"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Status ━━━
|
# ━━━ Status ━━━
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ if [[ ${#CERT_MONITOR_DOMAINS[@]} -eq 0 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
info "Domains to check: ${#CERT_MONITOR_DOMAINS[@]}"
|
info "Domains to check: ${#CERT_MONITOR_DOMAINS[@]}"
|
||||||
|
log "$ICON_GEAR Config: warn=${CERT_WARN_DAYS}d crit=${CERT_CRIT_DAYS}d timeout=${CERT_TIMEOUT}s"
|
||||||
|
log "$ICON_GEAR Domains: ${CERT_MONITOR_DOMAINS[*]}"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — results shown but no notifications sent"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — results shown but no notifications sent"
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ detect_hosts
|
|||||||
require_var EMBY_URL
|
require_var EMBY_URL
|
||||||
require_var EMBY_API_KEY
|
require_var EMBY_API_KEY
|
||||||
|
|
||||||
log "Emby: $EMBY_URL"
|
log "$ICON_GEAR Config: url=${EMBY_URL} period=${EMBY_REPORT_DAYS}d top=${EMBY_REPORT_TOP_N}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — API queried but no notification sent"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — API queried but no notification sent"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -190,7 +190,7 @@ SYSTEM_INFO=$(emby_api "System/Info" 2>/dev/null) || {
|
|||||||
|
|
||||||
SERVER_NAME=$(echo "$SYSTEM_INFO" | jq -r '.ServerName // "Unknown"' 2>/dev/null)
|
SERVER_NAME=$(echo "$SYSTEM_INFO" | jq -r '.ServerName // "Unknown"' 2>/dev/null)
|
||||||
SERVER_VERSION=$(echo "$SYSTEM_INFO" | jq -r '.Version // "Unknown"' 2>/dev/null)
|
SERVER_VERSION=$(echo "$SYSTEM_INFO" | jq -r '.Version // "Unknown"' 2>/dev/null)
|
||||||
log "Connected to: $SERVER_NAME (v$SERVER_VERSION)"
|
log "$ICON_EMBY Connected to: $SERVER_NAME (v$SERVER_VERSION) ✅"
|
||||||
|
|
||||||
# ── Active Sessions ───────────────────────────────────────────────────────────────────────────
|
# ── Active Sessions ───────────────────────────────────────────────────────────────────────────
|
||||||
echo "━━━ $ICON_EMBY Active Sessions ━━━"
|
echo "━━━ $ICON_EMBY Active Sessions ━━━"
|
||||||
@@ -216,6 +216,7 @@ if [[ "$ACTIVE_COUNT" -gt 0 ]]; then
|
|||||||
" \(.UserName // "Unknown") → \(.NowPlayingItem.Name // "Unknown") [\(if .TranscodingInfo != null then "transcode" else "direct" end)]"
|
" \(.UserName // "Unknown") → \(.NowPlayingItem.Name // "Unknown") [\(if .TranscodingInfo != null then "transcode" else "direct" end)]"
|
||||||
' 2>/dev/null || true
|
' 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
log "$ICON_EMBY Sessions: $ACTIVE_COUNT active ($DIRECT_NOW direct / $TRANSCODE_NOW transcode)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Library Stats ─────────────────────────────────────────────────────────────────────────────
|
# ── Library Stats ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -229,6 +230,7 @@ SONG_COUNT=$(echo "$ITEMS" | jq '.SongCount // 0' 2>/dev/null || echo 0)
|
|||||||
echo " $ICON_EMBY Movies: $MOVIE_COUNT"
|
echo " $ICON_EMBY Movies: $MOVIE_COUNT"
|
||||||
echo " $ICON_EMBY Episodes: $EPISODE_COUNT"
|
echo " $ICON_EMBY Episodes: $EPISODE_COUNT"
|
||||||
echo " $ICON_EMBY Songs: $SONG_COUNT"
|
echo " $ICON_EMBY Songs: $SONG_COUNT"
|
||||||
|
log "$ICON_EMBY Library: $MOVIE_COUNT movies $EPISODE_COUNT episodes $SONG_COUNT songs"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Activity History ──────────────────────────────────────────────────────────────────────────
|
# ── Activity History ──────────────────────────────────────────────────────────────────────────
|
||||||
@@ -256,6 +258,7 @@ if [[ "$TOTAL_PLAYS" -gt 0 ]]; then
|
|||||||
DIRECT_PCT=$(( 100 - TRANSCODE_PCT ))
|
DIRECT_PCT=$(( 100 - TRANSCODE_PCT ))
|
||||||
echo " $ICON_EMBY Direct play: ~${DIRECT_PCT}%"
|
echo " $ICON_EMBY Direct play: ~${DIRECT_PCT}%"
|
||||||
echo " $ICON_EMBY Transcoded: ~${TRANSCODE_PCT}%"
|
echo " $ICON_EMBY Transcoded: ~${TRANSCODE_PCT}%"
|
||||||
|
log "$ICON_EMBY Activity: $TOTAL_PLAYS plays — ~${DIRECT_PCT}% direct / ~${TRANSCODE_PCT}% transcode"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ format_delay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
collect_hosts
|
collect_hosts
|
||||||
|
log "$ICON_HOST Hosts: ${ALL_HOST_IDS[*]} (${#ALL_HOST_IDS[@]} in mesh)"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Status ━━━
|
# ━━━ Status ━━━
|
||||||
@@ -127,6 +128,7 @@ for h in "${ALL_HOST_IDS[@]}"; do
|
|||||||
email_var="${h}_OWNER_EMAIL"; email="${!email_var:-(not set)}"
|
email_var="${h}_OWNER_EMAIL"; email="${!email_var:-(not set)}"
|
||||||
printf " %-${W_HOST}s %-${W_SERVER}s %-${W_OWNER}s %s\n" \
|
printf " %-${W_HOST}s %-${W_SERVER}s %-${W_OWNER}s %s\n" \
|
||||||
"$h" "$server" "$owner" "$email"
|
"$h" "$server" "$owner" "$email"
|
||||||
|
log "$h: server=$server owner=$owner email=$email"
|
||||||
done
|
done
|
||||||
|
|
||||||
# ── Protected Services ────────────────────────────────────────────────────────────────────────
|
# ── Protected Services ────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ acquire_lock
|
|||||||
# detect_hosts() sets MY_ID — used in warning output
|
# detect_hosts() sets MY_ID — used in warning output
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: inotify-warn=${INOTIFY_WARN_PCT:-80}% php-fpm-warn=${PHP_FPM_WARN_PCT:-80}% max-workers=${PHP_MAX_CHILDREN:-250} retention=${TUNING_LOG_RETENTION:-30}d"
|
||||||
|
|
||||||
DATE=$(date '+%Y-%m-%d')
|
DATE=$(date '+%Y-%m-%d')
|
||||||
TIME=$(date '+%H:%M')
|
TIME=$(date '+%H:%M')
|
||||||
INOTIFY_WARN=0
|
INOTIFY_WARN=0
|
||||||
@@ -245,4 +247,5 @@ fi
|
|||||||
echo "${DATE}|${TIME}|${INOTIFY_USED}|${INOTIFY_LIMIT}|${INOTIFY_PCT}|${INOTIFY_WARN}|${PHPFPM_ACTIVE}|${PHPFPM_MAX}|${PHPFPM_PCT}|${PHPFPM_WARN}" \
|
echo "${DATE}|${TIME}|${INOTIFY_USED}|${INOTIFY_LIMIT}|${INOTIFY_PCT}|${INOTIFY_WARN}|${PHPFPM_ACTIVE}|${PHPFPM_MAX}|${PHPFPM_PCT}|${PHPFPM_WARN}" \
|
||||||
>> "$TUNING_MONITOR_LOG"
|
>> "$TUNING_MONITOR_LOG"
|
||||||
|
|
||||||
echo "Snapshot written: inotify ${INOTIFY_PCT}% php-fpm ${PHPFPM_PCT}%"
|
echo "Snapshot written: inotify ${INOTIFY_PCT}% php-fpm ${PHPFPM_PCT}%"
|
||||||
|
log "Entry: ${DATE}|${TIME}|${INOTIFY_USED}/${INOTIFY_LIMIT}(${INOTIFY_PCT}%,warn=${INOTIFY_WARN})|${PHPFPM_ACTIVE}/${PHPFPM_MAX}(${PHPFPM_PCT}%,warn=${PHPFPM_WARN})"
|
||||||
@@ -141,6 +141,9 @@ acquire_lock
|
|||||||
# detect_hosts() sets MY_ID and aliases all host-specific vars used in this report
|
# detect_hosts() sets MY_ID and aliases all host-specific vars used in this report
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: profile=${DIGEST_PROFILE} day=${DIGEST_DAY}"
|
||||||
|
log "$ICON_GEAR Smart triggers: watchdog=${DIGEST_SMART_ON_WATCHDOG} fallback=${DIGEST_SMART_ON_FALLBACK} cert=${DIGEST_SMART_ON_CERT_WARN} bandwidth=${DIGEST_SMART_ON_BANDWIDTH}"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — report generated but no notification sent"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — report generated but no notification sent"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -199,6 +202,7 @@ ISSUES=() # need attention
|
|||||||
DIGEST_LINES=() # full report lines
|
DIGEST_LINES=() # full report lines
|
||||||
|
|
||||||
# ── fallback State ────────────────────────────────────────────────────────────────────────────
|
# ── fallback State ────────────────────────────────────────────────────────────────────────────
|
||||||
|
log "Reading: fallback=$FALLBACK_STATE_FILE skip=$DOCKER_WATCHDOG_FAILED_FILE watchdog=$WATCHDOG_STATE_FILE sys=$SYS_WATCHDOG_STATE_FILE"
|
||||||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||||||
FALLBACK_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
FALLBACK_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||||
if [[ -n "$FALLBACK_STATE" ]]; then
|
if [[ -n "$FALLBACK_STATE" ]]; then
|
||||||
@@ -323,6 +327,8 @@ if [[ ${#CERT_MONITOR_DOMAINS[@]} -gt 0 ]] && command -v openssl >/dev/null 2>&1
|
|||||||
elif [[ "$days_remaining" -le "${CERT_WARN_DAYS:-30}" ]]; then
|
elif [[ "$days_remaining" -le "${CERT_WARN_DAYS:-30}" ]]; then
|
||||||
CERT_ISSUES+=("$domain: ${days_remaining}d warning")
|
CERT_ISSUES+=("$domain: ${days_remaining}d warning")
|
||||||
[[ "$DIGEST_SMART_ON_CERT_WARN" == true ]] && SHOULD_SEND=true
|
[[ "$DIGEST_SMART_ON_CERT_WARN" == true ]] && SHOULD_SEND=true
|
||||||
|
else
|
||||||
|
log "$ICON_CERT $domain: ${days_remaining}d remaining ✅"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ done
|
|||||||
|
|
||||||
log "Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
log "Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||||
log "Ignoring pools: ${ZFS_REPORT_IGNORE_POOLS[*]:-none}"
|
log "Ignoring pools: ${ZFS_REPORT_IGNORE_POOLS[*]:-none}"
|
||||||
|
log "$ICON_GEAR Config: arc-warn=${ZFS_REPORT_ARC_WARN_PCT}% free-warn=${ZFS_REPORT_FREE_WARN_GB}GB avail-warn=${ZFS_REPORT_AVAIL_WARN_GB}GB docker-top=${ZFS_REPORT_DOCKER_TOP}"
|
||||||
|
|
||||||
# Tee output to log file unless dry run
|
# Tee output to log file unless dry run
|
||||||
if [[ "$DRY_RUN" == false ]]; then
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
|
|||||||
@@ -323,7 +323,8 @@
|
|||||||
#vv-editor { flex-direction: column; gap: 0; }
|
#vv-editor { flex-direction: column; gap: 0; }
|
||||||
.vv-editor-body { font-family: monospace; font-size: 12px; background: #0d0d0d; color: #ccc;
|
.vv-editor-body { font-family: monospace; font-size: 12px; background: #0d0d0d; color: #ccc;
|
||||||
border: 1px solid #333; border-radius: 4px; padding: 10px 12px; resize: none;
|
border: 1px solid #333; border-radius: 4px; padding: 10px 12px; resize: none;
|
||||||
line-height: 1.5; scroll-behavior: auto; tab-size: 2; width: 100%; box-sizing: border-box; }
|
line-height: 1.5; scroll-behavior: auto; tab-size: 2; width: 100%; box-sizing: border-box;
|
||||||
|
white-space: pre; overflow-x: auto; }
|
||||||
|
|
||||||
/* Editor layout: gutter + inner area — unified bordered block */
|
/* Editor layout: gutter + inner area — unified bordered block */
|
||||||
#vv-editor-wrap { display: flex; border: 1px solid #2e2e2e; border-radius: 4px 4px 0 0;
|
#vv-editor-wrap { display: flex; border: 1px solid #2e2e2e; border-radius: 4px 4px 0 0;
|
||||||
@@ -347,7 +348,7 @@
|
|||||||
#vv-hl-overlay { display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
#vv-hl-overlay { display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
margin: 0; padding: 10px 12px; box-sizing: border-box;
|
margin: 0; padding: 10px 12px; box-sizing: border-box;
|
||||||
font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;
|
font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;
|
||||||
white-space: pre-wrap; word-break: break-all; overflow: hidden;
|
white-space: pre; overflow: hidden;
|
||||||
pointer-events: none; user-select: none;
|
pointer-events: none; user-select: none;
|
||||||
background: transparent; border: none; border-radius: 0; }
|
background: transparent; border: none; border-radius: 0; }
|
||||||
.vv-editor-hl #vv-hl-overlay { display: block; }
|
.vv-editor-hl #vv-hl-overlay { display: block; }
|
||||||
@@ -414,6 +415,12 @@
|
|||||||
.vv-es-btn.active { color: #6495ed; }
|
.vv-es-btn.active { color: #6495ed; }
|
||||||
#vv-es-fontsize { color: #444; font-size: 10px; min-width: 26px; text-align: center; }
|
#vv-es-fontsize { color: #444; font-size: 10px; min-width: 26px; text-align: center; }
|
||||||
|
|
||||||
|
/* Word wrap mode — applied to #vv-editor-wrap when wrap is enabled */
|
||||||
|
#vv-editor-wrap.vv-ed-wrap .vv-editor-body { white-space: pre-wrap !important; overflow-x: hidden !important; }
|
||||||
|
#vv-editor-wrap.vv-ed-wrap #vv-hl-overlay { white-space: pre-wrap !important; word-break: break-word !important; }
|
||||||
|
#vv-editor-wrap.vv-ed-wrap #vv-ln-gutter { display: none; }
|
||||||
|
#vv-editor-wrap.vv-ed-wrap #vv-cur-line { display: none !important; }
|
||||||
|
|
||||||
/* Indent guides — 1px lines every 2 chars, starting at indent level 1 */
|
/* Indent guides — 1px lines every 2 chars, starting at indent level 1 */
|
||||||
#vv-editor-inner {
|
#vv-editor-inner {
|
||||||
position: relative; flex: 1; overflow: hidden;
|
position: relative; flex: 1; overflow: hidden;
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ function vvRenderMemory(mem) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
html += vvMemRow('System', mem.system_kb ?? 0, total, VV_MEM_COLORS.system)
|
html += vvMemRow('System', mem.system_kb ?? 0, total, VV_MEM_COLORS.system)
|
||||||
+ vvMemRow('VM', mem.vm_kb ?? 0, total, VV_MEM_COLORS.vm)
|
+ vvMemRow('VM', mem.vm_kb ?? 0, total, VV_MEM_COLORS.vm)
|
||||||
+ vvMemRow('ZFS', mem.zfs_kb ?? 0, total, VV_MEM_COLORS.zfs)
|
+ vvMemRow('ZFS', mem.arc_kb ?? 0, total, VV_MEM_COLORS.zfs)
|
||||||
+ vvMemRow('Docker', mem.docker_kb ?? 0, total, VV_MEM_COLORS.docker)
|
+ vvMemRow('Docker', mem.docker_kb ?? 0, total, VV_MEM_COLORS.docker)
|
||||||
+ vvMemRow('Free', mem.free_kb ?? 0, total, VV_MEM_COLORS.free);
|
+ vvMemRow('Free', mem.free_kb ?? 0, total, VV_MEM_COLORS.free);
|
||||||
|
|
||||||
|
|||||||
@@ -944,6 +944,7 @@ Still the same two servers, two households, the same media stack running itself.
|
|||||||
<button class="vv-es-btn" onclick="vvFontSize(-1)" title="Decrease font size">A−</button>
|
<button class="vv-es-btn" onclick="vvFontSize(-1)" title="Decrease font size">A−</button>
|
||||||
<span id="vv-es-fontsize">12px</span>
|
<span id="vv-es-fontsize">12px</span>
|
||||||
<button class="vv-es-btn" onclick="vvFontSize(1)" title="Increase font size">A+</button>
|
<button class="vv-es-btn" onclick="vvFontSize(1)" title="Increase font size">A+</button>
|
||||||
|
<button class="vv-es-btn" id="vv-es-wrap-btn" onclick="vvToggleWordWrap()" title="Word wrap OFF — click to enable">Wrap</button>
|
||||||
<span class="vv-es-lang" id="vv-es-lang"></span>
|
<span class="vv-es-lang" id="vv-es-lang"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1031,6 +1032,7 @@ const VV_PAIRS = {'(':')', '[':']', '{':'}', '"':'"', "'":"'", '`':'`'};
|
|||||||
const VV_CLOSE = new Set([')', ']', '}', '"', "'", '`']);
|
const VV_CLOSE = new Set([')', ']', '}', '"', "'", '`']);
|
||||||
// Editor display prefs
|
// Editor display prefs
|
||||||
let vvFontSizePx = parseInt(localStorage.getItem('vv-editor-fontsize') || '12') || 12;
|
let vvFontSizePx = parseInt(localStorage.getItem('vv-editor-fontsize') || '12') || 12;
|
||||||
|
let vvWordWrap = localStorage.getItem('vv-editor-wordwrap') === '1';
|
||||||
// Line height helper — used everywhere instead of hardcoded 18
|
// Line height helper — used everywhere instead of hardcoded 18
|
||||||
function vvLH() { return vvFontSizePx * 1.5; }
|
function vvLH() { return vvFontSizePx * 1.5; }
|
||||||
|
|
||||||
@@ -2693,6 +2695,7 @@ function vvEditRawConf(file) {
|
|||||||
|
|
||||||
function vvShowRawConfMode(title) {
|
function vvShowRawConfMode(title) {
|
||||||
vvEditorReset();
|
vvEditorReset();
|
||||||
|
vvRestoreEditorPrefs();
|
||||||
document.getElementById('vv-editor-status').classList.add('vv-ed-active');
|
document.getElementById('vv-editor-status').classList.add('vv-ed-active');
|
||||||
document.getElementById('vv-es-lang').textContent = 'Bash / Config';
|
document.getElementById('vv-es-lang').textContent = 'Bash / Config';
|
||||||
document.getElementById('vv-undo-btn').style.display = '';
|
document.getElementById('vv-undo-btn').style.display = '';
|
||||||
@@ -2836,7 +2839,7 @@ function vvSyncHlScroll() {
|
|||||||
function vvUpdateLineNums() {
|
function vvUpdateLineNums() {
|
||||||
const gutter = document.getElementById('vv-ln-gutter');
|
const gutter = document.getElementById('vv-ln-gutter');
|
||||||
const ta = document.getElementById('vv-editor-body');
|
const ta = document.getElementById('vv-editor-body');
|
||||||
if (!gutter || !ta) return;
|
if (!gutter || !ta || vvWordWrap) return;
|
||||||
const lines = ta.value.split('\n').length;
|
const lines = ta.value.split('\n').length;
|
||||||
const curLn = ta.value.slice(0, ta.selectionStart).split('\n').length;
|
const curLn = ta.value.slice(0, ta.selectionStart).split('\n').length;
|
||||||
let html = '';
|
let html = '';
|
||||||
@@ -2851,7 +2854,7 @@ function vvUpdateLineNums() {
|
|||||||
function vvUpdateCurLine() {
|
function vvUpdateCurLine() {
|
||||||
const ta = document.getElementById('vv-editor-body');
|
const ta = document.getElementById('vv-editor-body');
|
||||||
const cl = document.getElementById('vv-cur-line');
|
const cl = document.getElementById('vv-cur-line');
|
||||||
if (!cl || !ta) return;
|
if (!cl || !ta || vvWordWrap) return;
|
||||||
const lineH = vvLH();
|
const lineH = vvLH();
|
||||||
const padTop = 10; // overlay padding-top
|
const padTop = 10; // overlay padding-top
|
||||||
const lineN = ta.value.slice(0, ta.selectionStart).split('\n').length - 1; // 0-indexed
|
const lineN = ta.value.slice(0, ta.selectionStart).split('\n').length - 1; // 0-indexed
|
||||||
@@ -3058,6 +3061,30 @@ function vvRestoreEditorPrefs() {
|
|||||||
const fs_el = document.getElementById('vv-es-fontsize');
|
const fs_el = document.getElementById('vv-es-fontsize');
|
||||||
if (fs_el) fs_el.textContent = '12px';
|
if (fs_el) fs_el.textContent = '12px';
|
||||||
}
|
}
|
||||||
|
vvWordWrap = localStorage.getItem('vv-editor-wordwrap') === '1';
|
||||||
|
vvApplyWordWrap(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Word wrap toggle ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function vvApplyWordWrap(save) {
|
||||||
|
const wrap = document.getElementById('vv-editor-wrap');
|
||||||
|
const btn = document.getElementById('vv-es-wrap-btn');
|
||||||
|
if (!wrap) return;
|
||||||
|
if (vvWordWrap) {
|
||||||
|
wrap.classList.add('vv-ed-wrap');
|
||||||
|
if (btn) { btn.classList.add('active'); btn.title = 'Word wrap ON — click to disable'; }
|
||||||
|
} else {
|
||||||
|
wrap.classList.remove('vv-ed-wrap');
|
||||||
|
if (btn) { btn.classList.remove('active'); btn.title = 'Word wrap OFF — click to enable'; }
|
||||||
|
}
|
||||||
|
if (save) localStorage.setItem('vv-editor-wordwrap', vvWordWrap ? '1' : '0');
|
||||||
|
vvSyncHlOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvToggleWordWrap() {
|
||||||
|
vvWordWrap = !vvWordWrap;
|
||||||
|
vvApplyWordWrap(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Go to line ────────────────────────────────────────────────────────────────
|
// ── Go to line ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ read -r -a REMOTE_RESTART_CONTAINERS <<< "${PROFILE_REMOTE_RESTART_CONTAINE
|
|||||||
# Local containers use same names as remote (mirrored naming scheme)
|
# Local containers use same names as remote (mirrored naming scheme)
|
||||||
LOCAL_CRITICAL_CONTAINER_NAMES=("${CRITICAL_CONTAINER_NAMES[@]}")
|
LOCAL_CRITICAL_CONTAINER_NAMES=("${CRITICAL_CONTAINER_NAMES[@]}")
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: profile=${PROFILE_NAME} bw-limit=${BW_LIMIT}KB/s retry=${RETRY_COUNT} sleep=${SLEEP}s container-delay=${CONTAINER_DELAY}s"
|
||||||
|
log "$ICON_GEAR Containers: critical=${CRITICAL_CONTAINER_NAMES[*]:-none} delayed=${DELAYED_CONTAINERS[*]:-none} remote-restart=${REMOTE_RESTART_CONTAINERS[*]:-none}"
|
||||||
|
|
||||||
[[ "$SHOW_STATUS" == true ]] && show_status && exit 0
|
[[ "$SHOW_STATUS" == true ]] && show_status && exit 0
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,10 @@
|
|||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
#
|
#
|
||||||
# claude_startup.sh
|
# claude_startup.sh
|
||||||
# Set up persistent symlinks and launch Claude.
|
# Set up persistent symlinks only — default, used by array_started.sh on boot.
|
||||||
#
|
#
|
||||||
# claude_startup.sh --setup
|
# claude_startup.sh --launch
|
||||||
# Set up persistent symlinks only — do not launch Claude.
|
# Set up persistent symlinks and launch Claude interactively.
|
||||||
# Used by array_started.sh to prepare the environment on boot without
|
|
||||||
# immediately launching an interactive session.
|
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
@@ -36,8 +34,8 @@ PERSIST_DIR="/mnt/user/appdata/claude-code"
|
|||||||
CLAUDE_DATA="$PERSIST_DIR/.claude"
|
CLAUDE_DATA="$PERSIST_DIR/.claude"
|
||||||
CLAUDE_BIN="$PERSIST_DIR/local/share/claude"
|
CLAUDE_BIN="$PERSIST_DIR/local/share/claude"
|
||||||
|
|
||||||
LAUNCH=true
|
LAUNCH=false
|
||||||
[[ "$1" == "--setup" ]] && LAUNCH=false
|
[[ "$1" == "--launch" ]] && LAUNCH=true
|
||||||
|
|
||||||
# Standalone — no common.sh dependency
|
# Standalone — no common.sh dependency
|
||||||
_log() { echo " ✅ $*"; }
|
_log() { echo " ✅ $*"; }
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ detect_hosts
|
|||||||
CONF_FILE="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
|
CONF_FILE="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
|
||||||
[[ ! -f "$CONF_FILE" ]] && { error "Conf file not found: $CONF_FILE"; exit 1; }
|
[[ ! -f "$CONF_FILE" ]] && { error "Conf file not found: $CONF_FILE"; exit 1; }
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: conf=${CONF_FILE} overwrite=${OVERWRITE:-false} no-push=${NO_PUSH:-false}"
|
||||||
|
|
||||||
UPDATED=0
|
UPDATED=0
|
||||||
SKIPPED=0
|
SKIPPED=0
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ fi
|
|||||||
# ━━━ Run ━━━
|
# ━━━ Run ━━━
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
|
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
|
||||||
|
log "$ICON_GEAR Config: mode=${MODE_LABEL} dry-run=${DRY_RUN}"
|
||||||
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||||
|
|
||||||
TOTAL_RECLAIMED=0
|
TOTAL_RECLAIMED=0
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ if [[ -z "${LIDARR_URL:-}" || -z "${LIDARR_API_KEY:-}" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: emby=${EMBY_URL} lidarr=${LIDARR_URL}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added to Lidarr"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no artists will be added to Lidarr"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ if [[ -z "${RADARR_URL:-}" || -z "${RADARR_API_KEY:-}" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: emby=${EMBY_URL} radarr=${RADARR_URL}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no movies will be added to Radarr"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no movies will be added to Radarr"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ if [[ -z "${SONARR_URL:-}" || -z "${SONARR_API_KEY:-}" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: emby=${EMBY_URL} sonarr=${SONARR_URL}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no series will be added to Sonarr"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no series will be added to Sonarr"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ acquire_lock
|
|||||||
# detect_hosts() sets MY_ID — used in summary and notification
|
# detect_hosts() sets MY_ID — used in summary and notification
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: state-file=${FALLBACK_STATE_FILE}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||||
[[ "$FORCE" == true ]] && warn "FORCE mode — confirmation prompt skipped"
|
[[ "$FORCE" == true ]] && warn "FORCE mode — confirmation prompt skipped"
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ acquire_lock
|
|||||||
# detect_hosts() sets MY_ID — used in summary
|
# detect_hosts() sets MY_ID — used in summary
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: cfg-dir=${SHARE_CFG_DIR} marker=${MARKER_FILE}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no directories or markers will be created"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no directories or markers will be created"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ done
|
|||||||
|
|
||||||
mkdir -p /tmp/vv_cache
|
mkdir -p /tmp/vv_cache
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: target=${TARGET_HOST:-all hosts} ssh-key=${SSH_KEY}"
|
||||||
|
|
||||||
FETCH_OK=0
|
FETCH_OK=0
|
||||||
FETCH_FAIL=0
|
FETCH_FAIL=0
|
||||||
FETCH_SKIP=0
|
FETCH_SKIP=0
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ SRC="$SCRIPTS_DIR"
|
|||||||
DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR")
|
DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR")
|
||||||
NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false")
|
NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false")
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: to=${TO_MODE} src=${SRC} dst=${DST} dry-run=${DRY_RUN}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━"
|
echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━"
|
||||||
echo "$ICON_GEAR From: $SRC"
|
echo "$ICON_GEAR From: $SRC"
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ fi
|
|||||||
# detect_hosts() sets MY_ID — used in output
|
# detect_hosts() sets MY_ID — used in output
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
|
log "$ICON_GEAR Config: action=${ACTION} container=${TARGET_CONTAINER:-all} skip-file=${DOCKER_WATCHDOG_FAILED_FILE}"
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||||
[[ "$FORCE" == true ]] && warn "FORCE mode — confirmation prompt skipped"
|
[[ "$FORCE" == true ]] && warn "FORCE mode — confirmation prompt skipped"
|
||||||
|
|
||||||
|
|||||||
@@ -80,12 +80,14 @@
|
|||||||
# Missing files are silently skipped — sparse checkout intentionally withholds some.
|
# Missing files are silently skipped — sparse checkout intentionally withholds some.
|
||||||
# At least one host conf must be present or the ecosystem has no identity to work with.
|
# At least one host conf must be present or the ecosystem has no identity to work with.
|
||||||
_host_confs_loaded=0
|
_host_confs_loaded=0
|
||||||
|
declare -A _disk_conf_basenames=()
|
||||||
|
|
||||||
# Use a sorted array glob — avoids word-splitting on paths with spaces
|
# Use a sorted array glob — avoids word-splitting on paths with spaces
|
||||||
while IFS= read -r _conf; do
|
while IFS= read -r _conf; do
|
||||||
[[ -f "$_conf" ]] || continue
|
[[ -f "$_conf" ]] || continue
|
||||||
source "$_conf"
|
source "$_conf"
|
||||||
(( _host_confs_loaded++ ))
|
(( _host_confs_loaded++ ))
|
||||||
|
_disk_conf_basenames["$(basename "$_conf")"]=1
|
||||||
[[ "${ENABLE_LOGGING:-false}" == "true" ]] && \
|
[[ "${ENABLE_LOGGING:-false}" == "true" ]] && \
|
||||||
echo "[LOG] Loaded host config: $(basename "$_conf")" >&2
|
echo "[LOG] Loaded host config: $(basename "$_conf")" >&2
|
||||||
done < <(printf '%s\n' "$LOAD_CONFIG_DIR/Configurations"/host*.conf 2>/dev/null | sort)
|
done < <(printf '%s\n' "$LOAD_CONFIG_DIR/Configurations"/host*.conf 2>/dev/null | sort)
|
||||||
@@ -96,6 +98,26 @@
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ━━━ Source partner confs from /tmp cache ━━━
|
||||||
|
# conf_sync.sh pulls partner host*.conf files into /tmp/.vv/config/cached/.confs/
|
||||||
|
# on array start and after any conf save. Sourcing them here makes partner vars
|
||||||
|
# (HOST2_*, HOST3_*, …) available without committing credentials to the git repo
|
||||||
|
# or violating sparse checkout — partner confs live in RAM only, cleared on reboot.
|
||||||
|
# Confs already loaded from disk are skipped — disk copy is authoritative.
|
||||||
|
_VV_CONF_CACHE="/tmp/.vv/config/cached/.confs"
|
||||||
|
if [[ -d "$_VV_CONF_CACHE" ]]; then
|
||||||
|
while IFS= read -r _conf; do
|
||||||
|
[[ -f "$_conf" ]] || continue
|
||||||
|
_conf_base="$(basename "$_conf")"
|
||||||
|
# Skip if already sourced from disk
|
||||||
|
[[ -n "${_disk_conf_basenames[$_conf_base]:-}" ]] && continue
|
||||||
|
source "$_conf"
|
||||||
|
[[ "${ENABLE_LOGGING:-false}" == "true" ]] && \
|
||||||
|
echo "[LOG] Loaded cached partner config: $_conf_base" >&2
|
||||||
|
done < <(printf '%s\n' "$_VV_CONF_CACHE"/host*.conf 2>/dev/null | sort)
|
||||||
|
fi
|
||||||
|
unset _VV_CONF_CACHE _conf_base _disk_conf_basenames
|
||||||
|
|
||||||
# ━━━ Source shared functions ━━━
|
# ━━━ Source shared functions ━━━
|
||||||
# common.sh sourced last — it calls detect_hosts() which needs HOST* vars to be set.
|
# common.sh sourced last — it calls detect_hosts() which needs HOST* vars to be set.
|
||||||
if [[ ! -f "$LOAD_CONFIG_DIR/common.sh" ]]; then
|
if [[ ! -f "$LOAD_CONFIG_DIR/common.sh" ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user