Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix
- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs); remove standalone cert page and top-level tab - cert_monitor.sh: write JSON status cache to State_Files/cert_status.json after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals - api/cert.php: new — serves cached cert status; falls back to configured domains as UNKN when no cache exists; POST action=run triggers live check - arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now read from data/*.db files when log JSON files don't yet exist - config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar signs read correctly from conf files - host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS - Partnership adapter pattern: Unraid-specific container logic extracted to Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/ - First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved to checklist; auto SSH keygen and API key creation on save - api/checklist.php: live setup checklist with pull_master action - Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
This commit is contained in:
@@ -283,7 +283,7 @@
|
|||||||
ARRAY_START_SCRIPTS=(
|
ARRAY_START_SCRIPTS=(
|
||||||
"Transcodes/ramdisk_setup.sh" # creates ramdisk + symlink before Emby starts
|
"Transcodes/ramdisk_setup.sh" # creates ramdisk + symlink before Emby starts
|
||||||
"System_Essentials/docker_syslog_filter.sh" # suppress veth noise before logs fill
|
"System_Essentials/docker_syslog_filter.sh" # suppress veth noise before logs fill
|
||||||
"System_Essentials/php_fpm_max_children.sh" # WebGUI performance tuning
|
"Plugin/unraid/System_Essentials/php_fpm_max_children.sh" # WebGUI performance tuning
|
||||||
"System_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
"System_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
||||||
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
||||||
"Fallback/fallback.sh" # mutual failover — continuous
|
"Fallback/fallback.sh" # mutual failover — continuous
|
||||||
@@ -294,10 +294,10 @@
|
|||||||
# Run sequentially (foreground) — each must complete before the next starts.
|
# Run sequentially (foreground) — each must complete before the next starts.
|
||||||
# Order matters: user scripts first (prevents new ops), then data movement, then containers.
|
# Order matters: user scripts first (prevents new ops), then data movement, then containers.
|
||||||
ARRAY_STOP_SCRIPTS=(
|
ARRAY_STOP_SCRIPTS=(
|
||||||
"System_Essentials/user_scripts_stop.sh" # stop background scripts before they start new ops
|
"Plugin/unraid/System_Essentials/user_scripts_stop.sh" # stop background scripts before they start new ops
|
||||||
"Fallback/fallback.sh --stop" # gracefully stop fallback (not caught by user_scripts_stop)
|
"Fallback/fallback.sh --stop" # gracefully stop fallback (not caught by user_scripts_stop)
|
||||||
"System_Essentials/rsync_stop.sh --rsync-only" # kill rsync; skip container recovery (handled below)
|
"System_Essentials/rsync_stop.sh --rsync-only" # kill rsync; skip container recovery (handled below)
|
||||||
"System_Essentials/mover_stop.sh" # stop mover after rsync (they conflict on same files)
|
"Plugin/unraid/System_Essentials/mover_stop.sh" # stop mover after rsync (they conflict on same files)
|
||||||
"Docker_Essentials/docker_container_stop.sh" # stop all containers last
|
"Docker_Essentials/docker_container_stop.sh" # stop all containers last
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@
|
|||||||
# Called by watchdog_orchestrator.sh — not scheduled directly.
|
# Called by watchdog_orchestrator.sh — not scheduled directly.
|
||||||
SYSTEM_WATCHDOG_SCRIPTS=(
|
SYSTEM_WATCHDOG_SCRIPTS=(
|
||||||
"Watchdogs/System/storage_watchdog.sh" # pool growth + runaway log detection
|
"Watchdogs/System/storage_watchdog.sh" # pool growth + runaway log detection
|
||||||
"Watchdogs/System/webgui_watchdog.sh" # WebGUI availability — nginx → php-fpm → emhttp
|
"Plugin/unraid/Watchdogs/System/webgui_watchdog.sh" # WebGUI availability — nginx → php-fpm → emhttp
|
||||||
"Watchdogs/System/network_watchdog.sh" # internet, DDNS, Tailscale, NPM proxy
|
"Watchdogs/System/network_watchdog.sh" # internet, DDNS, Tailscale, NPM proxy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ fi
|
|||||||
check_cert() {
|
check_cert() {
|
||||||
local domain="$1"
|
local domain="$1"
|
||||||
local port="${2:-443}"
|
local port="${2:-443}"
|
||||||
|
_CERT_DAYS=""
|
||||||
|
_CERT_EXPIRY=""
|
||||||
|
|
||||||
local expiry_str
|
local expiry_str
|
||||||
expiry_str=$(echo | timeout "$CERT_TIMEOUT" openssl s_client \
|
expiry_str=$(echo | timeout "$CERT_TIMEOUT" openssl s_client \
|
||||||
@@ -186,6 +188,8 @@ check_cert() {
|
|||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
days_remaining=$(( (expiry_epoch - now) / 86400 ))
|
days_remaining=$(( (expiry_epoch - now) / 86400 ))
|
||||||
expiry_display=$(date -d "$expiry_str" '+%Y-%m-%d' 2>/dev/null)
|
expiry_display=$(date -d "$expiry_str" '+%Y-%m-%d' 2>/dev/null)
|
||||||
|
_CERT_DAYS=$days_remaining
|
||||||
|
_CERT_EXPIRY=$expiry_display
|
||||||
|
|
||||||
if [[ "$days_remaining" -le "$CERT_CRIT_DAYS" ]]; then
|
if [[ "$days_remaining" -le "$CERT_CRIT_DAYS" ]]; then
|
||||||
error "$ICON_CERT $domain — CRITICAL: ${days_remaining} days remaining (expires $expiry_display)"
|
error "$ICON_CERT $domain — CRITICAL: ${days_remaining} days remaining (expires $expiry_display)"
|
||||||
@@ -214,12 +218,14 @@ HEALTHY=()
|
|||||||
WARNING=()
|
WARNING=()
|
||||||
CRITICAL=()
|
CRITICAL=()
|
||||||
FAILED=()
|
FAILED=()
|
||||||
declare -A DOMAIN_STATUS
|
declare -A DOMAIN_STATUS DOMAIN_DAYS DOMAIN_EXPIRY
|
||||||
|
|
||||||
for domain in "${CERT_MONITOR_DOMAINS[@]}"; do
|
for domain in "${CERT_MONITOR_DOMAINS[@]}"; do
|
||||||
[[ -z "$domain" ]] && continue
|
[[ -z "$domain" ]] && continue
|
||||||
check_cert "$domain"
|
check_cert "$domain"
|
||||||
result=$?
|
result=$?
|
||||||
|
DOMAIN_DAYS["$domain"]="${_CERT_DAYS:-}"
|
||||||
|
DOMAIN_EXPIRY["$domain"]="${_CERT_EXPIRY:-}"
|
||||||
case $result in
|
case $result in
|
||||||
0) HEALTHY+=("$domain"); DOMAIN_STATUS["$domain"]="OK" ;;
|
0) HEALTHY+=("$domain"); DOMAIN_STATUS["$domain"]="OK" ;;
|
||||||
1) WARNING+=("$domain"); DOMAIN_STATUS["$domain"]="WARN" ;;
|
1) WARNING+=("$domain"); DOMAIN_STATUS["$domain"]="WARN" ;;
|
||||||
@@ -280,5 +286,24 @@ else
|
|||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# ── Write JSON status cache ───────────────────────────────────────────────────
|
||||||
|
_CERT_CACHE_FILE="$SCRIPTS_DIR/State_Files/cert_status.json"
|
||||||
|
{
|
||||||
|
printf '{"checked_at":%d,"host":"%s","warn_days":%d,"crit_days":%d,"dry_run":%s,"domains":[\n' \
|
||||||
|
"$(date +%s)" "$MY_ID" "$CERT_WARN_DAYS" "$CERT_CRIT_DAYS" \
|
||||||
|
"$([[ $DRY_RUN == true ]] && echo true || echo false)"
|
||||||
|
_first=true
|
||||||
|
for _d in "${CERT_MONITOR_DOMAINS[@]}"; do
|
||||||
|
[[ -z "$_d" ]] && continue
|
||||||
|
[[ "$_first" != true ]] && printf ','
|
||||||
|
_first=false
|
||||||
|
_days="${DOMAIN_DAYS[$_d]:-null}"
|
||||||
|
_exp="${DOMAIN_EXPIRY[$_d]:-}"
|
||||||
|
printf '{"domain":"%s","status":"%s","days":%s,"expires":"%s"}\n' \
|
||||||
|
"$_d" "${DOMAIN_STATUS[$_d]:-UNKN}" "$_days" "$_exp"
|
||||||
|
done
|
||||||
|
printf ']}\n'
|
||||||
|
} > "$_CERT_CACHE_FILE" 2>/dev/null
|
||||||
|
|
||||||
[[ ${#CRITICAL[@]} -gt 0 || ${#FAILED[@]} -gt 0 ]] && exit 1
|
[[ ${#CRITICAL[@]} -gt 0 || ${#FAILED[@]} -gt 0 ]] && exit 1
|
||||||
exit 0
|
exit 0
|
||||||
@@ -157,7 +157,6 @@ HOST2 (mirror) runs:
|
|||||||
HOST1 (owner) runs:
|
HOST1 (owner) runs:
|
||||||
partnership_onboard.sh
|
partnership_onboard.sh
|
||||||
├─ ssh_setup.sh generates keypair, copies to mirror
|
├─ ssh_setup.sh generates keypair, copies to mirror
|
||||||
├─ [plugin install on mirror] FolderView3 if configured
|
|
||||||
├─ [stop mirror auth stack] PARTNERSHIP_REPLACE_CONTAINERS via SSH
|
├─ [stop mirror auth stack] PARTNERSHIP_REPLACE_CONTAINERS via SSH
|
||||||
├─ deploy_container_from_xml() pushes auth XMLs to mirror + starts containers
|
├─ deploy_container_from_xml() pushes auth XMLs to mirror + starts containers
|
||||||
│ └─ wait_for_container_healthy() Mariadb/Redis health-checked before Authelia
|
│ └─ wait_for_container_healthy() Mariadb/Redis health-checked before Authelia
|
||||||
|
|||||||
@@ -274,8 +274,8 @@ MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db"
|
|||||||
OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"
|
OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"
|
||||||
|
|
||||||
# ── Exit Trap — restart locally stopped containers if script crashes mid-cleanup ──────────────
|
# ── Exit Trap — restart locally stopped containers if script crashes mid-cleanup ──────────────
|
||||||
# Used by folderview3_remove_partner_folder() and cleanup_partner_containers() — also shared
|
# Used by cleanup_partner_containers() — also shared with partnership_offboard.sh which
|
||||||
# with partnership_offboard.sh which sources this file and registers the same trap.
|
# sources this file and registers the same trap.
|
||||||
declare -a _PM_TRAP_STOPPED=()
|
declare -a _PM_TRAP_STOPPED=()
|
||||||
_pm_trap_restart_stopped() {
|
_pm_trap_restart_stopped() {
|
||||||
[[ ${#_PM_TRAP_STOPPED[@]} -eq 0 ]] && return
|
[[ ${#_PM_TRAP_STOPPED[@]} -eq 0 ]] && return
|
||||||
@@ -573,166 +573,6 @@ do_ssh_key_revocation() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
|
|
||||||
FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
|
|
||||||
FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"
|
|
||||||
|
|
||||||
# Derive short partner name: strip unraid- prefix (case-insensitive) if present
|
|
||||||
derive_partner_folder_name() {
|
|
||||||
local hostname="$1"
|
|
||||||
local short="${hostname,,}"
|
|
||||||
[[ "$short" == unraid-* ]] && short="${short:7}"
|
|
||||||
# Capitalise first char for readability: jayred365 → Jayred365-fallback
|
|
||||||
echo "${short^}-Fallback"
|
|
||||||
}
|
|
||||||
|
|
||||||
folderview3_ensure_plugin() {
|
|
||||||
if [[ -d "$FOLDERVIEW3_DIR" ]]; then
|
|
||||||
log "FolderView3 plugin present ✅"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
|
|
||||||
warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"
|
|
||||||
warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
warn "FolderView3 not found — installing from CA..."
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \
|
|
||||||
log "FolderView3 installed ✅" || {
|
|
||||||
warn "FolderView3 install failed — folder will not be created"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
folderview3_create_partner_folder() {
|
|
||||||
local partner_name="$1"
|
|
||||||
shift
|
|
||||||
local containers=("$@")
|
|
||||||
|
|
||||||
log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."
|
|
||||||
|
|
||||||
folderview3_ensure_plugin || return 1
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn "DRY RUN — would create FolderView3 folder: $partner_name"
|
|
||||||
for c in "${containers[@]}"; do
|
|
||||||
[[ -n "$c" ]] && warn " DRY RUN — container: $c"
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Initialise JSON if missing
|
|
||||||
if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
|
|
||||||
mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"
|
|
||||||
echo '{}' > "$FOLDERVIEW3_JSON"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check folder doesn't already exist
|
|
||||||
local existing
|
|
||||||
existing=$(jq -r --arg name "$partner_name" \
|
|
||||||
'to_entries[] | select(.value.name == $name) | .key' \
|
|
||||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
|
||||||
if [[ -n "$existing" ]]; then
|
|
||||||
log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build containers JSON array
|
|
||||||
local containers_json
|
|
||||||
containers_json=$(printf '%s\n' "${containers[@]}" | \
|
|
||||||
grep -v '^$' | jq -R . | jq -s .)
|
|
||||||
|
|
||||||
# Generate a stable random ID from timestamp+name
|
|
||||||
local folder_id
|
|
||||||
folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)
|
|
||||||
|
|
||||||
jq --arg id "$folder_id" --arg name "$partner_name" \
|
|
||||||
--argjson containers "$containers_json" \
|
|
||||||
'.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \
|
|
||||||
"$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
|
|
||||||
mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
|
|
||||||
log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {
|
|
||||||
warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
folderview3_remove_partner_folder() {
|
|
||||||
local partner_name="$1"
|
|
||||||
|
|
||||||
log "FolderView3: removing folder '$partner_name' and stopping its containers..."
|
|
||||||
|
|
||||||
if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
|
|
||||||
log "FolderView3: JSON not found — nothing to remove"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
|
||||||
warn "jq not found — cannot manage FolderView3 JSON"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get containers from this folder
|
|
||||||
local containers_json
|
|
||||||
containers_json=$(jq -r --arg name "$partner_name" \
|
|
||||||
'to_entries[] | select(.value.name == $name) | .value.containers[]' \
|
|
||||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ -z "$containers_json" ]]; then
|
|
||||||
log "FolderView3: folder '$partner_name' not found — nothing to remove"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop and remove each container in the folder
|
|
||||||
local stopped=0 removed=0 failed=0
|
|
||||||
while IFS= read -r container; do
|
|
||||||
[[ -z "$container" ]] && continue
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn "DRY RUN — would stop + rm: $container"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Stop if running
|
|
||||||
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
|
|
||||||
if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then
|
|
||||||
log "$container stopped ✅"
|
|
||||||
_PM_TRAP_STOPPED+=("$container")
|
|
||||||
(( stopped++ ))
|
|
||||||
else
|
|
||||||
warn "$container stop failed"
|
|
||||||
(( failed++ ))
|
|
||||||
fi
|
|
||||||
if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then
|
|
||||||
log "$container removed ✅"
|
|
||||||
(( removed++ ))
|
|
||||||
else
|
|
||||||
warn "$container rm failed (may not exist)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "$container not found locally — skipping"
|
|
||||||
fi
|
|
||||||
done <<< "$containers_json"
|
|
||||||
|
|
||||||
# Remove folder entry from JSON
|
|
||||||
if [[ "$DRY_RUN" == false ]]; then
|
|
||||||
jq --arg name "$partner_name" \
|
|
||||||
'with_entries(select(.value.name != $name))' \
|
|
||||||
"$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
|
|
||||||
mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
|
|
||||||
log "FolderView3: folder '$partner_name' removed ✅" || \
|
|
||||||
warn "FolderView3: failed to remove folder from JSON"
|
|
||||||
else
|
|
||||||
warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ "$DRY_RUN" == false ]] && \
|
|
||||||
log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gather all partner fallback containers for this server (all tiers)
|
# Gather all partner fallback containers for this server (all tiers)
|
||||||
gather_partner_fallback_containers() {
|
gather_partner_fallback_containers() {
|
||||||
local out_var="$1"
|
local out_var="$1"
|
||||||
@@ -803,19 +643,14 @@ start_own_stack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Remove partnership containers on this server + their appdata bind-mount paths.
|
# Remove partnership containers on this server + their appdata bind-mount paths.
|
||||||
# Uses FolderView3 folder if enabled (precise list), else falls back to FALLBACK_*_COVERS_* config.
|
|
||||||
# Appdata paths collected via docker inspect BEFORE removal — inspect fails on removed containers.
|
# Appdata paths collected via docker inspect BEFORE removal — inspect fails on removed containers.
|
||||||
# Safety gate: only paths matching /mnt/*/appdata* are deleted.
|
# Safety gate: only paths matching /mnt/*/appdata* are deleted.
|
||||||
cleanup_partner_containers() {
|
cleanup_partner_containers() {
|
||||||
local folder_name="$1"
|
|
||||||
declare -a containers=()
|
declare -a containers=()
|
||||||
gather_partner_fallback_containers containers
|
gather_partner_fallback_containers containers
|
||||||
|
|
||||||
if [[ ${#containers[@]} -eq 0 ]]; then
|
if [[ ${#containers[@]} -eq 0 ]]; then
|
||||||
log "No partner containers found to remove"
|
log "No partner containers found to remove"
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
|
||||||
folderview3_remove_partner_folder "$folder_name"
|
|
||||||
fi
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -832,25 +667,21 @@ cleanup_partner_containers() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Remove containers
|
# Remove containers
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
for container in "${containers[@]}"; do
|
||||||
folderview3_remove_partner_folder "$folder_name"
|
[[ -z "$container" ]] && continue
|
||||||
else
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
for container in "${containers[@]}"; do
|
warn "DRY RUN — would stop + rm: $container"
|
||||||
[[ -z "$container" ]] && continue
|
continue
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
fi
|
||||||
warn "DRY RUN — would stop + rm: $container"
|
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
|
||||||
continue
|
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1 || true
|
||||||
fi
|
_PM_TRAP_STOPPED+=("$container")
|
||||||
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
|
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1 && \
|
||||||
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1 || true
|
log "$container removed ✅" || warn "$container rm failed"
|
||||||
_PM_TRAP_STOPPED+=("$container")
|
else
|
||||||
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1 && \
|
log "$container not found — skipping"
|
||||||
log "$container removed ✅" || warn "$container rm failed"
|
fi
|
||||||
else
|
done
|
||||||
log "$container not found — skipping"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Delete appdata after containers are gone
|
# Delete appdata after containers are gone
|
||||||
while IFS= read -r path; do
|
while IFS= read -r path; do
|
||||||
@@ -1271,27 +1102,6 @@ if [[ "$MODE" == "status" ]]; then
|
|||||||
echo " ${entry%%|*} → port ${entry##*|}"
|
echo " ${entry%%|*} → port ${entry##*|}"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
|
||||||
echo ""
|
|
||||||
FOLDER_STATUS_NAME=$(derive_partner_folder_name "$REMOTE_SERVER_NAME")
|
|
||||||
if [[ -f "$FOLDERVIEW3_JSON" ]]; then
|
|
||||||
FOLDER_EXISTS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
|
|
||||||
'to_entries[] | select(.value.name == $name) | .key' \
|
|
||||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
|
||||||
if [[ -n "$FOLDER_EXISTS" ]]; then
|
|
||||||
FOLDER_CONTAINERS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
|
|
||||||
'[to_entries[] | select(.value.name == $name) | .value.containers[]] | join(", ")' \
|
|
||||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
|
||||||
echo " FolderView3: $FOLDER_STATUS_NAME ✅"
|
|
||||||
echo " Containers: $FOLDER_CONTAINERS"
|
|
||||||
else
|
|
||||||
echo " FolderView3: $FOLDER_STATUS_NAME — not found in JSON"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " FolderView3: config not found ($FOLDERVIEW3_JSON)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$OFFLINE_COUNTER" ]]; then
|
if [[ -f "$OFFLINE_COUNTER" ]]; then
|
||||||
OFFLINE_DAYS=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
|
OFFLINE_DAYS=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
|
||||||
[[ "$OFFLINE_DAYS" -gt 0 ]] && \
|
[[ "$OFFLINE_DAYS" -gt 0 ]] && \
|
||||||
@@ -1414,26 +1224,12 @@ if [[ "$MODE" == "onboard" ]]; then
|
|||||||
# ── LOCAL-ONLY PATH ──────────────────────────────────────────────────────────
|
# ── LOCAL-ONLY PATH ──────────────────────────────────────────────────────────
|
||||||
# HOST1-local setup steps that don't need HOST2 present. Called from
|
# HOST1-local setup steps that don't need HOST2 present. Called from
|
||||||
# partnership_onboard.sh --phase1-only so HOST1 can complete its own side
|
# partnership_onboard.sh --phase1-only so HOST1 can complete its own side
|
||||||
# (FolderView3, setup.db flag) while waiting for HOST2 to install and onboard.
|
# (PARTNERSHIP_ENABLED flag, setup.db) while waiting for HOST2 to install and onboard.
|
||||||
if [[ "$LOCAL_ONLY" == true ]]; then
|
if [[ "$LOCAL_ONLY" == true ]]; then
|
||||||
log "Mode: local-only — skipping remote pre-flight and WebUI steps"
|
log "Mode: local-only — skipping remote pre-flight and WebUI steps"
|
||||||
log "Owner: $OWNER_ID ($OWNER) · Mirror: $MIRROR_ID ($MIRROR)"
|
log "Owner: $OWNER_ID ($OWNER) · Mirror: $MIRROR_ID ($MIRROR)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
|
||||||
echo "FolderView3 Integration"
|
|
||||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
|
||||||
declare -a PARTNER_CONTAINERS=()
|
|
||||||
gather_partner_fallback_containers PARTNER_CONTAINERS
|
|
||||||
if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
|
|
||||||
folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
|
|
||||||
else
|
|
||||||
log "No FALLBACK_${MY_ID}_COVERS_${MIRROR_ID}_TIER* containers — skipping folder"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "FolderView3 not configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Enable partnership in master.conf + push to all hosts
|
# Enable partnership in master.conf + push to all hosts
|
||||||
echo ""
|
echo ""
|
||||||
echo "Enabling partnership in master.conf..."
|
echo "Enabling partnership in master.conf..."
|
||||||
@@ -1570,20 +1366,6 @@ if (vv_write_conf_raw('master.conf', \$master)) {
|
|||||||
warn "DRY RUN — would write ACTIVE state and push to remote"
|
warn "DRY RUN — would write ACTIVE state and push to remote"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# FolderView3 — create partner folder with this server's fallback containers for remote
|
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "━━━ $ICON_CONTAINERS FolderView3 Integration ━━━"
|
|
||||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
|
||||||
declare -a PARTNER_CONTAINERS=()
|
|
||||||
gather_partner_fallback_containers PARTNER_CONTAINERS
|
|
||||||
if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
|
|
||||||
folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
|
|
||||||
else
|
|
||||||
log "No FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER* containers configured — skipping folder creation"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Emby admin provisioning — runs after container deployment (deploy step not yet built)
|
# Emby admin provisioning — runs after container deployment (deploy step not yet built)
|
||||||
provision_emby_admin "$MIRROR_IP"
|
provision_emby_admin "$MIRROR_IP"
|
||||||
|
|
||||||
|
|||||||
@@ -71,10 +71,10 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
||||||
TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
|
|
||||||
SSH_TIMEOUT=15
|
SSH_TIMEOUT=15
|
||||||
|
|
||||||
source "$SCRIPTS_ROOT/load_config.sh"
|
source "$SCRIPTS_ROOT/load_config.sh"
|
||||||
|
source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
|
||||||
|
|
||||||
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
||||||
REASON="manual"
|
REASON="manual"
|
||||||
@@ -148,140 +148,6 @@ echo " Reason: $REASON"
|
|||||||
echo ""
|
echo ""
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
||||||
|
|
||||||
# ==============================================================================================
|
|
||||||
# ── HELPER: remove owner-deployed containers from a remote host ───────────────────────────────
|
|
||||||
#
|
|
||||||
# Uses PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK arrays (owner's conf) to derive
|
|
||||||
# container names from local XML templates. SSHes to remote to stop, remove, and delete
|
|
||||||
# appdata. Appdata paths are collected via docker inspect before removal so they aren't
|
|
||||||
# lost once the container is gone. Safety gate: only /mnt/*/appdata* paths are deleted.
|
|
||||||
# ==============================================================================================
|
|
||||||
cleanup_deployed_stack_on_remote() {
|
|
||||||
local remote_ip="$1" ssh_key="$2"
|
|
||||||
local -a xml_names=()
|
|
||||||
[[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
|
|
||||||
[[ ${#PARTNERSHIP_ARR_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
|
|
||||||
|
|
||||||
if [[ ${#xml_names[@]} -eq 0 ]]; then
|
|
||||||
log "No auth/arr stack arrays configured — skipping deployed stack cleanup"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Removing owner-deployed containers (auth/arr stacks) from $MIRROR..."
|
|
||||||
for xml_name in "${xml_names[@]}"; do
|
|
||||||
[[ -z "$xml_name" ]] && continue
|
|
||||||
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
|
||||||
if [[ ! -f "$xml_file" ]]; then
|
|
||||||
warn " $xml_name not found in local $TEMPLATES_DIR — skipping"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local cname
|
|
||||||
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
|
||||||
[[ -z "$cname" ]] && continue
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn " DRY RUN — would stop + rm $cname on $MIRROR"
|
|
||||||
warn " DRY RUN — would delete appdata for $cname on $MIRROR"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect appdata paths via docker inspect before removal
|
|
||||||
local appdata_paths
|
|
||||||
appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
|
|
||||||
"docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$cname' 2>/dev/null \
|
|
||||||
| awk -F: '{print \$1}' | grep '^/mnt/.*/appdata'" 2>/dev/null)
|
|
||||||
|
|
||||||
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
|
||||||
"docker stop '$cname' >/dev/null 2>&1
|
|
||||||
docker rm '$cname' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
|
|
||||||
grep -q removed && \
|
|
||||||
log " $cname removed from $MIRROR ✅" || \
|
|
||||||
log " $cname not found on $MIRROR — skipping"
|
|
||||||
|
|
||||||
while IFS= read -r path; do
|
|
||||||
[[ -z "$path" ]] && continue
|
|
||||||
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
|
||||||
"rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
|
|
||||||
log " Appdata removed on $MIRROR: $path ✅" || \
|
|
||||||
warn " Failed to remove appdata on $MIRROR: $path"
|
|
||||||
done <<< "$appdata_paths"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==============================================================================================
|
|
||||||
# ── HELPER: remove owner-deployed containers locally (mirror-initiated offboard) ─────────────
|
|
||||||
#
|
|
||||||
# SSHes to owner to read PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK, then uses the
|
|
||||||
# local templates-user/ copies (SCPed there during onboard) to get container names and
|
|
||||||
# appdata paths. Appdata collected before removal. Skips gracefully if owner unreachable.
|
|
||||||
# ==============================================================================================
|
|
||||||
cleanup_deployed_stack_locally() {
|
|
||||||
local owner_ip="$1" ssh_key="$2"
|
|
||||||
local -a xml_names=()
|
|
||||||
|
|
||||||
if [[ -n "$owner_ip" ]]; then
|
|
||||||
local -a auth_arr arr_arr
|
|
||||||
mapfile -t auth_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
|
|
||||||
"source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
|
|
||||||
detect_hosts 2>/dev/null
|
|
||||||
printf '%s\n' \"\${PARTNERSHIP_AUTH_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
|
|
||||||
mapfile -t arr_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
|
|
||||||
"source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
|
|
||||||
detect_hosts 2>/dev/null
|
|
||||||
printf '%s\n' \"\${PARTNERSHIP_ARR_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
|
|
||||||
xml_names=("${auth_arr[@]}" "${arr_arr[@]}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#xml_names[@]} -eq 0 ]]; then
|
|
||||||
log "Could not read deployed stack from owner — skipping auth/arr cleanup"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Removing owner-deployed containers (auth/arr stacks) locally..."
|
|
||||||
for xml_name in "${xml_names[@]}"; do
|
|
||||||
[[ -z "$xml_name" ]] && continue
|
|
||||||
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
|
||||||
if [[ ! -f "$xml_file" ]]; then
|
|
||||||
warn " $xml_name not found locally — skipping"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local cname
|
|
||||||
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
|
||||||
[[ -z "$cname" ]] && continue
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn " DRY RUN — would stop + rm $cname"
|
|
||||||
warn " DRY RUN — would delete appdata for $cname"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local appdata_paths=""
|
|
||||||
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$cname" >/dev/null 2>&1; then
|
|
||||||
appdata_paths=$(docker inspect \
|
|
||||||
--format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
|
|
||||||
"$cname" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
|
|
||||||
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$cname" >/dev/null 2>&1 || true
|
|
||||||
_PM_TRAP_STOPPED+=("$cname")
|
|
||||||
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$cname" >/dev/null 2>&1 && \
|
|
||||||
log " $cname removed ✅" || warn " $cname rm failed"
|
|
||||||
else
|
|
||||||
log " $cname not found locally — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r path; do
|
|
||||||
[[ -z "$path" ]] && continue
|
|
||||||
rm -rf "$path" && log " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
|
||||||
done <<< "$appdata_paths"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── HELPER: revoke own admin account from local Emby instance ────────────────────────────────
|
# ── HELPER: revoke own admin account from local Emby instance ────────────────────────────────
|
||||||
#
|
#
|
||||||
@@ -401,8 +267,7 @@ if [[ "$AM_MIRROR" == true ]]; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"
|
echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"
|
||||||
|
|
||||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
|
cleanup_partner_containers || STEP_FALLBACK_CLEANUP_OK=false
|
||||||
cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false
|
|
||||||
|
|
||||||
# ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────
|
# ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
@@ -559,8 +424,7 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"
|
echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"
|
||||||
|
|
||||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
cleanup_partner_containers
|
||||||
cleanup_partner_containers "$PARTNER_FOLDER_NAME"
|
|
||||||
|
|
||||||
# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────
|
# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────
|
||||||
start_own_stack
|
start_own_stack
|
||||||
@@ -574,26 +438,6 @@ if [[ "$MIRROR_REACHABLE" == true ]]; then
|
|||||||
cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||||
# Remove fallback coverage containers (by *-owner_short naming pattern)
|
# Remove fallback coverage containers (by *-owner_short naming pattern)
|
||||||
cleanup_owner_containers_on_mirror "$MIRROR_IP"
|
cleanup_owner_containers_on_mirror "$MIRROR_IP"
|
||||||
# Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)
|
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
|
||||||
OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")
|
|
||||||
log "Removing FolderView3 folder '$OWNER_FOLDER_ON_MIRROR' from $MIRROR..."
|
|
||||||
if [[ "$DRY_RUN" == false ]]; then
|
|
||||||
timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
|
|
||||||
"fv3='/boot/config/plugins/folder.view3/docker.json'
|
|
||||||
[[ -f \"\$fv3\" ]] && command -v jq >/dev/null 2>&1 && \
|
|
||||||
jq --arg n '$OWNER_FOLDER_ON_MIRROR' \
|
|
||||||
'with_entries(select(.value.name != \$n))' \
|
|
||||||
\"\$fv3\" > \"\${fv3}.tmp\" && \
|
|
||||||
mv \"\${fv3}.tmp\" \"\$fv3\" && echo removed" 2>/dev/null | \
|
|
||||||
grep -q removed && \
|
|
||||||
log "FolderView3 '$OWNER_FOLDER_ON_MIRROR' removed from $MIRROR ✅" || \
|
|
||||||
warn "FolderView3 folder not found on $MIRROR or jq unavailable — skipping"
|
|
||||||
else
|
|
||||||
warn "DRY RUN — would remove FolderView3 folder '$OWNER_FOLDER_ON_MIRROR' from $MIRROR"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
warn "$MIRROR unreachable — remote container cleanup skipped"
|
warn "$MIRROR unreachable — remote container cleanup skipped"
|
||||||
warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"
|
warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"
|
||||||
@@ -696,8 +540,6 @@ echo " Step 9 — Keys revoked: $(_revoke_status)"
|
|||||||
echo " Step 10 — State: INACTIVE ✅"
|
echo " Step 10 — State: INACTIVE ✅"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
|
echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
|
||||||
[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
|
|
||||||
echo " FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"
|
|
||||||
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
|
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
|
||||||
echo " Tailscale: $MIRROR removed ✅"
|
echo " Tailscale: $MIRROR removed ✅"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -20,15 +20,14 @@
|
|||||||
#
|
#
|
||||||
# OWNER PATH (8 steps)
|
# OWNER PATH (8 steps)
|
||||||
# Step 1: SSH key setup — generate keypair, install on mirror, update conf
|
# Step 1: SSH key setup — generate keypair, install on mirror, update conf
|
||||||
# Step 2: Plugin install — FolderView3 and required plugins on mirror
|
# Step 2: Stop mirror auth — stop mirror's existing auth containers before replacing
|
||||||
# Step 3: Stop mirror auth — stop mirror's existing auth containers before replacing
|
# Step 3: Deploy auth stack — push XMLs, pull images, create + start on mirror
|
||||||
# Step 4: Deploy auth stack — push XMLs, pull images, create + start on mirror
|
|
||||||
# Mariadb/Redis health-checked before Authelia deploys
|
# Mariadb/Redis health-checked before Authelia deploys
|
||||||
# Step 5: Stop mirror arr — stop mirror's existing arr containers before replacing
|
# Step 4: Stop mirror arr — stop mirror's existing arr containers before replacing
|
||||||
# Step 6: Deploy arr stack — push arr XMLs, pull images, create + start on mirror
|
# Step 5: Deploy arr stack — push arr XMLs, pull images, create + start on mirror
|
||||||
# Step 7: Partnership onboard — configure WebUIs → owner IP, write state, FolderView3, Emby
|
# Step 6: Partnership onboard — configure WebUIs → owner IP, write state, Emby
|
||||||
# Step 8: Arr bootstrap — bidirectional library sync (arr_sync.sh)
|
# Step 7: Arr bootstrap — bidirectional library sync (arr_sync.sh)
|
||||||
# Step 9: Conf push — push master.conf + setup state to all listed hosts
|
# Step 8: Conf push — push master.conf + setup state to all listed hosts
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# DESIGN PRINCIPLES
|
# DESIGN PRINCIPLES
|
||||||
@@ -137,10 +136,10 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
||||||
TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
|
|
||||||
SSH_TIMEOUT=15
|
SSH_TIMEOUT=15
|
||||||
|
|
||||||
source "$SCRIPTS_ROOT/load_config.sh"
|
source "$SCRIPTS_ROOT/load_config.sh"
|
||||||
|
source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
|
||||||
|
|
||||||
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
||||||
SKIP_SSH=false
|
SKIP_SSH=false
|
||||||
@@ -224,165 +223,6 @@ echo " Partner: $( [[ "$AM_OWNER" == true ]] && echo "$MIRROR_ID ($MIRROR)" ||
|
|||||||
echo ""
|
echo ""
|
||||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
||||||
|
|
||||||
# ==============================================================================================
|
|
||||||
# ── HELPER: deploy a container from a local Unraid template XML to a remote host ─────────────
|
|
||||||
#
|
|
||||||
# Parses Port / Path / Variable Config entries from the XML, SCPs the template and a
|
|
||||||
# self-contained deploy script to the remote, executes it, then cleans up both sides.
|
|
||||||
# Credentials are never passed as SSH command-line args — they stay in the SCPed script.
|
|
||||||
# ==============================================================================================
|
|
||||||
deploy_container_from_xml() {
|
|
||||||
local xml_file="$1" remote_ip="$2" ssh_key="$3"
|
|
||||||
local xml_name
|
|
||||||
xml_name=$(basename "$xml_file")
|
|
||||||
|
|
||||||
# Extract top-level fields
|
|
||||||
local name repo network extra privileged
|
|
||||||
name=$( awk 'match($0,/<Name>([^<]+)<\/Name>/, a){print a[1];exit}' "$xml_file")
|
|
||||||
repo=$( awk 'match($0,/<Repository>([^<]+)<\/Repository>/,a){print a[1];exit}' "$xml_file")
|
|
||||||
network=$( awk 'match($0,/<Network>([^<]+)<\/Network>/, a){print a[1];exit}' "$xml_file")
|
|
||||||
extra=$( awk 'match($0,/<ExtraParams>([^<]*)<\/ExtraParams>/,a){print a[1];exit}' "$xml_file")
|
|
||||||
privileged=$( awk 'match($0,/<Privileged>([^<]+)<\/Privileged>/,a){print a[1];exit}' "$xml_file")
|
|
||||||
|
|
||||||
if [[ -z "$name" || -z "$repo" ]]; then
|
|
||||||
warn " Cannot parse Name/Repository from $xml_name — skipping"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Deploying $name..."
|
|
||||||
|
|
||||||
# SCP the XML so Unraid Docker Manager recognises and can manage the container
|
|
||||||
if [[ "$DRY_RUN" == false ]]; then
|
|
||||||
timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
|
||||||
"$xml_file" "root@${remote_ip}:${TEMPLATES_DIR}/${xml_name}" 2>/dev/null || {
|
|
||||||
warn " SCP failed for $xml_name — skipping $name"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
else
|
|
||||||
warn " DRY RUN — would SCP $xml_name → $MIRROR:${TEMPLATES_DIR}/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build a self-contained deploy script locally.
|
|
||||||
# Writing to a temp file keeps credentials out of SSH command strings.
|
|
||||||
local tmp_script
|
|
||||||
tmp_script=$(mktemp /tmp/deploy_XXXXXX.sh)
|
|
||||||
chmod 600 "$tmp_script"
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "#!/bin/bash"
|
|
||||||
echo "set -e"
|
|
||||||
echo ""
|
|
||||||
printf "docker pull %q 2>/dev/null || true\n" "$repo"
|
|
||||||
printf "docker stop %q 2>/dev/null || true\n" "$name"
|
|
||||||
printf "docker rm %q 2>/dev/null || true\n" "$name"
|
|
||||||
echo ""
|
|
||||||
printf "docker create --name %q --restart=unless-stopped" "$name"
|
|
||||||
[[ -n "$network" ]] && printf " --network=%q" "$network"
|
|
||||||
[[ "$privileged" == "true" ]] && printf " --privileged"
|
|
||||||
[[ -n "$extra" ]] && printf " %s" "$extra"
|
|
||||||
|
|
||||||
# Port mappings → -p host:container/proto
|
|
||||||
awk '/Type="Port"/ {
|
|
||||||
match($0, /Target="([^"]+)"/, t)
|
|
||||||
match($0, /Mode="([^"]+)"/, m)
|
|
||||||
match($0, />([^<]+)<\/Config>/, v)
|
|
||||||
if (t[1] != "" && v[1] != "") {
|
|
||||||
proto = (m[1] == "udp") ? "udp" : "tcp"
|
|
||||||
printf " -p %s:%s/%s", v[1], t[1], proto
|
|
||||||
}
|
|
||||||
}' "$xml_file"
|
|
||||||
|
|
||||||
# Volume mappings → -v 'host:container:mode'
|
|
||||||
awk 'BEGIN{q=sprintf("%c",39)} /Type="Path"/ {
|
|
||||||
match($0, /Target="([^"]+)"/, t)
|
|
||||||
match($0, /Mode="([^"]+)"/, m)
|
|
||||||
match($0, />([^<]+)<\/Config>/, v)
|
|
||||||
if (t[1] != "" && v[1] != "") {
|
|
||||||
mode = (m[1] == "ro") ? "ro" : "rw"
|
|
||||||
printf " -v %s%s:%s:%s%s", q, v[1], t[1], mode, q
|
|
||||||
}
|
|
||||||
}' "$xml_file"
|
|
||||||
|
|
||||||
# Environment variables → -e 'KEY=VALUE' (single-quoted to protect $ and special chars)
|
|
||||||
awk 'BEGIN{q=sprintf("%c",39)} /Type="Variable"/ {
|
|
||||||
match($0, /Target="([^"]+)"/, t)
|
|
||||||
match($0, />([^<]+)<\/Config>/, v)
|
|
||||||
if (t[1] != "" && v[1] != "") {
|
|
||||||
printf " -e %s%s=%s%s", q, t[1], v[1], q
|
|
||||||
}
|
|
||||||
}' "$xml_file"
|
|
||||||
|
|
||||||
printf " %q\n" "$repo"
|
|
||||||
echo ""
|
|
||||||
printf "docker start %q && echo 'deployed:%s'\n" "$name" "$name"
|
|
||||||
} > "$tmp_script"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn " DRY RUN — would deploy $name on $MIRROR"
|
|
||||||
rm -f "$tmp_script"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# SCP deploy script → remote, execute, clean up both sides
|
|
||||||
local remote_script="/tmp/deploy_${name//[^a-zA-Z0-9_]/_}.sh"
|
|
||||||
|
|
||||||
if timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
|
||||||
"$tmp_script" "root@${remote_ip}:${remote_script}" 2>/dev/null && \
|
|
||||||
timeout 120 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
|
||||||
"bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
|
|
||||||
grep -q "deployed:${name}"; then
|
|
||||||
log " $name deployed ✅"
|
|
||||||
rm -f "$tmp_script"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
warn " $name deployment failed — check $MIRROR manually"
|
|
||||||
rm -f "$tmp_script"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==============================================================================================
|
|
||||||
# ── HELPER: wait for a container on the remote to be healthy/running ─────────────────────────
|
|
||||||
#
|
|
||||||
# Polls docker inspect on the remote. Prefers the health status if a healthcheck is defined;
|
|
||||||
# falls back to the running state for containers with no healthcheck. Non-fatal after timeout
|
|
||||||
# — Authelia may take time to fully initialize but the deploy itself succeeded.
|
|
||||||
# ==============================================================================================
|
|
||||||
wait_for_container_healthy() {
|
|
||||||
local name="$1" remote_ip="$2" ssh_key="$3"
|
|
||||||
local max_wait=60 interval=5 elapsed=0
|
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && return 0
|
|
||||||
|
|
||||||
log " Waiting for $name to be ready..."
|
|
||||||
while (( elapsed < max_wait )); do
|
|
||||||
local status
|
|
||||||
status=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
|
|
||||||
"h=\$(docker inspect --format '{{.State.Health.Status}}' '$name' 2>/dev/null)
|
|
||||||
r=\$(docker inspect --format '{{.State.Running}}' '$name' 2>/dev/null)
|
|
||||||
echo \${h:-\$r}" 2>/dev/null)
|
|
||||||
|
|
||||||
case "$status" in
|
|
||||||
healthy|true)
|
|
||||||
log " $name ready ✅"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
starting|unhealthy|false|"")
|
|
||||||
sleep "$interval"
|
|
||||||
(( elapsed += interval ))
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
sleep "$interval"
|
|
||||||
(( elapsed += interval ))
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
warn " $name not confirmed healthy after ${max_wait}s — continuing (may affect dependents)"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── HELPER: stop containers on the mirror by reading its own conf via SSH ────────────────────
|
# ── HELPER: stop containers on the mirror by reading its own conf via SSH ────────────────────
|
||||||
#
|
#
|
||||||
@@ -427,46 +267,6 @@ stop_mirror_stack() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==============================================================================================
|
|
||||||
# ── HELPER: deploy a stack of XMLs to the mirror, health-checking db deps between batches ────
|
|
||||||
#
|
|
||||||
# Sets globals _STACK_DEPLOYED and _STACK_FAILED rather than printing to stdout.
|
|
||||||
# This avoids the process-substitution capture problem: warn() writes to stdout, so any
|
|
||||||
# read -r X Y < <(func) would capture warn output as the count values.
|
|
||||||
# ==============================================================================================
|
|
||||||
_STACK_DEPLOYED=0
|
|
||||||
_STACK_FAILED=0
|
|
||||||
|
|
||||||
deploy_xml_stack() {
|
|
||||||
local -n xml_array_ref="$1"
|
|
||||||
_STACK_DEPLOYED=0
|
|
||||||
_STACK_FAILED=0
|
|
||||||
|
|
||||||
for xml_name in "${xml_array_ref[@]}"; do
|
|
||||||
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
|
||||||
if [[ ! -f "$xml_file" ]]; then
|
|
||||||
warn "$xml_name not found in $TEMPLATES_DIR — skipping"
|
|
||||||
(( _STACK_FAILED++ ))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract container name to use for health-wait matching
|
|
||||||
local cname
|
|
||||||
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
|
||||||
|
|
||||||
if deploy_container_from_xml "$xml_file" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then
|
|
||||||
(( _STACK_DEPLOYED++ ))
|
|
||||||
# Health-check database deps before continuing — they must be ready before
|
|
||||||
# Authelia/app containers that depend on them can start cleanly.
|
|
||||||
if [[ -n "$cname" ]] && echo "$cname" | grep -qiE 'mariadb|redis|postgres|mysql'; then
|
|
||||||
wait_for_container_healthy "$cname" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
(( _STACK_FAILED++ ))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
|
# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -541,7 +341,6 @@ log "Mirror: $MIRROR ($MIRROR_IP)"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
STEP_SSH_OK=false
|
STEP_SSH_OK=false
|
||||||
STEP_PLUGINS_OK=true
|
|
||||||
STEP_STOP_AUTH_OK=true
|
STEP_STOP_AUTH_OK=true
|
||||||
STEP_AUTH_OK=true
|
STEP_AUTH_OK=true
|
||||||
AUTH_DEPLOYED=0
|
AUTH_DEPLOYED=0
|
||||||
@@ -620,7 +419,7 @@ if [[ "$PHASE1_ONLY" == true ]]; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"
|
echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"
|
||||||
bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
|
bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
|
||||||
warn "Local setup had issues — FolderView3 may need manual setup"
|
warn "Local setup had issues — check partnership_manager.sh output above"
|
||||||
|
|
||||||
END=$(date +%s)
|
END=$(date +%s)
|
||||||
echo ""
|
echo ""
|
||||||
@@ -674,7 +473,7 @@ exit(\$failed > 0 ? 1 : 0);
|
|||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Phase 1 — HOST1 Local Setup ━━━"
|
echo "━━━ Phase 1 — HOST1 Local Setup ━━━"
|
||||||
bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
|
bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
|
||||||
warn "Local setup had issues — FolderView3 may need manual setup"
|
warn "Local setup had issues — check partnership_manager.sh output above"
|
||||||
|
|
||||||
[[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1
|
[[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1
|
||||||
|
|
||||||
@@ -693,36 +492,9 @@ exit(\$failed > 0 ? 1 : 0);
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Step 2: Plugins ───────────────────────────────────────────────────────────────────────────
|
# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Step 2 — Plugin Install on Mirror ━━━"
|
echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"
|
||||||
|
|
||||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && [[ -n "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
|
|
||||||
FV3_PRESENT=$(timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
|
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
|
|
||||||
"test -d /usr/local/emhttp/plugins/folder.view3 && echo yes" 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ "$FV3_PRESENT" == "yes" ]]; then
|
|
||||||
log "FolderView3 already installed on $MIRROR ✅"
|
|
||||||
elif [[ "$DRY_RUN" == true ]]; then
|
|
||||||
warn "DRY RUN — would install FolderView3 on $MIRROR"
|
|
||||||
else
|
|
||||||
log "Installing FolderView3 on $MIRROR..."
|
|
||||||
timeout 60 ssh -i "$MIRROR_SSH_KEY" -o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
|
|
||||||
"plugin install '$PARTNERSHIP_FOLDERVIEW3_URL' 2>/dev/null && echo installed" \
|
|
||||||
2>/dev/null | grep -q installed && \
|
|
||||||
log "FolderView3 installed ✅" || {
|
|
||||||
warn "FolderView3 install failed — install manually from Community Applications"
|
|
||||||
STEP_PLUGINS_OK=false
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "FolderView3 not configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 3: Stop mirror's existing auth stack ─────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━"
|
|
||||||
|
|
||||||
if [[ "$SKIP_AUTH_STACK" == true ]]; then
|
if [[ "$SKIP_AUTH_STACK" == true ]]; then
|
||||||
warn "Skipping (--skip-auth-stack)"
|
warn "Skipping (--skip-auth-stack)"
|
||||||
@@ -732,7 +504,7 @@ fi
|
|||||||
|
|
||||||
# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
|
# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Step 4 — Deploy Auth Stack on Mirror ━━━"
|
echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"
|
||||||
|
|
||||||
if [[ "$SKIP_AUTH_STACK" == true ]]; then
|
if [[ "$SKIP_AUTH_STACK" == true ]]; then
|
||||||
warn "Skipping (--skip-auth-stack)"
|
warn "Skipping (--skip-auth-stack)"
|
||||||
@@ -750,7 +522,7 @@ fi
|
|||||||
|
|
||||||
# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
|
# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Step 5 — Stop Mirror Arr Stack ━━━"
|
echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"
|
||||||
|
|
||||||
if [[ "$SKIP_ARR_STACK" == true ]]; then
|
if [[ "$SKIP_ARR_STACK" == true ]]; then
|
||||||
warn "Skipping (--skip-arr-stack)"
|
warn "Skipping (--skip-arr-stack)"
|
||||||
@@ -763,7 +535,7 @@ fi
|
|||||||
|
|
||||||
# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
|
# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Step 6 — Deploy Arr Stack on Mirror ━━━"
|
echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"
|
||||||
|
|
||||||
if [[ "$SKIP_ARR_STACK" == true ]]; then
|
if [[ "$SKIP_ARR_STACK" == true ]]; then
|
||||||
warn "Skipping (--skip-arr-stack)"
|
warn "Skipping (--skip-arr-stack)"
|
||||||
@@ -777,7 +549,7 @@ fi
|
|||||||
|
|
||||||
# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
|
# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Step 7 — Partnership Onboard ━━━"
|
echo "━━━ Step 6 — Partnership Onboard ━━━"
|
||||||
|
|
||||||
if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
|
if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
|
||||||
echo "Partnership onboard complete ✅"
|
echo "Partnership onboard complete ✅"
|
||||||
@@ -789,7 +561,7 @@ fi
|
|||||||
|
|
||||||
# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
|
# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Step 8 — Arr Library Bootstrap ━━━"
|
echo "━━━ Step 7 — Arr Library Bootstrap ━━━"
|
||||||
|
|
||||||
if [[ "$ONBOARD_OK" == false ]]; then
|
if [[ "$ONBOARD_OK" == false ]]; then
|
||||||
warn "Skipping — onboard did not complete"
|
warn "Skipping — onboard did not complete"
|
||||||
@@ -809,7 +581,7 @@ fi
|
|||||||
# SSH is now established and all partners have the plugin installed.
|
# SSH is now established and all partners have the plugin installed.
|
||||||
# Push the authoritative master.conf so every listed host is in sync immediately.
|
# Push the authoritative master.conf so every listed host is in sync immediately.
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_GEAR Step 9 — master.conf Push ━━━"
|
echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"
|
||||||
|
|
||||||
if [[ "$ONBOARD_OK" == false ]]; then
|
if [[ "$ONBOARD_OK" == false ]]; then
|
||||||
warn "Skipping — onboard did not complete"
|
warn "Skipping — onboard did not complete"
|
||||||
@@ -857,14 +629,13 @@ _ok() { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
|
|||||||
_skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
|
_skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
|
||||||
|
|
||||||
echo " Step 1 — SSH keys: $(_skip "$SKIP_SSH" "$STEP_SSH_OK")"
|
echo " Step 1 — SSH keys: $(_skip "$SKIP_SSH" "$STEP_SSH_OK")"
|
||||||
echo " Step 2 — Plugins: $(_ok "$STEP_PLUGINS_OK")"
|
echo " Step 2 — Stop auth: $(_skip "$SKIP_AUTH_STACK" "$STEP_STOP_AUTH_OK")"
|
||||||
echo " Step 3 — Stop auth: $(_skip "$SKIP_AUTH_STACK" "$STEP_STOP_AUTH_OK")"
|
echo " Step 3 — Auth stack: $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
|
||||||
echo " Step 4 — Auth stack: $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
|
echo " Step 4 — Stop arr: $(_skip "$SKIP_ARR_STACK" "$STEP_STOP_ARR_OK")"
|
||||||
echo " Step 5 — Stop arr: $(_skip "$SKIP_ARR_STACK" "$STEP_STOP_ARR_OK")"
|
echo " Step 5 — Arr stack: $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
|
||||||
echo " Step 6 — Arr stack: $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
|
echo " Step 6 — Onboard: $(_ok "$ONBOARD_OK")"
|
||||||
echo " Step 7 — Onboard: $(_ok "$ONBOARD_OK")"
|
echo " Step 7 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
|
||||||
echo " Step 8 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
|
echo " Step 8 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
|
||||||
echo " Step 9 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ "$ONBOARD_OK" == true ]]; then
|
if [[ "$ONBOARD_OK" == true ]]; then
|
||||||
|
|||||||
@@ -45,14 +45,16 @@ source "$SCRIPTS_ROOT/load_config.sh"
|
|||||||
# ── Parse --force before parse_args ───────────────────────────────────────────────────────────
|
# ── Parse --force before parse_args ───────────────────────────────────────────────────────────
|
||||||
MODE="setup"
|
MODE="setup"
|
||||||
FORCE=false
|
FORCE=false
|
||||||
|
LOCAL_ONLY=false
|
||||||
FILTERED_ARGS=()
|
FILTERED_ARGS=()
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--force) FORCE=true ;;
|
--force) FORCE=true ;;
|
||||||
--validate) MODE="validate" ;;
|
--validate) MODE="validate" ;;
|
||||||
--status) MODE="status" ;;
|
--status) MODE="status" ;;
|
||||||
*) FILTERED_ARGS+=("$arg") ;;
|
--local-only) LOCAL_ONLY=true ;;
|
||||||
|
*) FILTERED_ARGS+=("$arg") ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -325,6 +327,19 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Copy to remote ────────────────────────────────────────────────────────────────────────────
|
# ── Copy to remote ────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$LOCAL_ONLY" == true ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━ $ICON_SUMMARY SSH SETUP SUMMARY (local) ━━━━━"
|
||||||
|
echo " Key: $SSH_KEY_PATH"
|
||||||
|
echo " Pubkey: $SSH_PUB_PATH"
|
||||||
|
echo " Conf: ${KEY_CONF_VAR} in host${HOST_NUM}.conf"
|
||||||
|
echo ""
|
||||||
|
warn "Local setup complete — copy public key to remote manually:"
|
||||||
|
warn " ssh-copy-id -i $SSH_PUB_PATH root@<remote>"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_NET Copy Public Key to Remote ($REMOTE_SERVER_NAME) ━━━"
|
echo "━━━ $ICON_NET Copy Public Key to Remote ($REMOTE_SERVER_NAME) ━━━"
|
||||||
|
|
||||||
@@ -356,7 +371,6 @@ echo "━━━ $ICON_VERIFY Verify SSH Auth ━━━"
|
|||||||
if [[ "$DRY_RUN" == false ]]; then
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
if test_ssh_auth "$REMOTE_SERVER"; then
|
if test_ssh_auth "$REMOTE_SERVER"; then
|
||||||
echo "SSH auth to $REMOTE_SERVER_NAME working ✅"
|
echo "SSH auth to $REMOTE_SERVER_NAME working ✅"
|
||||||
# Reset any existing strikes
|
|
||||||
if [[ -f "$SSH_STRIKE_FILE" ]]; then
|
if [[ -f "$SSH_STRIKE_FILE" ]]; then
|
||||||
write_strike_file 0 "" "$(date '+%Y-%m-%d %H:%M:%S')"
|
write_strike_file 0 "" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+338
@@ -0,0 +1,338 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================================
|
||||||
|
# ====================== Partnership — Unraid Container Adapter ================================
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# Sourced by partnership_onboard.sh and partnership_offboard.sh via:
|
||||||
|
# source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
|
||||||
|
#
|
||||||
|
# Provides container deploy/cleanup functions specific to the Unraid platform:
|
||||||
|
# - Docker container deployment from Unraid CA XML templates
|
||||||
|
#
|
||||||
|
# Functions use variables from the calling script's scope (sourced, not exec'd):
|
||||||
|
# MIRROR, MIRROR_IP, MIRROR_SSH_KEY, SSH_TIMEOUT, DRY_RUN, SCRIPTS_ROOT
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
|
||||||
|
|
||||||
|
_STACK_DEPLOYED=0
|
||||||
|
_STACK_FAILED=0
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Wait for a container on the remote to be healthy/running ─────────────────────────────────
|
||||||
|
#
|
||||||
|
# Polls docker inspect on the remote. Prefers the health status if a healthcheck is defined;
|
||||||
|
# falls back to the running state. Non-fatal after timeout — some containers take time to
|
||||||
|
# fully initialize but the deploy itself succeeded.
|
||||||
|
# ==============================================================================================
|
||||||
|
wait_for_container_healthy() {
|
||||||
|
local name="$1" remote_ip="$2" ssh_key="$3"
|
||||||
|
local max_wait=60 interval=5 elapsed=0
|
||||||
|
|
||||||
|
[[ "$DRY_RUN" == true ]] && return 0
|
||||||
|
|
||||||
|
log " Waiting for $name to be ready..."
|
||||||
|
while (( elapsed < max_wait )); do
|
||||||
|
local status
|
||||||
|
status=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
|
||||||
|
"h=\$(docker inspect --format '{{.State.Health.Status}}' '$name' 2>/dev/null)
|
||||||
|
r=\$(docker inspect --format '{{.State.Running}}' '$name' 2>/dev/null)
|
||||||
|
echo \${h:-\$r}" 2>/dev/null)
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
healthy|true)
|
||||||
|
log " $name ready ✅"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
sleep "$interval"
|
||||||
|
(( elapsed += interval ))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
warn " $name not confirmed healthy after ${max_wait}s — continuing (may affect dependents)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Deploy a container from a local Unraid CA XML template to a remote host ──────────────────
|
||||||
|
#
|
||||||
|
# Parses Port / Path / Variable Config entries from the Unraid XML, SCPs the template and a
|
||||||
|
# self-contained deploy script to the remote, executes it, then cleans up both sides.
|
||||||
|
# Credentials are never passed as SSH command-line args — they stay in the SCPed script.
|
||||||
|
# ==============================================================================================
|
||||||
|
deploy_container_from_xml() {
|
||||||
|
local xml_file="$1" remote_ip="$2" ssh_key="$3"
|
||||||
|
local xml_name
|
||||||
|
xml_name=$(basename "$xml_file")
|
||||||
|
|
||||||
|
local name repo network extra privileged
|
||||||
|
name=$( awk 'match($0,/<Name>([^<]+)<\/Name>/, a){print a[1];exit}' "$xml_file")
|
||||||
|
repo=$( awk 'match($0,/<Repository>([^<]+)<\/Repository>/,a){print a[1];exit}' "$xml_file")
|
||||||
|
network=$( awk 'match($0,/<Network>([^<]+)<\/Network>/, a){print a[1];exit}' "$xml_file")
|
||||||
|
extra=$( awk 'match($0,/<ExtraParams>([^<]*)<\/ExtraParams>/,a){print a[1];exit}' "$xml_file")
|
||||||
|
privileged=$( awk 'match($0,/<Privileged>([^<]+)<\/Privileged>/,a){print a[1];exit}' "$xml_file")
|
||||||
|
|
||||||
|
if [[ -z "$name" || -z "$repo" ]]; then
|
||||||
|
warn " Cannot parse Name/Repository from $xml_name — skipping"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Deploying $name..."
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
|
timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
||||||
|
"$xml_file" "root@${remote_ip}:${TEMPLATES_DIR}/${xml_name}" 2>/dev/null || {
|
||||||
|
warn " SCP failed for $xml_name — skipping $name"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
warn " DRY RUN — would SCP $xml_name → $MIRROR:${TEMPLATES_DIR}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_script
|
||||||
|
tmp_script=$(mktemp /tmp/deploy_XXXXXX.sh)
|
||||||
|
chmod 600 "$tmp_script"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "#!/bin/bash"
|
||||||
|
echo "set -e"
|
||||||
|
echo ""
|
||||||
|
printf "docker pull %q 2>/dev/null || true\n" "$repo"
|
||||||
|
printf "docker stop %q 2>/dev/null || true\n" "$name"
|
||||||
|
printf "docker rm %q 2>/dev/null || true\n" "$name"
|
||||||
|
echo ""
|
||||||
|
printf "docker create --name %q --restart=unless-stopped" "$name"
|
||||||
|
[[ -n "$network" ]] && printf " --network=%q" "$network"
|
||||||
|
[[ "$privileged" == "true" ]] && printf " --privileged"
|
||||||
|
[[ -n "$extra" ]] && printf " %s" "$extra"
|
||||||
|
|
||||||
|
# Port mappings → -p host:container/proto
|
||||||
|
awk '/Type="Port"/ {
|
||||||
|
match($0, /Target="([^"]+)"/, t)
|
||||||
|
match($0, /Mode="([^"]+)"/, m)
|
||||||
|
match($0, />([^<]+)<\/Config>/, v)
|
||||||
|
if (t[1] != "" && v[1] != "") {
|
||||||
|
proto = (m[1] == "udp") ? "udp" : "tcp"
|
||||||
|
printf " -p %s:%s/%s", v[1], t[1], proto
|
||||||
|
}
|
||||||
|
}' "$xml_file"
|
||||||
|
|
||||||
|
# Volume mappings → -v 'host:container:mode'
|
||||||
|
awk 'BEGIN{q=sprintf("%c",39)} /Type="Path"/ {
|
||||||
|
match($0, /Target="([^"]+)"/, t)
|
||||||
|
match($0, /Mode="([^"]+)"/, m)
|
||||||
|
match($0, />([^<]+)<\/Config>/, v)
|
||||||
|
if (t[1] != "" && v[1] != "") {
|
||||||
|
mode = (m[1] == "ro") ? "ro" : "rw"
|
||||||
|
printf " -v %s%s:%s:%s%s", q, v[1], t[1], mode, q
|
||||||
|
}
|
||||||
|
}' "$xml_file"
|
||||||
|
|
||||||
|
# Environment variables → -e 'KEY=VALUE' (single-quoted to protect $ and special chars)
|
||||||
|
awk 'BEGIN{q=sprintf("%c",39)} /Type="Variable"/ {
|
||||||
|
match($0, /Target="([^"]+)"/, t)
|
||||||
|
match($0, />([^<]+)<\/Config>/, v)
|
||||||
|
if (t[1] != "" && v[1] != "") {
|
||||||
|
printf " -e %s%s=%s%s", q, t[1], v[1], q
|
||||||
|
}
|
||||||
|
}' "$xml_file"
|
||||||
|
|
||||||
|
printf " %q\n" "$repo"
|
||||||
|
echo ""
|
||||||
|
printf "docker start %q && echo 'deployed:%s'\n" "$name" "$name"
|
||||||
|
} > "$tmp_script"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn " DRY RUN — would deploy $name on $MIRROR"
|
||||||
|
rm -f "$tmp_script"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local remote_script="/tmp/deploy_${name//[^a-zA-Z0-9_]/_}.sh"
|
||||||
|
|
||||||
|
if timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
||||||
|
"$tmp_script" "root@${remote_ip}:${remote_script}" 2>/dev/null && \
|
||||||
|
timeout 120 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||||
|
"bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
|
||||||
|
grep -q "deployed:${name}"; then
|
||||||
|
log " $name deployed ✅"
|
||||||
|
rm -f "$tmp_script"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn " $name deployment failed — check $MIRROR manually"
|
||||||
|
rm -f "$tmp_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Deploy a stack of Unraid CA XMLs to the mirror ───────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Sets globals _STACK_DEPLOYED and _STACK_FAILED rather than printing to stdout.
|
||||||
|
# Health-checks database deps (Mariadb/Redis/Postgres) between batches so dependents
|
||||||
|
# (e.g. Authelia) start cleanly.
|
||||||
|
# ==============================================================================================
|
||||||
|
deploy_xml_stack() {
|
||||||
|
local -n xml_array_ref="$1"
|
||||||
|
_STACK_DEPLOYED=0
|
||||||
|
_STACK_FAILED=0
|
||||||
|
|
||||||
|
for xml_name in "${xml_array_ref[@]}"; do
|
||||||
|
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
||||||
|
if [[ ! -f "$xml_file" ]]; then
|
||||||
|
warn "$xml_name not found in $TEMPLATES_DIR — skipping"
|
||||||
|
(( _STACK_FAILED++ ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cname
|
||||||
|
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
||||||
|
|
||||||
|
if deploy_container_from_xml "$xml_file" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then
|
||||||
|
(( _STACK_DEPLOYED++ ))
|
||||||
|
if [[ -n "$cname" ]] && echo "$cname" | grep -qiE 'mariadb|redis|postgres|mysql'; then
|
||||||
|
wait_for_container_healthy "$cname" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
(( _STACK_FAILED++ ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Remove owner-deployed containers from a remote host ──────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Uses PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK (owner's conf) to derive container
|
||||||
|
# names from local XML templates. SSHes to remote to stop, remove, and delete appdata.
|
||||||
|
# Appdata paths collected via docker inspect before removal. Safety gate: only
|
||||||
|
# /mnt/*/appdata* paths are deleted.
|
||||||
|
# ==============================================================================================
|
||||||
|
cleanup_deployed_stack_on_remote() {
|
||||||
|
local remote_ip="$1" ssh_key="$2"
|
||||||
|
local -a xml_names=()
|
||||||
|
[[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
|
||||||
|
[[ ${#PARTNERSHIP_ARR_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
|
||||||
|
|
||||||
|
if [[ ${#xml_names[@]} -eq 0 ]]; then
|
||||||
|
log "No auth/arr stack arrays configured — skipping deployed stack cleanup"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Removing owner-deployed containers (auth/arr stacks) from $MIRROR..."
|
||||||
|
for xml_name in "${xml_names[@]}"; do
|
||||||
|
[[ -z "$xml_name" ]] && continue
|
||||||
|
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
||||||
|
if [[ ! -f "$xml_file" ]]; then
|
||||||
|
warn " $xml_name not found in local $TEMPLATES_DIR — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cname
|
||||||
|
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
||||||
|
[[ -z "$cname" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn " DRY RUN — would stop + rm $cname on $MIRROR"
|
||||||
|
warn " DRY RUN — would delete appdata for $cname on $MIRROR"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local appdata_paths
|
||||||
|
appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
|
||||||
|
"docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$cname' 2>/dev/null \
|
||||||
|
| awk -F: '{print \$1}' | grep '^/mnt/.*/appdata'" 2>/dev/null)
|
||||||
|
|
||||||
|
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||||
|
"docker stop '$cname' >/dev/null 2>&1
|
||||||
|
docker rm '$cname' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
|
||||||
|
grep -q removed && \
|
||||||
|
log " $cname removed from $MIRROR ✅" || \
|
||||||
|
log " $cname not found on $MIRROR — skipping"
|
||||||
|
|
||||||
|
while IFS= read -r path; do
|
||||||
|
[[ -z "$path" ]] && continue
|
||||||
|
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||||
|
"rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
|
||||||
|
log " Appdata removed on $MIRROR: $path ✅" || \
|
||||||
|
warn " Failed to remove appdata on $MIRROR: $path"
|
||||||
|
done <<< "$appdata_paths"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Remove owner-deployed containers locally (mirror-initiated offboard) ─────────────────────
|
||||||
|
#
|
||||||
|
# SSHes to owner to read PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK, then uses the
|
||||||
|
# local templates-user/ copies (SCPed there during onboard) to get container names and
|
||||||
|
# appdata paths. Appdata collected before removal. Skips gracefully if owner unreachable.
|
||||||
|
# ==============================================================================================
|
||||||
|
cleanup_deployed_stack_locally() {
|
||||||
|
local owner_ip="$1" ssh_key="$2"
|
||||||
|
local -a xml_names=()
|
||||||
|
|
||||||
|
if [[ -n "$owner_ip" ]]; then
|
||||||
|
local -a auth_arr arr_arr
|
||||||
|
mapfile -t auth_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
|
||||||
|
"source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
|
||||||
|
detect_hosts 2>/dev/null
|
||||||
|
printf '%s\n' \"\${PARTNERSHIP_AUTH_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
|
||||||
|
mapfile -t arr_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
|
||||||
|
"source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
|
||||||
|
detect_hosts 2>/dev/null
|
||||||
|
printf '%s\n' \"\${PARTNERSHIP_ARR_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
|
||||||
|
xml_names=("${auth_arr[@]}" "${arr_arr[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#xml_names[@]} -eq 0 ]]; then
|
||||||
|
log "Could not read deployed stack from owner — skipping auth/arr cleanup"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Removing owner-deployed containers (auth/arr stacks) locally..."
|
||||||
|
for xml_name in "${xml_names[@]}"; do
|
||||||
|
[[ -z "$xml_name" ]] && continue
|
||||||
|
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
||||||
|
if [[ ! -f "$xml_file" ]]; then
|
||||||
|
warn " $xml_name not found locally — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cname
|
||||||
|
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
||||||
|
[[ -z "$cname" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn " DRY RUN — would stop + rm $cname"
|
||||||
|
warn " DRY RUN — would delete appdata for $cname"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local appdata_paths=""
|
||||||
|
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$cname" >/dev/null 2>&1; then
|
||||||
|
appdata_paths=$(docker inspect \
|
||||||
|
--format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
|
||||||
|
"$cname" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
|
||||||
|
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$cname" >/dev/null 2>&1 || true
|
||||||
|
_PM_TRAP_STOPPED+=("$cname")
|
||||||
|
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$cname" >/dev/null 2>&1 && \
|
||||||
|
log " $cname removed ✅" || warn " $cname rm failed"
|
||||||
|
else
|
||||||
|
log " $cname not found locally — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r path; do
|
||||||
|
[[ -z "$path" ]] && continue
|
||||||
|
rm -rf "$path" && log " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
||||||
|
done <<< "$appdata_paths"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
+1
-1
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
acquire_lock
|
acquire_lock
|
||||||
+1
-1
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
//
|
//
|
||||||
// Called by api_cache_writer.sh (bash wrapper required by the scheduler).
|
// Called by api_cache_writer.sh (bash wrapper required by the scheduler).
|
||||||
|
|
||||||
$_base = dirname(__DIR__) . '/Plugin/unraid';
|
$_base = dirname(__DIR__);
|
||||||
require_once $_base . '/include/monitor.php';
|
require_once $_base . '/include/monitor.php';
|
||||||
require_once $_base . '/include/vms.php';
|
require_once $_base . '/include/vms.php';
|
||||||
require_once $_base . '/include/docker_folders.php';
|
require_once $_base . '/include/docker_folders.php';
|
||||||
@@ -16,17 +16,29 @@
|
|||||||
# AUTO-DETECTED FIELDS
|
# AUTO-DETECTED FIELDS
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
#
|
#
|
||||||
|
# HOSTN_OWNER from hostname (strip unRAID- prefix, lowercase)
|
||||||
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
|
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
|
||||||
|
# HOSTN_RADARR_URL from Radarr config.xml port
|
||||||
# HOSTN_SONARR_API_KEY from Sonarr config.xml
|
# HOSTN_SONARR_API_KEY from Sonarr config.xml
|
||||||
|
# HOSTN_SONARR_URL from Sonarr config.xml port
|
||||||
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
|
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
|
||||||
# HOSTN_SLSKD_API_KEY from slskd config.yml
|
# HOSTN_LIDARR_URL from Lidarr config.xml port
|
||||||
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
|
|
||||||
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
|
|
||||||
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
|
|
||||||
# HOSTN_RADARR_MOVIE_ROOT from Radarr rootFolder API
|
# HOSTN_RADARR_MOVIE_ROOT from Radarr rootFolder API
|
||||||
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
|
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
|
||||||
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
|
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
|
||||||
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
|
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
|
||||||
|
# HOSTN_SABNZBD_URL from sabnzbd.ini port
|
||||||
|
# HOSTN_SLSKD_API_KEY from slskd config.yml
|
||||||
|
# HOSTN_SLSKD_URL from slskd config.yml port
|
||||||
|
# HOSTN_QBIT_URL from qBittorrent.conf WebUI port
|
||||||
|
# HOSTN_QBIT_USERNAME from qBittorrent.conf WebUI username
|
||||||
|
# HOSTN_QBIT_PASSWORD from qBittorrent.conf WebUI password (plaintext only)
|
||||||
|
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
|
||||||
|
# HOSTN_EMBY_URL from docker port binding
|
||||||
|
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
|
||||||
|
# HOSTN_JELLYFIN_URL from docker port binding
|
||||||
|
# HOSTN_TRANSCODE_SSD from Emby/Jellyfin container /transcode volume mount
|
||||||
|
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# RUNTIME MODES
|
# RUNTIME MODES
|
||||||
@@ -78,7 +90,6 @@ _set_conf_var() {
|
|||||||
local var_name="$1" value="$2" label="$3"
|
local var_name="$1" value="$2" label="$3"
|
||||||
[[ -z "$value" ]] && return
|
[[ -z "$value" ]] && return
|
||||||
|
|
||||||
# Check current value in conf
|
|
||||||
local current
|
local current
|
||||||
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$CONF_FILE" 2>/dev/null | head -1)
|
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$CONF_FILE" 2>/dev/null | head -1)
|
||||||
|
|
||||||
@@ -93,7 +104,6 @@ _set_conf_var() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update or append the var line
|
|
||||||
if grep -q "^\s*${var_name}=" "$CONF_FILE"; then
|
if grep -q "^\s*${var_name}=" "$CONF_FILE"; then
|
||||||
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$CONF_FILE"
|
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$CONF_FILE"
|
||||||
else
|
else
|
||||||
@@ -104,13 +114,10 @@ _set_conf_var() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── Helper: find arr config dir via docker volume mount ───────────────────────
|
# ── Helper: find arr config dir via docker volume mount ───────────────────────
|
||||||
# Looks for a container matching the pattern, then reads its /config volume path.
|
|
||||||
# Falls back to DOCKER_APPDATA_BASE/<ContainerName> if volume not found.
|
|
||||||
_arr_config_dir() {
|
_arr_config_dir() {
|
||||||
local pattern="$1"
|
local pattern="$1"
|
||||||
local container_name
|
local container_name
|
||||||
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | \
|
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
||||||
grep -im1 "^${pattern}")
|
|
||||||
[[ -z "$container_name" ]] && return 1
|
[[ -z "$container_name" ]] && return 1
|
||||||
|
|
||||||
local config_path
|
local config_path
|
||||||
@@ -127,8 +134,31 @@ _xml_val() {
|
|||||||
grep -oP "(?<=<${tag}>)[^<]+" "$file" 2>/dev/null | head -1
|
grep -oP "(?<=<${tag}>)[^<]+" "$file" 2>/dev/null | head -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Helper: get host-side port for a container's internal port ────────────────
|
||||||
|
_docker_host_port() {
|
||||||
|
local container="$1" container_port="$2"
|
||||||
|
docker inspect "$container" 2>/dev/null | \
|
||||||
|
jq -r --arg p "${container_port}/tcp" \
|
||||||
|
'.[0].NetworkSettings.Ports[$p]?[0].HostPort // empty' 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helper: get host path for a container destination mount ──────────────────
|
||||||
|
_docker_volume_host() {
|
||||||
|
local container="$1" dest="$2"
|
||||||
|
docker inspect "$container" 2>/dev/null | \
|
||||||
|
jq -r --arg d "$dest" \
|
||||||
|
'.[0].Mounts[]? | select(.Destination == $d) | .Source' 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── Arr API keys + root paths ─────────────────────────────────────────────────────────────────
|
# ── Owner short name ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
owner=$(echo "$LOCAL_SERVER_NAME" | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//i' | tr '[:upper:]' '[:lower:]')
|
||||||
|
_set_conf_var "${MY_ID}_OWNER" "$owner" "Owner short name"
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Arr API keys + URLs + root paths ─────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
for arr in radarr sonarr lidarr; do
|
for arr in radarr sonarr lidarr; do
|
||||||
@@ -146,64 +176,106 @@ for arr in radarr sonarr lidarr; do
|
|||||||
|
|
||||||
key=$(_xml_val "$config_xml" "ApiKey")
|
key=$(_xml_val "$config_xml" "ApiKey")
|
||||||
port=$(_xml_val "$config_xml" "Port")
|
port=$(_xml_val "$config_xml" "Port")
|
||||||
url_base="http://localhost:${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
|
port="${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
|
||||||
|
url_base="http://localhost:${port}"
|
||||||
|
|
||||||
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
|
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
|
||||||
|
_set_conf_var "${MY_ID}_${arr_upper}_URL" "$url_base" "${arr_upper} URL"
|
||||||
|
|
||||||
# Root paths from arr's own rootFolder API
|
|
||||||
if [[ -n "$key" ]]; then
|
if [[ -n "$key" ]]; then
|
||||||
local api_ver; case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac
|
case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac
|
||||||
root_json=$(curl -sf --max-time 5 \
|
root_json=$(curl -sf --max-time 5 \
|
||||||
-H "X-Api-Key: $key" "${url_base}/api/${api_ver}/rootfolder" 2>/dev/null)
|
-H "X-Api-Key: $key" "${url_base}/api/${api_ver}/rootfolder" 2>/dev/null)
|
||||||
root_path=$(echo "$root_json" | jq -r '.[0].path // empty' 2>/dev/null)
|
root_path=$(echo "$root_json" | jq -r '.[0].path // empty' 2>/dev/null)
|
||||||
|
|
||||||
case "$arr" in
|
case "$arr" in
|
||||||
radarr) _set_conf_var "${MY_ID}_RADARR_MOVIE_ROOT" "$root_path" "Radarr movie root" ;;
|
radarr) _set_conf_var "${MY_ID}_RADARR_MOVIE_ROOT" "$root_path" "Radarr movie root" ;;
|
||||||
sonarr) _set_conf_var "${MY_ID}_SONARR_TV_ROOT" "$root_path" "Sonarr TV root" ;;
|
sonarr) _set_conf_var "${MY_ID}_SONARR_TV_ROOT" "$root_path" "Sonarr TV root" ;;
|
||||||
lidarr) _set_conf_var "${MY_ID}_LIDARR_MUSIC_ROOT" "$root_path" "Lidarr music root" ;;
|
lidarr) _set_conf_var "${MY_ID}_LIDARR_MUSIC_ROOT" "$root_path" "Lidarr music root" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── SABnzbd API key ───────────────────────────────────────────────────────────────────────────
|
# ── SABnzbd API key + URL ─────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
sab_dir=$(_arr_config_dir "sabnzbd") && {
|
sab_dir=$(_arr_config_dir "sabnzbd") && {
|
||||||
sab_ini=$(find "$sab_dir" -maxdepth 2 -name "sabnzbd.ini" 2>/dev/null | head -1)
|
sab_ini=$(find "$sab_dir" -maxdepth 2 -name "sabnzbd.ini" 2>/dev/null | head -1)
|
||||||
if [[ -f "$sab_ini" ]]; then
|
if [[ -f "$sab_ini" ]]; then
|
||||||
sab_key=$(grep -oP '(?<=^api_key\s*=\s*)\S+' "$sab_ini" 2>/dev/null | head -1)
|
sab_key=$(grep -oP '(?<=^api_key\s*=\s*)\S+' "$sab_ini" 2>/dev/null | head -1)
|
||||||
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
|
sab_port=$(grep -oP '(?<=^port\s*=\s*)\d+' "$sab_ini" 2>/dev/null | head -1)
|
||||||
|
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
|
||||||
|
_set_conf_var "${MY_ID}_SABNZBD_URL" "http://localhost:${sab_port:-8080}" "SABnzbd URL"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── slskd API key ─────────────────────────────────────────────────────────────────────────────
|
# ── slskd API key + URL ───────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
slskd_dir=$(_arr_config_dir "slskd") && {
|
slskd_dir=$(_arr_config_dir "slskd") && {
|
||||||
slskd_yml=$(find "$slskd_dir" -maxdepth 2 -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -1)
|
slskd_yml=$(find "$slskd_dir" -maxdepth 2 \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | head -1)
|
||||||
if [[ -f "$slskd_yml" ]]; then
|
if [[ -f "$slskd_yml" ]]; then
|
||||||
slskd_key=$(grep -oP '(?<=api_key:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
slskd_key=$(grep -oP '(?<=api_key:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
||||||
[[ -z "$slskd_key" ]] && \
|
[[ -z "$slskd_key" ]] && \
|
||||||
slskd_key=$(grep -oP '(?<=apikey:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
slskd_key=$(grep -oP '(?<=apikey:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
||||||
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
|
slskd_port=$(grep -oP '(?<=port:\s)\d+' "$slskd_yml" 2>/dev/null | head -1)
|
||||||
|
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
|
||||||
|
_set_conf_var "${MY_ID}_SLSKD_URL" "http://localhost:${slskd_port:-5030}" "slskd URL"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── Container names ───────────────────────────────────────────────────────────────────────────
|
# ── qBittorrent URL + credentials ────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
|
qbit_dir=$(_arr_config_dir "qbittorrent") && {
|
||||||
|
qbit_conf=$(find "$qbit_dir" -maxdepth 3 -name "qBittorrent.conf" 2>/dev/null | head -1)
|
||||||
|
if [[ -f "$qbit_conf" ]]; then
|
||||||
|
qbit_port=$(grep -oP '(?<=WebUI\\Port=)\d+' "$qbit_conf" 2>/dev/null | head -1)
|
||||||
|
qbit_user=$(grep -oP '(?<=WebUI\\Username=)\S+' "$qbit_conf" 2>/dev/null | head -1)
|
||||||
|
# Only capture plaintext password — PBKDF2 hashes are not usable
|
||||||
|
qbit_pass=$(grep -oP '(?<=WebUI\\Password=)[^\r\n]+' "$qbit_conf" 2>/dev/null | \
|
||||||
|
grep -v '@ByteArray' | head -1)
|
||||||
|
_set_conf_var "${MY_ID}_QBIT_URL" "http://localhost:${qbit_port:-8080}" "qBittorrent URL"
|
||||||
|
_set_conf_var "${MY_ID}_QBIT_USERNAME" "$qbit_user" "qBittorrent username"
|
||||||
|
_set_conf_var "${MY_ID}_QBIT_PASSWORD" "$qbit_pass" "qBittorrent password"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Media server container names + URLs + transcode path ─────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
transcode_dir=""
|
||||||
|
|
||||||
for pattern in "emby" "jellyfin"; do
|
for pattern in "emby" "jellyfin"; do
|
||||||
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
||||||
[[ -z "$container" ]] && continue
|
[[ -z "$container" ]] && continue
|
||||||
|
|
||||||
case "$pattern" in
|
case "$pattern" in
|
||||||
emby) _set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name" ;;
|
emby)
|
||||||
jellyfin) _set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name" ;;
|
host_port=$(_docker_host_port "$container" "8096")
|
||||||
|
_set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name"
|
||||||
|
_set_conf_var "${MY_ID}_EMBY_URL" "http://localhost:${host_port:-8096}" "Emby URL"
|
||||||
|
;;
|
||||||
|
jellyfin)
|
||||||
|
host_port=$(_docker_host_port "$container" "8096")
|
||||||
|
_set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name"
|
||||||
|
_set_conf_var "${MY_ID}_JELLYFIN_URL" "http://localhost:${host_port:-8095}" "Jellyfin URL"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Transcode path: first container with a /transcode mount wins
|
||||||
|
if [[ -z "$transcode_dir" ]]; then
|
||||||
|
transcode_dir=$(_docker_volume_host "$container" "/transcode")
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
[[ -n "$transcode_dir" ]] && \
|
||||||
|
_set_conf_var "${MY_ID}_TRANSCODE_SSD" "${transcode_dir%/}/" "Transcode SSD path"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── Network interface ─────────────────────────────────────────────────────────────────────────
|
# ── Network interface ─────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
+3
-3
@@ -38,7 +38,7 @@ for arg in "$@"; do
|
|||||||
case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac
|
case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac
|
||||||
done
|
done
|
||||||
|
|
||||||
mkdir -p /tmp/vv_cache
|
mkdir -p "$VV_CACHE_DIR"
|
||||||
|
|
||||||
log "$ICON_GEAR Config: target=${TARGET_HOST:-all hosts} ssh-key=${SSH_KEY}"
|
log "$ICON_GEAR Config: target=${TARGET_HOST:-all hosts} ssh-key=${SSH_KEY}"
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
host_id="${host_var,,}" # host1, host2, …
|
host_id="${host_var,,}" # host1, host2, …
|
||||||
cache_file="/tmp/vv_cache/arrs_remote_${host_id}.json"
|
cache_file="$VV_CACHE_DIR/arrs_remote_${host_id}.json"
|
||||||
|
|
||||||
echo " $host_var ($hostname)…"
|
echo " $host_var ($hostname)…"
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
|
|||||||
" 2>/dev/null
|
" 2>/dev/null
|
||||||
|
|
||||||
# Save monitor cache
|
# Save monitor cache
|
||||||
MONITOR_CACHE="/tmp/vv_cache/monitor_remote_${host_id}.json"
|
MONITOR_CACHE="$VV_CACHE_DIR/monitor_remote_${host_id}.json"
|
||||||
echo "$RESULT" | php -r "
|
echo "$RESULT" | php -r "
|
||||||
\$d = json_decode(file_get_contents('php://stdin'), true);
|
\$d = json_decode(file_get_contents('php://stdin'), true);
|
||||||
file_put_contents('$MONITOR_CACHE', json_encode(\$d['monitor']));
|
file_put_contents('$MONITOR_CACHE', json_encode(\$d['monitor']));
|
||||||
@@ -278,7 +278,7 @@ fi
|
|||||||
if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then
|
if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━"
|
echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━"
|
||||||
if rsync -a --delete "$DST/Plugin/" "/boot/config/plugins/varaverk/Plugin/" 2>/dev/null; then
|
if rsync -a --delete "$DST/Plugin/" "$INTERNAL_DIR/Plugin/" 2>/dev/null; then
|
||||||
echo " Plugin/ synced to /boot/ ✅"
|
echo " Plugin/ synced to /boot/ ✅"
|
||||||
else
|
else
|
||||||
warn "Plugin/ sync to /boot/ failed — webUI may be stale"
|
warn "Plugin/ sync to /boot/ failed — webUI may be stale"
|
||||||
@@ -40,12 +40,15 @@ $tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'D
|
|||||||
<?= $tabLabels[$t] ?? ucfirst($t) ?>
|
<?= $tabLabels[$t] ?? ucfirst($t) ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<a href="https://github.com/FailedProxy/Varaverk" target="_blank"
|
<div style="margin-left:auto;display:flex;align-items:center;gap:2px;">
|
||||||
style="margin-left:auto;padding:0 10px;font-size:10px;color:#333;text-decoration:none;
|
<a href="https://github.com/FailedProxy/Varaverk" target="_blank"
|
||||||
display:flex;align-items:center;letter-spacing:.03em;"
|
style="padding:0 10px;font-size:10px;color:#333;text-decoration:none;
|
||||||
title="GitHub — source, issues, changelog">
|
display:flex;align-items:center;letter-spacing:.03em;"
|
||||||
⎋ GitHub
|
title="GitHub — source, issues, changelog">
|
||||||
</a>
|
⎋ GitHub
|
||||||
|
</a>
|
||||||
|
<button id="vv-expand-btn" onclick="vvToggleExpand()" title="Expand">⤢</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab content -->
|
<!-- Tab content -->
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../../load_config.sh"
|
source "$SCRIPT_DIR/../../../../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ $result = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Show last debug log if present
|
// Show last debug log if present
|
||||||
$debugFile = '/tmp/vv_api_debug.json';
|
$debugFile = VV_CACHE_DIR . '/vv_api_debug.json';
|
||||||
if (file_exists($debugFile)) {
|
if (file_exists($debugFile)) {
|
||||||
$result['debug_log'] = json_decode(file_get_contents($debugFile), true);
|
$result['debug_log'] = json_decode(file_get_contents($debugFile), true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if ($_action === 'refresh_remote') {
|
|||||||
if (!preg_match('/^host\d+$/', $host)) {
|
if (!preg_match('/^host\d+$/', $host)) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
|
echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
|
||||||
}
|
}
|
||||||
$script = dirname(__DIR__) . '/tools/remote_arr_cache_writer.sh';
|
$script = dirname(__DIR__) . '/Tools/remote_arr_cache_writer.sh';
|
||||||
if (!file_exists($script)) {
|
if (!file_exists($script)) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
|
echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
|
$action = ($_SERVER['REQUEST_METHOD'] === 'POST')
|
||||||
|
? trim($_POST['action'] ?? '')
|
||||||
|
: trim($_GET['action'] ?? '');
|
||||||
|
|
||||||
|
$cacheFile = STATE_DIR . '/cert_status.json';
|
||||||
|
|
||||||
|
// ── Read configured domains (without running checks) ─────────────────────────
|
||||||
|
if ($action === 'domains') {
|
||||||
|
$hostId = vv_detect_host();
|
||||||
|
$hostIdUp = strtoupper($hostId);
|
||||||
|
$confRaw = ($hostId !== 'unknown') ? vv_read_conf_raw($hostId . '.conf') : '';
|
||||||
|
$master = vv_read_conf_raw('master.conf');
|
||||||
|
|
||||||
|
// Extract CERT_WARN_DAYS / CERT_CRIT_DAYS from master
|
||||||
|
preg_match('/^\s*CERT_WARN_DAYS\s*=\s*(\d+)/m', $master, $w);
|
||||||
|
preg_match('/^\s*CERT_CRIT_DAYS\s*=\s*(\d+)/m', $master, $c);
|
||||||
|
|
||||||
|
// Extract domains array from host conf
|
||||||
|
$domains = [];
|
||||||
|
if (preg_match('/' . $hostIdUp . '_CERT_MONITOR_DOMAINS\s*=\s*\(([^)]*)\)/s', $confRaw, $dm)) {
|
||||||
|
preg_match_all('/"([^"]+)"/', $dm[1], $dd);
|
||||||
|
$domains = $dd[1] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'host_id' => $hostId,
|
||||||
|
'domains' => $domains,
|
||||||
|
'warn_days' => (int)($w[1] ?? 30),
|
||||||
|
'crit_days' => (int)($c[1] ?? 7),
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Run cert_monitor.sh now ───────────────────────────────────────────────────
|
||||||
|
if ($action === 'run') {
|
||||||
|
$script = SCRIPTS_DIR . '/Monitors/cert_monitor.sh';
|
||||||
|
if (!file_exists($script)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'cert_monitor.sh not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
set_time_limit(180);
|
||||||
|
exec('bash ' . escapeshellarg($script) . ' 2>&1', $out, $rc);
|
||||||
|
// Read freshly written cache
|
||||||
|
$data = file_exists($cacheFile)
|
||||||
|
? (json_decode(file_get_contents($cacheFile), true) ?: null)
|
||||||
|
: null;
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'output' => array_slice(array_filter(array_map('trim', $out)), 0, 30),
|
||||||
|
'rc' => $rc,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Default: return cached status ─────────────────────────────────────────────
|
||||||
|
if (!file_exists($cacheFile)) {
|
||||||
|
// No cache yet — return configured domains so UI can show them unchecked
|
||||||
|
$hostId = vv_detect_host();
|
||||||
|
$hostIdUp = strtoupper($hostId);
|
||||||
|
$confRaw = ($hostId !== 'unknown') ? vv_read_conf_raw($hostId . '.conf') : '';
|
||||||
|
$master = vv_read_conf_raw('master.conf');
|
||||||
|
|
||||||
|
preg_match('/^\s*CERT_WARN_DAYS\s*=\s*(\d+)/m', $master, $w);
|
||||||
|
preg_match('/^\s*CERT_CRIT_DAYS\s*=\s*(\d+)/m', $master, $c);
|
||||||
|
$domains = [];
|
||||||
|
if (preg_match('/' . $hostIdUp . '_CERT_MONITOR_DOMAINS\s*=\s*\(([^)]*)\)/s', $confRaw, $dm)) {
|
||||||
|
preg_match_all('/"([^"]+)"/', $dm[1], $dd);
|
||||||
|
foreach ($dd[1] ?? [] as $d) {
|
||||||
|
$domains[] = ['domain' => $d, 'status' => 'UNKN', 'days' => null, 'expires' => ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'checked_at' => null,
|
||||||
|
'host' => $hostId !== 'unknown' ? strtoupper($hostId) : null,
|
||||||
|
'warn_days' => (int)($w[1] ?? 30),
|
||||||
|
'crit_days' => (int)($c[1] ?? 7),
|
||||||
|
'domains' => $domains,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents($cacheFile), true) ?: [];
|
||||||
|
echo json_encode(array_merge(['ok' => true], $data));
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
|
$hostId = vv_detect_host();
|
||||||
|
$hostIdUp = strtoupper($hostId);
|
||||||
|
$master = vv_read_conf_raw('master.conf');
|
||||||
|
$confRaw = ($hostId !== 'unknown') ? vv_read_conf_raw($hostId . '.conf') : '';
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
// ── Identity ──────────────────────────────────────────────────────────────────
|
||||||
|
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $master, $m1);
|
||||||
|
$host1 = trim($m1[1] ?? '');
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'identity',
|
||||||
|
'label' => 'Server identity',
|
||||||
|
'ok' => !empty($host1),
|
||||||
|
'detail' => $host1 ? "HOST1: $host1" : 'HOST1 blank in master.conf',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Host conf ─────────────────────────────────────────────────────────────────
|
||||||
|
$confExists = $hostId !== 'unknown' && file_exists(CONF_DIR . '/' . $hostId . '.conf');
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'host_conf',
|
||||||
|
'label' => 'Host configuration',
|
||||||
|
'ok' => $confExists,
|
||||||
|
'detail' => $confExists
|
||||||
|
? "$hostId.conf present"
|
||||||
|
: ($hostId === 'unknown' ? 'Server not yet identified' : "$hostId.conf missing"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Unraid API key ─────────────────────────────────────────────────────────────
|
||||||
|
$apiKey = trim(vv_parse_conf_scalar($confRaw, $hostIdUp . '_UNRAID_API_KEY'));
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'api_key',
|
||||||
|
'label' => 'Unraid API key',
|
||||||
|
'ok' => !empty($apiKey),
|
||||||
|
'detail' => $apiKey ? 'Key present' : 'Not set',
|
||||||
|
'action' => $apiKey ? null : 'create_key',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── SSH key ────────────────────────────────────────────────────────────────────
|
||||||
|
$sshPath = trim(vv_parse_conf_scalar($confRaw, $hostIdUp . '_SSH_KEY'));
|
||||||
|
$sshOk = $sshPath && file_exists($sshPath);
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'ssh_key',
|
||||||
|
'label' => 'SSH key',
|
||||||
|
'ok' => $sshOk,
|
||||||
|
'detail' => $sshOk
|
||||||
|
? basename($sshPath)
|
||||||
|
: ($sshPath ? "Path set but file missing: $sshPath" : 'No key path in host.conf'),
|
||||||
|
'action' => $sshOk ? null : 'ssh_setup',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Auto-populate (any service key or container detected) ──────────────────────
|
||||||
|
$populated = false;
|
||||||
|
foreach (['_RADARR_API_KEY','_SONARR_API_KEY','_LIDARR_API_KEY','_EMBY_CONTAINER','_JELLYFIN_CONTAINER'] as $f) {
|
||||||
|
if (trim(vv_parse_conf_scalar($confRaw, $hostIdUp . $f)) !== '') {
|
||||||
|
$populated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'populated',
|
||||||
|
'label' => 'Auto-populate',
|
||||||
|
'ok' => $populated,
|
||||||
|
'detail' => $populated ? 'Services detected in host.conf' : 'No services detected yet',
|
||||||
|
'action' => $populated ? null : 'run_populate',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── master.conf pull (partner servers only) ───────────────────────────────────────────────────
|
||||||
|
if ($hostId !== 'host1' && $hostId !== 'unknown') {
|
||||||
|
$state = vv_setup_state_read();
|
||||||
|
$pulled = !empty($state['master_conf_pulled']);
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'master_conf',
|
||||||
|
'label' => 'master.conf',
|
||||||
|
'ok' => $pulled,
|
||||||
|
'detail' => $pulled
|
||||||
|
? 'Synced from HOST1'
|
||||||
|
: ($host1 ? "Not yet pulled from $host1" : 'HOST1 hostname not set in master.conf'),
|
||||||
|
'action' => (!$pulled && $host1) ? 'pull_master' : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Partnership (only if a partner is configured) ──────────────────────────────
|
||||||
|
preg_match('/^\s*HOST2\s*=\s*"([^"]*)"/m', $master, $m2);
|
||||||
|
$host2 = trim($m2[1] ?? '');
|
||||||
|
if (!empty($host2)) {
|
||||||
|
$state = vv_setup_state_read();
|
||||||
|
$p1done = !empty($state['HOST2_PHASE1_DONE']) || !empty($state['host2_phase1_done']);
|
||||||
|
$p2done = !empty($state['HOST2_PHASE2_DONE']) || !empty($state['host2_phase2_done']);
|
||||||
|
$items[] = [
|
||||||
|
'id' => 'partnership',
|
||||||
|
'label' => 'Partnership',
|
||||||
|
'ok' => $p1done && $p2done,
|
||||||
|
'detail' => ($p1done && $p2done)
|
||||||
|
? "Active with $host2"
|
||||||
|
: ($p1done ? "Phase 1 done — waiting for HOST2 to complete" : "Not started — run partnership_onboard.sh"),
|
||||||
|
'action' => (!$p1done) ? 'onboard' : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allOk = !in_array(false, array_column($items, 'ok'), true);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'complete' => $allOk, 'host_id' => $hostId, 'items' => $items]);
|
||||||
@@ -9,76 +9,4 @@ if (!preg_match('/^host\d+$/', $host)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hostUpper = strtoupper($host);
|
echo json_encode(vv_auto_create_api_key($host, $host . '.conf'));
|
||||||
$varName = $hostUpper . '_UNRAID_API_KEY';
|
|
||||||
$confFile = $host . '.conf';
|
|
||||||
|
|
||||||
// Create/overwrite the Varaverk API key.
|
|
||||||
// --description and --roles are required to suppress interactive prompts.
|
|
||||||
// --overwrite replaces any existing key with the same name (keeps it to one).
|
|
||||||
$dbg = ['ts' => date('H:i:s'), 'user' => trim(shell_exec('whoami'))];
|
|
||||||
$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
|
|
||||||
$dbg['raw'] = $output;
|
|
||||||
file_put_contents('/tmp/vv_apikey_debug.json', json_encode($dbg, JSON_PRETTY_PRINT));
|
|
||||||
|
|
||||||
if (!$output) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'unraid-api returned no output — check /tmp/vv_apikey_debug.json']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode(trim($output), true);
|
|
||||||
if (!is_array($data)) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Could not parse unraid-api output', 'raw' => substr($output, 0, 300)]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = $data['key'] ?? null;
|
|
||||||
if (!$key) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'No key in response', 'raw' => substr($output, 0, 300)]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read conf, replace the key value, write back
|
|
||||||
$raw = vv_read_conf_raw($confFile);
|
|
||||||
if ($raw === '') {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Cannot read ' . $confFile]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If line is missing (older conf created before this field was added to the template),
|
|
||||||
// insert it after HOST*_OWNER_EMAIL, or after HOST*_SSH_KEY, or append to file.
|
|
||||||
if (!str_contains($raw, $varName)) {
|
|
||||||
$inserted = false;
|
|
||||||
foreach ([$hostUpper . '_OWNER_EMAIL', $hostUpper . '_SSH_KEY'] as $anchor) {
|
|
||||||
if (str_contains($raw, $anchor)) {
|
|
||||||
$raw = preg_replace(
|
|
||||||
'/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
|
|
||||||
'$1' . "\n " . $varName . '=""',
|
|
||||||
$raw, 1
|
|
||||||
);
|
|
||||||
$inserted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$inserted) {
|
|
||||||
$raw = rtrim($raw) . "\n " . $varName . '=""' . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace quoted value in-place
|
|
||||||
$updated = preg_replace(
|
|
||||||
'/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
|
|
||||||
'${1}"' . $key . '"',
|
|
||||||
$raw
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!vv_write_conf_raw($confFile, $updated)) {
|
|
||||||
echo json_encode(['ok' => false, 'error' => 'Failed to write ' . $confFile]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'ok' => true,
|
|
||||||
'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4),
|
|
||||||
'conf_file' => $confFile,
|
|
||||||
]);
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
$logDir = '/var/log/varaverk';
|
$logDir = LOG_DIR;
|
||||||
$runs = [];
|
$runs = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
$scriptsDir = trim($_POST['scripts_dir'] ?? '');
|
$scriptsDir = trim($_POST['scripts_dir'] ?? '');
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ if (!is_dir($scriptsDir)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cfgFile = '/boot/config/plugins/varaverk/varaverk.cfg';
|
$cfgFile = PLUGIN_CFG;
|
||||||
$cfgDir = dirname($cfgFile);
|
$cfgDir = dirname($cfgFile);
|
||||||
if (!is_dir($cfgDir)) mkdir($cfgDir, 0755, true);
|
if (!is_dir($cfgDir)) mkdir($cfgDir, 0755, true);
|
||||||
|
|
||||||
|
|||||||
+110
-10
@@ -2,21 +2,89 @@
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once dirname(__DIR__) . '/include/config.php';
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
|
$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
|
||||||
|
? trim($_GET['action'] ?? '')
|
||||||
|
: trim($_POST['action'] ?? 'save');
|
||||||
|
|
||||||
|
// ── GET: detect environment ────────────────────────────────────────────────────────────────────
|
||||||
|
if ($action === 'detect') {
|
||||||
|
$bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||||
|
$bootDisk = $bootPart
|
||||||
|
? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
|
||||||
|
: '';
|
||||||
|
$transport = $bootDisk
|
||||||
|
? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
|
||||||
|
: 'unknown';
|
||||||
|
|
||||||
|
$isUsb = ($transport === 'usb');
|
||||||
|
|
||||||
|
preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'hostname' => vv_get_hostname(),
|
||||||
|
'unraid_ver' => $vm[1] ?? 'unknown',
|
||||||
|
'transport' => $transport,
|
||||||
|
'boot_device' => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
|
||||||
|
'mode' => $isUsb ? 'flash' : 'internal',
|
||||||
|
'scripts_dir' => SCRIPTS_DIR,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
|
||||||
|
if ($action === 'ssh_generate') {
|
||||||
|
$script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
|
||||||
|
if (!file_exists($script)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
|
||||||
|
// Derive pubkey path from hostname
|
||||||
|
$hostname = vv_get_hostname();
|
||||||
|
$shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
|
||||||
|
$pubPath = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
|
||||||
|
$pubKey = trim(@file_get_contents($pubPath) ?: '');
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => $rc === 0 && !empty($pubKey),
|
||||||
|
'pubkey' => $pubKey,
|
||||||
|
'error' => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
|
||||||
|
if ($action === 'populate') {
|
||||||
|
$script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
|
||||||
|
if (!file_exists($script)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
|
||||||
|
$lines = array_values(array_filter(array_map('trim', $out)));
|
||||||
|
echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$action = trim($_POST['action'] ?? 'save');
|
$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
|
||||||
|
|
||||||
// ── HOST2 pull: pull master.conf from HOST1 via SSH ──────────────────────────────────────────
|
// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
|
||||||
if ($action === 'pull') {
|
if ($action === 'pull') {
|
||||||
$host1Hostname = trim($_POST['host1_hostname'] ?? '');
|
$mySlot = trim($_POST['my_slot'] ?? '') ?: strtolower(vv_detect_host());
|
||||||
$mySlot = trim($_POST['my_slot'] ?? 'host2');
|
$myHostname = trim($_POST['my_hostname'] ?? '') ?: vv_get_hostname();
|
||||||
$myHostname = trim($_POST['my_hostname'] ?? '');
|
$host1Hostname = trim($_POST['host1_hostname'] ?? '');
|
||||||
|
|
||||||
if (!$host1Hostname) {
|
if (!$host1Hostname) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname required']);
|
$masterRaw = vv_read_conf_raw('master.conf');
|
||||||
|
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
|
||||||
|
$host1Hostname = trim($_mh[1] ?? '');
|
||||||
|
}
|
||||||
|
if (!$host1Hostname) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if (!preg_match('/^host\d+$/', $mySlot)) {
|
if (!preg_match('/^host\d+$/', $mySlot)) {
|
||||||
@@ -71,17 +139,31 @@ if ($action === 'pull') {
|
|||||||
if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
||||||
$template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
|
$template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
|
||||||
if ($template) {
|
if ($template) {
|
||||||
$hostname = $myHostname ?: vv_get_hostname();
|
$bootPart2 = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||||
$sshKeyPath = $sshKey;
|
$bootDisk2 = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
|
||||||
|
$transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
|
||||||
|
$storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
|
||||||
$conf = str_replace('HOSTN', $hostId, $template);
|
$conf = str_replace('HOSTN', $hostId, $template);
|
||||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||||
'${1}"' . $sshKeyPath . '"', $conf);
|
'${1}"' . $sshKey . '"', $conf);
|
||||||
|
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||||
|
'${1}' . $storageInternal2, $conf);
|
||||||
vv_write_conf_raw($confFile, $conf);
|
vv_write_conf_raw($confFile, $conf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file_exists($sshScript)) {
|
||||||
|
exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
|
||||||
|
}
|
||||||
|
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
|
||||||
|
|
||||||
|
$state = vv_setup_state_read();
|
||||||
|
$state['master_conf_pulled'] = 'true';
|
||||||
|
vv_setup_state_write($state);
|
||||||
|
|
||||||
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
|
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
|
||||||
|
'api_key' => $apiKeyResult,
|
||||||
'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
|
'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -138,10 +220,19 @@ if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
|||||||
if ($template) {
|
if ($template) {
|
||||||
$sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
|
$sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
|
||||||
$sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
|
$sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
|
||||||
|
|
||||||
|
// Auto-detect storage mode from boot device transport
|
||||||
|
$bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||||
|
$bootDisk = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
|
||||||
|
$transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
|
||||||
|
$storageInternal = ($transport !== 'usb') ? 'true' : 'false';
|
||||||
|
|
||||||
$conf = str_replace('HOSTN', $hostId, $template);
|
$conf = str_replace('HOSTN', $hostId, $template);
|
||||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||||
'${1}"' . $sshKeyPath . '"', $conf);
|
'${1}"' . $sshKeyPath . '"', $conf);
|
||||||
|
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||||
|
'${1}' . $storageInternal, $conf);
|
||||||
if (!vv_write_conf_raw($confFile, $conf)) {
|
if (!vv_write_conf_raw($confFile, $conf)) {
|
||||||
echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
|
echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
|
||||||
exit;
|
exit;
|
||||||
@@ -152,8 +243,17 @@ if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
|||||||
// Write setup state file — lets partner servers know HOST1 is configured
|
// Write setup state file — lets partner servers know HOST1 is configured
|
||||||
vv_setup_state_write(['host1_hostname' => $host1]);
|
vv_setup_state_write(['host1_hostname' => $host1]);
|
||||||
|
|
||||||
|
// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
|
||||||
|
if (file_exists($sshScript)) {
|
||||||
|
exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-create Unraid API key and write into the fresh conf
|
||||||
|
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'host_id' => $hostId,
|
'host_id' => $hostId,
|
||||||
|
'api_key' => $apiKeyResult,
|
||||||
'redirect' => '?tab=scheduler&vv_setup=master.conf',
|
'redirect' => '?tab=scheduler&vv_setup=master.conf',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$script = dirname(__DIR__) . '/tools/storage_migrate.sh';
|
$script = dirname(__DIR__) . '/Tools/storage_migrate.sh';
|
||||||
if (!file_exists($script)) {
|
if (!file_exists($script)) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
|
echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
|
||||||
exit;
|
exit;
|
||||||
@@ -141,7 +141,7 @@ if ($action === 'api_status') {
|
|||||||
|
|
||||||
// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
|
// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
|
||||||
if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$script = SCRIPTS_DIR . '/System_Essentials/unraid_api_key_renew.sh';
|
$script = SCRIPTS_DIR . '/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh';
|
||||||
if (!file_exists($script)) {
|
if (!file_exists($script)) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
|
echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ $cmd = match($action) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$logLine = date('Y-m-d H:i:s') . " action={$action} ip=" . ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . "\n";
|
$logLine = date('Y-m-d H:i:s') . " action={$action} ip=" . ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . "\n";
|
||||||
@file_put_contents('/boot/config/plugins/varaverk/actions.log', $logLine, FILE_APPEND | LOCK_EX);
|
@file_put_contents(SCRIPTS_DIR . '/actions.log', $logLine, FILE_APPEND | LOCK_EX);
|
||||||
|
|
||||||
exec($cmd . ' > /dev/null 2>&1 &');
|
exec($cmd . ' > /dev/null 2>&1 &');
|
||||||
echo json_encode(['ok' => true]);
|
echo json_encode(['ok' => true]);
|
||||||
|
|||||||
@@ -3,11 +3,26 @@
|
|||||||
#varaverk-wrap { padding: 10px; font-family: inherit; }
|
#varaverk-wrap { padding: 10px; font-family: inherit; }
|
||||||
|
|
||||||
/* Tab bar */
|
/* Tab bar */
|
||||||
#vv-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid #444; }
|
#vv-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid #444; align-items: flex-end; }
|
||||||
.vv-tab { padding: 6px 16px; text-decoration: none; color: #aaa; border-radius: 4px 4px 0 0; }
|
.vv-tab { padding: 6px 16px; text-decoration: none; color: #aaa; border-radius: 4px 4px 0 0; }
|
||||||
.vv-tab:hover { color: #fff; background: #333; }
|
.vv-tab:hover { color: #fff; background: #333; }
|
||||||
.vv-tab.active { color: #fff; background: #555; border-bottom: 2px solid #fff; }
|
.vv-tab.active { color: #fff; background: #555; border-bottom: 2px solid #fff; }
|
||||||
|
|
||||||
|
/* Expand toggle button */
|
||||||
|
#vv-expand-btn {
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
color: #333; font-size: 15px; padding: 2px 8px 4px;
|
||||||
|
line-height: 1; border-radius: 3px; transition: color .15s;
|
||||||
|
margin-left: 6px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#vv-expand-btn:hover { color: #888; }
|
||||||
|
#vv-expand-btn.active { color: #aaa; }
|
||||||
|
|
||||||
|
/* Fullscreen mode — hide Unraid chrome, reclaim the space */
|
||||||
|
body.vv-fullscreen #header { display: none !important; }
|
||||||
|
body.vv-fullscreen #menu { display: none !important; }
|
||||||
|
body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5rem !important; }
|
||||||
|
|
||||||
/* Cards / layout */
|
/* Cards / layout */
|
||||||
.vv-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
.vv-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||||
.vv-card { flex: 1; min-width: 200px; background: #1e1e1e; border: 1px solid #444;
|
.vv-card { flex: 1; min-width: 200px; background: #1e1e1e; border: 1px solid #444;
|
||||||
|
|||||||
+103
-37
@@ -131,29 +131,51 @@ function vv_arr_cleanup_stats(string $type): array {
|
|||||||
'orphans' => 0, 'orphans_sz' => '0B', 'junk' => 0];
|
'orphans' => 0, 'orphans_sz' => '0B', 'junk' => 0];
|
||||||
|
|
||||||
$jf = $base . '.json';
|
$jf = $base . '.json';
|
||||||
if (!file_exists($jf)) return $out;
|
if (file_exists($jf)) {
|
||||||
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
||||||
$out['last_run'] = $meta['start'] ?? null;
|
$out['last_run'] = $meta['start'] ?? null;
|
||||||
$out['end'] = $meta['end'] ?? null;
|
$out['end'] = $meta['end'] ?? null;
|
||||||
$out['status'] = $meta['status'] ?? null;
|
$out['status'] = $meta['status'] ?? null;
|
||||||
|
|
||||||
$lf = $base . '.log';
|
$lf = $base . '.log';
|
||||||
if (!file_exists($lf)) return $out;
|
if (file_exists($lf)) {
|
||||||
$log = file_get_contents($lf);
|
$log = file_get_contents($lf);
|
||||||
$parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
|
$parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
|
||||||
$blk = count($parts) > 1 ? end($parts) : $log;
|
$blk = count($parts) > 1 ? end($parts) : $log;
|
||||||
|
|
||||||
if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
|
if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
|
||||||
$out['tracked'] = (int)str_replace(',', '', $m[1]);
|
$out['tracked'] = (int)str_replace(',', '', $m[1]);
|
||||||
$out['total'] = (int)str_replace(',', '', $m[2]);
|
$out['total'] = (int)str_replace(',', '', $m[2]);
|
||||||
|
}
|
||||||
|
if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
|
||||||
|
$out['orphans'] = (int)str_replace(',', '', $m[1]);
|
||||||
|
$out['orphans_sz'] = trim($m[2]);
|
||||||
|
}
|
||||||
|
if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
|
||||||
|
$out['junk'] = (int)str_replace(',', '', $m[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
|
|
||||||
$out['orphans'] = (int)str_replace(',', '', $m[1]);
|
// Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count
|
||||||
$out['orphans_sz'] = trim($m[2]);
|
if ($out['last_run'] === null) {
|
||||||
}
|
$dbFile = DATA_DIR . '/arr_cleanup_stats.db';
|
||||||
if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
|
if (file_exists($dbFile)) {
|
||||||
$out['junk'] = (int)str_replace(',', '', $m[1]);
|
$last = null;
|
||||||
|
foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
$p = explode('|', $line);
|
||||||
|
if (count($p) >= 8 && $p[1] === $type) $last = $p;
|
||||||
|
}
|
||||||
|
if ($last) {
|
||||||
|
$out['last_run'] = strtotime($last[0] . ' 23:59:00') ?: null;
|
||||||
|
$out['status'] = 'ok';
|
||||||
|
$out['orphans'] = (int)$last[2];
|
||||||
|
$out['junk'] = (int)$last[4];
|
||||||
|
$out['tracked'] = (int)$last[7];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,17 +187,39 @@ function vv_arr_discovery_stats(string $type): array {
|
|||||||
$out = ['last_run' => null, 'status' => null, 'added' => null];
|
$out = ['last_run' => null, 'status' => null, 'added' => null];
|
||||||
|
|
||||||
$jf = $base . '.json';
|
$jf = $base . '.json';
|
||||||
if (!file_exists($jf)) return $out;
|
if (file_exists($jf)) {
|
||||||
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
||||||
$out['last_run'] = $meta['start'] ?? null;
|
$out['last_run'] = $meta['start'] ?? null;
|
||||||
$out['status'] = $meta['status'] ?? null;
|
$out['status'] = $meta['status'] ?? null;
|
||||||
|
|
||||||
$lf = $base . '.log';
|
$lf = $base . '.log';
|
||||||
if (file_exists($lf)) {
|
if (file_exists($lf)) {
|
||||||
$log = file_get_contents($lf);
|
$log = file_get_contents($lf);
|
||||||
if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
|
if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
|
||||||
elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
|
elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: per-title history db — status|id|date[|title]
|
||||||
|
if ($out['last_run'] === null) {
|
||||||
|
$dbFile = DATA_DIR . '/' . $type . '_discovery_history.db';
|
||||||
|
if (file_exists($dbFile)) {
|
||||||
|
$lastDate = null; $added = 0;
|
||||||
|
foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
$p = explode('|', $line);
|
||||||
|
if (count($p) < 3) continue;
|
||||||
|
$date = $p[2];
|
||||||
|
if ($date !== $lastDate) { $lastDate = $date; $added = 0; }
|
||||||
|
if ($p[0] === 'ACCEPT') $added++;
|
||||||
|
}
|
||||||
|
if ($lastDate) {
|
||||||
|
$out['last_run'] = strtotime($lastDate . ' 23:59:00') ?: null;
|
||||||
|
$out['status'] = 'ok';
|
||||||
|
$out['added'] = $added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,17 +258,39 @@ function vv_arr_recovery_stats(): array {
|
|||||||
$out = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];
|
$out = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];
|
||||||
|
|
||||||
$jf = $base . '.json';
|
$jf = $base . '.json';
|
||||||
if (!file_exists($jf)) return $out;
|
if (file_exists($jf)) {
|
||||||
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
$meta = json_decode(file_get_contents($jf), true) ?: [];
|
||||||
$out['last_run'] = $meta['start'] ?? null;
|
$out['last_run'] = $meta['start'] ?? null;
|
||||||
$out['status'] = $meta['status'] ?? null;
|
$out['status'] = $meta['status'] ?? null;
|
||||||
|
|
||||||
$lf = $base . '.log';
|
$lf = $base . '.log';
|
||||||
if (file_exists($lf)) {
|
if (file_exists($lf)) {
|
||||||
$log = file_get_contents($lf);
|
$log = file_get_contents($lf);
|
||||||
if (preg_match('/Removed[:\s]+(\d+)/i', $log, $m)) $out['fixed'] = (int)$m[1];
|
if (preg_match('/Removed[:\s]+(\d+)/i', $log, $m)) $out['fixed'] = (int)$m[1];
|
||||||
if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
|
if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: daily aggregate db — date|time|count|bytes
|
||||||
|
if ($out['last_run'] === null) {
|
||||||
|
$dbFile = DATA_DIR . '/arr_recovery_stats.db';
|
||||||
|
if (file_exists($dbFile)) {
|
||||||
|
$lines = file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
$last = $lines ? end($lines) : null;
|
||||||
|
if ($last) {
|
||||||
|
$p = explode('|', $last);
|
||||||
|
if (count($p) >= 3) {
|
||||||
|
$ts = strtotime(($p[0] ?? '') . ' ' . ($p[1] ?? '00:00')) ?: null;
|
||||||
|
if ($ts) {
|
||||||
|
$out['last_run'] = $ts;
|
||||||
|
$out['status'] = 'ok';
|
||||||
|
$out['fixed'] = (int)($p[2] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function vv_cpu_per_core(): array {
|
|||||||
$raw[$m[1]] = [(int)$m[2],(int)$m[3],(int)$m[4],(int)$m[5],(int)$m[6],(int)$m[7],(int)$m[8]];
|
$raw[$m[1]] = [(int)$m[2],(int)$m[3],(int)$m[4],(int)$m[5],(int)$m[6],(int)$m[7],(int)$m[8]];
|
||||||
}
|
}
|
||||||
|
|
||||||
$stateFile = '/tmp/vv_cpu_stat.json';
|
$stateFile = VV_CACHE_DIR . '/vv_cpu_stat.json';
|
||||||
$prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : [];
|
$prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : [];
|
||||||
// Atomic write — concurrent fast/slow polls read a consistent snapshot
|
// Atomic write — concurrent fast/slow polls read a consistent snapshot
|
||||||
$tmp = $stateFile . '.tmp';
|
$tmp = $stateFile . '.tmp';
|
||||||
@@ -292,7 +292,7 @@ function vv_network_stats(): array {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stateFile = '/tmp/vv_net_stat.json';
|
$stateFile = VV_CACHE_DIR . '/vv_net_stat.json';
|
||||||
$now = ['rx' => $rxBytes, 'tx' => $txBytes, 'ts' => microtime(true)];
|
$now = ['rx' => $rxBytes, 'tx' => $txBytes, 'ts' => microtime(true)];
|
||||||
$prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : [];
|
$prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : [];
|
||||||
$tmp = $stateFile . '.tmp';
|
$tmp = $stateFile . '.tmp';
|
||||||
@@ -536,7 +536,7 @@ function vv_array_disks(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vv_disk_io_rates(): array {
|
function vv_disk_io_rates(): array {
|
||||||
$snapFile = '/tmp/vv_diskio_snap.json';
|
$snapFile = VV_CACHE_DIR . '/vv_diskio_snap.json';
|
||||||
$now = microtime(true);
|
$now = microtime(true);
|
||||||
|
|
||||||
// Read current whole-disk stats from /proc/diskstats
|
// Read current whole-disk stats from /proc/diskstats
|
||||||
@@ -631,7 +631,7 @@ function vv_remote_hosts_stats(): array {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheFile = "/tmp/vv_remote_{$id}.json";
|
$cacheFile = VV_CACHE_DIR . "/vv_remote_{$id}.json";
|
||||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) {
|
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) {
|
||||||
$cached = json_decode(file_get_contents($cacheFile), true);
|
$cached = json_decode(file_get_contents($cacheFile), true);
|
||||||
if ($cached) { $results[$id] = $cached; continue; }
|
if ($cached) { $results[$id] = $cached; continue; }
|
||||||
@@ -727,7 +727,7 @@ function vv_parse_bash_array(string $raw, string $varName): array {
|
|||||||
|
|
||||||
function vv_transcode_sessions(): array {
|
function vv_transcode_sessions(): array {
|
||||||
$v = vv_conf_vars();
|
$v = vv_conf_vars();
|
||||||
$stateDir = rtrim($v['STATE_DIR'] ?? '/boot/config/plugins/varaverk/State_Files', '/');
|
$stateDir = rtrim($v['STATE_DIR'] ?? STATE_DIR, '/');
|
||||||
$stateFile = "$stateDir/transcode_state.db";
|
$stateFile = "$stateDir/transcode_state.db";
|
||||||
if (!file_exists($stateFile)) return ['available' => false];
|
if (!file_exists($stateFile)) return ['available' => false];
|
||||||
|
|
||||||
@@ -773,7 +773,7 @@ function vv_transcode_sessions(): array {
|
|||||||
// Last cleanup values from transcode management log
|
// Last cleanup values from transcode management log
|
||||||
$lastRdFreed = null;
|
$lastRdFreed = null;
|
||||||
$lastSsdFreed = null;
|
$lastSsdFreed = null;
|
||||||
$logFile = '/var/log/varaverk/Orchestrators/transcode_management.log';
|
$logFile = LOG_DIR . '/Orchestrators/transcode_management.log';
|
||||||
if (file_exists($logFile)) {
|
if (file_exists($logFile)) {
|
||||||
$lines = file($logFile, FILE_IGNORE_NEW_LINES) ?: [];
|
$lines = file($logFile, FILE_IGNORE_NEW_LINES) ?: [];
|
||||||
foreach (array_reverse($lines) as $line) {
|
foreach (array_reverse($lines) as $line) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const VV_SCRIPT_CONF_SECTIONS = [
|
|||||||
'Watchdogs/docker_watchdog.sh' => ['Docker Watchdog'],
|
'Watchdogs/docker_watchdog.sh' => ['Docker Watchdog'],
|
||||||
'Watchdogs/resource_watchdog.sh' => ['Pressure Levels'],
|
'Watchdogs/resource_watchdog.sh' => ['Pressure Levels'],
|
||||||
'Watchdogs/System/network_watchdog.sh' => ['Network Watchdog'],
|
'Watchdogs/System/network_watchdog.sh' => ['Network Watchdog'],
|
||||||
'Watchdogs/System/webgui_watchdog.sh' => ['WebGUI Watchdog'],
|
'Plugin/unraid/Watchdogs/System/webgui_watchdog.sh' => ['WebGUI Watchdog'],
|
||||||
// Media
|
// Media
|
||||||
'Media/media_cleaner.sh' => ['Media Cleaner'],
|
'Media/media_cleaner.sh' => ['Media Cleaner'],
|
||||||
'Media/media_shares_permissions.sh' => ['Media Permissions'],
|
'Media/media_shares_permissions.sh' => ['Media Permissions'],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ define('LOG_DIR', '/var/log/varaverk');
|
|||||||
unset($_vv_cfg);
|
unset($_vv_cfg);
|
||||||
|
|
||||||
define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
|
define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
|
||||||
|
define('VV_CACHE_DIR', '/tmp/vv_cache');
|
||||||
|
|
||||||
// Read the setup state file into a key=>value array.
|
// Read the setup state file into a key=>value array.
|
||||||
function vv_setup_state_read(): array {
|
function vv_setup_state_read(): array {
|
||||||
@@ -202,7 +203,7 @@ function vv_conf_vars(): array {
|
|||||||
// Match: VAR_NAME="value" or VAR_NAME=value (no quotes)
|
// Match: VAR_NAME="value" or VAR_NAME=value (no quotes)
|
||||||
preg_match_all('/^\s*([A-Z0-9_]+)\s*=\s*["\']?([^"\'#\n]*?)["\']?\s*(?:#.*)?$/m', $raw, $m);
|
preg_match_all('/^\s*([A-Z0-9_]+)\s*=\s*["\']?([^"\'#\n]*?)["\']?\s*(?:#.*)?$/m', $raw, $m);
|
||||||
foreach ($m[1] as $i => $key) {
|
foreach ($m[1] as $i => $key) {
|
||||||
$vars[$key] = trim($m[2][$i]);
|
$vars[$key] = str_replace('\\$', '$', trim($m[2][$i]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $vars;
|
return $vars;
|
||||||
@@ -263,7 +264,7 @@ function vv_unraid_api_query(string $hostId, string $gql, int $timeoutSec = 5, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($resp === false || $resp === '' || ($httpCode !== 0 && $httpCode !== 200)) {
|
if ($resp === false || $resp === '' || ($httpCode !== 0 && $httpCode !== 200)) {
|
||||||
@file_put_contents('/tmp/vv_api_debug.json', json_encode([
|
@file_put_contents(VV_CACHE_DIR . '/vv_api_debug.json', json_encode([
|
||||||
'ts' => time(),
|
'ts' => time(),
|
||||||
'host' => $hostId,
|
'host' => $hostId,
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@@ -278,7 +279,7 @@ function vv_unraid_api_query(string $hostId, string $gql, int $timeoutSec = 5, s
|
|||||||
|
|
||||||
// If the API returned GraphQL errors, log them for diagnosis.
|
// If the API returned GraphQL errors, log them for diagnosis.
|
||||||
if (!empty($decoded['errors'])) {
|
if (!empty($decoded['errors'])) {
|
||||||
@file_put_contents('/tmp/vv_api_debug.json', json_encode([
|
@file_put_contents(VV_CACHE_DIR . '/vv_api_debug.json', json_encode([
|
||||||
'ts' => time(),
|
'ts' => time(),
|
||||||
'host' => $hostId,
|
'host' => $hostId,
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@@ -294,8 +295,6 @@ function vv_unraid_api_query(string $hostId, string $gql, int $timeoutSec = 5, s
|
|||||||
|
|
||||||
// ── File-based API cache (/tmp/vv_cache — tmpfs, cleared on reboot) ───────────
|
// ── File-based API cache (/tmp/vv_cache — tmpfs, cleared on reboot) ───────────
|
||||||
|
|
||||||
define('VV_CACHE_DIR', '/tmp/vv_cache');
|
|
||||||
|
|
||||||
// Read a cached payload. Returns null if missing or older than $maxAge seconds.
|
// Read a cached payload. Returns null if missing or older than $maxAge seconds.
|
||||||
function vv_cache_read(string $key, int $maxAge = 90): ?array {
|
function vv_cache_read(string $key, int $maxAge = 90): ?array {
|
||||||
$f = VV_CACHE_DIR . '/' . $key . '.json';
|
$f = VV_CACHE_DIR . '/' . $key . '.json';
|
||||||
@@ -358,6 +357,38 @@ function vv_known_hosts(): array {
|
|||||||
return $hosts ?: ['host1' => 'HOST1'];
|
return $hosts ?: ['host1' => 'HOST1'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create (or overwrite) the Varaverk Unraid API key and write it into host conf.
|
||||||
|
// Returns ['ok'=>true,'key_preview'=>'...'] or ['ok'=>false,'error'=>'...'].
|
||||||
|
function vv_auto_create_api_key(string $hostId, string $confFile): array {
|
||||||
|
$varName = strtoupper($hostId) . '_UNRAID_API_KEY';
|
||||||
|
$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1');
|
||||||
|
if (!$output) {
|
||||||
|
return ['ok' => false, 'error' => 'unraid-api returned no output'];
|
||||||
|
}
|
||||||
|
$data = json_decode(trim($output), true);
|
||||||
|
$key = $data['key'] ?? null;
|
||||||
|
if (!$key) {
|
||||||
|
return ['ok' => false, 'error' => 'No key in response'];
|
||||||
|
}
|
||||||
|
$raw = vv_read_conf_raw($confFile);
|
||||||
|
if ($raw === '') {
|
||||||
|
return ['ok' => false, 'error' => 'Cannot read ' . $confFile];
|
||||||
|
}
|
||||||
|
if (!str_contains($raw, $varName)) {
|
||||||
|
foreach ([strtoupper($hostId) . '_OWNER_EMAIL', strtoupper($hostId) . '_SSH_KEY'] as $anchor) {
|
||||||
|
if (str_contains($raw, $anchor)) {
|
||||||
|
$raw = preg_replace('/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m',
|
||||||
|
'$1' . "\n " . $varName . '=""', $raw, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$raw = preg_replace('/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m',
|
||||||
|
'${1}"' . $key . '"', $raw);
|
||||||
|
vv_write_conf_raw($confFile, $raw);
|
||||||
|
return ['ok' => true, 'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4)];
|
||||||
|
}
|
||||||
|
|
||||||
// Local LAN IP via routing table — static-cached per request.
|
// Local LAN IP via routing table — static-cached per request.
|
||||||
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
|
// Previously duplicated in include/docker_folders.php and inline in include/docker.php.
|
||||||
function vv_local_ip(): string {
|
function vv_local_ip(): string {
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ function vv_script_suggested_cron(string $path): array {
|
|||||||
// Parse user_script_plug-in.sh into an array of script blocks.
|
// Parse user_script_plug-in.sh into an array of script blocks.
|
||||||
// Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron})
|
// Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron})
|
||||||
function vv_parse_user_script_template(): array {
|
function vv_parse_user_script_template(): array {
|
||||||
$file = SCRIPTS_DIR . '/user_script_plug-in.sh';
|
$file = SCRIPTS_DIR . '/Plugin/unraid/user_script_plug-in.sh';
|
||||||
if (!file_exists($file)) return [];
|
if (!file_exists($file)) return [];
|
||||||
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||||
$prefix = rtrim(SCRIPTS_DIR, '/') . '/';
|
$prefix = rtrim(SCRIPTS_DIR, '/') . '/';
|
||||||
@@ -231,41 +231,69 @@ function vv_script_description(string $path): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vv_tools_scripts(): array {
|
function vv_tools_scripts(): array {
|
||||||
$dir = SCRIPTS_DIR . '/Tools';
|
// Background writers managed automatically — not user-facing tools
|
||||||
|
static $EXCLUDE = ['api_cache_writer.sh', 'remote_arr_cache_writer.sh'];
|
||||||
|
|
||||||
$schedule = vv_schedule_load();
|
$schedule = vv_schedule_load();
|
||||||
$scripts = [];
|
$scripts = [];
|
||||||
foreach (glob("$dir/*.sh") ?: [] as $path) {
|
|
||||||
$rel = 'Tools/' . basename($path);
|
$collect = function(string $dir, string $relPrefix) use ($schedule, $EXCLUDE, &$scripts): void {
|
||||||
$entry = $schedule[$rel] ?? [];
|
foreach (glob("$dir/*.sh") ?: [] as $path) {
|
||||||
$scripts[] = [
|
$base = basename($path);
|
||||||
'id' => $rel,
|
if (in_array($base, $EXCLUDE, true)) continue;
|
||||||
'label' => vv_pretty_label(basename($path, '.sh')),
|
$rel = $relPrefix . $base;
|
||||||
'desc' => vv_script_description($path),
|
$entry = $schedule[$rel] ?? [];
|
||||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
$scripts[] = [
|
||||||
'cron' => $entry['cron'] ?? '',
|
'id' => $rel,
|
||||||
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
'label' => vv_pretty_label(basename($path, '.sh')),
|
||||||
];
|
'desc' => vv_script_description($path),
|
||||||
|
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||||
|
'cron' => $entry['cron'] ?? '',
|
||||||
|
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// General tools
|
||||||
|
$collect(SCRIPTS_DIR . '/Tools', 'Tools/');
|
||||||
|
|
||||||
|
// Platform adapter tools (Plugin/<platform>/Tools/)
|
||||||
|
foreach (glob(SCRIPTS_DIR . '/Plugin/*/Tools') ?: [] as $toolsDir) {
|
||||||
|
$platform = basename(dirname($toolsDir));
|
||||||
|
$collect($toolsDir, "Plugin/$platform/Tools/");
|
||||||
}
|
}
|
||||||
|
|
||||||
usort($scripts, fn($a, $b) => strcmp($a['label'], $b['label']));
|
usort($scripts, fn($a, $b) => strcmp($a['label'], $b['label']));
|
||||||
return $scripts;
|
return $scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function vv_custom_scripts(): array {
|
function vv_custom_scripts(): array {
|
||||||
$dir = SCRIPTS_DIR . '/Custom';
|
|
||||||
$schedule = vv_schedule_load();
|
$schedule = vv_schedule_load();
|
||||||
$scripts = [];
|
$scripts = [];
|
||||||
foreach (glob("$dir/*.sh") ?: [] as $path) {
|
|
||||||
$rel = 'Custom/' . basename($path);
|
$collect = function(string $dir, string $relPrefix) use ($schedule, &$scripts): void {
|
||||||
$entry = $schedule[$rel] ?? [];
|
foreach (glob("$dir/*.sh") ?: [] as $path) {
|
||||||
$scripts[] = [
|
$rel = $relPrefix . basename($path);
|
||||||
'id' => $rel,
|
$entry = $schedule[$rel] ?? [];
|
||||||
'label' => vv_pretty_label(basename($path, '.sh')),
|
$scripts[] = [
|
||||||
'desc' => vv_script_description($path),
|
'id' => $rel,
|
||||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
'label' => vv_pretty_label(basename($path, '.sh')),
|
||||||
'cron' => $entry['cron'] ?? '',
|
'desc' => vv_script_description($path),
|
||||||
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||||
];
|
'cron' => $entry['cron'] ?? '',
|
||||||
|
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$collect(SCRIPTS_DIR . '/Custom', 'Custom/');
|
||||||
|
|
||||||
|
// Platform adapter custom scripts (Plugin/<platform>/Custom/)
|
||||||
|
foreach (glob(SCRIPTS_DIR . '/Plugin/*/Custom') ?: [] as $customDir) {
|
||||||
|
$platform = basename(dirname($customDir));
|
||||||
|
$collect($customDir, "Plugin/$platform/Custom/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $scripts;
|
return $scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +315,7 @@ function vv_orch_conf_arrays(string $orchPath): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
|
// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
|
||||||
// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.
|
// master.conf *_SCRIPTS array and are not shown in any other scheduler card.
|
||||||
function vv_script_library(): array {
|
function vv_script_library(): array {
|
||||||
$scriptsDir = SCRIPTS_DIR;
|
$scriptsDir = SCRIPTS_DIR;
|
||||||
$confMap = vv_conf_script_map();
|
$confMap = vv_conf_script_map();
|
||||||
@@ -295,7 +323,17 @@ function vv_script_library(): array {
|
|||||||
foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
|
foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
|
||||||
$orchIds[] = 'Orchestrators/' . basename($p);
|
$orchIds[] = 'Orchestrators/' . basename($p);
|
||||||
}
|
}
|
||||||
$exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];
|
// Scripts already shown in their own cards are not "unlisted"
|
||||||
|
$schedule = vv_schedule_load();
|
||||||
|
$cardIds = array_flip(array_merge(
|
||||||
|
array_column(vv_tools_scripts(), 'id'),
|
||||||
|
array_column(vv_custom_scripts(), 'id')
|
||||||
|
));
|
||||||
|
|
||||||
|
// UI-only subdirs under Plugin/<platform>/ — no runnable scripts
|
||||||
|
$pluginUiDirs = ['api', 'include', 'pages', 'css', 'js', 'icons', 'event'];
|
||||||
|
|
||||||
|
$exclude = ['.git', 'Orchestrators', 'Custom', 'Configurations'];
|
||||||
$library = [];
|
$library = [];
|
||||||
try {
|
try {
|
||||||
$ri = new RecursiveIteratorIterator(
|
$ri = new RecursiveIteratorIterator(
|
||||||
@@ -306,8 +344,16 @@ function vv_script_library(): array {
|
|||||||
if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
|
if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
|
||||||
$rel = ltrim(str_replace($base, '', $rf->getPathname()), '/');
|
$rel = ltrim(str_replace($base, '', $rf->getPathname()), '/');
|
||||||
$parts = explode('/', $rel);
|
$parts = explode('/', $rel);
|
||||||
if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
|
if (in_array($parts[0], $exclude)) continue;
|
||||||
if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;
|
if ($parts[0] === 'Plugin') {
|
||||||
|
// Require Plugin/<platform>/<category>/<script>.sh — skip root-level adapter files
|
||||||
|
if (count($parts) < 4) continue;
|
||||||
|
// Skip UI-only category dirs
|
||||||
|
if (in_array($parts[2], $pluginUiDirs)) continue;
|
||||||
|
} elseif (count($parts) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel]) || isset($schedule[$rel])) continue;
|
||||||
$library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
|
$library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {}
|
} catch (Exception $e) {}
|
||||||
|
|||||||
@@ -7,3 +7,24 @@ function vvFlashStatus(el, msg, ok) {
|
|||||||
el.style.color = ok ? '#4caf50' : '#f44336';
|
el.style.color = ok ? '#4caf50' : '#f44336';
|
||||||
setTimeout(() => { el.textContent = ''; }, 3000);
|
setTimeout(() => { el.textContent = ''; }, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Fullscreen toggle — hides Unraid header + menu ────────────────────────────
|
||||||
|
function vvToggleExpand() {
|
||||||
|
const on = document.body.classList.toggle('vv-fullscreen');
|
||||||
|
const btn = document.getElementById('vv-expand-btn');
|
||||||
|
if (btn) { btn.classList.toggle('active', on); btn.title = on ? 'Collapse' : 'Expand'; }
|
||||||
|
localStorage.setItem('vv-fullscreen', on ? '1' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore state on every page load
|
||||||
|
(function() {
|
||||||
|
if (localStorage.getItem('vv-fullscreen') !== '1') return;
|
||||||
|
document.body.classList.add('vv-fullscreen');
|
||||||
|
// Button may not exist yet if script runs before DOM — wait for it
|
||||||
|
const apply = () => {
|
||||||
|
const btn = document.getElementById('vv-expand-btn');
|
||||||
|
if (btn) { btn.classList.add('active'); btn.title = 'Collapse'; }
|
||||||
|
};
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply);
|
||||||
|
else apply();
|
||||||
|
})();
|
||||||
|
|||||||
@@ -111,6 +111,12 @@
|
|||||||
.vv-au-domain { font-size:12px;color:#bbb;font-weight:bold; }
|
.vv-au-domain { font-size:12px;color:#bbb;font-weight:bold; }
|
||||||
.vv-au-fwd { font-size:10px;color:#444; }
|
.vv-au-fwd { font-size:10px;color:#444; }
|
||||||
.vv-au-loading { color:#333;font-size:11px;padding:16px;text-align:center; }
|
.vv-au-loading { color:#333;font-size:11px;padding:16px;text-align:center; }
|
||||||
|
|
||||||
|
/* ── Certs panel ─────────────────────────────────────────────────────────── */
|
||||||
|
.vv-au-cert-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px; }
|
||||||
|
.vv-au-cert-days { font-size:28px;font-weight:700;line-height:1;margin:6px 0 2px; }
|
||||||
|
.vv-au-cert-bar { height:3px;border-radius:2px;background:#1a1a1a;overflow:hidden;margin-top:8px; }
|
||||||
|
.vv-au-cert-fill { height:100%;border-radius:2px;transition:width .3s; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
@@ -123,6 +129,7 @@ $isOwner = vv_is_owner();
|
|||||||
<button class="vv-au-tab active" data-tab="proxies">Proxies</button>
|
<button class="vv-au-tab active" data-tab="proxies">Proxies</button>
|
||||||
<button class="vv-au-tab" data-tab="users">Users & Groups</button>
|
<button class="vv-au-tab" data-tab="users">Users & Groups</button>
|
||||||
<button class="vv-au-tab" data-tab="acl">Access Control</button>
|
<button class="vv-au-tab" data-tab="acl">Access Control</button>
|
||||||
|
<button class="vv-au-tab" data-tab="certs">Certs</button>
|
||||||
<button class="vv-au-btn" id="vv-au-refresh" title="Refresh current tab">↻ Refresh</button>
|
<button class="vv-au-btn" id="vv-au-refresh" title="Refresh current tab">↻ Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,6 +210,21 @@ $isOwner = vv_is_owner();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Certs ───────────────────────────────────────────────────────────────── -->
|
||||||
|
<div class="vv-au-panel" id="vv-au-panel-certs">
|
||||||
|
<div class="vv-au-sec-bar">
|
||||||
|
<span class="vv-au-sec-title" id="vv-au-cert-ts"></span>
|
||||||
|
<button class="vv-au-btn prim" id="vv-au-cert-run">Run now</button>
|
||||||
|
</div>
|
||||||
|
<div class="vv-au-cert-grid" id="vv-au-cert-grid">
|
||||||
|
<div class="vv-au-loading">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<div id="vv-au-cert-log" style="display:none;margin-top:12px;background:#0d0d0d;border:1px solid #1e1e1e;
|
||||||
|
border-radius:4px;padding:10px 12px;font-size:10px;color:#555;font-family:monospace;
|
||||||
|
max-height:140px;overflow-y:auto;white-space:pre-wrap;"></div>
|
||||||
|
<div id="vv-au-cert-cfg" style="margin-top:10px;font-size:10px;color:#3a3a3a;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Modal overlay ──────────────────────────────────────────────────────── -->
|
<!-- ── Modal overlay ──────────────────────────────────────────────────────── -->
|
||||||
<div class="vv-au-overlay" id="vv-au-overlay">
|
<div class="vv-au-overlay" id="vv-au-overlay">
|
||||||
<div class="vv-au-modal" id="vv-au-modal"></div>
|
<div class="vv-au-modal" id="vv-au-modal"></div>
|
||||||
@@ -212,7 +234,8 @@ $isOwner = vv_is_owner();
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const API = '/plugins/varaverk/api/auth.php';
|
const API = '/plugins/varaverk/api/auth.php';
|
||||||
|
const CERT_API = '/plugins/varaverk/api/cert.php';
|
||||||
const IS_OWNER = <?= $isOwner ? 'true' : 'false' ?>;
|
const IS_OWNER = <?= $isOwner ? 'true' : 'false' ?>;
|
||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────────
|
// ── State ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -287,6 +310,7 @@ function _loadTab(tab) {
|
|||||||
if (tab === 'proxies') _loadProxies();
|
if (tab === 'proxies') _loadProxies();
|
||||||
if (tab === 'users') { _loadUsers(); _loadGroups(); }
|
if (tab === 'users') { _loadUsers(); _loadGroups(); }
|
||||||
if (tab === 'acl') _loadAcl();
|
if (tab === 'acl') _loadAcl();
|
||||||
|
if (tab === 'certs') _loadCerts();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Proxies ───────────────────────────────────────────────────────────────────
|
// ── Proxies ───────────────────────────────────────────────────────────────────
|
||||||
@@ -910,6 +934,86 @@ document.getElementById('vv-au-overlay').addEventListener('click', e => {
|
|||||||
if (e.target === document.getElementById('vv-au-overlay')) _closeModal();
|
if (e.target === document.getElementById('vv-au-overlay')) _closeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Certs ─────────────────────────────────────────────────────────────────────
|
||||||
|
function _certBadgeCls(s) {
|
||||||
|
return ({OK:'ssl', WARN:'one_factor', CRIT:'deny', FAIL:'deny'})[s] || 'nossl';
|
||||||
|
}
|
||||||
|
function _certBadgeTxt(s) {
|
||||||
|
return ({OK:'healthy', WARN:'warning', CRIT:'critical', FAIL:'failed', UNKN:'not checked'})[s] || s;
|
||||||
|
}
|
||||||
|
function _certDayColor(days, warn, crit) {
|
||||||
|
if (days == null) return '#3a3a3a';
|
||||||
|
return days <= crit ? '#ef5350' : days <= warn ? '#ffb74d' : '#4caf50';
|
||||||
|
}
|
||||||
|
function _certRel(ts) {
|
||||||
|
if (!ts) return '—';
|
||||||
|
const d = Math.floor(Date.now()/1000) - ts;
|
||||||
|
if (d < 60) return 'just now';
|
||||||
|
if (d < 3600) return Math.floor(d/60) + 'm ago';
|
||||||
|
if (d < 86400) return Math.floor(d/3600) + 'h ago';
|
||||||
|
return Math.floor(d/86400) + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderCerts(data) {
|
||||||
|
const grid = document.getElementById('vv-au-cert-grid');
|
||||||
|
const ts = document.getElementById('vv-au-cert-ts');
|
||||||
|
const cfg = document.getElementById('vv-au-cert-cfg');
|
||||||
|
const warn = data.warn_days || 30, crit = data.crit_days || 7;
|
||||||
|
|
||||||
|
ts.textContent = data.checked_at ? 'Last checked: ' + _certRel(data.checked_at) : 'Not yet checked';
|
||||||
|
cfg.textContent = `Warn: ${warn}d · Crit: ${crit}d`;
|
||||||
|
|
||||||
|
const domains = data.domains || [];
|
||||||
|
if (!domains.length) {
|
||||||
|
grid.innerHTML = '<div class="vv-au-empty">No domains configured — add HOST*_CERT_MONITOR_DOMAINS to host.conf</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = domains.map(d => {
|
||||||
|
const s = d.status || 'UNKN';
|
||||||
|
const days = d.days;
|
||||||
|
const col = _certDayColor(days, warn, crit);
|
||||||
|
const barPct = days != null ? Math.min(Math.round(days/90*100), 100) : 0;
|
||||||
|
const expStr = d.expires ? 'Expires ' + d.expires : (s === 'UNKN' ? 'Not yet checked' : '');
|
||||||
|
return `<div class="vv-au-card" style="padding:12px 14px;">
|
||||||
|
<div class="vv-au-domain">${_esc(d.domain)}</div>
|
||||||
|
<div class="vv-au-cert-days" style="color:${col}">${days != null ? days : '—'}</div>
|
||||||
|
<div style="font-size:9px;color:#444;margin-bottom:6px;">${days != null ? 'days remaining' : ''}</div>
|
||||||
|
<span class="vv-au-badge ${_certBadgeCls(s)}">${_certBadgeTxt(s)}</span>
|
||||||
|
<div style="font-size:9px;color:#3a3a3a;margin-top:5px;">${_esc(expStr)}</div>
|
||||||
|
${days != null ? `<div class="vv-au-cert-bar"><div class="vv-au-cert-fill" style="width:${barPct}%;background:${col};"></div></div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadCerts() {
|
||||||
|
const grid = document.getElementById('vv-au-cert-grid');
|
||||||
|
grid.innerHTML = '<div class="vv-au-loading">Loading…</div>';
|
||||||
|
fetch(CERT_API)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => { if (d.ok) _renderCerts(d); })
|
||||||
|
.catch(() => { grid.innerHTML = '<div class="vv-au-empty" style="color:#ef5350">Failed to load cert data</div>'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('vv-au-cert-run').addEventListener('click', function() {
|
||||||
|
const btn = this;
|
||||||
|
const log = document.getElementById('vv-au-cert-log');
|
||||||
|
btn.disabled = true; btn.textContent = 'Checking…';
|
||||||
|
log.style.display = 'none'; log.textContent = '';
|
||||||
|
const fd = new FormData(); fd.append('action', 'run');
|
||||||
|
fetch(CERT_API, { method: 'POST', body: fd })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
btn.disabled = false; btn.textContent = 'Run now';
|
||||||
|
if (d.data) _renderCerts(d.data);
|
||||||
|
if (d.output && d.output.length) {
|
||||||
|
log.style.display = 'block';
|
||||||
|
log.textContent = d.output.join('\n').replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { btn.disabled = false; btn.textContent = 'Run now'; });
|
||||||
|
});
|
||||||
|
|
||||||
// ── Boot ──────────────────────────────────────────────────────────────────────
|
// ── Boot ──────────────────────────────────────────────────────────────────────
|
||||||
_loadProxies();
|
_loadProxies();
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ $runningScripts = array_unique($runningScripts);
|
|||||||
</div>
|
</div>
|
||||||
<div class="vv-children" id="vv-tools-children" style="display:none;">
|
<div class="vv-children" id="vv-tools-children" style="display:none;">
|
||||||
<?php if (empty($tools)): ?>
|
<?php if (empty($tools)): ?>
|
||||||
<p class="vv-custom-empty">No scripts found in Tools/.</p>
|
<p class="vv-custom-empty">No scripts found in Tools/ or Plugin/*/tools/.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($tools as $ts): $tsid = htmlspecialchars($ts['id']); ?>
|
<?php foreach ($tools as $ts): $tsid = htmlspecialchars($ts['id']); ?>
|
||||||
<div class="vv-script" data-id="<?= $tsid ?>">
|
<div class="vv-script" data-id="<?= $tsid ?>">
|
||||||
|
|||||||
+347
-275
@@ -1,312 +1,384 @@
|
|||||||
<?php
|
<?php
|
||||||
// First-run setup wizard.
|
// First-run setup wizard — uniform flow for all hosts.
|
||||||
// HOST1 path: blank master.conf → fill hostnames → write master.conf + host1.conf → scheduler.
|
// Step 1: auto-detect environment + server identity form.
|
||||||
// HOST2 path: state file present → pull master.conf from HOST1 → fill host2.conf → scheduler.
|
// Step 2: auto-populate + guide + checklist.
|
||||||
|
// master.conf pull (for partner servers) lives in the checklist, not here.
|
||||||
|
|
||||||
$detectedHostname = trim(shell_exec('hostname -s') ?: '');
|
$detectedHostname = vv_get_hostname();
|
||||||
|
|
||||||
// Check for setup state file pushed by HOST1
|
|
||||||
$setupState = vv_setup_state_read();
|
|
||||||
$host1FromState = $setupState['host1_hostname'] ?? '';
|
|
||||||
|
|
||||||
// Determine if HOST2 scenario: state file present but master.conf has blank HOST1
|
|
||||||
$_master = vv_read_conf_raw('master.conf');
|
|
||||||
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
|
|
||||||
$masterHost1 = trim($_h1m[1] ?? '');
|
|
||||||
|
|
||||||
// Determine local host slot (if master.conf has hostnames, we may already know)
|
|
||||||
$isHost2Flow = !empty($host1FromState) && empty($masterHost1);
|
|
||||||
|
|
||||||
// If master.conf has HOST1/HOST2 filled but local host.conf is missing:
|
|
||||||
// This is the "master was pushed, just need the local conf" scenario
|
|
||||||
$myHostId = vv_detect_host(); // may be 'host2' if master.conf was already pushed
|
|
||||||
$confMissing = $myHostId !== 'unknown' && !file_exists(CONF_DIR . '/' . $myHostId . '.conf');
|
|
||||||
$isConfOnlyFlow = !empty($masterHost1) && $confMissing;
|
|
||||||
?>
|
?>
|
||||||
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#vv-setup {
|
#vv-setup {
|
||||||
max-width: 560px;
|
max-width: 580px; margin: 40px auto 0;
|
||||||
margin: 48px auto 0;
|
background: #141414; border: 1px solid #2a2a2a;
|
||||||
background: #141414;
|
border-radius: 6px; padding: 36px 40px 40px;
|
||||||
border: 1px solid #2a2a2a;
|
font-family: monospace; color: #ccc;
|
||||||
border-radius: 6px;
|
|
||||||
padding: 36px 40px 40px;
|
|
||||||
font-family: monospace;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
}
|
||||||
#vv-setup h1 { margin: 0 0 6px; font-size: 18px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; }
|
#vv-setup h1 { margin: 0 0 4px; font-size: 17px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; }
|
||||||
#vv-setup .vv-setup-sub { font-size: 12px; color: #555; margin-bottom: 32px; }
|
.vv-sub { font-size: 12px; color: #555; margin-bottom: 28px; }
|
||||||
#vv-setup .vv-setup-field { margin-bottom: 20px; }
|
.vv-field { margin-bottom: 18px; }
|
||||||
#vv-setup label { display: block; font-size: 11px; color: #888; margin-bottom: 6px; text-transform: uppercase; letter-spacing: .06em; }
|
.vv-field label { display: block; font-size: 11px; color: #888; margin-bottom: 5px; text-transform: uppercase; letter-spacing: .06em; }
|
||||||
#vv-setup input[type=text] { width: 100%; box-sizing: border-box; background: #0d0d0d; border: 1px solid #333; color: #ddd; padding: 7px 10px; border-radius: 3px; font-family: monospace; font-size: 13px; }
|
.vv-field input[type=text],
|
||||||
#vv-setup input[type=text]:focus { outline: none; border-color: #555; }
|
.vv-field select {
|
||||||
#vv-setup .vv-setup-hint { font-size: 11px; color: #555; margin-top: 5px; }
|
width: 100%; box-sizing: border-box; background: #0d0d0d;
|
||||||
#vv-setup .vv-setup-role { display: flex; gap: 10px; margin-bottom: 24px; }
|
border: 1px solid #333; color: #ddd; padding: 7px 10px;
|
||||||
#vv-setup .vv-setup-role-btn { flex: 1; padding: 10px 0; background: #1a1a1a; border: 1px solid #333; border-radius: 3px; color: #888; font-family: monospace; font-size: 12px; cursor: pointer; text-align: center; transition: border-color .15s, color .15s; }
|
border-radius: 3px; font-family: monospace; font-size: 13px;
|
||||||
#vv-setup .vv-setup-role-btn.active { border-color: #555; color: #ccc; background: #222; }
|
}
|
||||||
#vv-setup .vv-setup-conditional { display: none; }
|
.vv-field input:focus, .vv-field select:focus { outline: none; border-color: #555; }
|
||||||
#vv-setup .vv-setup-conditional.visible { display: block; }
|
.vv-hint { font-size: 11px; color: #555; margin-top: 4px; }
|
||||||
#vv-setup hr.vv-setup-divider { border: none; border-top: 1px solid #222; margin: 24px 0; }
|
.vv-role-row { display: flex; gap: 10px; margin-bottom: 22px; }
|
||||||
#vv-setup-btn { width: 100%; padding: 10px; background: #1e1e1e; border: 1px solid #444; color: #ccc; font-family: monospace; font-size: 13px; border-radius: 3px; cursor: pointer; letter-spacing: .03em; }
|
.vv-role-btn { flex: 1; padding: 9px 0; background: #1a1a1a; border: 1px solid #333;
|
||||||
#vv-setup-btn:hover { border-color: #666; color: #eee; }
|
border-radius: 3px; color: #777; font-family: monospace; font-size: 12px;
|
||||||
#vv-setup-btn:disabled { opacity: .45; cursor: default; }
|
cursor: pointer; text-align: center; transition: border-color .15s, color .15s; }
|
||||||
#vv-setup-status { margin-top: 12px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }
|
.vv-role-btn.active { border-color: #555; color: #ccc; background: #1e1e1e; }
|
||||||
#vv-setup-status.ok { color: #4a8; }
|
.vv-cond { display: none; }
|
||||||
#vv-setup-status.err { color: #a44; }
|
.vv-cond.show { display: block; }
|
||||||
.vv-setup-info-box { background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px; padding: 12px 14px; margin-bottom: 24px; font-size: 12px; color: #777; line-height: 1.6; }
|
hr.vv-hr { border: none; border-top: 1px solid #1e1e1e; margin: 22px 0; }
|
||||||
.vv-setup-info-box strong { color: #aaa; }
|
.vv-btn { width: 100%; padding: 10px; background: #1e1e1e; border: 1px solid #444;
|
||||||
|
color: #ccc; font-family: monospace; font-size: 13px; border-radius: 3px;
|
||||||
|
cursor: pointer; letter-spacing: .03em; }
|
||||||
|
.vv-btn:hover { border-color: #666; color: #eee; }
|
||||||
|
.vv-btn:disabled { opacity: .4; cursor: default; }
|
||||||
|
#vv-status { margin-top: 10px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }
|
||||||
|
#vv-status.ok { color: #4a8; }
|
||||||
|
#vv-status.err { color: #a44; }
|
||||||
|
|
||||||
|
/* Detection banner */
|
||||||
|
#vv-detect-banner {
|
||||||
|
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||||
|
padding: 11px 14px; margin-bottom: 22px; font-size: 12px; line-height: 1.8; color: #666;
|
||||||
|
}
|
||||||
|
#vv-detect-banner .vv-det-row { display: flex; gap: 8px; }
|
||||||
|
#vv-detect-banner .vv-det-lbl { color: #555; min-width: 100px; }
|
||||||
|
#vv-detect-banner .vv-det-val { color: #999; }
|
||||||
|
#vv-detect-banner .loading { color: #444; font-style: italic; }
|
||||||
|
|
||||||
|
/* Step 2 */
|
||||||
|
#vv-step2 { display: none; }
|
||||||
|
.vv-guide {
|
||||||
|
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||||
|
padding: 13px 16px; margin-bottom: 20px; font-size: 12px; color: #666; line-height: 1.9;
|
||||||
|
}
|
||||||
|
.vv-guide ol { margin: 8px 0 0 16px; padding: 0; }
|
||||||
|
.vv-guide li { margin-bottom: 3px; }
|
||||||
|
.vv-cl-title { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }
|
||||||
|
.vv-cl-item { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0;
|
||||||
|
border-bottom: 1px solid #1a1a1a; font-size: 12px; }
|
||||||
|
.vv-cl-item:last-child { border-bottom: none; }
|
||||||
|
.vv-cl-icon { font-size: 13px; min-width: 16px; margin-top: 1px; }
|
||||||
|
.vv-cl-body { flex: 1; }
|
||||||
|
.vv-cl-label { color: #bbb; }
|
||||||
|
.vv-cl-detail{ color: #555; font-size: 11px; margin-top: 2px; }
|
||||||
|
.vv-cl-act { margin-top: 5px; }
|
||||||
|
.vv-cl-act button { padding: 4px 10px; background: #1a1a1a; border: 1px solid #333; color: #888;
|
||||||
|
font-family: monospace; font-size: 11px; border-radius: 2px; cursor: pointer; }
|
||||||
|
.vv-cl-act button:hover { border-color: #555; color: #bbb; }
|
||||||
|
.vv-cl-err { font-size: 11px; color: #a44; margin-top: 4px; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="vv-setup">
|
<div id="vv-setup">
|
||||||
|
|
||||||
<?php if ($isConfOnlyFlow): ?>
|
|
||||||
<!-- master.conf was pushed by HOST1, just need local host.conf -->
|
|
||||||
<?php $slotLabel = strtoupper($myHostId); ?>
|
|
||||||
<h1>⬡ Varaverk — <?= htmlspecialchars($slotLabel) ?> Setup</h1>
|
|
||||||
<div class="vv-setup-sub">master.conf received from HOST1. Create your local configuration to continue.</div>
|
|
||||||
|
|
||||||
<div class="vv-setup-info-box">
|
|
||||||
<strong>HOST1:</strong> <?= htmlspecialchars($masterHost1) ?><br>
|
|
||||||
<strong>This server:</strong> <?= htmlspecialchars($detectedHostname) ?> → <?= htmlspecialchars($slotLabel) ?><br>
|
|
||||||
<strong>Creating:</strong> <?= htmlspecialchars($myHostId) ?>.conf
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="vv-setup-btn" onclick="vvDoConfOnly()">Create <?= htmlspecialchars($myHostId) ?>.conf and continue →</button>
|
|
||||||
<div id="vv-setup-status"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function vvDoConfOnly() {
|
|
||||||
const btn = document.getElementById('vv-setup-btn');
|
|
||||||
const status = document.getElementById('vv-setup-status');
|
|
||||||
btn.disabled = true; btn.textContent = 'Creating…';
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
|
||||||
action: 'save',
|
|
||||||
host1: <?= json_encode($masterHost1) ?>,
|
|
||||||
host2: <?= json_encode(trim(preg_match('/^\s*HOST2\s*=\s*"([^"]*)"/m', $_master, $m2) ? $m2[1] : '')) ?>,
|
|
||||||
my_slot: <?= json_encode($myHostId) ?>,
|
|
||||||
my_hostname: <?= json_encode($detectedHostname) ?>,
|
|
||||||
});
|
|
||||||
fetch('/plugins/varaverk/api/setup.php', {
|
|
||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
|
||||||
}).then(r => r.json()).then(d => {
|
|
||||||
if (d.ok) {
|
|
||||||
status.textContent = '✓ Created'; status.className = 'ok';
|
|
||||||
vvShowStep2('?tab=scheduler&vv_setup=' + encodeURIComponent(<?= json_encode($myHostId . '.conf') ?>));
|
|
||||||
} else {
|
|
||||||
btn.disabled = false; btn.textContent = 'Create <?= htmlspecialchars($myHostId) ?>.conf and continue →';
|
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
|
||||||
}
|
|
||||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Create <?= htmlspecialchars($myHostId) ?>.conf and continue →'; status.textContent = '✗ Request failed'; status.className = 'err'; });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php elseif ($isHost2Flow): ?>
|
|
||||||
<!-- State file present, master.conf blank — HOST2 pull flow -->
|
|
||||||
<h1>⬡ Varaverk — Partner Setup</h1>
|
|
||||||
<div class="vv-setup-sub">HOST1 has been configured. Pull their settings to continue.</div>
|
|
||||||
|
|
||||||
<div class="vv-setup-info-box">
|
|
||||||
<strong>HOST1 detected:</strong> <?= htmlspecialchars($host1FromState) ?><br>
|
|
||||||
This server will pull master.conf from HOST1 via Tailscale + SSH.<br>
|
|
||||||
<span style="color:#555">Requires SSH keys to be exchanged first (Partnership/ssh_setup.sh).</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vv-setup-field">
|
|
||||||
<label>This server's hostname</label>
|
|
||||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
|
|
||||||
<div class="vv-setup-hint">Must match Settings → Identification exactly</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vv-setup-field">
|
|
||||||
<label>Your slot</label>
|
|
||||||
<select id="vv-partner-slot" style="width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #333;color:#ddd;padding:7px 10px;border-radius:3px;font-family:monospace;font-size:13px;">
|
|
||||||
<option value="host2">HOST2</option>
|
|
||||||
<option value="host3">HOST3</option>
|
|
||||||
<option value="host4">HOST4</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="vv-setup-btn" onclick="vvDoPull()">Pull configuration from HOST1 →</button>
|
|
||||||
<div id="vv-setup-status"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function vvDoPull() {
|
|
||||||
const btn = document.getElementById('vv-setup-btn');
|
|
||||||
const status = document.getElementById('vv-setup-status');
|
|
||||||
const hostname = document.getElementById('vv-hostname').value.trim();
|
|
||||||
const slot = document.getElementById('vv-partner-slot').value;
|
|
||||||
if (!hostname) { status.textContent = '✗ Hostname required'; status.className = 'err'; return; }
|
|
||||||
btn.disabled = true; btn.textContent = 'Pulling…';
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
|
||||||
action: 'pull',
|
|
||||||
host1_hostname: <?= json_encode($host1FromState) ?>,
|
|
||||||
my_slot: slot,
|
|
||||||
my_hostname: hostname,
|
|
||||||
});
|
|
||||||
fetch('/plugins/varaverk/api/setup.php', {
|
|
||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
|
||||||
}).then(r => r.json()).then(d => {
|
|
||||||
if (d.ok) {
|
|
||||||
status.textContent = '✓ Configuration pulled'; status.className = 'ok';
|
|
||||||
vvShowStep2(d.redirect ?? '?tab=scheduler');
|
|
||||||
} else {
|
|
||||||
btn.disabled = false; btn.textContent = 'Pull configuration from HOST1 →';
|
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
|
||||||
}
|
|
||||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Pull configuration from HOST1 →'; status.textContent = '✗ Request failed'; status.className = 'err'; });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php else: ?>
|
|
||||||
<!-- Standard first-run: no state file, blank master.conf -->
|
|
||||||
<h1>⬡ Varaverk — First Run</h1>
|
<h1>⬡ Varaverk — First Run</h1>
|
||||||
<div class="vv-setup-sub">Set up your server identity before the plugin can start.</div>
|
<div class="vv-sub">Set up this server before the plugin can start.</div>
|
||||||
|
|
||||||
<div class="vv-setup-field">
|
<!-- ── Step 1: Detection + identity ──────────────────────────────────────── -->
|
||||||
<label>This server's hostname</label>
|
<div id="vv-step1">
|
||||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" placeholder="unRAID-MyServer" autocomplete="off" spellcheck="false">
|
|
||||||
<div class="vv-setup-hint">Must match Settings → Identification exactly (case-sensitive)</div>
|
<div id="vv-detect-banner"><div class="loading">Detecting environment…</div></div>
|
||||||
|
|
||||||
|
<div class="vv-field">
|
||||||
|
<label>This server's hostname</label>
|
||||||
|
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
|
||||||
|
<div class="vv-hint">Must match Unraid Settings → Identification exactly (case-sensitive)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="vv-hr">
|
||||||
|
<label style="display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Server role</label>
|
||||||
|
<div class="vv-role-row">
|
||||||
|
<div class="vv-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
||||||
|
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first server</span>
|
||||||
|
</div>
|
||||||
|
<div class="vv-role-btn" id="vv-role-partner" onclick="vvSetRole('partner')">
|
||||||
|
Partner<br><span style="color:#555;font-size:10px;">HOST2+ · joining primary</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vv-cond" id="vv-cond-primary">
|
||||||
|
<div class="vv-field">
|
||||||
|
<label>Partner's hostname <span style="color:#444;font-size:10px;">(optional — can fill in later)</span></label>
|
||||||
|
<input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vv-cond" id="vv-cond-partner">
|
||||||
|
<div class="vv-field">
|
||||||
|
<label>Primary server's hostname <span style="color:#a44;font-size:10px;">required</span></label>
|
||||||
|
<input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="vv-field">
|
||||||
|
<label>Your slot</label>
|
||||||
|
<select id="vv-partner-slot">
|
||||||
|
<option value="host2">HOST2</option>
|
||||||
|
<option value="host3">HOST3</option>
|
||||||
|
<option value="host4">HOST4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:#555;margin-bottom:4px;">
|
||||||
|
SSH key and master.conf pull are handled automatically after save.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>
|
||||||
|
<div id="vv-status"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="vv-setup-divider">
|
<!-- ── Step 2: Populate + guide + checklist ───────────────────────────────── -->
|
||||||
|
<div id="vv-step2">
|
||||||
|
<hr class="vv-hr">
|
||||||
|
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:14px;">Step 2 of 2</div>
|
||||||
|
|
||||||
<label style="margin-bottom:10px;display:block;">Server role</label>
|
<div id="vv-populate-status" style="font-size:12px;color:#555;margin-bottom:14px;">⟳ Running auto-populate…</div>
|
||||||
<div class="vv-setup-role">
|
|
||||||
<div class="vv-setup-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
<div class="vv-guide">
|
||||||
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first to be set up</span>
|
<strong style="color:#888;">Quick start</strong>
|
||||||
|
<ol>
|
||||||
|
<li>Create your Unraid API key below — needed for live monitor stats</li>
|
||||||
|
<li>Open <strong>Scheduler → Edit host.conf</strong> — only three things need manual entry:<br>
|
||||||
|
<span style="color:#444;">
|
||||||
|
<code>EMBY_API_KEY</code> — Emby Dashboard → API Keys → + New Key<br>
|
||||||
|
<code>DISCORD_WEBHOOK</code> — for notifications (optional)<br>
|
||||||
|
<code>DAILY_SYNC_SHARES</code> — media paths to rsync nightly<br>
|
||||||
|
Everything else was auto-populated or has working defaults
|
||||||
|
</span></li>
|
||||||
|
<li>If partnering: the checklist below will guide you through pulling HOST1's config and running onboard</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="vv-setup-role-btn" id="vv-role-partner" onclick="vvSetRole('partner')">
|
|
||||||
Partner<br><span style="color:#555;font-size:10px;">HOST2+ · joining an existing primary</span>
|
<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;">
|
||||||
|
<button id="vv-key-btn" onclick="vvCreateKey(this)" class="vv-btn" style="flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;">
|
||||||
|
Create API Key
|
||||||
|
</button>
|
||||||
|
<a href="#" onclick="vvGoScheduler(event)" style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
||||||
|
</div>
|
||||||
|
<div id="vv-key-status" style="font-size:12px;min-height:14px;margin-bottom:18px;"></div>
|
||||||
|
|
||||||
|
<hr class="vv-hr">
|
||||||
|
<div class="vv-cl-title">Setup checklist</div>
|
||||||
|
<div id="vv-checklist"><div style="font-size:12px;color:#444;">Loading…</div></div>
|
||||||
|
|
||||||
|
<div style="margin-top:18px;text-align:right;">
|
||||||
|
<a href="#" onclick="vvGoScheduler(event)" style="font-size:12px;color:#444;text-decoration:none;">Go to Scheduler →</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vv-setup-conditional" id="vv-cond-primary">
|
|
||||||
<div class="vv-setup-field">
|
|
||||||
<label>Partner's hostname <span style="color:#444">(optional — can fill in later)</span></label>
|
|
||||||
<input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vv-setup-conditional" id="vv-cond-partner">
|
|
||||||
<div class="vv-setup-field">
|
|
||||||
<label>Primary server's hostname <span style="color:#a44">*required</span></label>
|
|
||||||
<input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
|
||||||
</div>
|
|
||||||
<div class="vv-setup-field">
|
|
||||||
<label>Your slot</label>
|
|
||||||
<select id="vv-partner-slot" style="width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #333;color:#ddd;padding:7px 10px;border-radius:3px;font-family:monospace;font-size:13px;">
|
|
||||||
<option value="host2">HOST2</option>
|
|
||||||
<option value="host3">HOST3</option>
|
|
||||||
<option value="host4">HOST4</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="vv-setup-btn" onclick="vvDoSetup()">Save and continue →</button>
|
|
||||||
<div id="vv-setup-status"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let vvRole = 'primary';
|
|
||||||
function vvSetRole(role) {
|
|
||||||
vvRole = role;
|
|
||||||
document.getElementById('vv-role-primary').classList.toggle('active', role === 'primary');
|
|
||||||
document.getElementById('vv-role-partner').classList.toggle('active', role === 'partner');
|
|
||||||
document.getElementById('vv-cond-primary').classList.toggle('visible', role === 'primary');
|
|
||||||
document.getElementById('vv-cond-partner').classList.toggle('visible', role === 'partner');
|
|
||||||
}
|
|
||||||
function vvDoSetup() {
|
|
||||||
const hostname = document.getElementById('vv-hostname').value.trim();
|
|
||||||
const status = document.getElementById('vv-setup-status');
|
|
||||||
const btn = document.getElementById('vv-setup-btn');
|
|
||||||
if (!hostname) { status.textContent = '✗ Hostname is required'; status.className = 'err'; return; }
|
|
||||||
let host1 = '', host2 = '', mySlot = 'host1';
|
|
||||||
if (vvRole === 'primary') {
|
|
||||||
host1 = hostname;
|
|
||||||
host2 = document.getElementById('vv-partner-hostname').value.trim();
|
|
||||||
mySlot = 'host1';
|
|
||||||
} else {
|
|
||||||
const primary = document.getElementById('vv-primary-hostname').value.trim();
|
|
||||||
if (!primary) { status.textContent = '✗ Primary hostname required'; status.className = 'err'; return; }
|
|
||||||
mySlot = document.getElementById('vv-partner-slot').value;
|
|
||||||
host1 = primary;
|
|
||||||
if (mySlot === 'host2') host2 = hostname;
|
|
||||||
}
|
|
||||||
btn.disabled = true; btn.textContent = 'Saving…';
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
|
||||||
action: 'save', host1, host2, my_slot: mySlot, my_hostname: hostname,
|
|
||||||
});
|
|
||||||
fetch('/plugins/varaverk/api/setup.php', {
|
|
||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
|
||||||
}).then(r => r.json()).then(d => {
|
|
||||||
if (d.ok) {
|
|
||||||
status.textContent = '✓ Saved'; status.className = 'ok';
|
|
||||||
vvShowStep2(d.redirect ?? '?tab=scheduler&vv_setup=master.conf');
|
|
||||||
} else {
|
|
||||||
btn.disabled = false; btn.textContent = 'Save and continue →';
|
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
|
||||||
}
|
|
||||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Save and continue →'; status.textContent = '✗ Request failed'; status.className = 'err'; });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Step 2: API key — shown after any wizard flow completes -->
|
|
||||||
<div id="vv-setup-step2" style="display:none;">
|
|
||||||
<hr class="vv-setup-divider">
|
|
||||||
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Step 2 of 2 — Unraid API Key</div>
|
|
||||||
<div class="vv-setup-info-box">
|
|
||||||
Varaverk uses the local Unraid API to display live stats on the Monitor tab.
|
|
||||||
Creates a <strong>Varaverk</strong> key via <code style="color:#555;">unraid-api</code> and writes it to your host conf.
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:10px;align-items:center;">
|
|
||||||
<button id="vv-key-btn2" onclick="vvCreateApiKeyWizard(this)"
|
|
||||||
style="flex:1;padding:10px 0;background:#1a3a1a;border:1px solid #2e6b2e;color:#6fcf97;
|
|
||||||
font-family:monospace;font-size:13px;border-radius:3px;cursor:pointer;">
|
|
||||||
Create API Key
|
|
||||||
</button>
|
|
||||||
<a id="vv-skip-link" href="#" onclick="vvWizardContinue(event)"
|
|
||||||
style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
|
||||||
</div>
|
|
||||||
<div id="vv-key-status2" style="margin-top:8px;font-size:12px;min-height:16px;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let _vvWizardNext = '';
|
let _vvRedirect = '?tab=scheduler';
|
||||||
function vvShowStep2(redirect) {
|
|
||||||
_vvWizardNext = redirect;
|
// ── Detection banner ──────────────────────────────────────────────────────────
|
||||||
document.getElementById('vv-setup-step2').style.display = 'block';
|
(function() {
|
||||||
|
fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now())
|
||||||
|
.then(r => r.json()).then(d => {
|
||||||
|
const b = document.getElementById('vv-detect-banner');
|
||||||
|
if (!d.ok) { b.innerHTML = '<span style="color:#555">Detection unavailable</span>'; return; }
|
||||||
|
const modeLabel = d.mode === 'internal'
|
||||||
|
? '<span style="color:#4a8">internal (NVMe/SSD)</span>'
|
||||||
|
: '<span style="color:#a84">flash mode (USB boot)</span>';
|
||||||
|
b.innerHTML =
|
||||||
|
'<div class="vv-det-row"><span class="vv-det-lbl">OS</span><span class="vv-det-val">Unraid ' + (d.unraid_ver||'') + '</span></div>' +
|
||||||
|
'<div class="vv-det-row"><span class="vv-det-lbl">Boot device</span><span class="vv-det-val">' + d.boot_device + ' (' + d.transport + ')</span></div>' +
|
||||||
|
'<div class="vv-det-row"><span class="vv-det-lbl">Storage mode</span><span class="vv-det-val">' + modeLabel + '</span></div>' +
|
||||||
|
'<div class="vv-det-row"><span class="vv-det-lbl">Scripts dir</span><span class="vv-det-val" style="color:#666">' + d.scripts_dir + '</span></div>';
|
||||||
|
const hf = document.getElementById('vv-hostname');
|
||||||
|
if (hf && !hf.value.trim()) hf.value = d.hostname;
|
||||||
|
}).catch(() => {
|
||||||
|
document.getElementById('vv-detect-banner').innerHTML = '<span style="color:#444">Detection unavailable</span>';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── Role toggle ───────────────────────────────────────────────────────────────
|
||||||
|
let vvRole = 'primary';
|
||||||
|
function vvSetRole(role) {
|
||||||
|
vvRole = role;
|
||||||
|
document.getElementById('vv-role-primary')?.classList.toggle('active', role === 'primary');
|
||||||
|
document.getElementById('vv-role-partner')?.classList.toggle('active', role === 'partner');
|
||||||
|
document.getElementById('vv-cond-primary')?.classList.toggle('show', role === 'primary');
|
||||||
|
document.getElementById('vv-cond-partner')?.classList.toggle('show', role === 'partner');
|
||||||
}
|
}
|
||||||
function vvWizardContinue(e) {
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
function vvSetStatus(msg, cls) {
|
||||||
|
const s = document.getElementById('vv-status');
|
||||||
|
s.textContent = msg; s.className = cls || '';
|
||||||
|
}
|
||||||
|
function vvSetBtn(text, disabled) {
|
||||||
|
const b = document.getElementById('vv-main-btn');
|
||||||
|
if (b) { b.textContent = text; b.disabled = disabled; }
|
||||||
|
}
|
||||||
|
function vvGoScheduler(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
window.location.href = _vvWizardNext || '?tab=scheduler';
|
window.location.href = _vvRedirect || '?tab=scheduler';
|
||||||
}
|
}
|
||||||
function vvCreateApiKeyWizard(btn) {
|
|
||||||
const status = document.getElementById('vv-key-status2');
|
// ── Step 2 ────────────────────────────────────────────────────────────────────
|
||||||
|
function vvShowStep2(redirect, apiKey) {
|
||||||
|
_vvRedirect = redirect || '?tab=scheduler';
|
||||||
|
document.getElementById('vv-step1').style.display = 'none';
|
||||||
|
document.getElementById('vv-step2').style.display = 'block';
|
||||||
|
if (apiKey && apiKey.ok) {
|
||||||
|
const btn = document.getElementById('vv-key-btn');
|
||||||
|
const status = document.getElementById('vv-key-status');
|
||||||
|
if (btn) { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }
|
||||||
|
if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }
|
||||||
|
}
|
||||||
|
vvRunPopulate();
|
||||||
|
vvLoadChecklist();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Populate ──────────────────────────────────────────────────────────────────
|
||||||
|
function vvRunPopulate() {
|
||||||
|
const el = document.getElementById('vv-populate-status');
|
||||||
|
fetch('/plugins/varaverk/api/setup.php', {
|
||||||
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({action: 'populate'})
|
||||||
|
}).then(r => r.json()).then(d => {
|
||||||
|
if (d.ok) {
|
||||||
|
const found = (d.lines || []).filter(l => /✅|found|detected/i.test(l));
|
||||||
|
el.textContent = found.length
|
||||||
|
? '✓ Auto-populate: ' + found.length + ' field' + (found.length > 1 ? 's' : '') + ' detected'
|
||||||
|
: '✓ Auto-populate ran — arr keys will fill once services are running';
|
||||||
|
el.style.color = '#4a8';
|
||||||
|
} else {
|
||||||
|
el.textContent = 'Auto-populate skipped — run Tools/conf_populate.sh once your arr containers are up';
|
||||||
|
el.style.color = '#555';
|
||||||
|
}
|
||||||
|
vvLoadChecklist();
|
||||||
|
}).catch(() => {
|
||||||
|
el.textContent = 'Auto-populate unavailable — run manually from Scheduler';
|
||||||
|
el.style.color = '#555';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Checklist ─────────────────────────────────────────────────────────────────
|
||||||
|
const vvActionLabels = {
|
||||||
|
create_key: 'Create API key',
|
||||||
|
ssh_setup: 'SSH guide →',
|
||||||
|
run_populate: 'Run now',
|
||||||
|
pull_master: 'Pull from HOST1',
|
||||||
|
onboard: 'Partnership tab →',
|
||||||
|
};
|
||||||
|
const vvActionHref = {
|
||||||
|
ssh_setup: '?tab=partnership',
|
||||||
|
onboard: '?tab=partnership',
|
||||||
|
};
|
||||||
|
|
||||||
|
function vvLoadChecklist() {
|
||||||
|
fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
|
||||||
|
.then(r => r.json()).then(d => {
|
||||||
|
const el = document.getElementById('vv-checklist');
|
||||||
|
if (!d.ok || !d.items) { el.innerHTML = '<span style="color:#555">Unable to load checklist</span>'; return; }
|
||||||
|
el.innerHTML = d.items.map(item => {
|
||||||
|
const icon = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
|
||||||
|
const iclr = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
|
||||||
|
let act = '';
|
||||||
|
if (item.action) {
|
||||||
|
const lbl = vvActionLabels[item.action] || item.action;
|
||||||
|
const href = vvActionHref[item.action];
|
||||||
|
if (href) {
|
||||||
|
act = `<div class="vv-cl-act"><a href="${href}" style="font-size:11px;color:#556;">${lbl}</a></div>`;
|
||||||
|
} else if (item.action === 'create_key') {
|
||||||
|
act = `<div class="vv-cl-act"><button onclick="vvCreateKey(this)">${lbl}</button></div>`;
|
||||||
|
} else if (item.action === 'run_populate') {
|
||||||
|
act = `<div class="vv-cl-act"><button onclick="vvRunPopulateBtn(this)">${lbl}</button></div>`;
|
||||||
|
} else if (item.action === 'pull_master') {
|
||||||
|
act = `<div class="vv-cl-act"><button onclick="vvPullMaster(this)">${lbl}</button><div id="vv-pull-err" class="vv-cl-err"></div></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `<div class="vv-cl-item">
|
||||||
|
<div class="vv-cl-icon" style="color:${iclr}">${icon}</div>
|
||||||
|
<div class="vv-cl-body">
|
||||||
|
<div class="vv-cl-label">${item.label}</div>
|
||||||
|
<div class="vv-cl-detail">${item.detail || ''}</div>
|
||||||
|
${act}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvRunPopulateBtn(btn) {
|
||||||
|
btn.disabled = true; btn.textContent = '…';
|
||||||
|
fetch('/plugins/varaverk/api/setup.php', {
|
||||||
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({action: 'populate'})
|
||||||
|
}).then(() => { btn.textContent = 'Done'; vvLoadChecklist(); })
|
||||||
|
.catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPullMaster(btn) {
|
||||||
|
btn.disabled = true; btn.textContent = '⟳ Pulling…';
|
||||||
|
const errEl = document.getElementById('vv-pull-err');
|
||||||
|
if (errEl) errEl.textContent = '';
|
||||||
|
fetch('/plugins/varaverk/api/setup.php', {
|
||||||
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({action: 'pull'})
|
||||||
|
}).then(r => r.json()).then(d => {
|
||||||
|
if (d.ok) {
|
||||||
|
btn.textContent = '✓ Done';
|
||||||
|
setTimeout(vvLoadChecklist, 600);
|
||||||
|
} else {
|
||||||
|
if (errEl) errEl.textContent = d.error || 'Pull failed';
|
||||||
|
btn.disabled = false; btn.textContent = 'Retry';
|
||||||
|
}
|
||||||
|
}).catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── API key ───────────────────────────────────────────────────────────────────
|
||||||
|
function vvCreateKey(btn) {
|
||||||
|
const status = document.getElementById('vv-key-status');
|
||||||
btn.disabled = true; btn.textContent = '⟳ Creating…';
|
btn.disabled = true; btn.textContent = '⟳ Creating…';
|
||||||
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
|
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
|
||||||
.then(r => r.json())
|
.then(r => r.json()).then(d => {
|
||||||
.then(d => {
|
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
status.textContent = '✓ Key created — ' + d.key_preview; status.style.color = '#4a8';
|
status.textContent = '✓ Key created — ' + d.key_preview;
|
||||||
btn.textContent = 'Continue →'; btn.disabled = false;
|
status.style.color = '#4a8';
|
||||||
btn.onclick = vvWizardContinue;
|
btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
|
||||||
const skip = document.getElementById('vv-skip-link');
|
vvLoadChecklist();
|
||||||
if (skip) skip.style.display = 'none';
|
|
||||||
} else {
|
} else {
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Failed'); status.style.color = '#a44';
|
status.textContent = '✗ ' + (d.error || 'Failed');
|
||||||
|
status.style.color = '#a44';
|
||||||
btn.disabled = false; btn.textContent = 'Retry';
|
btn.disabled = false; btn.textContent = 'Retry';
|
||||||
}
|
}
|
||||||
})
|
}).catch(e => {
|
||||||
.catch(e => {
|
|
||||||
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
||||||
btn.disabled = false; btn.textContent = 'Retry';
|
btn.disabled = false; btn.textContent = 'Retry';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
</div>
|
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||||
|
function vvDoSave() {
|
||||||
|
const hostname = document.getElementById('vv-hostname')?.value.trim();
|
||||||
|
if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
|
||||||
|
let host1 = '', host2 = '', mySlot = 'host1';
|
||||||
|
if (vvRole === 'primary') {
|
||||||
|
host1 = hostname;
|
||||||
|
host2 = document.getElementById('vv-partner-hostname')?.value.trim() || '';
|
||||||
|
mySlot = 'host1';
|
||||||
|
} else {
|
||||||
|
const primary = document.getElementById('vv-primary-hostname')?.value.trim();
|
||||||
|
if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
|
||||||
|
mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
|
||||||
|
host1 = primary;
|
||||||
|
if (mySlot === 'host2') host2 = hostname;
|
||||||
|
}
|
||||||
|
vvSetBtn('Saving…', true);
|
||||||
|
fetch('/plugins/varaverk/api/setup.php', {
|
||||||
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname})
|
||||||
|
}).then(r => r.json()).then(d => {
|
||||||
|
if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler', d.api_key); }
|
||||||
|
else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
|
||||||
|
}).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -802,8 +802,8 @@
|
|||||||
# Symptom of saturation: WebGUI slow, settings saves hang, container UI starts timeout.
|
# Symptom of saturation: WebGUI slow, settings saves hang, container UI starts timeout.
|
||||||
# Resets on each reboot — reapplied at array start. Idempotent: silent when already correct.
|
# Resets on each reboot — reapplied at array start. Idempotent: silent when already correct.
|
||||||
#
|
#
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh --status
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/php_fpm_max_children.sh --status
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/php_fpm_max_children.sh
|
||||||
|
|
||||||
# docker_syslog_filter.sh — suppress Docker veth/docker0 interface log noise
|
# docker_syslog_filter.sh — suppress Docker veth/docker0 interface log noise
|
||||||
# Called by array_started.sh before containers start. Creates rsyslog drop rule.
|
# Called by array_started.sh before containers start. Creates rsyslog drop rule.
|
||||||
@@ -817,8 +817,8 @@
|
|||||||
# webgui_watchdog.sh — WebGUI availability watchdog (called by system_watchdog via SYSTEM_WATCHDOG_SCRIPTS)
|
# webgui_watchdog.sh — WebGUI availability watchdog (called by system_watchdog via SYSTEM_WATCHDOG_SCRIPTS)
|
||||||
# Not scheduled directly — runs as part of the every-15-minute watchdog chain.
|
# Not scheduled directly — runs as part of the every-15-minute watchdog chain.
|
||||||
# Escalation: nginx → php-fpm → emhttp, each with recheck before proceeding to next level.
|
# Escalation: nginx → php-fpm → emhttp, each with recheck before proceeding to next level.
|
||||||
# bash /boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh --dry-run
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System/webgui_watchdog.sh --dry-run
|
||||||
# bash /boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh --status
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System/webgui_watchdog.sh --status
|
||||||
|
|
||||||
# clear_logs.sh — size-threshold log cleanup (called by weekly_sync_maintenance via WEEKLY_MAINTENANCE_SCRIPTS)
|
# clear_logs.sh — size-threshold log cleanup (called by weekly_sync_maintenance via WEEKLY_MAINTENANCE_SCRIPTS)
|
||||||
# Not scheduled directly — runs as part of the Sunday 2:30am weekly window.
|
# Not scheduled directly — runs as part of the Sunday 2:30am weekly window.
|
||||||
@@ -831,9 +831,9 @@
|
|||||||
# SIGTERM (allows mover to finish current file — no partial files). Verify. SIGKILL last resort.
|
# SIGTERM (allows mover to finish current file — no partial files). Verify. SIGKILL last resort.
|
||||||
# Use before: planned reboots with mover running, disk replacement, array maintenance.
|
# Use before: planned reboots with mover running, disk replacement, array maintenance.
|
||||||
#
|
#
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/mover_stop.sh --status
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/mover_stop.sh --status
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/mover_stop.sh --dry-run
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/mover_stop.sh --dry-run
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/mover_stop.sh
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/mover_stop.sh
|
||||||
|
|
||||||
# server_reboot.sh — graceful reboot with pre-flight warnings and clean shutdown sequence
|
# server_reboot.sh — graceful reboot with pre-flight warnings and clean shutdown sequence
|
||||||
# Pre-flight warnings (inform not block): rsync running, mover running, active Emby sessions.
|
# Pre-flight warnings (inform not block): rsync running, mover running, active Emby sessions.
|
||||||
@@ -851,9 +851,9 @@
|
|||||||
# Use when plugin Abort button didn't work, or before a reboot to clean up running scripts.
|
# Use when plugin Abort button didn't work, or before a reboot to clean up running scripts.
|
||||||
# Called automatically by server_reboot.sh before reboot.
|
# Called automatically by server_reboot.sh before reboot.
|
||||||
#
|
#
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh --status
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/user_scripts_stop.sh --status
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh --dry-run
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/user_scripts_stop.sh --dry-run
|
||||||
# bash /boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/user_scripts_stop.sh
|
||||||
|
|
||||||
# git_pull_execute.sh — pull latest scripts from Gitea and set execute permissions
|
# git_pull_execute.sh — pull latest scripts from Gitea and set execute permissions
|
||||||
# Deployment mechanism for the ecosystem. Push from VS Code → Gitea → run on both servers.
|
# Deployment mechanism for the ecosystem. Push from VS Code → Gitea → run on both servers.
|
||||||
@@ -1401,9 +1401,9 @@
|
|||||||
# chown nobody:users on creation. Run once — then initial rsync populates the content.
|
# chown nobody:users on creation. Run once — then initial rsync populates the content.
|
||||||
# Without this: rsync aborts "remote share missing" even though the share shows in the UI.
|
# Without this: rsync aborts "remote share missing" even though the share shows in the UI.
|
||||||
#
|
#
|
||||||
# bash /boot/config/plugins/varaverk/Tools/recreate_shares.sh --dry-run
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/Tools/recreate_shares.sh --dry-run
|
||||||
# bash /boot/config/plugins/varaverk/Tools/recreate_shares.sh --status
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/Tools/recreate_shares.sh --status
|
||||||
# bash /boot/config/plugins/varaverk/Tools/recreate_shares.sh
|
# bash /boot/config/plugins/varaverk/Plugin/unraid/Tools/recreate_shares.sh
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
+3
-2
@@ -255,10 +255,11 @@ else
|
|||||||
# ── Flash mode: sync Plugin/ to /boot/ so the webUI picks up updates ─────
|
# ── Flash mode: sync Plugin/ to /boot/ so the webUI picks up updates ─────
|
||||||
# In flash mode SCRIPTS_DIR is in appdata — Plugin/ lives in the repo there
|
# In flash mode SCRIPTS_DIR is in appdata — Plugin/ lives in the repo there
|
||||||
# but Unraid serves PHP from /boot/. Sync after every pull to keep them in step.
|
# but Unraid serves PHP from /boot/. Sync after every pull to keep them in step.
|
||||||
if [[ "$TARGET_DIR" != "/boot/config/plugins/varaverk" ]]; then
|
_BOOT_DIR="/boot/config/plugins/varaverk"
|
||||||
|
if [[ "$TARGET_DIR" != "$_BOOT_DIR" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ $ICON_SYNC Flash mode: sync Plugin/ → /boot/ ━━━"
|
echo "━━━ $ICON_SYNC Flash mode: sync Plugin/ → /boot/ ━━━"
|
||||||
if rsync -a --delete "$TARGET_DIR/Plugin/" "/boot/config/plugins/varaverk/Plugin/" 2>/dev/null; then
|
if rsync -a --delete "$TARGET_DIR/Plugin/" "$_BOOT_DIR/Plugin/" 2>/dev/null; then
|
||||||
echo " Plugin/ synced to /boot/ ✅"
|
echo " Plugin/ synced to /boot/ ✅"
|
||||||
else
|
else
|
||||||
warn "Plugin/ sync to /boot/ failed — webUI may be stale until next pull"
|
warn "Plugin/ sync to /boot/ failed — webUI may be stale until next pull"
|
||||||
|
|||||||
@@ -132,5 +132,13 @@
|
|||||||
_adapter="$LOAD_CONFIG_DIR/Plugin/$PLATFORM/adapter.sh"
|
_adapter="$LOAD_CONFIG_DIR/Plugin/$PLATFORM/adapter.sh"
|
||||||
[[ -f "$_adapter" ]] && source "$_adapter"
|
[[ -f "$_adapter" ]] && source "$_adapter"
|
||||||
|
|
||||||
|
# ━━━ Derived path constants ━━━
|
||||||
|
# Centralised here so every script that sources load_config.sh has them without
|
||||||
|
# re-deriving from SCRIPTS_DIR or hardcoding /var/log or /tmp paths inline.
|
||||||
|
CONF_DIR="${SCRIPTS_DIR}/Configurations"
|
||||||
|
LOG_DIR="/var/log/varaverk"
|
||||||
|
VV_CACHE_DIR="/tmp/vv_cache"
|
||||||
|
export CONF_DIR LOG_DIR VV_CACHE_DIR
|
||||||
|
|
||||||
# ━━━ Cleanup ━━━
|
# ━━━ Cleanup ━━━
|
||||||
unset _conf _host_confs_loaded _adapter LOAD_CONFIG_DIR
|
unset _conf _host_confs_loaded _adapter LOAD_CONFIG_DIR
|
||||||
Reference in New Issue
Block a user