The file /boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh has been updated successfully. (file state is current in your context — no need to Read it back)

# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Pulls the latest images for configured containers. Two modes: normal (daily)
# and remainder (weekly).
#
# Normal mode is called by daily_sync_maintenance.sh before docker_daily_restart.sh.
# Containers stay running during the pull — no extra downtime beyond what the
# nightly restart already causes.
#
# Remainder mode is called by weekly_sync_maintenance.sh as the final update step.
# It catches everything that normal mode and the weekly sync window did not already
# update — derived automatically from docker ps, nothing to configure.

# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Pulls the latest images for configured containers. Three modes:
#
# Normal mode — called by daily_sync_maintenance.sh before docker_daily_restart.sh.
# Containers stay running during the pull — no extra downtime beyond what the
# nightly restart already causes.
#
# Weekly mode — called by weekly_sync_maintenance.sh via WEEKLY_MAINTENANCE_SCRIPTS
# before docker_weekly_restart.sh. Pulls WEEKLY_RESTART_CONTAINERS so the
# subsequent restart lands on the fresh image.
#
# Remainder mode — called by monthly_maintenance.sh. Catches everything not
# already owned by daily or weekly — derived from docker ps, nothing to configure.

-# Pulls the latest images for configured containers. Two modes: normal (daily)

+# Pulls the latest images for configured containers. Three modes:

-# Normal mode is called by daily_sync_maintenance.sh before docker_daily_restart.sh.

+# Normal mode — called by daily_sync_maintenance.sh before docker_daily_restart.sh.

-# Remainder mode is called by weekly_sync_maintenance.sh as the final update step.

-# It catches everything that normal mode and the weekly sync window did not already

-# update — derived automatically from docker ps, nothing to configure.

+# Weekly mode — called by weekly_sync_maintenance.sh via WEEKLY_MAINTENANCE_SCRIPTS

+# before docker_weekly_restart.sh. Pulls WEEKLY_RESTART_CONTAINERS so the

+# subsequent restart lands on the fresh image.

+# Remainder mode — called by monthly_maintenance.sh. Catches everything not

+# already owned by daily or weekly — derived from docker ps, nothing to configure.


Run docker network script during onboarding


Edit

# Normal mode (daily):
#   Targets DAILY_RESTART_CONTAINERS — same list used by docker_daily_restart.sh.
#   Pull → compare old vs new image ID → mark updated or already current.
#   docker_daily_restart.sh runs after — containers restart onto the fresh image.
#
# Remainder mode (weekly):
#   Targets all currently running containers NOT in:
#     DAILY_RESTART_CONTAINERS       — already updated daily
#     emby + critical-data profiles  — updated inline by the weekly sync window
#     FALLBACK_*_TIER*               — owned by the remote server's update cycle
#   WEEKLY_RESTART_CONTAINERS are included — docker_weekly_restart.sh restarts
#   them but doesn't pull images; remainder is the only place they get updated.
#   Pull → compare → prune dangling images.

# Normal mode (daily):
#   Targets DAILY_RESTART_CONTAINERS — same list used by docker_daily_restart.sh.
#   Pull → compare old vs new image ID → mark updated or already current.
#   docker_daily_restart.sh runs after — containers restart onto the fresh image.
#
# Weekly mode:
#   Targets WEEKLY_RESTART_CONTAINERS — same list used by docker_weekly_restart.sh.
#   Pull → compare → rebuild if changed.
#   docker_weekly_restart.sh runs after — containers restart onto the fresh image.
#
# Remainder mode (monthly):
#   Targets all currently running containers NOT in:
#     DAILY_RESTART_CONTAINERS       — already updated daily
#     WEEKLY_RESTART_CONTAINERS      — already updated weekly
#     emby + critical-data profiles  — updated inline by the weekly sync window
#     FALLBACK_*_TIER*               — owned by the remote server's update cycle
#   Pull → compare → prune dangling images.


# Normal mode (daily):
#   Targets DAILY_RESTART_CONTAINERS — same list used by docker_daily_restart.sh.
#   Pull → compare old vs new image ID → mark updated or already current.
#   docker_daily_restart.sh runs after — containers restart onto the fresh image.
#
# Remainder mode (weekly):
#   Targets all currently running containers NOT in:
#     DAILY_RESTART_CONTAINERS       — already updated daily
#     emby + critical-data profiles  — updated inline by the weekly sync window
#     FALLBACK_*_TIER*               — owned by the remote server's update cycle
#   WEEKLY_RESTART_CONTAINERS are included — docker_weekly_restart.sh restarts
#   them but doesn't pull images; remainder is the only place they get updated.
#   Pull → compare → prune dangling images.

# Normal mode (daily):
#   Targets DAILY_RESTART_CONTAINERS — same list used by docker_daily_restart.sh.
#   Pull → compare old vs new image ID → mark updated or already current.
#   docker_daily_restart.sh runs after — containers restart onto the fresh image.
#
# Weekly mode:
#   Targets WEEKLY_RESTART_CONTAINERS — same list used by docker_weekly_restart.sh.
#   Pull → compare → rebuild if changed.
#   docker_weekly_restart.sh runs after — containers restart onto the fresh image.
#
# Remainder mode (monthly):
#   Targets all currently running containers NOT in:
#     DAILY_RESTART_CONTAINERS       — already updated daily
#     WEEKLY_RESTART_CONTAINERS      — already updated weekly
#     emby + critical-data profiles  — updated inline by the weekly sync window
#     FALLBACK_*_TIER*               — owned by the remote server's update cycle
#   Pull → compare → prune dangling images.

+#   Targets WEEKLY_RESTART_CONTAINERS — same list used by docker_weekly_restart.sh.

+#   Pull → compare → rebuild if changed.

+#   docker_weekly_restart.sh runs after — containers restart onto the fresh image.

+#     WEEKLY_RESTART_CONTAINERS      — already updated weekly

-#   WEEKLY_RESTART_CONTAINERS are included — docker_weekly_restart.sh restarts

-#   them but doesn't pull images; remainder is the only place they get updated.


# DAILY_CONTAINER_UPDATES Toggle
#   Normal mode exits cleanly when disabled. docker_daily_restart.sh still runs
#   regardless — update and restart are independent operations.

# DAILY_CONTAINER_UPDATES / WEEKLY_CONTAINER_UPDATES Toggles
#   Each mode exits cleanly when disabled. Restart scripts run regardless —
#   update and restart are independent operations.


# DAILY_CONTAINER_UPDATES Toggle
#   Normal mode exits cleanly when disabled. docker_daily_restart.sh still runs
#   regardless — update and restart are independent operations.

# DAILY_CONTAINER_UPDATES / WEEKLY_CONTAINER_UPDATES Toggles
#   Each mode exits cleanly when disabled. Restart scripts run regardless —
#   update and restart are independent operations.

#   Docker operations require root privileges.

-#   Normal mode exits cleanly when disabled. docker_daily_restart.sh still runs

-#   regardless — update and restart are independent operations.

+# DAILY_CONTAINER_UPDATES / WEEKLY_CONTAINER_UPDATES Toggles

+#   Each mode exits cleanly when disabled. Restart scripts run regardless —

+#   update and restart are independent operations.

#   Remainder mode excludes containers owned by the remote server's update cycle


#   DAILY_CONTAINER_UPDATES
#       Enable or disable normal mode. docker_daily_restart.sh runs regardless.
#       (default: true)
#
#   MONTHLY_REMAINING_UPDATES
#       Enable or disable remainder mode. (default: true)
#
#   PROFILE_CRITICAL_CONTAINER_NAMES[emby|critical-data]
#       Container names for emby and critical-data profiles — excluded from
#       remainder mode (already updated by the weekly sync window)
#
# host*.conf
#
#   HOST*_DAILY_RESTART_CONTAINERS
#       Containers updated in normal mode. Aliased by detect_hosts() →
#       DAILY_RESTART_CONTAINERS

#   DAILY_CONTAINER_UPDATES
#       Enable or disable normal mode. docker_daily_restart.sh runs regardless.
#       (default: true)
#
#   WEEKLY_CONTAINER_UPDATES
#       Enable or disable weekly mode. docker_weekly_restart.sh runs regardless.
#       (default: true)
#
#   MONTHLY_REMAINING_UPDATES
#       Enable or disable remainder mode. (default: true)
#
#   PROFILE_CRITICAL_CONTAINER_NAMES[emby|critical-data]
#       Container names for emby and critical-data profiles — excluded from
#       remainder mode (already updated by the weekly sync window)
#
# host*.conf
#
#   HOST*_DAILY_RESTART_CONTAINERS
#       Containers updated in normal mode. Aliased by detect_hosts() →
#       DAILY_RESTART_CONTAINERS
#
#   HOST*_WEEKLY_RESTART_CONTAINERS
#       Containers updated in weekly mode. Aliased by detect_hosts() →
#       WEEKLY_RESTART_CONTAINERS


#   DAILY_CONTAINER_UPDATES
#       Enable or disable normal mode. docker_daily_restart.sh runs regardless.
#       (default: true)
#
#   MONTHLY_REMAINING_UPDATES
#       Enable or disable remainder mode. (default: true)
#
#   PROFILE_CRITICAL_CONTAINER_NAMES[emby|critical-data]
#       Container names for emby and critical-data profiles — excluded from
#       remainder mode (already updated by the weekly sync window)
#
# host*.conf
#
#   HOST*_DAILY_RESTART_CONTAINERS
#       Containers updated in normal mode. Aliased by detect_hosts() →
#       DAILY_RESTART_CONTAINERS

#   DAILY_CONTAINER_UPDATES
#       Enable or disable normal mode. docker_daily_restart.sh runs regardless.
#       (default: true)
#
#   WEEKLY_CONTAINER_UPDATES
#       Enable or disable weekly mode. docker_weekly_restart.sh runs regardless.
#       (default: true)
#
#   MONTHLY_REMAINING_UPDATES
#       Enable or disable remainder mode. (default: true)
#
#   PROFILE_CRITICAL_CONTAINER_NAMES[emby|critical-data]
#       Container names for emby and critical-data profiles — excluded from
#       remainder mode (already updated by the weekly sync window)
#
# host*.conf
#
#   HOST*_DAILY_RESTART_CONTAINERS
#       Containers updated in normal mode. Aliased by detect_hosts() →
#       DAILY_RESTART_CONTAINERS
#
#   HOST*_WEEKLY_RESTART_CONTAINERS
#       Containers updated in weekly mode. Aliased by detect_hosts() →
#       WEEKLY_RESTART_CONTAINERS

+#       Enable or disable weekly mode. docker_weekly_restart.sh runs regardless.

+#       Containers updated in weekly mode. Aliased by detect_hosts() →


# docker_update.sh
#     Normal mode — pull latest images for DAILY_RESTART_CONTAINERS
#     Called by daily_sync_maintenance.sh before docker_daily_restart.sh
#
# docker_update.sh --remainder
#     Remainder mode — pull all running containers not in managed lists,
#     restart those that received updates, prune dangling images
#     Called by monthly_maintenance.sh

# docker_update.sh
#     Normal mode — pull latest images for DAILY_RESTART_CONTAINERS
#     Called by daily_sync_maintenance.sh before docker_daily_restart.sh
#
# docker_update.sh --weekly
#     Weekly mode — pull latest images for WEEKLY_RESTART_CONTAINERS
#     Called by weekly_sync_maintenance.sh (WEEKLY_MAINTENANCE_SCRIPTS) before docker_weekly_restart.sh
#
# docker_update.sh --remainder
#     Remainder mode — pull all running containers not in managed lists,
#     restart those that received updates, prune dangling images
#     Called by monthly_maintenance.sh


# docker_update.sh
#     Normal mode — pull latest images for DAILY_RESTART_CONTAINERS
#     Called by daily_sync_maintenance.sh before docker_daily_restart.sh
#
# docker_update.sh --remainder
#     Remainder mode — pull all running containers not in managed lists,
#     restart those that received updates, prune dangling images
#     Called by monthly_maintenance.sh

# docker_update.sh
#     Normal mode — pull latest images for DAILY_RESTART_CONTAINERS
#     Called by daily_sync_maintenance.sh before docker_daily_restart.sh
#
# docker_update.sh --weekly
#     Weekly mode — pull latest images for WEEKLY_RESTART_CONTAINERS
#     Called by weekly_sync_maintenance.sh (WEEKLY_MAINTENANCE_SCRIPTS) before docker_weekly_restart.sh
#
# docker_update.sh --remainder
#     Remainder mode — pull all running containers not in managed lists,
#     restart those that received updates, prune dangling images
#     Called by monthly_maintenance.sh

+#     Weekly mode — pull latest images for WEEKLY_RESTART_CONTAINERS

+#     Called by weekly_sync_maintenance.sh (WEEKLY_MAINTENANCE_SCRIPTS) before docker_weekly_restart.sh


Now update the arg pre-parse and container discovery:


# Pre-parse --remainder before parse_args (unknown args pass through to PARSED_ARGS)
REMAINDER_MODE=false
_filtered_args=()
for _arg in "$@"; do
    if [[ "$_arg" == "--remainder" ]]; then
        REMAINDER_MODE=true
    else
        _filtered_args+=("$_arg")
    fi
done
unset _arg

# Pre-parse mode flags before parse_args (unknown args pass through to PARSED_ARGS)
REMAINDER_MODE=false
WEEKLY_MODE=false
_filtered_args=()
for _arg in "$@"; do
    case "$_arg" in
        --remainder) REMAINDER_MODE=true ;;
        --weekly)    WEEKLY_MODE=true ;;
        *)           _filtered_args+=("$_arg") ;;
    esac
done
unset _arg


# Pre-parse --remainder before parse_args (unknown args pass through to PARSED_ARGS)
REMAINDER_MODE=false
_filtered_args=()
for _arg in "$@"; do
    if [[ "$_arg" == "--remainder" ]]; then
        REMAINDER_MODE=true
    else
        _filtered_args+=("$_arg")
    fi
done
unset _arg

# Pre-parse mode flags before parse_args (unknown args pass through to PARSED_ARGS)
REMAINDER_MODE=false
WEEKLY_MODE=false
_filtered_args=()
for _arg in "$@"; do
    case "$_arg" in
        --remainder) REMAINDER_MODE=true ;;
        --weekly)    WEEKLY_MODE=true ;;
        *)           _filtered_args+=("$_arg") ;;
    esac
done
unset _arg

-# Pre-parse --remainder before parse_args (unknown args pass through to PARSED_ARGS)

+# Pre-parse mode flags before parse_args (unknown args pass through to PARSED_ARGS)

-    if [[ "$_arg" == "--remainder" ]]; then

+        --remainder) REMAINDER_MODE=true ;;

+        --weekly)    WEEKLY_MODE=true ;;

+        *)           _filtered_args+=("$_arg") ;;


Now update the container discovery block:


if [[ "$REMAINDER_MODE" == true ]]; then
    if [[ "${MONTHLY_REMAINING_UPDATES:-true}" != "true" ]]; then
        echo "MONTHLY_REMAINING_UPDATES=false — skipping remainder container updates"
        exit 0
    fi

    declare -A _exclude=()

    # Daily containers — updated by docker_update.sh normal mode
    for _c in "${DAILY_RESTART_CONTAINERS[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done

    # Weekly sync-window containers (emby + critical-data) — updated inline by weekly_sync_maintenance.sh
    _weekly_str="${PROFILE_CRITICAL_CONTAINER_NAMES[emby]:-} ${PROFILE_CRITICAL_CONTAINER_NAMES[critical-data]:-}"
    read -r -a _weekly_arr <<< "$_weekly_str"
    for _c in "${_weekly_arr[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done
    unset _weekly_str _weekly_arr

    # Fallback coverage containers — owned by the remote server's update cycle.
    # This server runs them during fallback but should never update them independently.
    # Updating them here risks version divergence: if remote's writeback after handback
    # encounters data written by a newer version, it may not handle it correctly.
    for _tier in 1 2 3 4; do
        _tier_var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER${_tier}"
        eval "_tier_arr=(\"\${${_tier_var}[@]:-}\")" 2>/dev/null
        for _c in "${_tier_arr[@]}"; do
            [[ -n "$_c" ]] && _exclude["$_c"]=1
        done
    done
    unset _tier _tier_var _tier_arr _c

    mapfile -t _all_running < <(docker ps --format '{{.Names}}' | sort)
    TARGET_CONTAINERS=()
    for _c in "${_all_running[@]}"; do
        [[ -z "${_exclude[$_c]+x}" ]] && TARGET_CONTAINERS+=("$_c")
    done
    unset _all_running _exclude _c
else
    if [[ "${DAILY_CONTAINER_UPDATES:-true}" != "true" ]]; then
        echo "DAILY_CONTAINER_UPDATES=false — skipping container updates"
        exit 0
    fi

    if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
        warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
        warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
        exit 0
    fi

    TARGET_CONTAINERS=("${DAILY_RESTART_CONTAINERS[@]}")
fi

if [[ "$REMAINDER_MODE" == true ]]; then
    if [[ "${MONTHLY_REMAINING_UPDATES:-true}" != "true" ]]; then
        echo "MONTHLY_REMAINING_UPDATES=false — skipping remainder container updates"
        exit 0
    fi

    declare -A _exclude=()

    # Daily containers — updated by docker_update.sh normal mode
    for _c in "${DAILY_RESTART_CONTAINERS[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done

    # Weekly containers — updated by docker_update.sh --weekly
    for _c in "${WEEKLY_RESTART_CONTAINERS[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done

    # Weekly sync-window containers (emby + critical-data) — updated inline by weekly_sync_maintenance.sh
    _weekly_str="${PROFILE_CRITICAL_CONTAINER_NAMES[emby]:-} ${PROFILE_CRITICAL_CONTAINER_NAMES[critical-data]:-}"
    read -r -a _weekly_arr <<< "$_weekly_str"
    for _c in "${_weekly_arr[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done
    unset _weekly_str _weekly_arr

    # Fallback coverage containers — owned by the remote server's update cycle.
    # This server runs them during fallback but should never update them independently.
    # Updating them here risks version divergence: if remote's writeback after handback
    # encounters data written by a newer version, it may not handle it correctly.
    for _tier in 1 2 3 4; do
        _tier_var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER${_tier}"
        eval "_tier_arr=(\"\${${_tier_var}[@]:-}\")" 2>/dev/null
        for _c in "${_tier_arr[@]}"; do
            [[ -n "$_c" ]] && _exclude["$_c"]=1
        done
    done
    unset _tier _tier_var _tier_arr _c

    mapfile -t _all_running < <(docker ps --format '{{.Names}}' | sort)
    TARGET_CONTAINERS=()
    for _c in "${_all_running[@]}"; do
        [[ -z "${_exclude[$_c]+x}" ]] && TARGET_CONTAINERS+=("$_c")
    done
    unset _all_running _exclude _c
elif [[ "$WEEKLY_MODE" == true ]]; then
    if [[ "${WEEKLY_CONTAINER_UPDATES:-true}" != "true" ]]; then
        echo "WEEKLY_CONTAINER_UPDATES=false — skipping weekly container updates"
        exit 0
    fi

    if [[ ${#WEEKLY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
        warn "WEEKLY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
        warn "Check HOST${MY_ID#HOST}_WEEKLY_RESTART_CONTAINERS in host*.conf"
        exit 0
    fi

    TARGET_CONTAINERS=("${WEEKLY_RESTART_CONTAINERS[@]}")
else
    if [[ "${DAILY_CONTAINER_UPDATES:-true}" != "true" ]]; then
        echo "DAILY_CONTAINER_UPDATES=false — skipping container updates"
        exit 0
    fi

    if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
        warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
        warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
        exit 0
    fi

    TARGET_CONTAINERS=("${DAILY_RESTART_CONTAINERS[@]}")
fi


if [[ "$REMAINDER_MODE" == true ]]; then
    if [[ "${MONTHLY_REMAINING_UPDATES:-true}" != "true" ]]; then
        echo "MONTHLY_REMAINING_UPDATES=false — skipping remainder container updates"
        exit 0
    fi

    declare -A _exclude=()

    # Daily containers — updated by docker_update.sh normal mode
    for _c in "${DAILY_RESTART_CONTAINERS[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done

    # Weekly sync-window containers (emby + critical-data) — updated inline by weekly_sync_maintenance.sh
    _weekly_str="${PROFILE_CRITICAL_CONTAINER_NAMES[emby]:-} ${PROFILE_CRITICAL_CONTAINER_NAMES[critical-data]:-}"
    read -r -a _weekly_arr <<< "$_weekly_str"
    for _c in "${_weekly_arr[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done
    unset _weekly_str _weekly_arr

    # Fallback coverage containers — owned by the remote server's update cycle.
    # This server runs them during fallback but should never update them independently.
    # Updating them here risks version divergence: if remote's writeback after handback
    # encounters data written by a newer version, it may not handle it correctly.
    for _tier in 1 2 3 4; do
        _tier_var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER${_tier}"
        eval "_tier_arr=(\"\${${_tier_var}[@]:-}\")" 2>/dev/null
        for _c in "${_tier_arr[@]}"; do
            [[ -n "$_c" ]] && _exclude["$_c"]=1
        done
    done
    unset _tier _tier_var _tier_arr _c

    mapfile -t _all_running < <(docker ps --format '{{.Names}}' | sort)
    TARGET_CONTAINERS=()
    for _c in "${_all_running[@]}"; do
        [[ -z "${_exclude[$_c]+x}" ]] && TARGET_CONTAINERS+=("$_c")
    done
    unset _all_running _exclude _c
else
    if [[ "${DAILY_CONTAINER_UPDATES:-true}" != "true" ]]; then
        echo "DAILY_CONTAINER_UPDATES=false — skipping container updates"
        exit 0
    fi

    if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
        warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
        warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
        exit 0
    fi

    TARGET_CONTAINERS=("${DAILY_RESTART_CONTAINERS[@]}")
fi

if [[ "$REMAINDER_MODE" == true ]]; then
    if [[ "${MONTHLY_REMAINING_UPDATES:-true}" != "true" ]]; then
        echo "MONTHLY_REMAINING_UPDATES=false — skipping remainder container updates"
        exit 0
    fi

    declare -A _exclude=()

    # Daily containers — updated by docker_update.sh normal mode
    for _c in "${DAILY_RESTART_CONTAINERS[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done

    # Weekly containers — updated by docker_update.sh --weekly
    for _c in "${WEEKLY_RESTART_CONTAINERS[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done

    # Weekly sync-window containers (emby + critical-data) — updated inline by weekly_sync_maintenance.sh
    _weekly_str="${PROFILE_CRITICAL_CONTAINER_NAMES[emby]:-} ${PROFILE_CRITICAL_CONTAINER_NAMES[critical-data]:-}"
    read -r -a _weekly_arr <<< "$_weekly_str"
    for _c in "${_weekly_arr[@]}"; do
        [[ -n "$_c" ]] && _exclude["$_c"]=1
    done
    unset _weekly_str _weekly_arr

    # Fallback coverage containers — owned by the remote server's update cycle.
    # This server runs them during fallback but should never update them independently.
    # Updating them here risks version divergence: if remote's writeback after handback
    # encounters data written by a newer version, it may not handle it correctly.
    for _tier in 1 2 3 4; do
        _tier_var="FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER${_tier}"
        eval "_tier_arr=(\"\${${_tier_var}[@]:-}\")" 2>/dev/null
        for _c in "${_tier_arr[@]}"; do
            [[ -n "$_c" ]] && _exclude["$_c"]=1
        done
    done
    unset _tier _tier_var _tier_arr _c

    mapfile -t _all_running < <(docker ps --format '{{.Names}}' | sort)
    TARGET_CONTAINERS=()
    for _c in "${_all_running[@]}"; do
        [[ -z "${_exclude[$_c]+x}" ]] && TARGET_CONTAINERS+=("$_c")
    done
    unset _all_running _exclude _c
elif [[ "$WEEKLY_MODE" == true ]]; then
    if [[ "${WEEKLY_CONTAINER_UPDATES:-true}" != "true" ]]; then
        echo "WEEKLY_CONTAINER_UPDATES=false — skipping weekly container updates"
        exit 0
    fi

    if [[ ${#WEEKLY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
        warn "WEEKLY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
        warn "Check HOST${MY_ID#HOST}_WEEKLY_RESTART_CONTAINERS in host*.conf"
        exit 0
    fi

    TARGET_CONTAINERS=("${WEEKLY_RESTART_CONTAINERS[@]}")
else
    if [[ "${DAILY_CONTAINER_UPDATES:-true}" != "true" ]]; then
        echo "DAILY_CONTAINER_UPDATES=false — skipping container updates"
        exit 0
    fi

    if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
        warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
        warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
        exit 0
    fi

    TARGET_CONTAINERS=("${DAILY_RESTART_CONTAINERS[@]}")
fi

+    # Weekly containers — updated by docker_update.sh --weekly

+    for _c in "${WEEKLY_RESTART_CONTAINERS[@]}"; do

+elif [[ "$WEEKLY_MODE" == true ]]; then

+    if [[ "${WEEKLY_CONTAINER_UPDATES:-true}" != "true" ]]; then

+        echo "WEEKLY_CONTAINER_UPDATES=false — skipping weekly container updates"

+    if [[ ${#WEEKLY_RESTART_CONTAINERS[@]} -eq 0 ]]; then

+        warn "WEEKLY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"

+        warn "Check HOST${MY_ID#HOST}_WEEKLY_RESTART_CONTAINERS in host*.conf"

+    TARGET_CONTAINERS=("${WEEKLY_RESTART_CONTAINERS[@]}")


Now update the status, pull header, and summary sections:


    echo "$ICON_GEAR       Mode:       $([[ "$REMAINDER_MODE" == true ]] && echo "remainder" || echo "normal (daily)")"
    if [[ "$REMAINDER_MODE" == true ]]; then
        echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly sync, and fallback)"
        for _c in "${TARGET_CONTAINERS[@]}"; do echo "  $_c"; done
    else
        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
        echo "$ICON_GEAR       Enabled:    ${DAILY_CONTAINER_UPDATES:-true}"
    fi

    if [[ "$REMAINDER_MODE" == true ]]; then
        echo "$ICON_GEAR       Mode:       remainder (monthly)"
        echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly, sync-window, fallback)"
        for _c in "${TARGET_CONTAINERS[@]}"; do echo "  $_c"; done
    elif [[ "$WEEKLY_MODE" == true ]]; then
        echo "$ICON_GEAR       Mode:       weekly"
        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
        echo "$ICON_GEAR       Enabled:    ${WEEKLY_CONTAINER_UPDATES:-true}"
    else
        echo "$ICON_GEAR       Mode:       normal (daily)"
        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
        echo "$ICON_GEAR       Enabled:    ${DAILY_CONTAINER_UPDATES:-true}"
    fi


    echo "$ICON_GEAR       Mode:       $([[ "$REMAINDER_MODE" == true ]] && echo "remainder" || echo "normal (daily)")"
    if [[ "$REMAINDER_MODE" == true ]]; then
        echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly sync, and fallback)"
        for _c in "${TARGET_CONTAINERS[@]}"; do echo "  $_c"; done
    else
        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
        echo "$ICON_GEAR       Enabled:    ${DAILY_CONTAINER_UPDATES:-true}"
    fi

    if [[ "$REMAINDER_MODE" == true ]]; then
        echo "$ICON_GEAR       Mode:       remainder (monthly)"
        echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly, sync-window, fallback)"
        for _c in "${TARGET_CONTAINERS[@]}"; do echo "  $_c"; done
    elif [[ "$WEEKLY_MODE" == true ]]; then
        echo "$ICON_GEAR       Mode:       weekly"
        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
        echo "$ICON_GEAR       Enabled:    ${WEEKLY_CONTAINER_UPDATES:-true}"
    else
        echo "$ICON_GEAR       Mode:       normal (daily)"
        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
        echo "$ICON_GEAR       Enabled:    ${DAILY_CONTAINER_UPDATES:-true}"
    fi

echo "$ICON_HOST       Identity:   $MY_ID ($LOCAL_SERVER_NAME)"

-    echo "$ICON_GEAR       Mode:       $([[ "$REMAINDER_MODE" == true ]] && echo "remainder" || echo "normal (daily)")"

-        echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly sync, and fallback)"

+        echo "$ICON_GEAR       Mode:       remainder (monthly)"

+        echo "$ICON_CONTAINERS Containers: ${#TARGET_CONTAINERS[@]} running (excluding daily, weekly, sync-window, fallback)"

+    elif [[ "$WEEKLY_MODE" == true ]]; then

+        echo "$ICON_GEAR       Mode:       weekly"

+        echo "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"

+        echo "$ICON_GEAR       Enabled:    ${WEEKLY_CONTAINER_UPDATES:-true}"

+        echo "$ICON_GEAR       Mode:       normal (daily)"


if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━ $ICON_CONTAINERS Docker Update (remainder) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily or weekly sync)"
    log "$ICON_CONTAINERS Remainder targets: ${TARGET_CONTAINERS[*]}"
else
    echo "━━━ $ICON_CONTAINERS Docker Update — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
fi

if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━ $ICON_CONTAINERS Docker Update (remainder) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily, weekly, or sync window)"
    log "$ICON_CONTAINERS Remainder targets: ${TARGET_CONTAINERS[*]}"
elif [[ "$WEEKLY_MODE" == true ]]; then
    echo "━━━ $ICON_CONTAINERS Docker Update (weekly) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
else
    echo "━━━ $ICON_CONTAINERS Docker Update — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
fi


if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━ $ICON_CONTAINERS Docker Update (remainder) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily or weekly sync)"
    log "$ICON_CONTAINERS Remainder targets: ${TARGET_CONTAINERS[*]}"
else
    echo "━━━ $ICON_CONTAINERS Docker Update — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
fi

if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━ $ICON_CONTAINERS Docker Update (remainder) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily, weekly, or sync window)"
    log "$ICON_CONTAINERS Remainder targets: ${TARGET_CONTAINERS[*]}"
elif [[ "$WEEKLY_MODE" == true ]]; then
    echo "━━━ $ICON_CONTAINERS Docker Update (weekly) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
else
    echo "━━━ $ICON_CONTAINERS Docker Update — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"
fi

-    echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily or weekly sync)"

+    echo "$ICON_CONTAINERS Updating ${#TARGET_CONTAINERS[@]} container(s) (not in daily, weekly, or sync window)"

+elif [[ "$WEEKLY_MODE" == true ]]; then

+    echo "━━━ $ICON_CONTAINERS Docker Update (weekly) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

+    log "$ICON_CONTAINERS Containers: ${TARGET_CONTAINERS[*]}"


if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINDER) SUMMARY ━━━━━"
else
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE SUMMARY ━━━━━"
fi

if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINDER) SUMMARY ━━━━━"
elif [[ "$WEEKLY_MODE" == true ]]; then
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (WEEKLY) SUMMARY ━━━━━"
else
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE SUMMARY ━━━━━"
fi


if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINDER) SUMMARY ━━━━━"
else
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE SUMMARY ━━━━━"
fi

if [[ "$REMAINDER_MODE" == true ]]; then
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (REMAINDER) SUMMARY ━━━━━"
elif [[ "$WEEKLY_MODE" == true ]]; then
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (WEEKLY) SUMMARY ━━━━━"
else
    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE SUMMARY ━━━━━"
fi

+    echo "━━━━━ $ICON_SUMMARY DOCKER UPDATE (WEEKLY) SUMMARY ━━━━━"


Now update `master.conf` — add toggle and wire `--weekly` into `WEEKLY_MAINTENANCE_SCRIPTS`:


    WEEKLY_MAINTENANCE_SCRIPTS=(
        "Docker_Essentials/docker_weekly_restart.sh"    # weekly container restarts after sync

    WEEKLY_MAINTENANCE_SCRIPTS=(
        "Docker_Essentials/docker_update.sh --weekly"   # pull latest images for WEEKLY_RESTART_CONTAINERS before restart
        "Docker_Essentials/docker_weekly_restart.sh"    # weekly container restarts after sync


The file /boot/config/plugins/varaverk/Configurations/master.conf has been updated successfully. (file state is current in your context — no need to Read it back)

    WEEKLY_MAINTENANCE_SCRIPTS=(
        "Docker_Essentials/docker_weekly_restart.sh"    # weekly container restarts after sync

    WEEKLY_MAINTENANCE_SCRIPTS=(
        "Docker_Essentials/docker_update.sh --weekly"   # pull latest images for WEEKLY_RESTART_CONTAINERS before restart
        "Docker_Essentials/docker_weekly_restart.sh"    # weekly container restarts after sync

# syncs WEEKLY_SYNC_SHARES → restarts → then iterates WEEKLY_MAINTENANCE_SCRIPTS.

+        "Docker_Essentials/docker_update.sh --weekly"   # pull latest images for WEEKLY_RESTART_CONTAINERS before restart

"System_Essentials/clear_logs.sh"               # purge aged logs — Sunday only, low priority

"Media/playback_aware_lidarr_discovery.sh"      # behavior-driven music discovery using weekly Emby playback history


# Pull updates for all running containers NOT in daily/weekly managed lists.
# Ensures every deployed container receives at least one image pull per month.
# Set false to skip.
    MONTHLY_REMAINING_UPDATES=true

# Pull updates for WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh runs.
# Set false to skip — docker_weekly_restart.sh still runs regardless.
    WEEKLY_CONTAINER_UPDATES=true

# Pull updates for all running containers NOT in daily/weekly managed lists.
# Ensures every deployed container receives at least one image pull per month.
# Set false to skip.
    MONTHLY_REMAINING_UPDATES=true


# Pull updates for WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh runs.
# Set false to skip — docker_weekly_restart.sh still runs regardless.
    WEEKLY_CONTAINER_UPDATES=true

# Pull updates for all running containers NOT in daily/weekly managed lists.
# Ensures every deployed container receives at least one image pull per month.
# Set false to skip.
    MONTHLY_REMAINING_UPDATES=true

"Media/playback_aware_sonarr_discovery.sh"      # behavior-driven TV discovery using TMDB recommendations

+# Pull updates for WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh runs.

+# Set false to skip — docker_weekly_restart.sh still runs regardless.


Now update `user_script_plug-in.sh`:


326	#   5. Pull images remotely     if WEEKLY_SYNC_UPDATES_REMOTE=true (SSH to remote)
327	#   6. rsync WEEKLY_SYNC_SHARES
328	#        Emby        full clean mirror — databases checkpointed on shutdown, all config included
329	#        Critical-Data full clean auth stack mirror
330	#   7. Start remote containers  dependency order, new image, verify each container up
331	#   8. Start local containers   dependency order, new image, verify each container up
332	#   9. docker_weekly_restart.sh restart less-critical services: NextCloud, AdGuard, Immich
333	#  10. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.
334	#                                playback_aware_lidarr_discovery.sh: 0–5 music adds/week
335	#                                  based on your Emby play history + Last.fm similar artists.
336	#                                playback_aware_radarr_discovery.sh: 0–5 movie adds/week
337	#                                  based on recently watched + TMDB recommendations.
338	#                                playback_aware_sonarr_discovery.sh: 0–3 TV adds/week
339	#                                  based on recently watched + TMDB recommendations.
340	#
341	# Why weekly not nightly for Emby: HOST2 builds warm thumbnail cache throughout the week.
342	# Nightly sync resets that cache — cold loads every morning. Weekly: cache warm 6 days,
343	# resets Sunday night while users sleep.
344	#
345	# bash /boot/config/plugins/varaverk/Orchestrators/weekly_sync_maintenance.sh

#   5. Pull images remotely     if WEEKLY_SYNC_UPDATES_REMOTE=true (SSH to remote)
#   6. rsync WEEKLY_SYNC_SHARES
#        Emby        full clean mirror — databases checkpointed on shutdown, all config included
#        Critical-Data full clean auth stack mirror
#   7. Start remote containers  dependency order, new image, verify each container up
#   8. Start local containers   dependency order, new image, verify each container up
#   9. docker_weekly_restart.sh restart less-critical services: NextCloud, AdGuard, Immich
#  10. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.
#                                playback_aware_lidarr_discovery.sh: 0–5 music adds/week
#                                  based on your Emby play history + Last.fm similar artists.
#                                playback_aware_radarr_discovery.sh: 0–5 movie adds/week
#                                  based on recently watched + TMDB recommendations.
#                                playback_aware_sonarr_discovery.sh: 0–3 TV adds/week
#                                  based on recently watched + TMDB recommendations.
#
# Why weekly not nightly for Emby: HOST2 builds warm thumbnail cache throughout the week.
# Nightly sync resets that cache — cold loads every morning. Weekly: cache warm 6 days,
# resets Sunday night while users sleep.
#
# bash /boot/config/plugins/varaverk/Orchestrators/weekly_sync_maintenance.sh


#   9. docker_weekly_restart.sh restart less-critical services: NextCloud, AdGuard, Immich
#  10. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.

#   9. docker_update.sh --weekly  pull latest images for WEEKLY_RESTART_CONTAINERS
#  10. docker_weekly_restart.sh  restart less-critical services: NextCloud, AdGuard, Immich
#  11. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.


The file /boot/config/plugins/varaverk/Plugin/unraid/user_script_plug-in.sh has been updated successfully. (file state is current in your context — no need to Read it back)

#   9. docker_weekly_restart.sh restart less-critical services: NextCloud, AdGuard, Immich
#  10. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.

#   9. docker_update.sh --weekly  pull latest images for WEEKLY_RESTART_CONTAINERS
#  10. docker_weekly_restart.sh  restart less-critical services: NextCloud, AdGuard, Immich
#  11. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.

#        Critical-Data full clean auth stack mirror

#   7. Start remote containers  dependency order, new image, verify each container up

#   8. Start local containers   dependency order, new image, verify each container up

-#   9. docker_weekly_restart.sh restart less-critical services: NextCloud, AdGuard, Immich

-#  10. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.

+#   9. docker_update.sh --weekly  pull latest images for WEEKLY_RESTART_CONTAINERS

+#  10. docker_weekly_restart.sh  restart less-critical services: NextCloud, AdGuard, Immich

+#  11. WEEKLY_MAINTENANCE_SCRIPTS — clear_logs.sh + playback_aware_*.sh discovery scripts.

#                                playback_aware_lidarr_discovery.sh: 0–5 music adds/week

#                                  based on your Emby play history + Last.fm similar artists.

#                                playback_aware_radarr_discovery.sh: 0–5 movie adds/week


Now update the docker_update description in user_script_plug-in.sh:


  Remainder (monthly) — targets all running containers not in managed lists.
#     Excludes daily list, emby+critical-data sync-window containers,
#     and fallback containers owned by the remote server's update cycle.
#     Rebuilds containers that received a new image. Toggle: MONTHLY_REMAINING_UPDATES in master.conf.

  Weekly (--weekly) — targets WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh.
#     Rebuilds containers that received a new image. Toggle: WEEKLY_CONTAINER_UPDATES in master.conf.
#
#   Remainder (--remainder, monthly) — targets all running containers not in managed lists.
#     Excludes daily, weekly, emby+critical-data sync-window, and fallback containers.
#     Rebuilds containers that received a new image. Toggle: MONTHLY_REMAINING_UPDATES in master.conf.


  Remainder (monthly) — targets all running containers not in managed lists.
#     Excludes daily list, emby+critical-data sync-window containers,
#     and fallback containers owned by the remote server's update cycle.
#     Rebuilds containers that received a new image. Toggle: MONTHLY_REMAINING_UPDATES in master.conf.

  Weekly (--weekly) — targets WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh.
#     Rebuilds containers that received a new image. Toggle: WEEKLY_CONTAINER_UPDATES in master.conf.
#
#   Remainder (--remainder, monthly) — targets all running containers not in managed lists.
#     Excludes daily, weekly, emby+critical-data sync-window, and fallback containers.
#     Rebuilds containers that received a new image. Toggle: MONTHLY_REMAINING_UPDATES in master.conf.

#     Containers stay running during the pull. Rebuilds containers that received a new image.

#     docker_daily_restart.sh restarts the rest. Toggle: DAILY_CONTAINER_UPDATES in master.conf.

-#   Remainder (monthly) — targets all running containers not in managed lists.

-#     Excludes daily list, emby+critical-data sync-window containers,

-#     and fallback containers owned by the remote server's update cycle.

+#   Weekly (--weekly) — targets WEEKLY_RESTART_CONTAINERS before docker_weekly_restart.sh.

+#     Rebuilds containers that received a new image. Toggle: WEEKLY_CONTAINER_UPDATES in master.conf.

+#   Remainder (--remainder, monthly) — targets all running containers not in managed lists.

+#     Excludes daily, weekly, emby+critical-data sync-window, and fallback containers.

# bash /boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh --dry-run


Verify no stray references then commit:


Bash

grep -rn "WEEKLY_RESTART_CONTAINERS" /boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh

Confirm WEEKLY_RESTART_CONTAINERS is in both weekly and remainder exclusion


/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:15:# before docker_weekly_restart.sh. Pulls WEEKLY_RESTART_CONTAINERS so the
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:31:#   Targets WEEKLY_RESTART_CONTAINERS — same list used by docker_weekly_restart.sh.
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:38:#     WEEKLY_RESTART_CONTAINERS      — already updated weekly
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:119:#   HOST*_WEEKLY_RESTART_CONTAINERS
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:121:#       WEEKLY_RESTART_CONTAINERS
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:132:#     Weekly mode — pull latest images for WEEKLY_RESTART_CONTAINERS
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:205:    for _c in "${WEEKLY_RESTART_CONTAINERS[@]}"; do
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:242:    if [[ ${#WEEKLY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:243:        warn "WEEKLY_RESTART_CONTAINERS is empty for $MY_ID — nothing to update"
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:244:        warn "Check HOST${MY_ID#HOST}_WEEKLY_RESTART_CONTAINERS in host*.conf"
/boot/config/plugins/varaverk/Docker_Essentials/docker_update.sh:248:    TARGET_CONTAINERS=("${WEEKLY_RESTART_CONTAINERS[@]}")


cd /boot/config/plugins/varaverk && git add -f Configurations/master.conf && git add Docker_Essentials/docker_update.sh Plugin/unraid/user_script_plug-in.sh && git commit -m "$(cat <<'EOF'
Add --weekly mode to docker_update.sh for WEEKLY_RESTART_CONTAINERS

Weekly now has the same update→restart pattern as daily. Remainder
excludes weekly containers since they're now owned by this new mode.
EOF
)" && git push

Commit and push --weekly mode addition


[main a2bcd58] Add --weekly mode to docker_update.sh for WEEKLY_RESTART_CONTAINERS
 3 files changed, 81 insertions(+), 26 deletions(-)
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://192.168.50.2:221/FailedProxy/Varaverk.git
   55f8842..a2bcd58  main -> main
Shell cwd was reset to /root


Done. The full update ownership chain is now clean:

| Cadence | Update script | Restart script | Containers |
|---------|--------------|----------------|------------|
| Daily | `docker_update.sh` | `docker_daily_restart.sh` | `DAILY_RESTART_CONTAINERS` |
| Weekly (inline) | `weekly_sync_maintenance.sh` | same script | emby + critical-data |
| Weekly (scripts) | `docker_update.sh --weekly` | `docker_weekly_restart.sh` | `WEEKLY_RESTART_CONTAINERS` |
| Monthly | `docker_update.sh --remainder` | inline rebuild | everything else |

Every running container has exactly one owner. Toggle is `WEEKLY_CONTAINER_UPDATES` in `master.conf`.


ok somthing is going on with thranscoding, ━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      12s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 2 streams
  🎬 Sunny        — Food Network                   — Live TV — Direct Play
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play

  🎬 Total: 2  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   2 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2
⏱  Duration:  1s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

── 2026-06-14 10:42:01 ────────────────────────────────────────────────
━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      12s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 2 streams
  🎬 Sunny        — Food Network                   — Live TV — Direct Play
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play

  🎬 Total: 2  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   2 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2
⏱  Duration:  2s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

── 2026-06-14 10:49:01 ────────────────────────────────────────────────
━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      12s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 2 streams
  🎬 Sunny        — Food Network                   — Live TV — Direct Play
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play

  🎬 Total: 2  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   2 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2
⏱  Duration:  0s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

── 2026-06-14 10:56:01 ────────────────────────────────────────────────
━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      12s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 2 streams
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play
  🎬 Sunny        — Food Network                   — Live TV — Direct Play

  🎬 Total: 2  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   2 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 2
⏱  Duration:  4s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

── 2026-06-14 11:00:01 ────────────────────────────────────────────────
━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      12s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 3 streams
  🎬 Gmer4Lfe     — Yorktown                       — Music — Direct Play
  🎬 Sunny        — Food Network                   — Live TV — Direct Play
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play

  🎬 Total: 3  |  Live TV: 2  |  Transcoding: 0  |  Direct: 3

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   3 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 3
⏱  Duration:  1s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

── 2026-06-14 11:07:01 ────────────────────────────────────────────────
━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      11s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 3 streams
  🎬 Gmer4Lfe     — Freak on a Leash               — Music — Direct Play
  🎬 Sunny        — Food Network                   — Live TV — Direct Play
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play

  🎬 Total: 3  |  Live TV: 2  |  Transcoding: 0  |  Direct: 3

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   3 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 3
⏱  Duration:  1s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

── 2026-06-14 11:14:01 ────────────────────────────────────────────────
━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━
🖥   Identity: HOST1 (unRAID-Gmer4Lfe)
💨    Ramdisk freed: 0B
💾   SSD freed:     0B
🗑  Removed:       0 files
🟢 Streaming:    0 files (open by Emby — untouched)
🛡  Protected:    0 aged files saved by open-file check
🗑  Skipped:       0 files
⏱   Duration:      13s
🏁 Status: done ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🎬 Emby — 6 streams
  🎬 Gmer4Lfe     — What’s My Age Again?         — Music — Direct Play
  🎬 Mama Bear    — Excelsis Dei                   — TV Show — Direct Stream
  🎬 jaden        — Fast Five                      — Movie — Direct Stream
  🎬 choak178     — Fast Five                      — Movie — Direct Stream
  🎬 Sunny        — ABC (WTAE)                     — Live TV — Direct Play
  🎬 Sunny        — Food Network                   — Live TV — Direct Play

  🎬 Total: 6  |  Live TV: 2  |  Transcoding: 0  |  Direct: 6

━━━━━ 📋 TRANSCODE MANAGER SUMMARY ━━━━━
🖥  Identity:  HOST1 (unRAID-Gmer4Lfe)
⚙  Mode:      smart
💨   Ramdisk:   0.00GB used / 10.00GB available
🔗  Symlink:   /mnt/ram-transcode → /mnt/ramdisk_transcodes
🔗  Flips:     0 this hour (warn at 3)
🎬  Streams:   6 total  |  Live TV: 2  |  Transcoding: 0  |  Direct: 6
⏱  Duration:  0s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CPU


ok somthing is going on with thranscoding, ━━━━━ 📋 TRANSCODE CLEANUP SUMMARY ━━━━━ 🖥   Identity: HOST1 (unRAID-Gmer4Lfe) 💨    Ramdisk freed: 0B 💾   SSD freed:     0B 🗑  Removed:       0 files …


Your message ends with "CPU" — looks like it got cut off. The logs themselves look healthy: all streams are Direct Play/Direct Stream, 0 transcoding, ramdisk at 0.00GB. What were you going to say about CPU?


no, i didnt catch direct play i seldomly have all direct play. also assumed that live tv reguardless used the ramdisk


Let me look at the transcode scripts to understand what they're checking and where they expect files.


find /boot/config/plugins/varaverk -name "*transcode*" -o -name "*ramdisk*" | sort

Find all transcode-related scripts


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Transcode Cleanup ==========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Removes stale transcode files from both ramdisk and SSD fallback locations.
9	# Called by transcode_management.sh (Orchestrators/) before transcode_manager.sh —
10	# cleanup must run first so the manager sees real active-session usage, not
11	# inflated usage from stale files. Must be fast and non-blocking.
12	#
13	# A file is eligible for deletion only if ALL conditions are true:
14	#   1. Older than TRANSCODE_MAX_AGE minutes (mtime — last write time)
15	#   2. Not currently open by any process (checked via lsof pre-built map)
16	#
17	# ==============================================================================================
18	# DESIGN PRINCIPLES
19	# ==============================================================================================
20	#
21	# lsof Called Once, Not Per File
22	#   On a busy Live TV system the ramdisk contains thousands of HLS segment files.
23	#   Calling lsof once per file creates thousands of subprocess calls every 7 minutes.
24	#   lsof is called once per location to build a complete open-file map. All subsequent
25	#   checks are O(1) lookups against that map — thousands of files, one lsof call.
26	#
27	# No Session-Aware Cleanup
28	#   ffmpeg generates folder names independently of the media server API session IDs.
29	#   There is no reliable correlation between API session IDs and transcoding-temp
30	#   subfolder names. Attempting to correlate them would falsely treat active sessions
31	#   as ended. lsof is the correct check — if ffmpeg has a file open, it is active
32	#   regardless of folder naming or session state.
33	#
34	# transcoding-temp Is Never Deleted
35	#   If cleanup removes the empty transcoding-temp folder from the ramdisk, Emby
36	#   searches all accessible paths for an existing one, finds the SSD fallback version,
37	#   and routes all new sessions there until Emby restarts. The directory is excluded
38	#   from find by name — protected even when completely empty.
39	#
40	# Post-Cleanup Flip-Back
41	#   After removing stale files, checks whether ramdisk usage dropped below
42	#   RAMDISK_LOW_GB. If so — and symlink currently points at SSD — triggers a
43	#   flip back to ramdisk. This is the recovery path; the manager handles
44	#   the fill-up path.
45	#
46	# ==============================================================================================
47	# OPERATIONAL SAFEGUARDS
48	# ==============================================================================================
49	#
50	# Wait Lock
51	#   acquire_lock "wait" — waits if a previous cleanup run is still active rather
52	#   than exiting. The caller's 7-minute interval can overlap on a slow system.
53	#
54	# lsof Timeout
55	#   lsof call capped at 15 seconds per location — prevents blocking indefinitely
56	#   on a system with many open files.
57	#
58	# transcoding-temp Guard
59	#   `! -name "transcoding-temp"` in the find command — protected unconditionally.
60	#
61	# Silent by Default
62	#   Runs every 7 minutes — must not produce noise when healthy.
63	#
64	# ==============================================================================================
65	# CONFIGURATION
66	# ==============================================================================================
67	#
68	# host*.conf
69	#
70	#   HOST*_RAMDISK_PATH / HOST*_TRANSCODE_SSD / HOST*_RAMDISK_LOW_GB
71	#       Aliased by detect_hosts() → RAMDISK_PATH / TRANSCODE_SSD / RAMDISK_LOW_GB.
72	#
73	# master.conf
74	#
75	#   TRANSCODE_MAX_AGE
76	#       Minutes before an inactive transcode file is eligible for deletion. (default: 20)
77	#
78	#   TRANSCODE_ORPHAN_AGE
79	#       Minutes for orphan folder detection. (default: 30)
80	#
81	# ==============================================================================================
82	# RUNTIME MODES
83	# ==============================================================================================
84	#
85	# transcode_cleanup.sh
86	#     Remove stale files from ramdisk and SSD. Check for flip-back opportunity.
87	#
88	# transcode_cleanup.sh --dry-run
89	#     Show which files would be deleted. No deletions, no flip.
90	#
91	# transcode_cleanup.sh --status
92	#     Show current file counts, ages, and open-file status per location.
93	#
94	# transcode_cleanup.sh --log
95	#     Verbose per-file output including age, open status, and deletion result.
96	#
97	# ==============================================================================================
98
99	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
100
101	source "$SCRIPT_DIR/../load_config.sh"
102
103	parse_args "$@"
104
105	STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}"
106
107	# ==============================================================================================
108	# ━━━ Setup ━━━
109	# ==============================================================================================
110	if [[ "$EUID" -ne 0 ]]; then
111	    error "Must be run as root"
112	    exit 1
113	fi
114
115
116	acquire_lock "wait"
117
118	# detect_hosts() sets MY_ID and aliases RAMDISK_PATH, TRANSCODE_SSD, RAMDISK_LOW_GB
119	detect_hosts
120
121	# lsof availability check
122	LSOF_AVAILABLE=false
123	if command -v lsof >/dev/null 2>&1; then
124	    LSOF_AVAILABLE=true
125	    log "lsof available — active file check enabled"
126	else
127	    warn "lsof not available — active file check skipped, all aged files eligible for deletion"
128	fi
129
130	log "Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
131	log "$ICON_GEAR Config:    max-age=${TRANSCODE_MAX_AGE}min orphan-age=${TRANSCODE_ORPHAN_AGE}min flip-back-below=${RAMDISK_LOW_GB}GB"
132	log "$ICON_RAM  Locations: ramdisk=$RAMDISK_PATH  ssd=$TRANSCODE_SSD"
133
134	# ==============================================================================================
135	# ━━━ Status ━━━
136	# ==============================================================================================
137	if [[ "$SHOW_STATUS" == true ]]; then
138	    echo ""
139	    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
140	    echo "$ICON_HOST   Identity:     $MY_ID ($LOCAL_SERVER_NAME)"
141	    echo "$ICON_RAM    Ramdisk:      $RAMDISK_PATH"
142	    echo "$ICON_DISK   SSD fallback: $TRANSCODE_SSD"
143	    echo "$ICON_TRASH  Max age:      ${TRANSCODE_MAX_AGE} minutes"
144	    echo "$ICON_TRASH  Orphan age:   ${TRANSCODE_ORPHAN_AGE} minutes"
145	    echo "$ICON_RAM    Flip at:      ${RAMDISK_LOW_GB}GB"
146	    echo "$ICON_GEAR   lsof check:   $LSOF_AVAILABLE"
147	    echo "$ICON_GEAR   Dry Run:      $DRY_RUN"
148	    echo ""
149
150	    if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
151	        USAGE=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $3}')
152	        AVAIL=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $4}')
153	        echo "  $ICON_RAM  Ramdisk: mounted — $USAGE used / $AVAIL available ✅"
154	    else
155	        echo "  $ICON_RAM  Ramdisk: not mounted"
156	    fi
157
158	    CURRENT_TARGET=$(grep "^current_target=" "$STATE_FILE" 2>/dev/null | cut -d= -f2)
159	    echo "  $ICON_LINK Current target: ${CURRENT_TARGET:-unknown}"
160	    echo "━━━━━━━━━━━━━━━━━━━━━━━"
161	    exit 0
162	fi
163
164	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"
165
166	# ==============================================================================================
167	# ── CLEANUP FUNCTION ──────────────────────────────────────────────────────────────────────────
168	# ==============================================================================================
169	# Scans a location and removes eligible files.
170	# Calls lsof ONCE per location — builds in-memory OPEN_FILES_MAP for O(1) lookup.
171	# Returns via LOCATION_REMOVED, LOCATION_FREED, LOCATION_SKIPPED, LOCATION_ACTIVE
172
173	cleanup_location() {
174	    local location="$1" label="$2" max_age="$3"
175	    local files_removed=0 bytes_freed=0 files_skipped=0 files_active=0 files_streaming=0
176
177	    if [[ ! -d "$location" ]]; then
178	        log "$label does not exist — skipping"
179	        LOCATION_REMOVED=0 LOCATION_FREED="0B" LOCATION_SKIPPED=0 LOCATION_ACTIVE=0 LOCATION_STREAMING=0
180	        return
181	    fi
182
183	    local file_count
184	    file_count=$(find "$location" -type f 2>/dev/null | wc -l)
185	    log "$label: $file_count files to scan (age threshold: ${max_age}min)"
186
187	    # Build in-memory open file map — O(1) lookup per file
188	    # One lsof call per location — never per file
189	    declare -A OPEN_FILES_MAP
190	    if [[ "$LSOF_AVAILABLE" == true ]]; then
191	        log "Building open file map for $label..."
192	        while IFS= read -r open_file; do
193	            [[ -n "$open_file" ]] && OPEN_FILES_MAP["$open_file"]=1
194	        done < <(timeout 15 lsof +D "$location" 2>/dev/null | awk 'NR>1 {print $9}' | sort -u)
195	        files_streaming=${#OPEN_FILES_MAP[@]}
196	        log "$files_streaming files currently open in $label"
197	    fi
198
199	    # Process aged files
200	    while IFS= read -r file; do
201	        [[ -z "$file" ]] && continue
202
203	        # O(1) open file check — in-memory map
204	        if [[ -n "${OPEN_FILES_MAP[$file]:-}" ]]; then
205	            (( files_active++ ))
206	            log "Skipping open file: $file"
207	            continue
208	        fi
209
210	        local file_size
211	        file_size=$(stat -c%s "$file" 2>/dev/null || echo 0)
212
213	        if [[ "$DRY_RUN" == true ]]; then
214	            log "DRY RUN — would delete: $(basename "$file")"
215	            (( files_skipped++ ))
216	        else
217	            if rm -f "$file" 2>/dev/null; then
218	                (( files_removed++ ))
219	                bytes_freed=$(( bytes_freed + file_size ))
220	                log "Deleted: $file"
221	            else
222	                warn "Could not delete: $file"
223	                (( files_skipped++ ))
224	            fi
225	        fi
226
227	    done < <(find "$location" -type f -mmin +"$max_age" 2>/dev/null)
228
229	    # Remove empty directories — but NEVER remove transcoding-temp
230	    # transcoding-temp must always exist on ramdisk so Emby finds it there first
231	    if [[ "$DRY_RUN" == false ]]; then
232	        find "$location" -mindepth 1 -type d -empty \
233	            ! -name "transcoding-temp" -delete 2>/dev/null
234	    fi
235
236	    # Format bytes freed
237	    local freed_human
238	    if (( bytes_freed > 1073741824 )); then
239	        freed_human=$(awk "BEGIN {printf \"%.1fGB\", $bytes_freed / 1073741824}")
240	    elif (( bytes_freed > 1048576 )); then
241	        freed_human=$(awk "BEGIN {printf \"%.1fMB\", $bytes_freed / 1048576}")
242	    elif (( bytes_freed > 0 )); then
243	        freed_human="${bytes_freed}B"
244	    else
245	        freed_human="0B"
246	    fi
247
248	    log "$label — removed $files_removed files ($freed_human freed) | streaming: $files_streaming | aged+open: $files_active | skipped: $files_skipped"
249
250	    LOCATION_REMOVED=$files_removed
251	    LOCATION_FREED=$freed_human
252	    LOCATION_SKIPPED=$files_skipped
253	    LOCATION_ACTIVE=$files_active
254	    LOCATION_STREAMING=$files_streaming
255	}
256
257	# ==============================================================================================
258	# ━━━ Transcode Cleanup ━━━
259	# ==============================================================================================
260	START=$(date +%s)
261	TOTAL_REMOVED=0 TOTAL_SKIPPED=0 TOTAL_ACTIVE=0 TOTAL_STREAMING=0
262	RAMDISK_FREED="0B" SSD_FREED="0B"
263
264	# Cleanup ramdisk
265	if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
266	    cleanup_location "$RAMDISK_PATH" "Ramdisk" "$TRANSCODE_MAX_AGE"
267	    TOTAL_REMOVED=$(( TOTAL_REMOVED + LOCATION_REMOVED ))
268	    TOTAL_SKIPPED=$(( TOTAL_SKIPPED + LOCATION_SKIPPED ))
269	    TOTAL_ACTIVE=$(( TOTAL_ACTIVE + LOCATION_ACTIVE ))
270	    TOTAL_STREAMING=$(( TOTAL_STREAMING + LOCATION_STREAMING ))
271	    RAMDISK_FREED=$LOCATION_FREED
272	else
273	    log "Ramdisk not mounted — skipping ramdisk cleanup"
274	fi
275
276	# Cleanup SSD fallback
277	if [[ -d "$TRANSCODE_SSD" ]]; then
278	    cleanup_location "$TRANSCODE_SSD" "SSD fallback" "$TRANSCODE_MAX_AGE"
279	    TOTAL_REMOVED=$(( TOTAL_REMOVED + LOCATION_REMOVED ))
280	    TOTAL_SKIPPED=$(( TOTAL_SKIPPED + LOCATION_SKIPPED ))
281	    TOTAL_ACTIVE=$(( TOTAL_ACTIVE + LOCATION_ACTIVE ))
282	    TOTAL_STREAMING=$(( TOTAL_STREAMING + LOCATION_STREAMING ))
283	    SSD_FREED=$LOCATION_FREED
284	else
285	    log "SSD fallback not found — skipping SSD cleanup"
286	fi
287
288	# Post-cleanup — check if ramdisk recovered enough to flip symlink back to ramdisk
289	if [[ "$DRY_RUN" == false ]] && mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
290	    RAMDISK_USED_KB=$(df "$RAMDISK_PATH" --output=used 2>/dev/null | tail -1 | tr -d ' ')
291	    RAMDISK_USED_GB=$(awk "BEGIN {printf \"%.2f\", ${RAMDISK_USED_KB:-0} / 1048576}")
292	    LOW_RECOVERED=$(awk "BEGIN {print ($RAMDISK_USED_GB < $RAMDISK_LOW_GB) ? 1 : 0}")
293	    CURRENT_TARGET=$(grep "^current_target=" "$STATE_FILE" 2>/dev/null | cut -d= -f2)
294
295	    if [[ "$LOW_RECOVERED" == "1" && "$CURRENT_TARGET" == "$TRANSCODE_SSD" ]]; then
296	        echo "Ramdisk has space after cleanup (${RAMDISK_USED_GB}GB < ${RAMDISK_LOW_GB}GB) — triggering manager to flip back"
297	        bash "$SCRIPT_DIR/transcode_manager.sh" --no-log
298	    fi
299	fi
300
301	END=$(date +%s)
302
303	# ==============================================================================================
304	# ━━━ Summary ━━━
305	# ==============================================================================================
306	echo "━━━━━ $ICON_SUMMARY TRANSCODE CLEANUP SUMMARY ━━━━━"
307	echo "$ICON_HOST   Identity: $MY_ID ($LOCAL_SERVER_NAME)"
308	echo "$ICON_RAM    Ramdisk freed: $RAMDISK_FREED"
309	echo "$ICON_DISK   SSD freed:     $SSD_FREED"
310	echo "$ICON_TRASH  Removed:       $TOTAL_REMOVED files"
311	echo "$ICON_RUNNING Streaming:    $TOTAL_STREAMING files (open by Emby — untouched)"
312	echo "$ICON_SHIELD  Protected:    $TOTAL_ACTIVE aged files saved by open-file check"
313	echo "$ICON_TRASH  Skipped:       $TOTAL_SKIPPED files"
314	echo "$ICON_TIME   Duration:      $(format_duration $(( END - START )))"
315
316	if [[ "$DRY_RUN" == true ]]; then
317	    warn "DRY RUN — no files deleted"
318	else
319	    echo "$ICON_DONE Status: done ✅"
320	fi
321	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

#!/bin/bash
# ==============================================================================================
# ================================= Transcode Cleanup ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Removes stale transcode files from both ramdisk and SSD fallback locations.
# Called by transcode_management.sh (Orchestrators/) before transcode_manager.sh —
# cleanup must run first so the manager sees real active-session usage, not
# inflated usage from stale files. Must be fast and non-blocking.
#
# A file is eligible for deletion only if ALL conditions are true:
#   1. Older than TRANSCODE_MAX_AGE minutes (mtime — last write time)
#   2. Not currently open by any process (checked via lsof pre-built map)
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# lsof Called Once, Not Per File
#   On a busy Live TV system the ramdisk contains thousands of HLS segment files.
#   Calling lsof once per file creates thousands of subprocess calls every 7 minutes.
#   lsof is called once per location to build a complete open-file map. All subsequent
#   checks are O(1) lookups against that map — thousands of files, one lsof call.
#
# No Session-Aware Cleanup
#   ffmpeg generates folder names independently of the media server API session IDs.
#   There is no reliable correlation between API session IDs and transcoding-temp
#   subfolder names. Attempting to correlate them would falsely treat active sessions
#   as ended. lsof is the correct check — if ffmpeg has a file open, it is active
#   regardless of folder naming or session state.
#
# transcoding-temp Is Never Deleted
#   If cleanup removes the empty transcoding-temp folder from the ramdisk, Emby
#   searches all accessible paths for an existing one, finds the SSD fallback version,
#   and routes all new sessions there until Emby restarts. The directory is excluded
#   from find by name — protected even when completely empty.
#
# Post-Cleanup Flip-Back
#   After removing stale files, checks whether ramdisk usage dropped below
#   RAMDISK_LOW_GB. If so — and symlink currently points at SSD — triggers a
#   flip back to ramdisk. This is the recovery path; the manager handles
#   the fill-up path.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Wait Lock
#   acquire_lock "wait" — waits if a previous cleanup run is still active rather
#   than exiting. The caller's 7-minute interval can overlap on a slow system.
#
# lsof Timeout
#   lsof call capped at 15 seconds per location — prevents blocking indefinitely
#   on a system with many open files.
#
# transcoding-temp Guard
#   `! -name "transcoding-temp"` in the find command — protected unconditionally.
#
# Silent by Default
#   Runs every 7 minutes — must not produce noise when healthy.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_RAMDISK_PATH / HOST*_TRANSCODE_SSD / HOST*_RAMDISK_LOW_GB
#       Aliased by detect_hosts() → RAMDISK_PATH / TRANSCODE_SSD / RAMDISK_LOW_GB.
#
# master.conf
#
#   TRANSCODE_MAX_AGE
#       Minutes before an inactive transcode file is eligible for deletion. (default: 20)
#
#   TRANSCODE_ORPHAN_AGE
#       Minutes for orphan folder detection. (default: 30)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# transcode_cleanup.sh
#     Remove stale files from ramdisk and SSD. Check for flip-back opportunity.
#
# transcode_cleanup.sh --dry-run
#     Show which files would be deleted. No deletions, no flip.
#
# transcode_cleanup.sh --status
#     Show current file counts, ages, and open-file status per location.
#
# transcode_cleanup.sh --log
#     Verbose per-file output including age, open status, and deletion result.
#
# ==============================================================================================

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

source "$SCRIPT_DIR/../load_config.sh"

parse_args "$@"

STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

acquire_lock "wait"

# detect_hosts() sets MY_ID and aliases RAMDISK_PATH, TRANSCODE_SSD, RAMDISK_LOW_GB
detect_hosts

# lsof availability check
LSOF_AVAILABLE=false
if command -v lsof >/dev/null 2>&1; then
    LSOF_AVAILABLE=true
    log "lsof available — active file check enabled"
else
    warn "lsof not available — active file check skipped, all aged files eligible for deletion"
fi

log "Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
log "$ICON_GEAR Config:    max-age=${TRANSCODE_MAX_AGE}min orphan-age=${TRANSCODE_ORPHAN_AGE}min flip-back-below=${RAMDISK_LOW_GB}GB"
log "$ICON_RAM  Locations: ramdisk=$RAMDISK_PATH  ssd=$TRANSCODE_SSD"

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_HOST   Identity:     $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_RAM    Ramdisk:      $RAMDISK_PATH"
    echo "$ICON_DISK   SSD fallback: $TRANSCODE_SSD"
    echo "$ICON_TRASH  Max age:      ${TRANSCODE_MAX_AGE} minutes"
    echo "$ICON_TRASH  Orphan age:   ${TRANSCODE_ORPHAN_AGE} minutes"
    echo "$ICON_RAM    Flip at:      ${RAMDISK_LOW_GB}GB"
    echo "$ICON_GEAR   lsof check:   $LSOF_AVAILABLE"
    echo "$ICON_GEAR   Dry Run:      $DRY_RUN"
    echo ""

    if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
        USAGE=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $3}')
        AVAIL=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $4}')
        echo "  $ICON_RAM  Ramdisk: mounted — $USAGE used / $AVAIL available ✅"
    else
        echo "  $ICON_RAM  Ramdisk: not mounted"
    fi

    CURRENT_TARGET=$(grep "^current_target=" "$STATE_FILE" 2>/dev/null | cut -d= -f2)
    echo "  $ICON_LINK Current target: ${CURRENT_TARGET:-unknown}"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be deleted"

# ==============================================================================================
# ── CLEANUP FUNCTION ──────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# Scans a location and removes eligible files.
# Calls lsof ONCE per location — builds in-memory OPEN_FILES_MAP for O(1) lookup.
# Returns via LOCATION_REMOVED, LOCATION_FREED, LOCATION_SKIPPED, LOCATION_ACTIVE

cleanup_location() {
    local location="$1" label="$2" max_age="$3"
    local files_removed=0 bytes_freed=0 files_skipped=0 files_active=0 files_streaming=0

    if [[ ! -d "$location" ]]; then
        log "$label does not exist — skipping"
        LOCATION_REMOVED=0 LOCATION_FREED="0B" LOCATION_SKIPPED=0 LOCATION_ACTIVE=0 LOCATION_STREAMING=0
        return
    fi

    local file_count
    file_count=$(find "$location" -type f 2>/dev/null | wc -l)
    log "$label: $file_count files to scan (age threshold: ${max_age}min)"

    # Build in-memory open file map — O(1) lookup per file
    # One lsof call per location — never per file
    declare -A OPEN_FILES_MAP
    if [[ "$LSOF_AVAILABLE" == true ]]; then
        log "Building open file map for $label..."
        while IFS= read -r open_file; do
            [[ -n "$open_file" ]] && OPEN_FILES_MAP["$open_file"]=1
        done < <(timeout 15 lsof +D "$location" 2>/dev/null | awk 'NR>1 {print $9}' | sort -u)
        files_streaming=${#OPEN_FILES_MAP[@]}
        log "$files_streaming files currently open in $label"
    fi

    # Process aged files
    while IFS= read -r file; do
        [[ -z "$file" ]] && continue

        # O(1) open file check — in-memory map
        if [[ -n "${OPEN_FILES_MAP[$file]:-}" ]]; then
            (( files_active++ ))
            log "Skipping open file: $file"
            continue
        fi

        local file_size
        file_size=$(stat -c%s "$file" 2>/dev/null || echo 0)

        if [[ "$DRY_RUN" == true ]]; then
            log "DRY RUN — would delete: $(basename "$file")"
            (( files_skipped++ ))
        else
            if rm -f "$file" 2>/dev/null; then
                (( files_removed++ ))
                bytes_freed=$(( bytes_freed + file_size ))
                log "Deleted: $file"
            else
                warn "Could not delete: $file"
                (( files_skipped++ ))
            fi
        fi

    done < <(find "$location" -type f -mmin +"$max_age" 2>/dev/null)

    # Remove empty directories — but NEVER remove transcoding-temp
    # transcoding-temp must always exist on ramdisk so Emby finds it there first
    if [[ "$DRY_RUN" == false ]]; then
        find "$location" -mindepth 1 -type d -empty \
            ! -name "transcoding-temp" -delete 2>/dev/null
    fi

    # Format bytes freed
    local freed_human
    if (( bytes_freed > 1073741824 )); then
        freed_human=$(awk "BEGIN {printf \"%.1fGB\", $bytes_freed / 1073741824}")
    elif (( bytes_freed > 1048576 )); then
        freed_human=$(awk "BEGIN {printf \"%.1fMB\", $bytes_freed / 1048576}")
    elif (( bytes_freed > 0 )); then
        freed_human="${bytes_freed}B"
    else
        freed_human="0B"
    fi

    log "$label — removed $files_removed files ($freed_human freed) | streaming: $files_streaming | aged+open: $files_active | skipped: $files_skipped"

    LOCATION_REMOVED=$files_removed
    LOCATION_FREED=$freed_human
    LOCATION_SKIPPED=$files_skipped
    LOCATION_ACTIVE=$files_active
    LOCATION_STREAMING=$files_streaming
}

# ==============================================================================================
# ━━━ Transcode Cleanup ━━━
# ==============================================================================================
START=$(date +%s)
TOTAL_REMOVED=0 TOTAL_SKIPPED=0 TOTAL_ACTIVE=0 TOTAL_STREAMING=0
RAMDISK_FREED="0B" SSD_FREED="0B"

# Cleanup ramdisk
if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
    cleanup_location "$RAMDISK_PATH" "Ramdisk" "$TRANSCODE_MAX_AGE"
    TOTAL_REMOVED=$(( TOTAL_REMOVED + LOCATION_REMOVED ))
    TOTAL_SKIPPED=$(( TOTAL_SKIPPED + LOCATION_SKIPPED ))
    TOTAL_ACTIVE=$(( TOTAL_ACTIVE + LOCATION_ACTIVE ))
    TOTAL_STREAMING=$(( TOTAL_STREAMING + LOCATION_STREAMING ))
    RAMDISK_FREED=$LOCATION_FREED
else
    log "Ramdisk not mounted — skipping ramdisk cleanup"
fi

# Cleanup SSD fallback
if [[ -d "$TRANSCODE_SSD" ]]; then
    cleanup_location "$TRANSCODE_SSD" "SSD fallback" "$TRANSCODE_MAX_AGE"
    TOTAL_REMOVED=$(( TOTAL_REMOVED + LOCATION_REMOVED ))
    TOTAL_SKIPPED=$(( TOTAL_SKIPPED + LOCATION_SKIPPED ))
    TOTAL_ACTIVE=$(( TOTAL_ACTIVE + LOCATION_ACTIVE ))
    TOTAL_STREAMING=$(( TOTAL_STREAMING + LOCATION_STREAMING ))
    SSD_FREED=$LOCATION_FREED
else
    log "SSD fallback not found — skipping SSD cleanup"
fi

# Post-cleanup — check if ramdisk recovered enough to flip symlink back to ramdisk
if [[ "$DRY_RUN" == false ]] && mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
    RAMDISK_USED_KB=$(df "$RAMDISK_PATH" --output=used 2>/dev/null | tail -1 | tr -d ' ')
    RAMDISK_USED_GB=$(awk "BEGIN {printf \"%.2f\", ${RAMDISK_USED_KB:-0} / 1048576}")
    LOW_RECOVERED=$(awk "BEGIN {print ($RAMDISK_USED_GB < $RAMDISK_LOW_GB) ? 1 : 0}")
    CURRENT_TARGET=$(grep "^current_target=" "$STATE_FILE" 2>/dev/null | cut -d= -f2)

    if [[ "$LOW_RECOVERED" == "1" && "$CURRENT_TARGET" == "$TRANSCODE_SSD" ]]; then
        echo "Ramdisk has space after cleanup (${RAMDISK_USED_GB}GB < ${RAMDISK_LOW_GB}GB) — triggering manager to flip back"
        bash "$SCRIPT_DIR/transcode_manager.sh" --no-log
    fi
fi

END=$(date +%s)

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo "━━━━━ $ICON_SUMMARY TRANSCODE CLEANUP SUMMARY ━━━━━"
echo "$ICON_HOST   Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_RAM    Ramdisk freed: $RAMDISK_FREED"
echo "$ICON_DISK   SSD freed:     $SSD_FREED"
echo "$ICON_TRASH  Removed:       $TOTAL_REMOVED files"
echo "$ICON_RUNNING Streaming:    $TOTAL_STREAMING files (open by Emby — untouched)"
echo "$ICON_SHIELD  Protected:    $TOTAL_ACTIVE aged files saved by open-file check"
echo "$ICON_TRASH  Skipped:       $TOTAL_SKIPPED files"
echo "$ICON_TIME   Duration:      $(format_duration $(( END - START )))"

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — no files deleted"
else
    echo "$ICON_DONE Status: done ✅"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"