Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d56b02e410 | ||
|
|
da3d402f1a | ||
|
|
9a26478498 | ||
|
|
5edb32f48d | ||
|
|
7e01f6d3db | ||
|
|
10fc703e57 | ||
|
|
d09ea11694 | ||
|
|
0121462576 | ||
|
|
974e3cf2cf | ||
|
|
64f2c89a40 | ||
|
|
af89258bbd | ||
|
|
1e9681a71e | ||
|
|
6549ed0301 | ||
|
|
5ebf1fc96f | ||
|
|
b5d7413d88 | ||
|
|
da0c303f22 | ||
|
|
26f3778786 |
+17
@@ -0,0 +1,17 @@
|
||||
# ── Personal configuration — never push to public repo ────────────────────────
|
||||
# These contain API keys, passwords, SSH paths, personal hostnames.
|
||||
# Templates (*.template) are safe and remain tracked.
|
||||
Configurations/host*.conf
|
||||
Configurations/master.conf
|
||||
Configurations/*.bak
|
||||
|
||||
# ── Runtime state, data, logs ─────────────────────────────────────────────────
|
||||
data/
|
||||
*.log
|
||||
*.lock
|
||||
|
||||
# ── OS / editor ───────────────────────────────────────────────────────────────
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
.vscode/
|
||||
@@ -0,0 +1,457 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ========================== HOSTN CONFIGURATION — (hostname) ==================================
|
||||
# ==============================================================================================
|
||||
# HOSTN-specific variables — credentials, container names, share paths, failover lists.
|
||||
# Sourced after master.conf — values here extend shared profile arrays and add HOSTN-specific
|
||||
# identity, credentials, and container configuration.
|
||||
#
|
||||
# Sparse checkout (git) ensures other hosts never receive this file.
|
||||
#
|
||||
# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
|
||||
# DO NOT put other hosts' variables here — they belong in their own host*.conf files.
|
||||
#
|
||||
# ── HOW TO USE THIS TEMPLATE ──────────────────────────────────────────────────────────────────
|
||||
# This file was generated by the Varaverk first-run wizard.
|
||||
# Fill in the sections that apply to your setup — leave unused sections empty.
|
||||
# All scripts self-guard against empty values — safe to leave sections blank until needed.
|
||||
#
|
||||
# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
|
||||
# IDENTITY hostname, SSH key
|
||||
# EMBY container name, URL, API key
|
||||
# NOTIFICATIONS Discord webhook
|
||||
# PARTNERSHIP auth containers, backup paths
|
||||
#
|
||||
# ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
|
||||
# DAILY SYNC SHARES media shares this host owns and pushes
|
||||
# WEEKLY SYNC SHARES appdata shares synced weekly
|
||||
# CRITICAL SYNC SHARES appdata shares synced every 30 minutes
|
||||
# HOSTN RSYNC PROFILE host-specific appdata sync profile
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers
|
||||
# DOCKER NETWORK CONNECT networks and containers for array start
|
||||
#
|
||||
# ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
|
||||
# DDNS DDNS containers managed by this host
|
||||
# INTERNET LOSS containers stopped when internet is lost
|
||||
# FALLBACK TIERS what this host runs for the remote per tier
|
||||
# TIER DELAYS delays before each tier activates
|
||||
# RSYNC WRITEBACK appdata synced back on handback
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for permissions script
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── MONITORS ───────────────────────────────────────────────────────────────────────────────
|
||||
# CERTIFICATE MONITOR domains checked for SSL expiry
|
||||
# SMART HEALTH drives to skip in SMART monitoring
|
||||
# ZFS REPORT pools to exclude from ZFS health report
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
|
||||
# DOWNLOADERS slskd, SABnzbd, qBittorrent credentials and URLs
|
||||
# LIDARR / SONARR / RADARR URL, API key, path map
|
||||
# ARR RECOVERY per-arr recovery toggles
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── IDENTITY & CONNECTIVITY ───────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Identity ━━━
|
||||
# HOSTN hostname lives in master.conf (not a credential — safe for all servers).
|
||||
# SSH key used for all server-to-server operations — rsync, failover, conf sync.
|
||||
# Convention: /root/.ssh/<hostname-lowercase-no-unraid-prefix>_rsync_automation
|
||||
# Must be in /root/.ssh/ and authorised in the partner's /root/.ssh/authorized_keys.
|
||||
# Run Partnership/ssh_setup.sh to generate the key and copy it to the partner.
|
||||
HOSTN_SSH_KEY="" # e.g. /root/.ssh/myserver_rsync_automation
|
||||
HOSTN_OWNER="" # short identifier for this server (e.g. myserver)
|
||||
HOSTN_OWNER_EMAIL=""
|
||||
|
||||
# ━━━ Unraid API ━━━
|
||||
# Used by the Varaverk plugin to query this server's Unraid GraphQL API.
|
||||
# Generate in Unraid: Settings → Management Access → API Keys → + New Key
|
||||
HOSTN_UNRAID_API_KEY=""
|
||||
|
||||
# ━━━ Emby ━━━
|
||||
HOSTN_EMBY_CONTAINER="Emby"
|
||||
HOSTN_EMBY_URL="http://localhost:8096"
|
||||
HOSTN_EMBY_API_KEY="" # Emby Dashboard → API Keys → + New Key
|
||||
|
||||
# ━━━ Jellyfin ━━━
|
||||
HOSTN_JELLYFIN_CONTAINER="Jellyfin"
|
||||
HOSTN_JELLYFIN_URL="http://localhost:8095"
|
||||
HOSTN_JELLYFIN_API_KEY="" # Jellyfin Dashboard → Administration → API Keys
|
||||
|
||||
# ━━━ Gitea ━━━
|
||||
# Personal access token for gitea_ssh_setup.sh.
|
||||
# Create in Gitea: Settings → Applications → Generate Token → scope: write:user
|
||||
HOSTN_GITEA_API_TOKEN=""
|
||||
|
||||
# ━━━ Notifications ━━━
|
||||
# Discord webhook — leave blank to disable.
|
||||
HOSTN_DISCORD_WEBHOOK=""
|
||||
|
||||
# ━━━ Partnership ━━━
|
||||
|
||||
# Auth containers reconfigured on onboard/offboard.
|
||||
# Format: "ContainerName|WebUIPort"
|
||||
HOSTN_PARTNERSHIP_AUTH_WEBUIS=(
|
||||
# "NginxProxyManager|81"
|
||||
# "Authelia|9091"
|
||||
)
|
||||
|
||||
# XML templates pushed to mirror during onboard — auth stack.
|
||||
# Dependencies (databases) must come before apps that depend on them.
|
||||
HOSTN_PARTNERSHIP_AUTH_STACK=(
|
||||
# "my-Authelia.xml"
|
||||
# "my-NginxProxyManager.xml"
|
||||
)
|
||||
|
||||
# XML templates pushed to mirror during onboard — arr stack.
|
||||
HOSTN_PARTNERSHIP_ARR_STACK=(
|
||||
# "my-Sonarr.xml"
|
||||
# "my-Radarr.xml"
|
||||
)
|
||||
|
||||
# Paths the partner should collect during the grace window after offboard.
|
||||
HOSTN_PARTNERSHIP_MIRROR_BACKUPS=(
|
||||
# "/mnt/user/appdata-Fallback/Partner-Emby"
|
||||
)
|
||||
|
||||
# Containers parked on this server when partnership is active.
|
||||
HOSTN_PARTNERSHIP_OWN_CONTAINERS=(
|
||||
# "Emby"
|
||||
)
|
||||
|
||||
# Emby admin provisioning — owner controls whether Emby is shared.
|
||||
HOSTN_PARTNERSHIP_PROVISION_EMBY_ADMIN=false
|
||||
HOSTN_PARTNERSHIP_EMBY_PORT=8096
|
||||
HOSTN_PARTNERSHIP_EMBY_ADMIN_USER=""
|
||||
HOSTN_PARTNERSHIP_EMBY_ADMIN_PASS=""
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RSYNC ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Daily Sync Shares ━━━
|
||||
# Media shares this host pushes to all other nodes every night.
|
||||
# Uses DEFAULT_RSYNC_OPTS from master.conf — no profile needed.
|
||||
HOSTN_DAILY_SYNC_SHARES=(
|
||||
# /mnt/user/Movies
|
||||
# /mnt/user/Tv_Shows
|
||||
# /mnt/user/Music
|
||||
)
|
||||
|
||||
# ━━━ Weekly Sync Shares ━━━
|
||||
# Appdata shares synced during the weekly maintenance window.
|
||||
# Profiles (emby, critical-data) drive container stops — define in master.conf.
|
||||
HOSTN_WEEKLY_SYNC_SHARES=(
|
||||
# "/mnt/user/Media_Server/Emby" # emby profile
|
||||
# "/mnt/user/appdata-Fallback/Critical-Data" # critical-data profile
|
||||
)
|
||||
|
||||
# ━━━ Intermediate Sync Shares ━━━
|
||||
# Shares synced every 4 hours. Leave empty to skip mid-day rsync.
|
||||
HOSTN_INTERMEDIATE_SYNC_SHARES=(
|
||||
# Add shares here to enable mid-day rsync
|
||||
)
|
||||
|
||||
# ━━━ Critical Sync Shares ━━━
|
||||
# Appdata shares synced every 30 minutes.
|
||||
# Format: "/path/to/share" or "/path/to/share|profile-name"
|
||||
HOSTN_CRITICAL_SYNC_SHARES=(
|
||||
# "/mnt/user/appdata-Fallback/Critical-Data|critical-fallback"
|
||||
# "/mnt/user/Media_Server/Emby|emby-fallback"
|
||||
)
|
||||
|
||||
# ━━━ Backup Verify ━━━
|
||||
# Leave empty to use HOSTN_DAILY_SYNC_SHARES automatically.
|
||||
HOSTN_BACKUP_VERIFY_SHARES=(
|
||||
# leave empty to use HOSTN_DAILY_SYNC_SHARES automatically
|
||||
)
|
||||
|
||||
# ━━━ HOSTN Rsync Profile — hostn-appdata ━━━
|
||||
# Host-specific appdata sync profile.
|
||||
PROFILE_RSYNC_OPTS[hostn-appdata]="-av --info=progress2 --bwlimit=${PROFILE_BW_LIMIT[hostn-appdata]:-8000}"
|
||||
PROFILE_BW_LIMIT[hostn-appdata]=8000
|
||||
PROFILE_RETRY_COUNT[hostn-appdata]=3
|
||||
PROFILE_SLEEP[hostn-appdata]=300
|
||||
PROFILE_CRITICAL_CONTAINER_NAMES[hostn-appdata]=""
|
||||
PROFILE_DELAYED_CONTAINERS[hostn-appdata]=""
|
||||
PROFILE_CONTAINER_DELAY[hostn-appdata]=5
|
||||
PROFILE_EXCLUDE_DIRS[hostn-appdata]="logs *.tmp"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── DOCKER ────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Docker Daily Restart ━━━
|
||||
HOSTN_DAILY_RESTART_CONTAINERS=(
|
||||
# "NginxProxyManager"
|
||||
# "Authelia"
|
||||
)
|
||||
|
||||
# ━━━ Docker Weekly Restart ━━━
|
||||
HOSTN_WEEKLY_RESTART_CONTAINERS=(
|
||||
# "NextCloud"
|
||||
# "AdGuard-Home"
|
||||
)
|
||||
|
||||
# ━━━ Docker Watchdog ━━━
|
||||
|
||||
# Memory hard limits in MB — immediate restart if exceeded.
|
||||
# 20GB=20480 16GB=16384 8GB=8192 4GB=4096 2GB=2048 1GB=1024
|
||||
declare -A HOSTN_WATCHDOG_CONTAINERS=(
|
||||
# ["Emby"]=18432
|
||||
)
|
||||
|
||||
# HTTP health check URLs — checked every cycle.
|
||||
declare -A HOSTN_WATCHDOG_CONTAINER_URLS=(
|
||||
# ["Emby"]="http://localhost:8096"
|
||||
)
|
||||
|
||||
# Required containers — must always be running.
|
||||
HOSTN_WATCHDOG_REQUIRED_CONTAINERS=(
|
||||
# "NginxProxyManager"
|
||||
# "Authelia"
|
||||
)
|
||||
|
||||
# Containers to skip in Tier 2 global scan.
|
||||
HOSTN_WATCHDOG_SCAN_IGNORE=(
|
||||
# "my-occasional-container"
|
||||
)
|
||||
|
||||
# Dependency ordering — skip restarting a container if its dependency is also down.
|
||||
declare -A HOSTN_WATCHDOG_DEPENDENCIES=(
|
||||
# ["Authelia"]="Mariadb Redis-Authelia"
|
||||
)
|
||||
|
||||
# Per-container appdata growth suppress ceilings in MB.
|
||||
declare -A HOSTN_WATCHDOG_APPDATA_SIZES=(
|
||||
# ["Tdarr"]="25600"
|
||||
)
|
||||
|
||||
# ━━━ Network Watchdog ━━━
|
||||
HOSTN_NETWORK_WATCHDOG_DDNS_DOMAIN="" # e.g. myserver.com
|
||||
HOSTN_NETWORK_WATCHDOG_DDNS_CONTAINER="" # e.g. MyServer.com
|
||||
HOSTN_NETWORK_WATCHDOG_NPM_URL="" # e.g. https://myserver.com
|
||||
|
||||
# ━━━ Docker Network Connect ━━━
|
||||
HOSTN_NETWORK_CONNECT_CONTAINERS=(
|
||||
# "memcached"
|
||||
)
|
||||
|
||||
HOSTN_NETWORK_CONNECT_NETWORKS=(
|
||||
# "high-availability"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ DDNS ━━━
|
||||
# DDNS containers this host manages.
|
||||
HOSTN_DDNS_CONTAINERS=(
|
||||
# "MyServer.com"
|
||||
)
|
||||
|
||||
# ━━━ Internet Loss ━━━
|
||||
# Containers stopped immediately when internet is lost.
|
||||
FALLBACK_HOSTN_STOP_ON_NO_NET=(
|
||||
# "MyServer.com"
|
||||
)
|
||||
|
||||
# ━━━ Fallback Tiers — HOSTN Runs for Partner ━━━
|
||||
# Containers this host starts when the partner goes down.
|
||||
# Replace REMOTE_ID below with the actual remote host ID (HOST1, HOST2, etc.)
|
||||
FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER1=(
|
||||
# "Partner-DDNS-Container"
|
||||
)
|
||||
|
||||
FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER2=(
|
||||
# "container-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER3=(
|
||||
# "container-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOSTN_COVERS_REMOTE_ID_TIER4=(
|
||||
# "container-placeholder"
|
||||
)
|
||||
|
||||
# ━━━ Tier Delays — Partner's Containers on this Host ━━━
|
||||
# How long the partner must be down before each tier activates here — in minutes.
|
||||
# Replace REMOTE_ID with the actual remote host ID (HOST1, HOST2, etc.)
|
||||
REMOTE_ID_TIER2_DELAY=240 # 4 hours
|
||||
REMOTE_ID_TIER3_DELAY=720 # 12 hours
|
||||
REMOTE_ID_TIER4_DELAY=1440 # 24 hours
|
||||
|
||||
# ━━━ Rsync Writeback ━━━
|
||||
HOSTN_TIER1_WRITEBACK_DELAY=60 # skip Emby writeback if outage under 1hr
|
||||
|
||||
FALLBACK_HOSTN_WRITEBACK_TIER1=(
|
||||
# "/mnt/user/Media_Server/Emby"
|
||||
)
|
||||
|
||||
FALLBACK_HOSTN_WRITEBACK_TIER2=(
|
||||
# "/mnt/user/appdata-Fallback/Important-Data"
|
||||
)
|
||||
|
||||
FALLBACK_HOSTN_WRITEBACK_TIER3=(
|
||||
# "location-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOSTN_WRITEBACK_TIER4=(
|
||||
# "/mnt/user/appdata-Fallback/Arrs_Stack"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MEDIA ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Media Permissions ━━━
|
||||
HOSTN_MEDIA_PERMISSION_SHARES=(
|
||||
# /mnt/user/Movies
|
||||
# /mnt/user/Tv_Shows
|
||||
# /mnt/user/Music
|
||||
# /mnt/user/Downloads
|
||||
)
|
||||
|
||||
# ━━━ Media Cleaner ━━━
|
||||
HOSTN_ANIME_CLEAN_FOLDERS=(
|
||||
# /mnt/user/Anime_Movies
|
||||
# /mnt/user/Anime_Shows
|
||||
)
|
||||
|
||||
HOSTN_MEDIA_CLEAN_FOLDERS=(
|
||||
# /mnt/user/Movies
|
||||
# /mnt/user/Tv_Shows
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Certificate Monitor ━━━
|
||||
HOSTN_CERT_MONITOR_DOMAINS=(
|
||||
# "myserver.com"
|
||||
)
|
||||
|
||||
# ━━━ SMART Health ━━━
|
||||
HOSTN_SMART_IGNORE_DRIVES=(
|
||||
"sda" # boot USB — SMART not meaningful on flash drives
|
||||
)
|
||||
|
||||
# ━━━ ZFS Report ━━━
|
||||
HOSTN_ZFS_REPORT_IGNORE_POOLS=(
|
||||
# "disk5"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_RAMDISK_SIZE="10G"
|
||||
HOSTN_RAMDISK_WARN_GB=8.5
|
||||
HOSTN_RAMDISK_LOW_GB=7
|
||||
HOSTN_TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/"
|
||||
|
||||
HOSTN_TRANSCODE_SERVERS=(
|
||||
"${HOSTN_EMBY_CONTAINER}|${HOSTN_EMBY_URL}|${HOSTN_EMBY_API_KEY}|emby"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── ARR STACK ─────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Downloaders ━━━
|
||||
HOSTN_SLSKD_URL="http://localhost:8980"
|
||||
HOSTN_SLSKD_API_KEY=""
|
||||
HOSTN_SLSKD_FAILED_IMPORTS_DIR=""
|
||||
|
||||
HOSTN_SABNZBD_URL="http://localhost:8180"
|
||||
HOSTN_SABNZBD_API_KEY=""
|
||||
|
||||
HOSTN_QBIT_URL="http://localhost:8080"
|
||||
HOSTN_QBIT_USERNAME="admin"
|
||||
HOSTN_QBIT_PASSWORD=""
|
||||
|
||||
# ━━━ Lidarr ━━━
|
||||
HOSTN_LIDARR_URL="http://localhost:8686"
|
||||
HOSTN_LIDARR_API_KEY=""
|
||||
HOSTN_LIDARR_MUSIC_ROOT="/mnt/user/Music"
|
||||
HOSTN_FANART_API_KEY=""
|
||||
HOSTN_LASTFM_API_KEY=""
|
||||
|
||||
declare -A HOSTN_LIDARR_PATH_MAP=(
|
||||
# ["/music"]="/mnt/user/Music"
|
||||
)
|
||||
|
||||
# ━━━ Sonarr ━━━
|
||||
HOSTN_SONARR_URL="http://localhost:8989"
|
||||
HOSTN_SONARR_API_KEY=""
|
||||
HOSTN_SONARR_TV_ROOT="/mnt/user/Tv_Shows"
|
||||
|
||||
declare -A HOSTN_SONARR_PATH_MAP=(
|
||||
# ["/tv"]="/mnt/user/Tv_Shows"
|
||||
)
|
||||
|
||||
# ━━━ Radarr ━━━
|
||||
HOSTN_RADARR_URL="http://localhost:7878"
|
||||
HOSTN_RADARR_API_KEY=""
|
||||
HOSTN_RADARR_MOVIE_ROOT="/mnt/user/Movies"
|
||||
|
||||
declare -A HOSTN_RADARR_PATH_MAP=(
|
||||
# ["/movies"]="/mnt/user/Movies"
|
||||
)
|
||||
|
||||
# ━━━ Arr Recovery Toggles ━━━
|
||||
HOSTN_LIDARR_RECOVERY=false
|
||||
HOSTN_SONARR_RECOVERY=true
|
||||
HOSTN_RADARR_RECOVERY=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_SYS_WATCHDOG_CHECK_DOCKER=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ROOTFS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RAM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOAD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_TEMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ZOMBIES=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOG=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_TMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_FD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RUNAWAY=true
|
||||
|
||||
HOSTN_SYS_WATCHDOG_NIC="" # e.g. eth0 — for network monitoring
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_RW_PAUSE_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
# "LidaTube"
|
||||
)
|
||||
|
||||
HOSTN_RW_STOP_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
)
|
||||
@@ -1,775 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ========================== HOST1 CONFIGURATION — unRAID-Gmer4Lfe ============================
|
||||
# ==============================================================================================
|
||||
# HOST1-specific variables — credentials, container names, share paths, failover lists.
|
||||
# Sourced after master.conf — values here extend shared profile arrays and add HOST1-specific
|
||||
# identity, credentials, and container configuration.
|
||||
#
|
||||
# Sparse checkout (git) ensures HOST2 never receives this file.
|
||||
# HOST2 never sees HOST1 credentials — clean separation at the file level.
|
||||
#
|
||||
# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
|
||||
# DO NOT put HOST2 variables here — they belong in host2.conf.
|
||||
#
|
||||
# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
|
||||
# IDENTITY hostname, SSH key
|
||||
# EMBY container name, URL, API key
|
||||
# NOTIFICATIONS Discord webhook
|
||||
# PARTNERSHIP auth containers, backup paths
|
||||
#
|
||||
# ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
|
||||
# DAILY SYNC SHARES media shares HOST1 owns and pushes to HOST2
|
||||
# WEEKLY SYNC SHARES appdata shares synced weekly (Sunday 2:30am)
|
||||
# CRITICAL SYNC SHARES appdata shares synced every 30 minutes
|
||||
# BACKUP VERIFY shares for checksum verification against remote
|
||||
# HOST1 RSYNC PROFILE host1-appdata profile for HOST1-specific appdata syncs
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers, ignore list
|
||||
# DOCKER NETWORK CONNECT networks and containers for docker_network_connect.sh
|
||||
#
|
||||
# ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
|
||||
# DDNS DDNS containers managed by HOST1
|
||||
# INTERNET LOSS containers stopped when internet is lost
|
||||
# FALLBACK TIERS what HOST1 runs for HOST2 per tier
|
||||
# TIER DELAYS how long HOST1 must be down before each tier activates on HOST2
|
||||
# RSYNC WRITEBACK HOST1 appdata synced back on handback
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for media_shares_permissions.sh
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── MONITORS ───────────────────────────────────────────────────────────────────────────────
|
||||
# CERTIFICATE MONITOR domains checked for SSL expiry
|
||||
# SMART HEALTH drives to skip in SMART monitoring
|
||||
# ZFS REPORT pools to exclude from ZFS health report
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
|
||||
# DOWNLOADERS slskd, SABnzbd, qBittorrent credentials and URLs
|
||||
# LIDARR URL, API key, path map
|
||||
# SONARR URL, API key, path map
|
||||
# RADARR URL, API key, path map
|
||||
# ARR RECOVERY per-arr recovery toggles
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── IDENTITY & CONNECTIVITY ───────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Identity ━━━
|
||||
# HOST1 hostname lives in master.conf (not a credential — safe for all servers).
|
||||
# SSH key used for all server-to-server operations — rsync, failover container commands.
|
||||
# Must be in /root/.ssh/ and authorised in HOST2's /root/.ssh/authorized_keys.
|
||||
HOST1_SSH_KEY="/root/.ssh/gmer4lfe_rsync_automation"
|
||||
HOST1_OWNER="gmer4lfe"
|
||||
HOST1_OWNER_EMAIL="gmer4lfe@gmail.com"
|
||||
|
||||
# ━━━ Unraid API ━━━
|
||||
# Local API key used by the Varaverk plugin to query this server's Unraid GraphQL API.
|
||||
# Used for remote host monitoring in the partner card — HOST2 uses HOST2_UNRAID_API_KEY.
|
||||
# Generate in Unraid: Settings → Management Access → API Keys → + New Key
|
||||
HOST1_UNRAID_API_KEY="8282344978921ef157abf11e0e9f59bc89201f621ccf3bf2c5333b6a13fdde6e"
|
||||
|
||||
# ━━━ Emby ━━━
|
||||
# Referenced by transcode_manager.sh, emby_session_report.sh, emby_database_repair.sh,
|
||||
# weekly_sync_maintenance.sh, and HOST1_TRANSCODE_SERVERS below.
|
||||
# API key: Emby Dashboard → API Keys → + New Key
|
||||
HOST1_EMBY_CONTAINER="Emby"
|
||||
HOST1_EMBY_URL="http://localhost:8096"
|
||||
HOST1_EMBY_API_KEY="0c27448d93a7431f9ac63569f7655829"
|
||||
|
||||
# ━━━ Jellyfin ━━━
|
||||
# API key: Jellyfin Dashboard → Administration → API Keys → + New Key
|
||||
HOST1_JELLYFIN_CONTAINER="Jellyfin"
|
||||
HOST1_JELLYFIN_URL="http://localhost:8095"
|
||||
HOST1_JELLYFIN_API_KEY="4e820e7df74c4933acec212b1996314e"
|
||||
|
||||
# ━━━ Gitea ━━━
|
||||
# Personal access token for gitea_ssh_setup.sh — registers this server's SSH public key
|
||||
# with Gitea so git operations use key auth instead of passwords.
|
||||
# Create in Gitea: Settings → Applications → Generate Token → scope: write:user
|
||||
HOST1_GITEA_API_TOKEN=""
|
||||
|
||||
# ━━━ Notifications ━━━
|
||||
# Discord webhook — leave blank to disable.
|
||||
# Per-host so HOST1 and HOST2 can post to different channels or only one server notifies.
|
||||
HOST1_DISCORD_WEBHOOK=""
|
||||
|
||||
# ━━━ Partnership ━━━
|
||||
# HOST1 is always the owner (source of truth) unless --transfer has been run.
|
||||
# See README-Partnership.md and master.conf PARTNERSHIP section for full lifecycle docs.
|
||||
|
||||
# Auth containers reconfigured on onboard/offboard.
|
||||
# Format: "ContainerName|WebUIPort"
|
||||
# On onboard → WebUI pointed at owner's Tailscale IP (mirror clicks NPM, gets owner's auth)
|
||||
# On offboard → WebUI pointed back at localhost
|
||||
HOST1_PARTNERSHIP_AUTH_WEBUIS=(
|
||||
"NginxProxyManager|81"
|
||||
"Lldap-Gmer4Lfe|17170"
|
||||
"Authelia|9091"
|
||||
"Authelia-Secondary|9092"
|
||||
)
|
||||
|
||||
# XML templates (from this server's templates-user/) pushed to mirror during onboard.
|
||||
# These become the mirror's active auth stack, backed by the rsync-synced appdata.
|
||||
# Update filename if Lldap is renamed to drop the host suffix.
|
||||
HOST1_PARTNERSHIP_AUTH_STACK=(
|
||||
# Dependencies first — Mariadb/Redis must be healthy before Authelia starts
|
||||
"my-Mariadb-Authelia.xml"
|
||||
"my-Mariadb-Authelia-Secondary.xml"
|
||||
"my-Redis-Authelia.xml"
|
||||
"my-Redis-Authelia-Secondary.xml"
|
||||
# Auth apps — deployed after their deps are confirmed healthy
|
||||
"my-Authelia.xml"
|
||||
"my-Authelia-Secondary.xml"
|
||||
"my-NginxProxyManager.xml"
|
||||
"my-Lldap-Gmer4Lfe.xml"
|
||||
# Source of truth — must be available on HOST2 independently of the auth stack
|
||||
"my-Gitea.xml"
|
||||
)
|
||||
|
||||
# XML templates pushed to mirror for the arr stack during onboard.
|
||||
# Deps (e.g. databases) first if any — same ordering rule as auth stack.
|
||||
HOST1_PARTNERSHIP_ARR_STACK=(
|
||||
# "my-Sonarr.xml"
|
||||
# "my-Radarr.xml"
|
||||
# "my-Lidarr.xml"
|
||||
# "my-Prowlarr.xml"
|
||||
# "my-Bazarr.xml"
|
||||
)
|
||||
|
||||
# Paths HOST2 should collect during the grace window after offboard.
|
||||
# Notified on offboard — no auto-deletion, HOST2 must collect manually within PARTNERSHIP_GRACE_HOURS.
|
||||
HOST1_PARTNERSHIP_MIRROR_BACKUPS=(
|
||||
# "/mnt/user/appdata-Fallback/Jayred365-Emby"
|
||||
)
|
||||
|
||||
# Containers parked on this server when partnership is active.
|
||||
# Stopped on onboard (owner deploys its stack instead), restarted on offboard.
|
||||
HOST1_PARTNERSHIP_OWN_CONTAINERS=(
|
||||
# "Emby"
|
||||
# "NginxProxyManager"
|
||||
)
|
||||
|
||||
# Emby admin provisioning — toggle is owner-only, credentials are per-host.
|
||||
# Owner enables/disables the feature. Each host sets the account they want on the shared Emby.
|
||||
# On onboard: owner reads mirror's HOST*_PARTNERSHIP_EMBY_ADMIN_* and creates that account.
|
||||
# On offboard: account is deleted. Username collision → onboard exits with error.
|
||||
HOST1_PARTNERSHIP_PROVISION_EMBY_ADMIN=false # owner controls whether Emby is shared
|
||||
HOST1_PARTNERSHIP_EMBY_PORT=8096
|
||||
HOST1_PARTNERSHIP_EMBY_ADMIN_USER="" # this server's desired Emby username
|
||||
HOST1_PARTNERSHIP_EMBY_ADMIN_PASS="" # this server's desired Emby password
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RSYNC ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Daily Sync Shares ━━━
|
||||
# Shares HOST1 pushes to all other nodes every night (1am via daily_sync_maintenance.sh).
|
||||
# Mesh model: every node pushes every media share — no ownership, no mirrors.
|
||||
# arr_sync ensures all arr libraries converge (union). rsync spreads files (additive, no --delete).
|
||||
# arr_cleanup removes true orphans based on local arr state.
|
||||
# Any node can download content to any share — it propagates to all nodes on the next cycle.
|
||||
# Nextcloud is intentionally one-directional (HOST1→HOST2 offsite backup — not arr-managed).
|
||||
# Uses DEFAULT_RSYNC_OPTS from master.conf — no profile needed.
|
||||
# For shares needing container stops or custom options — add a profile in master.conf.
|
||||
HOST1_DAILY_SYNC_SHARES=(
|
||||
/mnt/user/Books
|
||||
/mnt/user/Intros
|
||||
/mnt/user/Kids_Movies
|
||||
/mnt/user/Kids_Tv_Shows
|
||||
/mnt/user/Movies
|
||||
/mnt/user/Music
|
||||
/mnt/user/Music_Videos
|
||||
/mnt/user/Nextcloud
|
||||
/mnt/user/stand-up_comedy
|
||||
/mnt/user/Sports
|
||||
/mnt/user/Tv_Shows
|
||||
/mnt/user/Anime_Shows-Old
|
||||
/mnt/user/Anime_Movies-Old
|
||||
/mnt/user/Anime_Movies
|
||||
/mnt/user/Anime_Shows
|
||||
)
|
||||
|
||||
# Personal encrypted shares — synced for offsite backup, independent of media shares.
|
||||
# ZFS encrypted at dataset level — remote receives encrypted blocks, cannot read content.
|
||||
# See README-Rsync_Setup.md for ZFS encryption setup before uncommenting.
|
||||
HOST1_PERSONAL_SHARES=(
|
||||
# /mnt/user/HOST1-Personal # uncomment after creating encrypted dataset
|
||||
)
|
||||
|
||||
# ━━━ Weekly Sync Shares ━━━
|
||||
# Appdata shares synced during the weekly maintenance window (Sunday 2:30am).
|
||||
# Containers stopped both sides before sync — full clean state guaranteed.
|
||||
# Profiles drive container stops, excludes, and options — configured in master.conf RSYNC section.
|
||||
# Order matters — Emby first (larger transfer), then Critical-Data (auth stack).
|
||||
HOST1_WEEKLY_SYNC_SHARES=(
|
||||
"/mnt/user/Media_Server/Emby" # emby profile — full clean mirror
|
||||
"/mnt/user/appdata-Fallback/Critical-Data" # critical-data profile — auth stack
|
||||
)
|
||||
|
||||
# ━━━ Intermediate Sync Shares ━━━
|
||||
# Shares synced every 4 hours by intermediate_sync_maintenance.sh.
|
||||
# Uses DEFAULT_RSYNC_OPTS (no --delete) — for sub-daily propagation of metadata or watch state.
|
||||
# Full media share sync stays in the daily window. Leave empty to skip mid-day rsync entirely.
|
||||
HOST1_INTERMEDIATE_SYNC_SHARES=(
|
||||
# Add shares here to enable mid-day rsync
|
||||
# Example: "/mnt/user/Emby_Metadata"
|
||||
)
|
||||
|
||||
# ━━━ Critical Sync Shares ━━━
|
||||
# Appdata shares synced every 30 minutes by critical_sync_maintenance.sh.
|
||||
# Format: "/path/to/share" or "/path/to/share|profile-name"
|
||||
# Order matters — Critical-Data first (auth stack), then Emby dirty sync.
|
||||
HOST1_CRITICAL_SYNC_SHARES=(
|
||||
"/mnt/user/appdata-Fallback/Critical-Data|critical-fallback" # auth dirty sync — stays running
|
||||
"/mnt/user/Media_Server/Emby|emby-fallback" # Emby dirty sync — stays running
|
||||
)
|
||||
|
||||
# ━━━ Backup Verify ━━━
|
||||
# Shares verified by backup_verify.sh — random file checksum comparison against remote.
|
||||
# Leave empty to use HOST1_DAILY_SYNC_SHARES automatically.
|
||||
# Sample size and minimum file size defined in master.conf.
|
||||
HOST1_BACKUP_VERIFY_SHARES=(
|
||||
# leave empty to use HOST1_DAILY_SYNC_SHARES automatically
|
||||
)
|
||||
|
||||
# ━━━ HOST1 Rsync Profile — host1-appdata ━━━
|
||||
# HOST1-specific appdata sync profile — extends the shared PROFILE_* arrays in master.conf.
|
||||
# Use for appdata unique to HOST1 (Organizrv2, VaultWarden, UptimeKuma etc.)
|
||||
# Shared appdata (auth stack, Emby) use dedicated profiles defined in master.conf.
|
||||
# Run manually: bash Rsync/rsync.sh /mnt/user/appdata-Fallback/HOST1-Appdata --profile=host1-appdata
|
||||
PROFILE_RSYNC_OPTS[host1-appdata]="-av --info=progress2 --bwlimit=${PROFILE_BW_LIMIT[host1-appdata]:-8000}"
|
||||
PROFILE_BW_LIMIT[host1-appdata]=8000
|
||||
PROFILE_RETRY_COUNT[host1-appdata]=3
|
||||
PROFILE_SLEEP[host1-appdata]=300
|
||||
PROFILE_CRITICAL_CONTAINER_NAMES[host1-appdata]="Organizrv2-Gmer4Lfe UptimeKuma-Gmer4Lfe VaultWarden-Gmer4Lfe"
|
||||
PROFILE_DELAYED_CONTAINERS[host1-appdata]=""
|
||||
PROFILE_CONTAINER_DELAY[host1-appdata]=5
|
||||
PROFILE_EXCLUDE_DIRS[host1-appdata]="logs *.tmp"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── DOCKER ────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Docker Daily Restart ━━━
|
||||
# Containers restarted every day via DAILY_MAINTENANCE_SCRIPTS.
|
||||
# Dispatcharr degrades over time without restart — daily is intentional, not just housekeeping.
|
||||
# Order matters — auth stack first, then media services.
|
||||
HOST1_DAILY_RESTART_CONTAINERS=(
|
||||
"NginxProxyManager"
|
||||
"Lldap-Gmer4Lfe"
|
||||
"Authelia"
|
||||
"Authelia-Secondary"
|
||||
"Dispatcharr-Iptv-Users"
|
||||
"Dispatcharr" # Live TV scheduler — degrades without daily restart
|
||||
"Dispatcharr-Basic"
|
||||
"ErsatzTV-Emby"
|
||||
"Slskd" # Soulseek connection drops after extended uptime; restart refreshes share index
|
||||
)
|
||||
|
||||
# ━━━ Docker Weekly Restart ━━━
|
||||
# Less critical services restarted weekly via WEEKLY_MAINTENANCE_SCRIPTS (Sunday 2:30am).
|
||||
# Containers already stopped for weekly sync — restart adds zero extra downtime.
|
||||
HOST1_WEEKLY_RESTART_CONTAINERS=(
|
||||
"NextCloud"
|
||||
"Organizrv2-Gmer4Lfe"
|
||||
"AdGuard-Home"
|
||||
"Immich-Gmer4Lfe"
|
||||
)
|
||||
|
||||
# ━━━ Docker Watchdog ━━━
|
||||
# Per-HOST1 container configuration for docker_watchdog.sh.
|
||||
# Shared thresholds and toggles live in master.conf.
|
||||
|
||||
# Memory hard limits in MB — immediate restart if exceeded.
|
||||
# Set at "container is clearly broken" not "container is busy".
|
||||
# 20GB=20480 18GB=18432 16GB=16384 12GB=12288 8GB=8192 4GB=4096 2GB=2048 1GB=1024
|
||||
declare -A HOST1_WATCHDOG_CONTAINERS=(
|
||||
["Emby"]=18432 # 18GB — large library + active transcodes
|
||||
["LidaTube"]=6144 # 6GB — memory leak over time
|
||||
["Tdarr"]=6144 # 6GB — encoding is memory intensive
|
||||
["Code-Server"]=1024 # 1GB — should never need more
|
||||
)
|
||||
|
||||
# HTTP health check URLs — checked every cycle, strike system before restart.
|
||||
# Only add containers with a meaningful web interface to check.
|
||||
declare -A HOST1_WATCHDOG_CONTAINER_URLS=(
|
||||
["Emby"]="http://localhost:8096"
|
||||
)
|
||||
|
||||
# Required containers — must always be running on HOST1.
|
||||
# Strike system before restart — repeated failures go on skip list, auto-clears on recovery.
|
||||
# Listed in dependency order — dependencies before dependents.
|
||||
HOST1_WATCHDOG_REQUIRED_CONTAINERS=(
|
||||
"NginxProxyManager"
|
||||
"Lldap-Gmer4Lfe"
|
||||
"Mariadb-Authelia"
|
||||
"Mariadb-Authelia-Secondary"
|
||||
"Redis-Authelia"
|
||||
"Redis-Authelia-Secondary"
|
||||
"Authelia"
|
||||
"Authelia-Secondary"
|
||||
)
|
||||
|
||||
# Containers to skip in Tier 2 global scan — legitimately stopped or frequently restarting.
|
||||
# Watchdog leaves these alone entirely — no restart attempts, no crash loop tracking.
|
||||
HOST1_WATCHDOG_SCAN_IGNORE=(
|
||||
"DashGate"
|
||||
"PIA-WG-Config-Generator"
|
||||
"Aperture"
|
||||
"Aperture-Kids"
|
||||
"pgvector-18-Apeture-Kids"
|
||||
"Pgvector18-Aperture"
|
||||
)
|
||||
|
||||
# Dependency ordering — skip restarting a container if its dependency is also down.
|
||||
# Prevents watchdog from restarting Authelia before Mariadb is back up.
|
||||
# SPACE-SEPARATED STRINGS — converted to array at runtime.
|
||||
declare -A HOST1_WATCHDOG_DEPENDENCIES=(
|
||||
["Authelia"]="Mariadb-Authelia Redis-Authelia"
|
||||
["Authelia-Secondary"]="Mariadb-Authelia Redis-Authelia-Secondary"
|
||||
["NextCloud"]="Postgres-NextCloud"
|
||||
)
|
||||
|
||||
# Per-container appdata growth suppress ceilings in MB.
|
||||
# ONLY needed in specific cases — growth rate detection covers all containers automatically.
|
||||
# Use this when a container legitimately has large stable data and you want to guarantee
|
||||
# it never triggers a false-positive growth alert. Growth warnings are suppressed while the
|
||||
# container's dir stays below this ceiling; above it, warnings resume as normal.
|
||||
# 50GB=51200 25GB=25600 20GB=20480 15GB=15360 10GB=10240 5GB=5120
|
||||
declare -A HOST1_WATCHDOG_APPDATA_SIZES=(
|
||||
["Tdarr"]="25600" # 25GB — transcode cache grows legitimately during active jobs
|
||||
["7dtd"]="20480" # 20GB — game server world data, expected to be large
|
||||
)
|
||||
|
||||
# ━━━ Network Watchdog ━━━
|
||||
# Host-specific connectivity config for Watchdogs/System/network_watchdog.sh.
|
||||
HOST1_NETWORK_WATCHDOG_DDNS_DOMAIN="gmer4lfe.com"
|
||||
HOST1_NETWORK_WATCHDOG_DDNS_CONTAINER="Gmer4Lfe.com"
|
||||
HOST1_NETWORK_WATCHDOG_NPM_URL="https://gmer4lfe.com"
|
||||
|
||||
# ━━━ Docker Network Connect ━━━
|
||||
# Containers connected to custom networks at array start by docker_network_connect.sh.
|
||||
# Networks created if they don't exist — idempotent, safe to re-run.
|
||||
HOST1_NETWORK_CONNECT_CONTAINERS=(
|
||||
"memcached"
|
||||
"Npm-CrowdSec"
|
||||
)
|
||||
|
||||
HOST1_NETWORK_CONNECT_NETWORKS=(
|
||||
"high-availability"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ DDNS ━━━
|
||||
# DDNS containers HOST1 manages — started/stopped by fallback.sh per DDNS absolute rules:
|
||||
# Internet loss → stop immediately
|
||||
# Failover → HOST2 starts HOST1's DDNS as Tier 1 (before any other containers)
|
||||
# Handback → stop HOST1's DDNS on HOST2 → rsync → start containers → start local DDNS last
|
||||
HOST1_DDNS_CONTAINERS=(
|
||||
"Gmer4Lfe.com"
|
||||
)
|
||||
|
||||
# ━━━ Internet Loss ━━━
|
||||
# Containers stopped immediately on HOST1 when internet connection is lost.
|
||||
# Prevents external-facing services from operating without connectivity.
|
||||
FALLBACK_HOST1_STOP_ON_NO_NET=(
|
||||
"Gmer4Lfe.com"
|
||||
)
|
||||
|
||||
# ━━━ Fallback Tiers — HOST1 Runs for HOST2 ━━━
|
||||
# Containers HOST1 starts when HOST2 goes down.
|
||||
# Tier 1 is always immediate — vital services cannot wait.
|
||||
# Higher tiers activate after HOST2_TIER*_DELAY minutes (set in host2.conf).
|
||||
FALLBACK_HOST1_COVERS_HOST2_TIER1=(
|
||||
"Gmer4Lfe.us"
|
||||
"VaultWarden-Jayred365"
|
||||
)
|
||||
|
||||
FALLBACK_HOST1_COVERS_HOST2_TIER2=(
|
||||
# "container-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOST1_COVERS_HOST2_TIER3=(
|
||||
# "container-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOST1_COVERS_HOST2_TIER4=(
|
||||
# "container-placeholder"
|
||||
)
|
||||
|
||||
# ━━━ Tier Delays — HOST1's Containers on HOST2 ━━━
|
||||
# How long HOST1 must be down before each tier activates on HOST2 — in minutes.
|
||||
# Tier 1 is always immediate — no delay var needed.
|
||||
HOST1_TIER2_DELAY=240 # 4 hours — NextCloud, Immich
|
||||
HOST1_TIER3_DELAY=720 # 12 hours — secondary services
|
||||
HOST1_TIER4_DELAY=1440 # 24 hours — arrs + downloaders
|
||||
|
||||
# ━━━ Rsync Writeback — HOST1 Appdata Back on Handback ━━━
|
||||
# Syncs HOST1 appdata BACK to HOST1 when it comes back online after a failover.
|
||||
# Containers stopped before writeback — clean source, no competing writes.
|
||||
#
|
||||
# HOST1_TIER1_WRITEBACK_DELAY: short outages skip Tier 1 writeback — primary state
|
||||
# is more reliable than dirty sync data for brief outages.
|
||||
HOST1_TIER1_WRITEBACK_DELAY=60 # skip Emby writeback if outage under 1hr
|
||||
|
||||
# Tier 4 automatically syncs HOST1_DAILY_SYNC_SHARES — only list paths NOT in that array.
|
||||
FALLBACK_HOST1_WRITEBACK_TIER1=(
|
||||
"/mnt/user/Media_Server/Emby" # watch states built up during outage
|
||||
)
|
||||
|
||||
FALLBACK_HOST1_WRITEBACK_TIER2=(
|
||||
"/mnt/user/appdata-Fallback/Important-Data" # NextCloud + Postgres — files added during outage
|
||||
)
|
||||
|
||||
FALLBACK_HOST1_WRITEBACK_TIER3=(
|
||||
# "location-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOST1_WRITEBACK_TIER4=(
|
||||
"/mnt/user/appdata-Fallback/Arrs_Stack" # arr databases — downloads queued during outage
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MEDIA ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Media Permissions ━━━
|
||||
# Shares that media_shares_permissions.sh applies PERMISSIONS_MODE and PERMISSIONS_OWNER to.
|
||||
# Runs first in DAILY_MAINTENANCE_SCRIPTS — arr cleanup depends on correct ownership.
|
||||
HOST1_MEDIA_PERMISSION_SHARES=(
|
||||
/mnt/user/Anime_Movies
|
||||
/mnt/user/Anime_Movies-Old
|
||||
/mnt/user/Anime_Shows
|
||||
/mnt/user/Anime_Shows-Old
|
||||
/mnt/user/appcache
|
||||
/mnt/user/Books
|
||||
/mnt/user/Downloads
|
||||
/mnt/user/Games
|
||||
/mnt/user/Intros
|
||||
/mnt/user/Kids_Movies
|
||||
/mnt/user/Kids_Tv_Shows
|
||||
/mnt/user/Movie_Recordings
|
||||
/mnt/user/Movies
|
||||
/mnt/user/Music
|
||||
/mnt/user/Music_Videos
|
||||
/mnt/user/Photo
|
||||
/mnt/user/Sports
|
||||
/mnt/user/stand-up_comedy
|
||||
/mnt/user/Temp_Storage
|
||||
/mnt/user/Tv_Recordings
|
||||
/mnt/user/Tv_Shows
|
||||
/mnt/user/YouTube
|
||||
)
|
||||
|
||||
# ━━━ Media Cleaner ━━━
|
||||
# Folder lists for media_cleaner.sh — two profiles: anime and media.
|
||||
# File patterns shared across all servers — defined in master.conf.
|
||||
# Called via DAILY_MAINTENANCE_SCRIPTS. Run manually: Media/media_cleaner.sh anime|media
|
||||
HOST1_ANIME_CLEAN_FOLDERS=(
|
||||
/mnt/user/Anime_Movies
|
||||
/mnt/user/Anime_Movies-Old
|
||||
/mnt/user/Anime_Shows
|
||||
/mnt/user/Anime_Shows-Old
|
||||
)
|
||||
|
||||
HOST1_MEDIA_CLEAN_FOLDERS=(
|
||||
/mnt/user/Kids_Movies
|
||||
/mnt/user/Kids_Tv_Shows
|
||||
/mnt/user/Movies
|
||||
/mnt/user/Music
|
||||
/mnt/user/Sports
|
||||
/mnt/user/stand-up_comedy
|
||||
/mnt/user/Tv_Shows
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Certificate Monitor ━━━
|
||||
# Domains checked via direct openssl connection — not relying on NPM's certificate state.
|
||||
# Checks the actual certificate served, not what NPM thinks it has.
|
||||
# Thresholds (CERT_WARN_DAYS, CERT_CRIT_DAYS) defined in master.conf.
|
||||
HOST1_CERT_MONITOR_DOMAINS=(
|
||||
"Gmer4Lfe.com"
|
||||
"Gmer4Lfe.us"
|
||||
)
|
||||
|
||||
# ━━━ SMART Health ━━━
|
||||
# Drives skipped in SMART attribute monitoring — hardware is server-specific.
|
||||
# Thresholds read from dynamix.cfg at runtime — fallbacks in master.conf.
|
||||
HOST1_SMART_IGNORE_DRIVES=(
|
||||
"sda" # boot USB — SMART not meaningful on flash drives
|
||||
)
|
||||
|
||||
# ━━━ ZFS Report ━━━
|
||||
# Pools excluded from the weekly ZFS health report — reduces noise from single-disk array pools.
|
||||
# These are individual array disks formatted as ZFS — converting to XFS over time via unBalance.
|
||||
# Pool health thresholds defined in master.conf.
|
||||
HOST1_ZFS_REPORT_IGNORE_POOLS=(
|
||||
"disk5"
|
||||
"disk6"
|
||||
"disk8"
|
||||
"disk9"
|
||||
"disk10"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# Ramdisk size ceiling — tmpfs only uses RAM actually needed, not the full size upfront.
|
||||
# Real-world: 9 streams peaked at ~5.5GB — 10G gives generous headroom on 128GB RAM.
|
||||
HOST1_RAMDISK_SIZE="10G"
|
||||
|
||||
# Usage thresholds — coupled to HOST1_RAMDISK_SIZE, adjust all three together if size changes.
|
||||
# Hysteresis gap (8.5 - 7 = 1.5GB) prevents flip-flop between ramdisk and SSD.
|
||||
HOST1_RAMDISK_WARN_GB=8.5 # flip to SSD when ramdisk usage reaches this
|
||||
HOST1_RAMDISK_LOW_GB=7 # flip back to ramdisk when usage drops to this
|
||||
|
||||
# SSD fallback path — where transcodes land when ramdisk exceeds HOST1_RAMDISK_WARN_GB.
|
||||
# Must be on cache pool — array disks too slow for active transcode writes.
|
||||
HOST1_TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/"
|
||||
|
||||
# Media servers sharing the ramdisk transcode space on HOST1.
|
||||
# Format: "ContainerName|URL|APIKey|Type" — Type: emby | jellyfin | plex
|
||||
# Entries with placeholder API keys are skipped automatically.
|
||||
# ⚠️ Tdarr does NOT belong here — keep Tdarr on SSD, not ramdisk.
|
||||
HOST1_TRANSCODE_SERVERS=(
|
||||
"${HOST1_EMBY_CONTAINER}|${HOST1_EMBY_URL}|${HOST1_EMBY_API_KEY}|emby"
|
||||
"${HOST1_JELLYFIN_CONTAINER}|${HOST1_JELLYFIN_URL}|${HOST1_JELLYFIN_API_KEY}|jellyfin"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── ARR STACK ─────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Used by arr cleanup scripts and arrs_failed_stalled_recovery.sh.
|
||||
# detect_hosts() selects HOST1 vars when running on HOST1.
|
||||
#
|
||||
# PATH MAPS — container path → host path translation.
|
||||
# Arr stores file paths using container-internal paths — scripts need host paths to scan.
|
||||
# Add one entry per root folder in arr Settings → Media Management → Root Folders.
|
||||
|
||||
# ━━━ Downloaders ━━━
|
||||
# Used by downloaders_reset.sh — runs every 30min via CRITICAL_MAINTENANCE_SCRIPTS.
|
||||
# Clears stuck states, purges old history, prepares each client for a clean cycle.
|
||||
|
||||
# slskd — clears stuck searches, dead transfers, purges expired failed imports.
|
||||
# SLSKD_FAILED_IMPORTS_DIR: where Soularr moves albums Lidarr rejected.
|
||||
HOST1_SLSKD_URL="http://localhost:8980"
|
||||
HOST1_SLSKD_API_KEY="4bF9kL2mNpQrT7vWxYz1A3dEgHjKoRsU"
|
||||
HOST1_SLSKD_FAILED_IMPORTS_DIR="/mnt/user/Temp_Storage/Slskd/completed/failed_imports"
|
||||
|
||||
# SABnzbd
|
||||
HOST1_SABNZBD_URL="http://localhost:8180"
|
||||
HOST1_SABNZBD_API_KEY="8bfefe41d83b4d50883e32859b55ca9a"
|
||||
|
||||
# qBittorrent — deleteFiles=false removes torrent from qBit but leaves files on disk.
|
||||
# Radarr/Sonarr manage actual files independently.
|
||||
HOST1_QBIT_URL="http://localhost:8080"
|
||||
HOST1_QBIT_USERNAME="root"
|
||||
HOST1_QBIT_PASSWORD="Stay0utD!ck"
|
||||
|
||||
# ━━━ Lidarr — HOST1 only ━━━
|
||||
# HOST2 does not run Lidarr — HOST1_LIDARR_RECOVERY flag handles the exit cleanly.
|
||||
HOST1_LIDARR_URL="http://localhost:8686"
|
||||
HOST1_LIDARR_API_KEY="b2977e71ef074bc0a0529d9fcce3b2dc"
|
||||
HOST1_LIDARR_MUSIC_ROOT="/mnt/user/Music-New"
|
||||
HOST1_FANART_API_KEY="Yd7147a43b692df0b364b94dc47efb81"
|
||||
HOST1_LASTFM_API_KEY="be6dc169c33ae263e690c30d18b7491d"
|
||||
|
||||
declare -A HOST1_LIDARR_PATH_MAP=(
|
||||
["/ext-music"]="/mnt/user/Music-New"
|
||||
)
|
||||
|
||||
# ━━━ Sonarr ━━━
|
||||
HOST1_SONARR_URL="http://localhost:8989"
|
||||
HOST1_SONARR_API_KEY="130decd3db5b4c25afad64864cd03f9f"
|
||||
HOST1_SONARR_TV_ROOT="/mnt/user/Tv_Shows"
|
||||
|
||||
# Note: stand-up_comedy in both Sonarr + Radarr — TV specials and movie specials, one folder
|
||||
declare -A HOST1_SONARR_PATH_MAP=(
|
||||
["/tv"]="/mnt/user/Tv_Shows"
|
||||
["/ext-standup-comedy"]="/mnt/user/stand-up_comedy"
|
||||
["/kids tv"]="/mnt/user/Kids_Tv_Shows"
|
||||
["/ext-anime-shows"]="/mnt/user/Anime_Shows-Old"
|
||||
)
|
||||
|
||||
# ━━━ Radarr ━━━
|
||||
HOST1_RADARR_URL="http://localhost:7878"
|
||||
HOST1_RADARR_API_KEY="d43a3ec6cf1549edb4af0cc63f98b2a9"
|
||||
HOST1_TMDB_API_KEY="3dac5e2e49b5540472d2eafec4f01260"
|
||||
HOST1_RADARR_MOVIES_ROOT="/mnt/user/Movies"
|
||||
|
||||
# Note: stand-up_comedy in both Radarr + Sonarr — movie specials and TV specials, one folder
|
||||
declare -A HOST1_RADARR_PATH_MAP=(
|
||||
["/movies"]="/mnt/user/Movies"
|
||||
["/kids movies"]="/mnt/user/Kids_Movies"
|
||||
["/ext-stand-up-comedy"]="/mnt/user/stand-up_comedy"
|
||||
["/anime-movies"]="/mnt/user/Anime_Movies-Old"
|
||||
)
|
||||
|
||||
# ━━━ Arr Recovery Toggles ━━━
|
||||
# false = skip that arr on this host — exits cleanly without error
|
||||
HOST1_SONARR_RECOVERY=true
|
||||
HOST1_RADARR_RECOVERY=true
|
||||
HOST1_LIDARR_RECOVERY=true # HOST1 only — exits cleanly on HOST2
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Per-host check toggles and NIC config for system_watchdog.sh.
|
||||
# Aliased by detect_hosts() — script uses unprefixed SYS_WATCHDOG_* names.
|
||||
# HOST1: TR1950X 128GB — full media server, active transcoding, ZFS cache pools.
|
||||
#
|
||||
# Three-tier response — all critical checks enabled by default on HOST1:
|
||||
# Tier 1 (bypass strikes, reboot now): docker daemon, rootfs full, kernel oops, FD, /boot
|
||||
# Tier 2 (bypass strikes with OOM): RAM critical + OOM kills in cycle
|
||||
# Tier 3 (standard strike system): everything else
|
||||
#
|
||||
# RAM tiers, OOM limits, and reboot loop settings in master.conf System Watchdog section.
|
||||
|
||||
# ━━━ Primary NIC ━━━
|
||||
# Network interface for NIC state check — verify with: ip link show | grep "^[0-9]"
|
||||
# Common values: eth0, bond0, br0, eno1
|
||||
HOST1_SYS_WATCHDOG_NIC="eth0"
|
||||
|
||||
# ━━━ Tier 1 — Critical Checks ━━━
|
||||
# These bypass the strike system — a single hit triggers immediate reboot.
|
||||
# Disabling any of these is not recommended — they protect against acute system failure.
|
||||
|
||||
# Docker daemon unresponsive → try restart, reboot if restart fails.
|
||||
# Without a working daemon docker_watchdog.sh is blind and containers cannot be managed.
|
||||
HOST1_SYS_WATCHDOG_CHECK_DOCKER_DAEMON=true
|
||||
|
||||
# rootfs at critical threshold (ROOTFS_CRITICAL_PCT=99) → reboot immediately.
|
||||
# At 99% rootfs writes fail silently — logs stop, Docker errors out, SSH may stop working.
|
||||
# Standard 95% threshold still uses strike system — only 99%+ is critical tier.
|
||||
HOST1_SYS_WATCHDOG_CHECK_ROOTFS=true
|
||||
|
||||
# Kernel BUG/Oops in dmesg delta since last cycle → reboot immediately.
|
||||
# A kernel oops means the kernel ran with a corrupted state — stability is not guaranteed.
|
||||
HOST1_SYS_WATCHDOG_CHECK_KERNEL_OOPS=true
|
||||
|
||||
# File descriptor exhaustion at FD_CRITICAL_PCT (95%) → reboot immediately.
|
||||
# At 95% FD: new connections fail, Docker can't spawn processes, SSH drops.
|
||||
HOST1_SYS_WATCHDOG_CHECK_FD=true
|
||||
|
||||
# /boot read-only detected → reboot immediately.
|
||||
# Unexpected read-only /boot means state files and config writes are silently failing.
|
||||
# Fallback state, watchdog reboot log, and lock files all go stale silently.
|
||||
HOST1_SYS_WATCHDOG_CHECK_BOOT=true
|
||||
|
||||
# ━━━ Tier 2 — Urgent OOM Check ━━━
|
||||
# Bypass strikes when RAM is critically low AND OOM kill rate confirms active crisis.
|
||||
# Both must be enabled for Tier 2 bypass to function — disable either to always use strikes.
|
||||
|
||||
# Track kernel OOM kills each cycle via /proc/vmstat oom_kill delta.
|
||||
# Also provides diagnostic context in reboot messages (which processes were killed).
|
||||
HOST1_SYS_WATCHDOG_CHECK_OOM=true
|
||||
|
||||
# Free RAM check — required for both Tier 2 bypass and RAM tier logic.
|
||||
# Tiers: MEM_WARN_GB(10) → notify | MEM_SHUTDOWN_GB(6) → stop containers | MEM_GB(4) → strikes
|
||||
HOST1_SYS_WATCHDOG_CHECK_RAM=true
|
||||
|
||||
# ━━━ Tier 3 — Standard Checks (strike system) ━━━
|
||||
# Each check must fail SYS_WATCHDOG_STRIKE_LIMIT consecutive cycles before action is taken.
|
||||
# Single spikes are ignored — sustained problems trigger reboot.
|
||||
|
||||
# /var/log filesystem usage above SYS_WATCHDOG_LOG_PCT.
|
||||
# Log spam (Docker log storms, syslog loops) fills rootfs — indicates something broken.
|
||||
HOST1_SYS_WATCHDOG_CHECK_LOG=true
|
||||
|
||||
# ZFS ARC memory pinned above SYS_WATCHDOG_ARC_PINNED_PCT after cache drop.
|
||||
# Enabled on HOST1 — ZFS cache pools actively used. Disable on hosts without ZFS.
|
||||
HOST1_SYS_WATCHDOG_CHECK_ARC=true
|
||||
|
||||
# CPU temperature above SYS_WATCHDOG_CPU_TEMP_MAX (95°C).
|
||||
# Sustained high temp causes kernel throttling or panic. Requires lm-sensors.
|
||||
HOST1_SYS_WATCHDOG_CHECK_CPU_TEMP=true
|
||||
|
||||
# Load average above SYS_WATCHDOG_LOAD_MULTIPLIER × core count.
|
||||
# DISABLED on HOST1 — Tdarr and Emby cause legitimate sustained load spikes during encoding.
|
||||
# Enable on idle servers or adjust SYS_WATCHDOG_LOAD_MULTIPLIER if load is always high.
|
||||
HOST1_SYS_WATCHDOG_CHECK_LOAD=false
|
||||
|
||||
# Zombie process count above SYS_WATCHDOG_ZOMBIE_LIMIT (50).
|
||||
# Large zombie counts indicate serious process management failure — something is stuck.
|
||||
HOST1_SYS_WATCHDOG_CHECK_ZOMBIES=true
|
||||
|
||||
# Check docker_watchdog.sh persistent skip list — required containers on skip list.
|
||||
# Cross-watchdog coordination: if docker_watchdog gave up, system_watchdog escalates.
|
||||
# ENABLED — HOST1 fully built and operational, skip list is meaningful.
|
||||
HOST1_SYS_WATCHDOG_CHECK_CONTAINERS=true
|
||||
|
||||
# /tmp filesystem usage above SYS_WATCHDOG_TMP_PCT with auto-clear attempt.
|
||||
# Script tries to clear aged /tmp files first — only strikes if clear fails.
|
||||
# Lock files, rsync temp files, and Docker ops use /tmp — 100% means lock failures.
|
||||
HOST1_SYS_WATCHDOG_CHECK_TMP=true
|
||||
|
||||
# Array disk error count delta in /proc/mdstat — accumulating errors = disk failing now.
|
||||
# Triggers on SYS_WATCHDOG_MDSTAT_ERROR_LIMIT new errors in one cycle.
|
||||
HOST1_SYS_WATCHDOG_CHECK_MDSTAT=true
|
||||
|
||||
# Primary NIC operstate — detects NIC going down (physical or driver failure).
|
||||
# Uses HOST1_SYS_WATCHDOG_NIC above. Strike system — brief flaps don't trigger reboot.
|
||||
HOST1_SYS_WATCHDOG_CHECK_NETWORK=true
|
||||
|
||||
# sshd running check — attempts restart before escalating.
|
||||
# sshd down = no remote access. Script tries rc.sshd start, notifies, strikes on failure.
|
||||
HOST1_SYS_WATCHDOG_CHECK_SSHD=true
|
||||
|
||||
# Runaway process detection — single process above SYS_WATCHDOG_RUNAWAY_CPU_PCT sustained.
|
||||
# DISABLED — Tdarr encoding and Emby transcoding legitimately peg CPU for extended periods.
|
||||
# Enable only if HOST1 has no CPU-intensive workloads.
|
||||
HOST1_SYS_WATCHDOG_CHECK_RUNAWAY=false
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Containers to manage under pressure — see master.conf RW_CRITICAL_CONTAINERS for exclusions.
|
||||
|
||||
# docker pause at medium pressure (RAM < RW_RAM_MEDIUM_GB or load > medium threshold)
|
||||
# Suspended in-place — instant to pause/unpause, no state lost, no restart delay.
|
||||
HOST1_RW_PAUSE_CONTAINERS=(
|
||||
"Huntarr" # arr search automation — safe to suspend
|
||||
"Cleanuparr" # download cleanup — safe to suspend
|
||||
"Healarr" # arr health checks — safe to suspend
|
||||
"Soularr" # Slskd automation — background only
|
||||
"ChannelTube" # YouTube archiver — background only
|
||||
"Pinchflat" # YouTube archiver — background only
|
||||
)
|
||||
|
||||
# docker stop at hard pressure (RAM < RW_RAM_HARD_GB)
|
||||
# Full stop — these are optional/heavy services that free significant RAM when stopped.
|
||||
# resource_watchdog.sh restarts them when pressure fully clears (RAM >= RW_RAM_RECOVER_GB).
|
||||
HOST1_RW_STOP_CONTAINERS=(
|
||||
"LocalAI" # GPU/CPU heavy — largest RAM consumer when idle
|
||||
"7DaysToDie" # game server — optional
|
||||
"V-Rising" # game server — optional
|
||||
"Code-Server" # IDE — not needed during pressure events
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ──────────────────────── End Of HOST1 Variables ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -1,644 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ========================== HOST2 CONFIGURATION — unRAID-Jayred365 ===========================
|
||||
# ==============================================================================================
|
||||
# HOST2-specific variables — credentials, container names, share paths, failover lists.
|
||||
# Sourced after master.conf — values here extend shared profile arrays and add HOST2-specific
|
||||
# identity, credentials, and container configuration.
|
||||
#
|
||||
# Sparse checkout (git) ensures HOST1 never receives this file.
|
||||
# HOST1 never sees HOST2 credentials — clean separation at the file level.
|
||||
#
|
||||
# DO NOT put shared config here — thresholds, toggles, profiles belong in master.conf.
|
||||
# DO NOT put HOST1 variables here — they belong in host1.conf.
|
||||
#
|
||||
# ── STATUS ────────────────────────────────────────────────────────────────────────────────────
|
||||
# HOST2 is currently being rebuilt — most sections scaffolded, fill in when back online.
|
||||
# When ready: set FALLBACK_ENABLED=true and DAILY_RSYNC_ENABLED=true in master.conf.
|
||||
#
|
||||
# ── INDEX ─────────────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# ── IDENTITY & CONNECTIVITY ────────────────────────────────────────────────────────────────
|
||||
# IDENTITY hostname, SSH key
|
||||
# EMBY container name, URL, API key
|
||||
# NOTIFICATIONS Discord webhook
|
||||
# PARTNERSHIP auth containers, backup paths
|
||||
#
|
||||
# ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
|
||||
# DAILY SYNC SHARES media shares HOST2 owns and pushes to HOST1
|
||||
# WEEKLY SYNC SHARES appdata shares synced weekly (Sunday 2:30am)
|
||||
# CRITICAL SYNC SHARES appdata shares synced every 30 minutes
|
||||
# BACKUP VERIFY shares for checksum verification against remote
|
||||
# HOST2 RSYNC PROFILE host2-appdata profile for HOST2-specific appdata syncs
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers, ignore list
|
||||
# DOCKER NETWORK CONNECT networks and containers for docker_network_connect.sh
|
||||
#
|
||||
# ── FALLBACK ───────────────────────────────────────────────────────────────────────────────
|
||||
# DDNS DDNS containers managed by HOST2
|
||||
# INTERNET LOSS containers stopped when internet is lost
|
||||
# FALLBACK TIERS what HOST2 runs for HOST1 per tier
|
||||
# TIER DELAYS how long HOST2 must be down before each tier activates on HOST1
|
||||
# RSYNC WRITEBACK HOST2 appdata synced back on handback
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for media_shares_permissions.sh
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── MONITORS ───────────────────────────────────────────────────────────────────────────────
|
||||
# CERTIFICATE MONITOR domains checked for SSL expiry
|
||||
# SMART HEALTH drives to skip in SMART monitoring
|
||||
# ZFS REPORT pools to exclude from ZFS health report
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
|
||||
# SONARR URL, API key, path map
|
||||
# RADARR URL, API key, path map
|
||||
# ARR RECOVERY per-arr recovery toggles (no Lidarr on HOST2)
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── IDENTITY & CONNECTIVITY ───────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Identity ━━━
|
||||
# HOST2 hostname lives in master.conf (not a credential — safe for all servers).
|
||||
# SSH key used for all server-to-server operations — rsync, failover container commands.
|
||||
# Must be in /root/.ssh/ and authorised in HOST1's /root/.ssh/authorized_keys.
|
||||
HOST2_SSH_KEY="/root/.ssh/Jayred365-rsync-key"
|
||||
HOST2_OWNER="jayred365"
|
||||
HOST2_OWNER_EMAIL="" # fill in when HOST2 is back online
|
||||
|
||||
# ━━━ Unraid API ━━━
|
||||
# Generate in Unraid: Settings → Management Access → API Keys → + New Key
|
||||
HOST2_UNRAID_API_KEY="2bdf5119d61eefa3023434748bd1c171bd23dc0b2ebc8586e24abe07df986acc"
|
||||
|
||||
# ━━━ Emby ━━━
|
||||
# Referenced by transcode_manager.sh, emby_session_report.sh, emby_database_repair.sh,
|
||||
# weekly_sync_maintenance.sh, and HOST2_TRANSCODE_SERVERS below.
|
||||
# API key: Emby Dashboard → API Keys → + New Key
|
||||
HOST2_EMBY_CONTAINER="Emby-Jayred365"
|
||||
HOST2_EMBY_URL="http://localhost:8096"
|
||||
HOST2_EMBY_API_KEY="your-host2-emby-api-key"
|
||||
|
||||
# ━━━ Jellyfin ━━━
|
||||
# API key: Jellyfin Dashboard → Administration → API Keys → + New Key
|
||||
HOST2_JELLYFIN_CONTAINER="Jellyfin"
|
||||
HOST2_JELLYFIN_URL="http://localhost:8095"
|
||||
HOST2_JELLYFIN_API_KEY="956d0168987f4e4680626653abb080f0"
|
||||
|
||||
# ━━━ Notifications ━━━
|
||||
# Discord webhook — leave blank to disable.
|
||||
# Per-host so HOST1 and HOST2 can post to different channels or only one server notifies.
|
||||
HOST2_DISCORD_WEBHOOK=""
|
||||
|
||||
# ━━━ Partnership ━━━
|
||||
# HOST2 is the mirror — HOST1 is always the owner unless --transfer has been run.
|
||||
# See README-Partnership.md and master.conf PARTNERSHIP section for full lifecycle docs.
|
||||
|
||||
# Auth containers reconfigured on onboard/offboard.
|
||||
# Format: "ContainerName|WebUIPort"
|
||||
# On onboard → WebUI pointed at owner's Tailscale IP (mirror clicks NPM, gets owner's auth)
|
||||
# On offboard → WebUI pointed back at localhost
|
||||
HOST2_PARTNERSHIP_AUTH_WEBUIS=(
|
||||
# fill in when HOST2 is back online
|
||||
# "NginxProxyManager|81"
|
||||
)
|
||||
|
||||
# Containers to stop on this server before the owner deploys the auth stack during onboard.
|
||||
# List whatever auth/proxy containers are currently running here.
|
||||
HOST2_PARTNERSHIP_REPLACE_CONTAINERS=(
|
||||
"NginxProxyManager"
|
||||
"Authelia"
|
||||
"Authelia-Secondary"
|
||||
"Mariadb-Authelia"
|
||||
"Mariadb-Authelia-Secondary"
|
||||
"Redis-Authelia"
|
||||
"Redis-Authelia-Secondary"
|
||||
"Lldap"
|
||||
)
|
||||
|
||||
# Arr containers to stop on this server before the owner deploys the arr stack during onboard.
|
||||
HOST2_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
|
||||
# "Sonarr"
|
||||
# "Radarr"
|
||||
# "Lidarr"
|
||||
# "Prowlarr"
|
||||
# "Bazarr"
|
||||
)
|
||||
|
||||
# Paths HOST1 should collect during the grace window after offboard.
|
||||
# Notified on offboard — no auto-deletion, HOST1 must collect manually within PARTNERSHIP_GRACE_HOURS.
|
||||
HOST2_PARTNERSHIP_MIRROR_BACKUPS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# Containers parked on this server when partnership is active.
|
||||
# Stopped on onboard (owner deploys its stack instead), restarted on offboard.
|
||||
HOST2_PARTNERSHIP_OWN_CONTAINERS=(
|
||||
# "Emby"
|
||||
# "NginxProxyManager"
|
||||
)
|
||||
|
||||
# This server's desired Emby admin account on the shared Emby instance.
|
||||
# Set these — owner reads them during --onboard to create the account.
|
||||
HOST2_PARTNERSHIP_EMBY_ADMIN_USER="" # desired Emby username
|
||||
HOST2_PARTNERSHIP_EMBY_ADMIN_PASS="" # desired Emby password
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RSYNC ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Daily Sync Shares ━━━
|
||||
# Shares HOST2 pushes to all other nodes every night (1am via daily_sync_maintenance.sh).
|
||||
# Mesh model: every node pushes every media share — no ownership, no mirrors.
|
||||
# arr_sync ensures all arr libraries converge (union). rsync spreads files (additive, no --delete).
|
||||
# arr_cleanup removes true orphans based on local arr state.
|
||||
# Any node can download content to any share — it propagates to all nodes on the next cycle.
|
||||
# Nextcloud excluded — personal data, not arr-managed, synced HOST1→HOST2 only as offsite backup.
|
||||
# Uses DEFAULT_RSYNC_OPTS from master.conf — no profile needed.
|
||||
# For shares needing container stops or custom options — add a profile in master.conf.
|
||||
HOST2_DAILY_SYNC_SHARES=(
|
||||
/mnt/user/Anime_Movies
|
||||
/mnt/user/Anime_Shows
|
||||
/mnt/user/Books
|
||||
/mnt/user/Intros
|
||||
/mnt/user/Kids_Movies
|
||||
/mnt/user/Kids_Tv_Shows
|
||||
/mnt/user/Movies
|
||||
/mnt/user/Music
|
||||
/mnt/user/Music_Videos
|
||||
/mnt/user/stand-up_comedy
|
||||
/mnt/user/Sports
|
||||
/mnt/user/Tv_Shows
|
||||
/mnt/user/Anime_Shows-Old
|
||||
/mnt/user/Anime_Movies-Old
|
||||
)
|
||||
|
||||
# Personal encrypted shares — synced for offsite backup, independent of media shares.
|
||||
# ZFS encrypted at dataset level — remote receives encrypted blocks, cannot read content.
|
||||
# See README-Rsync_Setup.md for ZFS encryption setup before uncommenting.
|
||||
HOST2_PERSONAL_SHARES=(
|
||||
# /mnt/user/HOST2-Personal # uncomment after creating encrypted dataset
|
||||
)
|
||||
|
||||
# ━━━ Weekly Sync Shares ━━━
|
||||
# Appdata shares synced during the weekly maintenance window (Sunday 2:30am).
|
||||
# Containers stopped both sides before sync — full clean state guaranteed.
|
||||
# Profiles drive container stops, excludes, and options — configured in master.conf RSYNC section.
|
||||
HOST2_WEEKLY_SYNC_SHARES=(
|
||||
# fill in when HOST2 is back online
|
||||
# "/mnt/user/Media_Server/Emby"
|
||||
# "/mnt/user/appdata-Fallback/Critical-Data"
|
||||
)
|
||||
|
||||
# ━━━ Intermediate Sync Shares ━━━
|
||||
# Shares synced every 4 hours by intermediate_sync_maintenance.sh.
|
||||
# Uses DEFAULT_RSYNC_OPTS (no --delete) — for sub-daily propagation of metadata or watch state.
|
||||
# Full media share sync stays in the daily window. Leave empty to skip mid-day rsync entirely.
|
||||
HOST2_INTERMEDIATE_SYNC_SHARES=(
|
||||
# fill in when HOST2 is back online
|
||||
# Example: "/mnt/user/Emby_Metadata"
|
||||
)
|
||||
|
||||
# ━━━ Critical Sync Shares ━━━
|
||||
# Appdata shares synced every 30 minutes by critical_sync_maintenance.sh.
|
||||
# Format: "/path/to/share" or "/path/to/share|profile-name"
|
||||
HOST2_CRITICAL_SYNC_SHARES=(
|
||||
# fill in when HOST2 is back online
|
||||
# "/mnt/user/appdata-Fallback/Critical-Data|critical-fallback"
|
||||
# "/mnt/user/Media_Server/Emby|emby-fallback"
|
||||
)
|
||||
|
||||
# ━━━ Backup Verify ━━━
|
||||
# Shares verified by backup_verify.sh — random file checksum comparison against remote.
|
||||
# Leave empty to use HOST2_DAILY_SYNC_SHARES automatically.
|
||||
# Sample size and minimum file size defined in master.conf.
|
||||
HOST2_BACKUP_VERIFY_SHARES=(
|
||||
# leave empty to use HOST2_DAILY_SYNC_SHARES automatically
|
||||
)
|
||||
|
||||
# ━━━ HOST2 Rsync Profile — host2-appdata ━━━
|
||||
# HOST2-specific appdata sync profile — extends the shared PROFILE_* arrays in master.conf.
|
||||
# Use for appdata unique to HOST2.
|
||||
# Shared appdata (auth stack, Emby) use dedicated profiles defined in master.conf.
|
||||
# Run manually: bash Rsync/rsync.sh /mnt/user/appdata-Fallback/HOST2-Appdata --profile=host2-appdata
|
||||
PROFILE_RSYNC_OPTS[host2-appdata]="-av --info=progress2 --bwlimit=${PROFILE_BW_LIMIT[host2-appdata]:-8000}"
|
||||
PROFILE_BW_LIMIT[host2-appdata]=8000
|
||||
PROFILE_RETRY_COUNT[host2-appdata]=3
|
||||
PROFILE_SLEEP[host2-appdata]=300
|
||||
PROFILE_CRITICAL_CONTAINER_NAMES[host2-appdata]="" # fill in when HOST2 is back online
|
||||
PROFILE_DELAYED_CONTAINERS[host2-appdata]=""
|
||||
PROFILE_CONTAINER_DELAY[host2-appdata]=5
|
||||
PROFILE_EXCLUDE_DIRS[host2-appdata]="logs *.tmp"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── DOCKER ────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Docker Daily Restart ━━━
|
||||
# Containers restarted every day via DAILY_MAINTENANCE_SCRIPTS.
|
||||
# Fill in when HOST2 is back online — add containers that degrade without daily restart.
|
||||
HOST2_DAILY_RESTART_CONTAINERS=(
|
||||
"NginxProxyManager"
|
||||
# add HOST2 daily restart containers here
|
||||
)
|
||||
|
||||
# ━━━ Docker Weekly Restart ━━━
|
||||
# Less critical services restarted weekly via WEEKLY_MAINTENANCE_SCRIPTS (Sunday 2:30am).
|
||||
# Containers already stopped for weekly sync — restart adds zero extra downtime.
|
||||
HOST2_WEEKLY_RESTART_CONTAINERS=(
|
||||
# add HOST2 weekly restart containers here
|
||||
)
|
||||
|
||||
# ━━━ Docker Watchdog ━━━
|
||||
# Per-HOST2 container configuration for docker_watchdog.sh.
|
||||
# Shared thresholds and toggles live in master.conf.
|
||||
|
||||
# Memory hard limits in MB — immediate restart if exceeded.
|
||||
# Set at "container is clearly broken" not "container is busy".
|
||||
# 20GB=20480 16GB=16384 12GB=12288 10GB=10240 8GB=8192 4GB=4096 2GB=2048 1GB=1024
|
||||
declare -A HOST2_WATCHDOG_CONTAINERS=(
|
||||
["Emby"]=16384 # fill in correct limit when HOST2 is back online
|
||||
)
|
||||
|
||||
# HTTP health check URLs — checked every cycle, strike system before restart.
|
||||
# Only add containers with a meaningful web interface to check.
|
||||
declare -A HOST2_WATCHDOG_CONTAINER_URLS=(
|
||||
["Emby"]="http://localhost:8096"
|
||||
)
|
||||
|
||||
# Required containers — must always be running on HOST2.
|
||||
# Strike system before restart — repeated failures go on skip list, auto-clears on recovery.
|
||||
# Listed in dependency order — dependencies before dependents.
|
||||
HOST2_WATCHDOG_REQUIRED_CONTAINERS=(
|
||||
"NginxProxyManager"
|
||||
# add HOST2 required containers here when back online
|
||||
)
|
||||
|
||||
# Containers to skip in Tier 2 global scan — legitimately stopped or frequently restarting.
|
||||
# Watchdog leaves these alone entirely — no restart attempts, no crash loop tracking.
|
||||
HOST2_WATCHDOG_SCAN_IGNORE=(
|
||||
# add HOST2 scan ignore containers here when back online
|
||||
)
|
||||
|
||||
# Dependency ordering — skip restarting a container if its dependency is also down.
|
||||
# Prevents watchdog from restarting dependent services before their dependencies are up.
|
||||
# SPACE-SEPARATED STRINGS — converted to array at runtime.
|
||||
declare -A HOST2_WATCHDOG_DEPENDENCIES=(
|
||||
# add HOST2 dependencies here when containers are defined
|
||||
# ["Authelia"]="Mariadb-Authelia Redis-Authelia"
|
||||
)
|
||||
|
||||
# Per-container appdata growth suppress ceilings in MB.
|
||||
# ONLY needed in specific cases — growth rate detection covers all containers automatically.
|
||||
# Use when a container legitimately has large stable data and you want to suppress false-positive
|
||||
# growth alerts. Add entries here only when a container triggers warnings it shouldn't.
|
||||
declare -A HOST2_WATCHDOG_APPDATA_SIZES=(
|
||||
# add HOST2 suppress entries here only as needed
|
||||
)
|
||||
|
||||
# ━━━ Docker Network Connect ━━━
|
||||
# Containers connected to custom networks at array start by docker_network_connect.sh.
|
||||
# Networks created if they don't exist — idempotent, safe to re-run.
|
||||
HOST2_NETWORK_CONNECT_CONTAINERS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
HOST2_NETWORK_CONNECT_NETWORKS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── FALLBACK ──────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ DDNS ━━━
|
||||
# DDNS containers HOST2 manages — started/stopped by fallback.sh per DDNS absolute rules:
|
||||
# Internet loss → stop immediately
|
||||
# Failover → HOST1 starts HOST2's DDNS as Tier 1 (before any other containers)
|
||||
# Handback → stop HOST2's DDNS on HOST1 → rsync → start containers → start local DDNS last
|
||||
HOST2_DDNS_CONTAINERS=(
|
||||
"Gmer4Lfe.us"
|
||||
)
|
||||
|
||||
# ━━━ Internet Loss ━━━
|
||||
# Containers stopped immediately on HOST2 when internet connection is lost.
|
||||
# Prevents external-facing services from operating without connectivity.
|
||||
FALLBACK_HOST2_STOP_ON_NO_NET=(
|
||||
"Gmer4Lfe.us"
|
||||
)
|
||||
|
||||
# ━━━ Fallback Tiers — HOST2 Runs for HOST1 ━━━
|
||||
# Containers HOST2 starts when HOST1 goes down.
|
||||
# Tier 1 is always immediate — vital services cannot wait.
|
||||
# Higher tiers activate after HOST1_TIER*_DELAY minutes (set in host1.conf).
|
||||
FALLBACK_HOST2_COVERS_HOST1_TIER1=(
|
||||
"Gmer4Lfe.com"
|
||||
"Gitea" # source of truth — must be reachable even when HOST1 auth stack is down
|
||||
"Emby"
|
||||
"VaultWarden-Gmer4Lfe"
|
||||
"Dispatcharr"
|
||||
"Dispatcharr-Basic"
|
||||
"Dispatcharr-Iptv-Users"
|
||||
"ErsatzTV-Emby"
|
||||
)
|
||||
|
||||
FALLBACK_HOST2_COVERS_HOST1_TIER2=(
|
||||
"Postgres-NextCloud"
|
||||
"NextCloud"
|
||||
"PostgreSQL_Immich"
|
||||
"Immich-Gmer4Lfe"
|
||||
)
|
||||
|
||||
FALLBACK_HOST2_COVERS_HOST1_TIER3=(
|
||||
"Gitea"
|
||||
)
|
||||
|
||||
FALLBACK_HOST2_COVERS_HOST1_TIER4=(
|
||||
"Sonarr"
|
||||
"Radarr"
|
||||
"Lidarr"
|
||||
"Readarr"
|
||||
"Prowlarr"
|
||||
"Bazarr"
|
||||
"SABnzbd-Gmer4Lfe"
|
||||
"Qbittorrent-Gmer4Lfe"
|
||||
"LidaTube"
|
||||
"Pinchflat"
|
||||
"ChannelTube"
|
||||
)
|
||||
|
||||
# ━━━ Tier Delays — HOST2's Containers on HOST1 ━━━
|
||||
# How long HOST2 must be down before each tier activates on HOST1 — in minutes.
|
||||
# Tier 1 is always immediate — no delay var needed.
|
||||
HOST2_TIER2_DELAY=240 # 4 hours — productivity services
|
||||
HOST2_TIER3_DELAY=720 # 12 hours — secondary services
|
||||
HOST2_TIER4_DELAY=1440 # 24 hours — arrs + downloaders
|
||||
|
||||
# ━━━ Rsync Writeback — HOST2 Appdata Back on Handback ━━━
|
||||
# Syncs HOST2 appdata BACK to HOST2 when it comes back online after a failover.
|
||||
# Containers stopped before writeback — clean source, no competing writes.
|
||||
#
|
||||
# HOST2_TIER1_WRITEBACK_DELAY: short outages skip Tier 1 writeback — primary state
|
||||
# is more reliable than dirty sync data for brief outages.
|
||||
HOST2_TIER1_WRITEBACK_DELAY=60 # skip writeback if outage under 1hr
|
||||
|
||||
# Tier 4 automatically syncs HOST2_DAILY_SYNC_SHARES — only list paths NOT in that array.
|
||||
FALLBACK_HOST2_WRITEBACK_TIER1=(
|
||||
# "/mnt/user/appdata-Fallback/Jayred365-Emby"
|
||||
)
|
||||
|
||||
FALLBACK_HOST2_WRITEBACK_TIER2=(
|
||||
# "/mnt/user/appdata-Fallback/Jayred365-Important"
|
||||
)
|
||||
|
||||
FALLBACK_HOST2_WRITEBACK_TIER3=(
|
||||
# "location-placeholder"
|
||||
)
|
||||
|
||||
FALLBACK_HOST2_WRITEBACK_TIER4=(
|
||||
"/mnt/user/appdata-Fallback/Arrs_Stack" # arr databases — downloads queued during outage
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MEDIA ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Media Permissions ━━━
|
||||
# Shares that media_shares_permissions.sh applies PERMISSIONS_MODE and PERMISSIONS_OWNER to.
|
||||
# Runs first in DAILY_MAINTENANCE_SCRIPTS — arr cleanup depends on correct ownership.
|
||||
HOST2_MEDIA_PERMISSION_SHARES=(
|
||||
/mnt/user/Anime_Movies
|
||||
/mnt/user/Anime_Shows
|
||||
)
|
||||
|
||||
# ━━━ Media Cleaner ━━━
|
||||
# Folder lists for media_cleaner.sh — two profiles: anime and media.
|
||||
# File patterns shared across all servers — defined in master.conf.
|
||||
# Called via DAILY_MAINTENANCE_SCRIPTS. Run manually: Media/media_cleaner.sh anime|media
|
||||
HOST2_ANIME_CLEAN_FOLDERS=(
|
||||
/mnt/user/Anime_Movies
|
||||
/mnt/user/Anime_Shows
|
||||
)
|
||||
|
||||
HOST2_MEDIA_CLEAN_FOLDERS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MONITORS ──────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Certificate Monitor ━━━
|
||||
# Domains checked via direct openssl connection — not relying on NPM's certificate state.
|
||||
# Checks the actual certificate served, not what NPM thinks it has.
|
||||
# Thresholds (CERT_WARN_DAYS, CERT_CRIT_DAYS) defined in master.conf.
|
||||
HOST2_CERT_MONITOR_DOMAINS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# ━━━ SMART Health ━━━
|
||||
# Drives skipped in SMART attribute monitoring — hardware is server-specific.
|
||||
# Thresholds read from dynamix.cfg at runtime — fallbacks in master.conf.
|
||||
HOST2_SMART_IGNORE_DRIVES=(
|
||||
"sda" # boot USB — SMART not meaningful on flash drives
|
||||
)
|
||||
|
||||
# ━━━ ZFS Report ━━━
|
||||
# Pools excluded from the weekly ZFS health report — reduces noise from single-disk array pools.
|
||||
# Pool health thresholds defined in master.conf.
|
||||
HOST2_ZFS_REPORT_IGNORE_POOLS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── TRANSCODES ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# Ramdisk size ceiling — tmpfs only uses RAM actually needed, not the full size upfront.
|
||||
# Adjust HOST2_RAMDISK_WARN_GB and HOST2_RAMDISK_LOW_GB together if this changes.
|
||||
HOST2_RAMDISK_SIZE="8G"
|
||||
|
||||
# Usage thresholds — coupled to HOST2_RAMDISK_SIZE, adjust all three together if size changes.
|
||||
# Hysteresis gap (6.8 - 5.5 = 1.3GB) prevents flip-flop between ramdisk and SSD.
|
||||
HOST2_RAMDISK_WARN_GB=6.8 # flip to SSD when ramdisk usage reaches this
|
||||
HOST2_RAMDISK_LOW_GB=5.5 # flip back to ramdisk when usage drops to this
|
||||
|
||||
# SSD fallback path — where transcodes land when ramdisk exceeds HOST2_RAMDISK_WARN_GB.
|
||||
# Must be on cache pool — array disks too slow for active transcode writes.
|
||||
HOST2_TRANSCODE_SSD="/mnt/cache/Temp_Storage/Emby/Transcodes/"
|
||||
|
||||
# Media servers sharing the ramdisk transcode space on HOST2.
|
||||
# Format: "ContainerName|URL|APIKey|Type" — Type: emby | jellyfin | plex
|
||||
# Entries with placeholder API keys are skipped automatically.
|
||||
# ⚠️ Tdarr does NOT belong here — keep Tdarr on SSD, not ramdisk.
|
||||
HOST2_TRANSCODE_SERVERS=(
|
||||
"${HOST2_EMBY_CONTAINER}|${HOST2_EMBY_URL}|${HOST2_EMBY_API_KEY}|emby"
|
||||
"${HOST2_JELLYFIN_CONTAINER}|${HOST2_JELLYFIN_URL}|${HOST2_JELLYFIN_API_KEY}|jellyfin"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── ARR STACK ─────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Used by arr cleanup scripts and arrs_failed_stalled_recovery.sh.
|
||||
# detect_hosts() selects HOST2 vars when running on HOST2.
|
||||
# Lidarr does not run on HOST2 — HOST1_LIDARR_RECOVERY flag handles the exit cleanly.
|
||||
#
|
||||
# PATH MAPS — container path → host path translation.
|
||||
# Arr stores file paths using container-internal paths — scripts need host paths to scan.
|
||||
# Add one entry per root folder in arr Settings → Media Management → Root Folders.
|
||||
HOST2_FANART_API_KEY="Yd7147a43b692df0b364b94dc47efb81"
|
||||
HOST2_LASTFM_API_KEY="be6dc169c33ae263e690c30d18b7491d"
|
||||
|
||||
# ━━━ Sonarr ━━━
|
||||
HOST2_SONARR_URL="http://localhost:8989"
|
||||
HOST2_SONARR_API_KEY="130decd3db5b4c25afad64864cd03f9f"
|
||||
HOST2_SONARR_TV_ROOT="/mnt/user/Anime_Shows"
|
||||
|
||||
declare -A HOST2_SONARR_PATH_MAP=(
|
||||
# fill in when HOST2 is back online
|
||||
# ["/tv"]="/mnt/user/Anime_Shows"
|
||||
)
|
||||
|
||||
# ━━━ Radarr ━━━
|
||||
HOST2_RADARR_URL="http://localhost:7878"
|
||||
HOST2_RADARR_API_KEY="d43a3ec6cf1549edb4af0cc63f98b2a9"
|
||||
HOST2_RADARR_MOVIES_ROOT="/mnt/user/Anime_Movies"
|
||||
|
||||
declare -A HOST2_RADARR_PATH_MAP=(
|
||||
# fill in when HOST2 is back online
|
||||
# ["/anime-movies"]="/mnt/user/Anime_Movies"
|
||||
)
|
||||
|
||||
# ━━━ Arr Recovery Toggles ━━━
|
||||
# false = skip that arr on this host — exits cleanly without error
|
||||
HOST2_SONARR_RECOVERY=true
|
||||
HOST2_RADARR_RECOVERY=true
|
||||
# HOST2_LIDARR_RECOVERY not set — Lidarr does not run on HOST2
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Per-host check toggles and NIC config for system_watchdog.sh.
|
||||
# Aliased by detect_hosts() — script uses unprefixed SYS_WATCHDOG_* names.
|
||||
# HOST2: i5 10th gen 64GB — being rebuilt, lighter workload, no ZFS cache pools.
|
||||
#
|
||||
# Conservative defaults during rebuild — re-enable checks as HOST2 stabilises.
|
||||
# Three-tier response — all critical checks enabled regardless of rebuild state:
|
||||
# Tier 1 (bypass strikes, reboot now): docker daemon, rootfs full, kernel oops, FD, /boot
|
||||
# Tier 2 (bypass strikes with OOM): RAM critical + OOM kills in cycle
|
||||
# Tier 3 (standard strike system): selectively disabled during rebuild
|
||||
#
|
||||
# RAM tiers, OOM limits, and reboot loop settings in master.conf System Watchdog section.
|
||||
|
||||
# ━━━ Primary NIC ━━━
|
||||
# Network interface for NIC state check — verify with: ip link show | grep "^[0-9]"
|
||||
# Common values: eth0, bond0, br0, eno1
|
||||
HOST2_SYS_WATCHDOG_NIC="eth0"
|
||||
|
||||
# ━━━ Tier 1 — Critical Checks ━━━
|
||||
# All critical checks always enabled — these protect against acute failure regardless of
|
||||
# rebuild state. Disabling any is not recommended.
|
||||
|
||||
# Docker daemon unresponsive → try restart, reboot if restart fails.
|
||||
HOST2_SYS_WATCHDOG_CHECK_DOCKER_DAEMON=true
|
||||
|
||||
# rootfs at critical threshold (ROOTFS_CRITICAL_PCT=99) → reboot immediately.
|
||||
HOST2_SYS_WATCHDOG_CHECK_ROOTFS=true
|
||||
|
||||
# Kernel BUG/Oops in dmesg delta since last cycle → reboot immediately.
|
||||
HOST2_SYS_WATCHDOG_CHECK_KERNEL_OOPS=true
|
||||
|
||||
# File descriptor exhaustion at FD_CRITICAL_PCT (95%) → reboot immediately.
|
||||
HOST2_SYS_WATCHDOG_CHECK_FD=true
|
||||
|
||||
# /boot read-only detected → reboot immediately.
|
||||
HOST2_SYS_WATCHDOG_CHECK_BOOT=true
|
||||
|
||||
# ━━━ Tier 2 — Urgent OOM Check ━━━
|
||||
# Both must be enabled for Tier 2 bypass to function.
|
||||
|
||||
# Track kernel OOM kills each cycle via /proc/vmstat oom_kill delta.
|
||||
HOST2_SYS_WATCHDOG_CHECK_OOM=true
|
||||
|
||||
# Free RAM check — 64GB RAM on HOST2, tiers adjusted relative to HOST1.
|
||||
# Update master.conf SYS_WATCHDOG_MEM_* thresholds if HOST2 needs different values.
|
||||
# Currently inheriting shared master.conf values — may want lower thresholds on 64GB.
|
||||
HOST2_SYS_WATCHDOG_CHECK_RAM=true
|
||||
|
||||
# ━━━ Tier 3 — Standard Checks (strike system) ━━━
|
||||
# Several checks disabled during rebuild — enable progressively as HOST2 stabilises.
|
||||
# Each check must fail SYS_WATCHDOG_STRIKE_LIMIT consecutive cycles before action.
|
||||
|
||||
# /var/log filesystem usage above SYS_WATCHDOG_LOG_PCT.
|
||||
HOST2_SYS_WATCHDOG_CHECK_LOG=true
|
||||
|
||||
# ZFS ARC memory check.
|
||||
# DISABLED — HOST2 has no ZFS cache pools. Enable if ZFS pools are added later.
|
||||
HOST2_SYS_WATCHDOG_CHECK_ARC=false
|
||||
|
||||
# CPU temperature above SYS_WATCHDOG_CPU_TEMP_MAX (95°C).
|
||||
HOST2_SYS_WATCHDOG_CHECK_CPU_TEMP=true
|
||||
|
||||
# Load average above SYS_WATCHDOG_LOAD_MULTIPLIER × core count.
|
||||
# DISABLED — rebuild operations cause legitimate load spikes. Enable after rebuild.
|
||||
HOST2_SYS_WATCHDOG_CHECK_LOAD=false
|
||||
|
||||
# Zombie process count above SYS_WATCHDOG_ZOMBIE_LIMIT (50).
|
||||
HOST2_SYS_WATCHDOG_CHECK_ZOMBIES=true
|
||||
|
||||
# docker_watchdog.sh persistent skip list check.
|
||||
# DISABLED during rebuild — skip list may be unreliable mid-rebuild, avoid false reboots.
|
||||
# Enable once HOST2 is fully operational and docker_watchdog.sh is running stably.
|
||||
HOST2_SYS_WATCHDOG_CHECK_CONTAINERS=false
|
||||
|
||||
# /tmp filesystem usage with auto-clear attempt.
|
||||
HOST2_SYS_WATCHDOG_CHECK_TMP=true
|
||||
|
||||
# Array disk error count delta in /proc/mdstat.
|
||||
HOST2_SYS_WATCHDOG_CHECK_MDSTAT=true
|
||||
|
||||
# Primary NIC operstate — uses HOST2_SYS_WATCHDOG_NIC above.
|
||||
HOST2_SYS_WATCHDOG_CHECK_NETWORK=true
|
||||
|
||||
# sshd running check — restart attempt before escalating.
|
||||
HOST2_SYS_WATCHDOG_CHECK_SSHD=true
|
||||
|
||||
# Runaway process detection.
|
||||
# DISABLED — rebuild workloads may legitimately peg CPU. Enable after rebuild.
|
||||
HOST2_SYS_WATCHDOG_CHECK_RUNAWAY=false
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Containers to manage under pressure — see master.conf RW_CRITICAL_CONTAINERS for exclusions.
|
||||
|
||||
# docker pause at medium pressure (RAM < RW_RAM_MEDIUM_GB or load > medium threshold)
|
||||
# Suspended in-place — instant to pause/unpause, no state lost, no restart delay.
|
||||
HOST2_RW_PAUSE_CONTAINERS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# docker stop at hard pressure (RAM < RW_RAM_HARD_GB)
|
||||
# Full stop — these are optional/heavy services that free significant RAM when stopped.
|
||||
# resource_watchdog.sh restarts them when pressure fully clears (RAM >= RW_RAM_RECOVER_GB).
|
||||
HOST2_RW_STOP_CONTAINERS=(
|
||||
# fill in when HOST2 is back online
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ──────────────────────── End Of HOST2 Variables ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -105,8 +105,8 @@
|
||||
# detect_hosts() in common.sh matches the local hostname against these to set MY_ID / REMOTE_ID.
|
||||
# Tailscale IP resolution uses these names — no hardcoded IPs needed.
|
||||
# To add a new server: add HOST3="unRAID-NewServer" here + create host3.conf.
|
||||
HOST1="unRAID-Gmer4Lfe"
|
||||
HOST2="unRAID-Jayred365"
|
||||
HOST1="" # this server's Unraid hostname — must match exactly (case-sensitive)
|
||||
HOST2="" # partner server hostname — leave blank if running standalone
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SHARED HOST CONFIGURATION ─────────────────────────────────────────────────────────────────
|
||||
@@ -248,7 +248,7 @@
|
||||
# Detects Gitea container location at runtime — works through failover automatically.
|
||||
# Falls back to GITEA_DOMAIN if local and Tailscale both fail.
|
||||
GITEA_CONTAINER="Gitea"
|
||||
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
|
||||
GITEA_REPO_PATH="" # e.g. YourUser/Varaverk.git
|
||||
GITEA_DOMAIN="" # e.g. git.yourdomain.com — requires NPM + DNS
|
||||
TARGET_DIR="/mnt/user/appdata/Varaverk"
|
||||
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
|
||||
@@ -462,9 +462,9 @@
|
||||
# → Run individual: bash Rsync/rsync.sh /mnt/user/Movies
|
||||
# → When ready: INTERMEDIATE_RSYNC_ENABLED=true DAILY_RSYNC_ENABLED=true
|
||||
RSYNC_ENABLED=true # Tier 1 — global gate, overrides everything below
|
||||
CRITICAL_RSYNC_ENABLED=false # Tier 2 — disabled during HOST2 rebuild, re-enable when ready
|
||||
CRITICAL_RSYNC_ENABLED=true # Tier 2 — critical_sync_maintenance.sh rsync section
|
||||
INTERMEDIATE_RSYNC_ENABLED=true # Tier 2 — intermediate_sync_maintenance.sh rsync section
|
||||
DAILY_RSYNC_ENABLED=false # Tier 2 — HOST2 rebuild in progress, re-enable when ready
|
||||
DAILY_RSYNC_ENABLED=true # Tier 2 — daily_sync_maintenance.sh rsync section
|
||||
WEEKLY_RSYNC_ENABLED=true # Tier 2 — weekly_sync_maintenance.sh rsync section
|
||||
FALLBACK_RSYNC_ENABLED=true # Tier 2 — fallback.sh writeback jobs on handback
|
||||
|
||||
@@ -563,9 +563,9 @@
|
||||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||||
declare -A PROFILE_CRITICAL_CONTAINER_NAMES=(
|
||||
[arrs_stack]="Sonarr Lidarr Readarr Radarr Prowlarr Bazarr Pinchflat"
|
||||
[critical-data]="Mariadb-Authelia Mariadb-Authelia-Secondary Redis-Authelia Redis-Authelia-Secondary Lldap-Gmer4Lfe NginxProxyManager Authelia Authelia-Secondary"
|
||||
[critical-data]="" # e.g. "Mariadb Authelia Redis NginxProxyManager"
|
||||
[critical-fallback]="" # dirty sync — auth stays running both sides
|
||||
[important-data]="Postgres-NextCloud NextCloud"
|
||||
[important-data]="" # e.g. "Postgres NextCloud"
|
||||
[emby]="Emby"
|
||||
[emby-fallback]="" # dirty sync — Emby stays running both sides
|
||||
)
|
||||
@@ -574,7 +574,7 @@
|
||||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||||
declare -A PROFILE_DELAYED_CONTAINERS=(
|
||||
[arrs_stack]=""
|
||||
[critical-data]="Authelia Authelia-Secondary" # wait for Mariadb + Redis
|
||||
[critical-data]="" # e.g. "Authelia" — containers that need a delay after db starts
|
||||
[critical-fallback]=""
|
||||
[important-data]="NextCloud" # wait for Postgres
|
||||
[emby]=""
|
||||
@@ -607,7 +607,7 @@
|
||||
# Used by dirty sync profiles (critical-fallback, emby-fallback) so remote picks up changes.
|
||||
# SPACE-SEPARATED STRINGS — converted to array at runtime
|
||||
declare -A PROFILE_REMOTE_RESTART_CONTAINERS=(
|
||||
[critical-fallback]="NginxProxyManager Authelia Authelia-Secondary Lldap-Gmer4Lfe Mariadb-Authelia Mariadb-Authelia-Secondary Redis-Authelia Redis-Authelia-Secondary"
|
||||
[critical-fallback]="" # containers to restart on remote after dirty sync
|
||||
[emby-fallback]="Emby"
|
||||
)
|
||||
|
||||
@@ -635,7 +635,7 @@
|
||||
FALLBACK_CHECK_INTERVAL=30 # seconds between fallback state checks
|
||||
FALLBACK_HANDBACK_STRIKES=3 # consecutive healthy checks before initiating handback (3×30s = 90s)
|
||||
FALLBACK_STATE_FILE="/boot/config/fallback_state.db"
|
||||
FALLBACK_ENABLED=true # HOST2 back online
|
||||
FALLBACK_ENABLED=false # set true once both servers are configured and paired
|
||||
# false = suppresses "not running" warnings in status scripts
|
||||
FALLBACK_PARTNERSHIP_REQUIRED=true # gate fallback on an active partnership
|
||||
# false = standalone mode, no partnership dependency
|
||||
@@ -28,6 +28,7 @@
|
||||
# Step 6: 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 8: Arr bootstrap — bidirectional library sync (arr_sync.sh)
|
||||
# Step 9: Conf push — push master.conf + setup state to all listed hosts
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
@@ -486,6 +487,7 @@ ARR_DEPLOYED=0
|
||||
ARR_FAILED=0
|
||||
ONBOARD_OK=false
|
||||
ARR_SYNC_OK=false
|
||||
MASTER_PUSH_OK=false
|
||||
|
||||
# ── Step 1: SSH ───────────────────────────────────────────────────────────────────────────────
|
||||
echo "━━━ Step 1/8 — SSH Key Setup ━━━"
|
||||
@@ -614,6 +616,42 @@ else
|
||||
warn "Re-run Media/arr_sync.sh once all arr containers are live"
|
||||
fi
|
||||
|
||||
# ── Step 9: Push master.conf to all listed hosts ──────────────────────────────────────────────
|
||||
# SSH is now established and all partners have the plugin installed.
|
||||
# Push the authoritative master.conf so every listed host is in sync immediately.
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step 9/9 — master.conf Push ━━━"
|
||||
|
||||
if [[ "$ONBOARD_OK" == false ]]; then
|
||||
warn "Skipping — onboard did not complete"
|
||||
elif [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would push master.conf to all listed hosts"
|
||||
MASTER_PUSH_OK=true
|
||||
elif ! command -v php &>/dev/null; then
|
||||
warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf"
|
||||
else
|
||||
push_output=$(php -r "
|
||||
require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
|
||||
\$results = vv_push_master_conf();
|
||||
vv_push_setup_state();
|
||||
if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
|
||||
\$failed = 0;
|
||||
foreach (\$results as \$r) {
|
||||
echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL;
|
||||
if (!\$r['ok']) \$failed++;
|
||||
}
|
||||
exit(\$failed > 0 ? 1 : 0);
|
||||
" 2>/dev/null)
|
||||
push_rc=$?
|
||||
echo "$push_output"
|
||||
if [[ $push_rc -eq 0 ]]; then
|
||||
echo "master.conf sync complete ✅"
|
||||
MASTER_PUSH_OK=true
|
||||
else
|
||||
warn "master.conf push had failures — retry via Scheduler → master.conf → Save Conf"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────────────────────
|
||||
END=$(date +%s)
|
||||
echo ""
|
||||
@@ -633,6 +671,7 @@ echo " Step 5 — Stop arr: $(_skip "$SKIP_ARR_STACK" "$STEP_STOP_ARR_OK
|
||||
echo " Step 6 — Arr stack: $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
|
||||
echo " Step 7 — Onboard: $(_ok "$ONBOARD_OK")"
|
||||
echo " Step 8 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
|
||||
echo " Step 9 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
|
||||
echo ""
|
||||
|
||||
if [[ "$ONBOARD_OK" == true ]]; then
|
||||
|
||||
@@ -294,8 +294,8 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
scp -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
-o StrictHostKeyChecking=no \
|
||||
"$SCRIPTS_ROOT/master.conf" \
|
||||
"root@${MIRROR_IP}:/mnt/user/appdata/Varaverk/master.conf" 2>/dev/null && \
|
||||
"$SCRIPTS_ROOT/Configurations/master.conf" \
|
||||
"root@${MIRROR_IP}:/mnt/user/appdata/Varaverk/Configurations/master.conf" 2>/dev/null && \
|
||||
log "master.conf pushed to $NEW_OWNER ✅" || \
|
||||
error "Failed to push master.conf to $NEW_OWNER — set PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\" manually"
|
||||
else
|
||||
|
||||
@@ -7,6 +7,21 @@ $plugin = 'varaverk';
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
$pluginDir = "$docroot/plugins/$plugin";
|
||||
|
||||
require_once "$pluginDir/include/config.php";
|
||||
|
||||
// First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing
|
||||
$_master = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
|
||||
$_host1_blank = empty(trim($_h1m[1] ?? ''));
|
||||
$_my_hostid = vv_detect_host();
|
||||
$_conf_missing = $_my_hostid !== 'unknown'
|
||||
&& !file_exists(CONF_DIR . '/' . $_my_hostid . '.conf');
|
||||
if ($_host1_blank || $_conf_missing) {
|
||||
include "$pluginDir/pages/setup.php";
|
||||
return;
|
||||
}
|
||||
unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing);
|
||||
|
||||
// Determine active tab
|
||||
$tab = $_GET['tab'] ?? 'monitor';
|
||||
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs'];
|
||||
@@ -25,6 +40,12 @@ $tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'D
|
||||
<?= $tabLabels[$t] ?? ucfirst($t) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<a href="https://github.com/FailedProxy/Varaverk" target="_blank"
|
||||
style="margin-left:auto;padding:0 10px;font-size:10px;color:#333;text-decoration:none;
|
||||
display:flex;align-items:center;letter-spacing:.03em;"
|
||||
title="GitHub — source, issues, changelog">
|
||||
⎋ GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
Menu="OtherSettings"
|
||||
Title="Varaverk"
|
||||
Icon="varaverk.png"
|
||||
---
|
||||
<?php
|
||||
$cfg = parse_plugin_cfg('varaverk');
|
||||
$scripts_dir = $cfg['SCRIPTS_DIR'] ?? '/mnt/user/appdata/Varaverk';
|
||||
?>
|
||||
|
||||
<form markdown="1" name="varaverk_settings" method="POST" action="/update.php" target="progressFrame">
|
||||
<input type="hidden" name="#file" value="varaverk/varaverk.cfg">
|
||||
|
||||
_(Scripts directory)_:
|
||||
: <input type="text" name="SCRIPTS_DIR" maxlength="255" value="<?=htmlspecialchars($scripts_dir)?>">
|
||||
|
||||
> Path where Varaverk is installed. Changing this only affects the plugin UI — update any scripts that reference the old path manually.
|
||||
|
||||
|
||||
: <input type="submit" value="_(Apply)_">
|
||||
|
||||
</form>
|
||||
|
||||
<div style="margin-top:24px;padding-top:16px;border-top:1px solid #2a2a2a;font-family:monospace;font-size:11px;color:#444;">
|
||||
<a href="https://github.com/FailedProxy/Varaverk" target="_blank"
|
||||
style="color:#555;text-decoration:none;">
|
||||
⎋ github.com/FailedProxy/Varaverk
|
||||
</a>
|
||||
·
|
||||
<a href="https://github.com/FailedProxy/Varaverk/issues" target="_blank"
|
||||
style="color:#555;text-decoration:none;">
|
||||
Issues
|
||||
</a>
|
||||
·
|
||||
<a href="https://github.com/FailedProxy/Varaverk/blob/main/CHANGELOG.md" target="_blank"
|
||||
style="color:#555;text-decoration:none;">
|
||||
Changelog
|
||||
</a>
|
||||
</div>
|
||||
@@ -22,7 +22,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not allowed']);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => vv_write_conf_raw($file, $content)]);
|
||||
$written = vv_write_conf_raw($file, $content);
|
||||
$push = [];
|
||||
if ($written && $file === 'master.conf') {
|
||||
$push = vv_push_master_conf();
|
||||
vv_push_setup_state();
|
||||
}
|
||||
echo json_encode(['ok' => $written, 'push' => $push]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = trim($_POST['action'] ?? 'save');
|
||||
|
||||
// ── HOST2 pull: pull master.conf from HOST1 via SSH ──────────────────────────────────────────
|
||||
if ($action === 'pull') {
|
||||
$host1Hostname = trim($_POST['host1_hostname'] ?? '');
|
||||
$mySlot = trim($_POST['my_slot'] ?? 'host2');
|
||||
$myHostname = trim($_POST['my_hostname'] ?? '');
|
||||
|
||||
if (!$host1Hostname) {
|
||||
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname required']);
|
||||
exit;
|
||||
}
|
||||
if (!preg_match('/^host\d+$/', $mySlot)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$hostId = strtoupper($mySlot);
|
||||
$hostIdLow = strtolower($mySlot);
|
||||
|
||||
// Derive SSH key path from this server's hostname
|
||||
$sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname()));
|
||||
$sshKey = '/root/.ssh/' . $sshOwner . '_rsync_automation';
|
||||
|
||||
if (!file_exists($sshKey)) {
|
||||
echo json_encode(['ok' => false, 'error' =>
|
||||
"SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Resolve HOST1 Tailscale IP
|
||||
$ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: '');
|
||||
if (!$ip) {
|
||||
echo json_encode(['ok' => false, 'error' =>
|
||||
"Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get HOST1's SCRIPTS_DIR from their varaverk.cfg
|
||||
$sshBase = 'ssh -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
|
||||
$remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: '');
|
||||
preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm);
|
||||
$remoteConf = rtrim($sm[1] ?? '/mnt/user/appdata/Varaverk', '/') . '/Configurations';
|
||||
|
||||
// SCP master.conf from HOST1
|
||||
$localMaster = CONF_DIR . '/master.conf';
|
||||
$src = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
|
||||
$cmd = 'scp -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
|
||||
. ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1';
|
||||
exec($cmd, $out, $rc);
|
||||
if ($rc !== 0) {
|
||||
echo json_encode(['ok' => false, 'error' =>
|
||||
'SCP failed: ' . implode('; ', $out) .
|
||||
' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create host conf from template if it doesn't exist
|
||||
$confFile = $hostIdLow . '.conf';
|
||||
if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
||||
$template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
|
||||
if ($template) {
|
||||
$hostname = $myHostname ?: vv_get_hostname();
|
||||
$sshKeyPath = $sshKey;
|
||||
$conf = str_replace('HOSTN', $hostId, $template);
|
||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||
'${1}"' . $sshKeyPath . '"', $conf);
|
||||
vv_write_conf_raw($confFile, $conf);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
|
||||
'redirect' => '?tab=scheduler&vv_setup=' . $confFile]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Default action: save (HOST1 first-run wizard) ────────────────────────────────────────────
|
||||
$host1 = trim($_POST['host1'] ?? '');
|
||||
$host2 = trim($_POST['host2'] ?? '');
|
||||
$mySlot = trim($_POST['my_slot'] ?? 'host1');
|
||||
$myHostname = trim($_POST['my_hostname'] ?? '');
|
||||
|
||||
if (empty($host1)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']);
|
||||
exit;
|
||||
}
|
||||
if (!preg_match('/^host\d+$/', $mySlot)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Write HOST1 / HOST2 into master.conf
|
||||
$master = vv_read_conf_raw('master.conf');
|
||||
if ($master === '') {
|
||||
echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
|
||||
$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);
|
||||
|
||||
$slotNum = (int) preg_replace('/\D/', '', $mySlot);
|
||||
if ($slotNum > 2 && !empty($myHostname)) {
|
||||
$hostKey = 'HOST' . $slotNum;
|
||||
if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
|
||||
$master = preg_replace('/^(\s*HOST2\s*=.*$)/m',
|
||||
'$1' . "\n {$hostKey}=\"" . addslashes($myHostname) . '"', $master);
|
||||
} else {
|
||||
$master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
|
||||
'${1}"' . addslashes($myHostname) . '"', $master);
|
||||
}
|
||||
}
|
||||
|
||||
if (!vv_write_conf_raw('master.conf', $master)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create host*.conf from template
|
||||
$hostId = strtoupper($mySlot);
|
||||
$hostIdLow = strtolower($mySlot);
|
||||
$confFile = $hostIdLow . '.conf';
|
||||
|
||||
if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
||||
$template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
|
||||
if ($template) {
|
||||
$sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
|
||||
$sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
|
||||
$conf = str_replace('HOSTN', $hostId, $template);
|
||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||
'${1}"' . $sshKeyPath . '"', $conf);
|
||||
if (!vv_write_conf_raw($confFile, $conf)) {
|
||||
echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write setup state file — lets partner servers know HOST1 is configured
|
||||
vv_setup_state_write(['host1_hostname' => $host1]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'host_id' => $hostId,
|
||||
'redirect' => '?tab=scheduler&vv_setup=master.conf',
|
||||
]);
|
||||
@@ -27,6 +27,15 @@
|
||||
.vv-card:hover .vv-card-cog { color: #4a4a4a; }
|
||||
.vv-card-cog:hover { color: #aaa !important; background: #333; }
|
||||
|
||||
/* Card header icon wrapper */
|
||||
.vv-ico { display:inline-flex; align-items:center; opacity:0.38; flex-shrink:0; }
|
||||
|
||||
/* Status accent — left border colour for state-driven cards */
|
||||
.vv-accent-ok { border-left-color: #2a5a2a !important; }
|
||||
.vv-accent-warn { border-left-color: #5a4000 !important; }
|
||||
.vv-accent-err { border-left-color: #5a1e1e !important; }
|
||||
#vv-monitor .vv-card { transition: border-left-color 0.5s; }
|
||||
|
||||
/* Status banner — coloured strip at top of card body */
|
||||
.vv-banner { border-radius: 4px; padding: 5px 10px; margin-bottom: 10px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
|
||||
@@ -572,7 +572,11 @@ function vv_remote_hosts_stats(): array {
|
||||
foreach ($hostIds as $id) {
|
||||
if (strtolower($id) === strtolower($myHost)) continue;
|
||||
$key = $vars[strtoupper($id) . '_UNRAID_API_KEY'] ?? '';
|
||||
if (!$key) continue;
|
||||
if (!$key) {
|
||||
$results[$id] = ['available' => false, 'no_api_key' => true,
|
||||
'host_id' => $id, 'hostname' => $vars[$id]];
|
||||
continue;
|
||||
}
|
||||
|
||||
$cacheFile = "/tmp/vv_remote_{$id}.json";
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 30) {
|
||||
|
||||
@@ -10,6 +10,118 @@ define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
|
||||
define('LOG_DIR', '/var/log/varaverk');
|
||||
unset($_vv_cfg);
|
||||
|
||||
const VV_SETUP_STATE_FILE = '/boot/config/varaverk_setup.db';
|
||||
|
||||
// Read the setup state file into a key=>value array.
|
||||
function vv_setup_state_read(): array {
|
||||
$out = [];
|
||||
foreach (file(VV_SETUP_STATE_FILE) ?: [] as $line) {
|
||||
[$k, $v] = explode('=', trim($line), 2) + ['', ''];
|
||||
if ($k !== '') $out[$k] = $v;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// Write the setup state file (creates or overwrites).
|
||||
function vv_setup_state_write(array $data): void {
|
||||
$content = '';
|
||||
foreach ($data as $k => $v) $content .= "$k=$v\n";
|
||||
file_put_contents(VV_SETUP_STATE_FILE, $content);
|
||||
}
|
||||
|
||||
// Push the setup state file to all remote hosts via scp.
|
||||
// Unlike master.conf push, this does NOT require the plugin to be installed on the remote —
|
||||
// it only needs SSH to be reachable, and pushes to /boot/config/ (always available).
|
||||
function vv_push_setup_state(): void {
|
||||
if (!file_exists(VV_SETUP_STATE_FILE)) return;
|
||||
$myHostId = vv_detect_host();
|
||||
$vars = vv_conf_vars();
|
||||
$sshKey = $vars[strtoupper($myHostId) . '_SSH_KEY'] ?? '';
|
||||
if (!$sshKey || !file_exists($sshKey)) return;
|
||||
|
||||
$master = vv_read_conf_raw('master.conf');
|
||||
preg_match_all('/^\s*(HOST\d+)(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m);
|
||||
$seen = [];
|
||||
foreach ($m[1] as $i => $hostKey) {
|
||||
$hostId = strtolower($hostKey);
|
||||
if ($hostId === $myHostId || isset($seen[$hostId])) continue;
|
||||
$seen[$hostId] = true;
|
||||
$hostname = trim($m[2][$i]);
|
||||
if (!$hostname) continue;
|
||||
$ip = vv_resolve_tailscale_ip($hostname);
|
||||
if (!$ip) continue;
|
||||
$sshBase = 'ssh -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
|
||||
// Ensure the target dir exists (it always should on Unraid, but be safe)
|
||||
shell_exec($sshBase . ' "mkdir -p /boot/config" 2>/dev/null');
|
||||
$dest = escapeshellarg('root@' . $ip . ':/boot/config/varaverk_setup.db');
|
||||
exec('scp -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
|
||||
. ' ' . escapeshellarg(VV_SETUP_STATE_FILE) . ' ' . $dest . ' 2>&1');
|
||||
}
|
||||
}
|
||||
|
||||
// Push master.conf to all remote hosts via scp after a local save.
|
||||
// Returns one result entry per remote found in master.conf.
|
||||
// Silently returns [] on non-owner hosts (no SSH key, no remote access).
|
||||
function vv_push_master_conf(): array {
|
||||
$myHostId = vv_detect_host();
|
||||
$vars = vv_conf_vars();
|
||||
$sshKey = $vars[strtoupper($myHostId) . '_SSH_KEY'] ?? '';
|
||||
if (!$sshKey || !file_exists($sshKey)) return [];
|
||||
|
||||
$localPath = CONF_DIR . '/master.conf';
|
||||
$master = vv_read_conf_raw('master.conf');
|
||||
preg_match_all('/^\s*(HOST\d+)(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m);
|
||||
|
||||
$results = [];
|
||||
$seen = [];
|
||||
foreach ($m[1] as $i => $hostKey) {
|
||||
$hostId = strtolower($hostKey);
|
||||
if ($hostId === $myHostId || isset($seen[$hostId])) continue;
|
||||
$seen[$hostId] = true;
|
||||
|
||||
$hostname = trim($m[2][$i]);
|
||||
$ip = vv_resolve_tailscale_ip($hostname);
|
||||
if (!$ip) {
|
||||
$results[] = ['host' => $hostKey, 'ok' => false, 'ready' => false, 'error' => 'Tailscale IP not found'];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Single SSH call: get remote SCRIPTS_DIR and verify plugin is installed,
|
||||
// Configurations/ exists, and master.conf is already present.
|
||||
// Any missing piece means the remote isn't ready — skip rather than push blind.
|
||||
$sshBase = 'ssh -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
|
||||
$probe = trim(shell_exec(
|
||||
$sshBase . ' "cfg=$(grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null)'
|
||||
. ' && sd=$(echo \"$cfg\" | grep -oP \'(?<=SCRIPTS_DIR=\")[^\"]+\')'
|
||||
. ' && test -d \"${sd}/Configurations\"'
|
||||
. ' && test -f \"${sd}/Configurations/master.conf\"'
|
||||
. ' && echo \"$sd\""'
|
||||
) ?: '');
|
||||
|
||||
if ($probe === '') {
|
||||
$results[] = ['host' => $hostKey, 'ok' => false, 'ready' => false,
|
||||
'error' => 'plugin not installed, dir missing, or master.conf absent — skipped'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteConf = rtrim($probe, '/') . '/Configurations';
|
||||
$dest = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
|
||||
$cmd = 'scp -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
|
||||
. ' ' . escapeshellarg($localPath) . ' ' . $dest . ' 2>&1';
|
||||
exec($cmd, $out, $rc);
|
||||
$results[] = [
|
||||
'host' => $hostKey,
|
||||
'ok' => $rc === 0,
|
||||
'error' => $rc !== 0 ? implode('; ', $out) : '',
|
||||
];
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
function vv_get_hostname(): string {
|
||||
return trim(shell_exec('hostname -s') ?: '');
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ require_once __DIR__ . '/config.php';
|
||||
// ── Fallback tracking ─────────────────────────────────────────────────────────
|
||||
|
||||
function &_vv_api_fallbacks(): array { static $f = []; return $f; }
|
||||
function &_vv_api_key_missing(): bool { static $m = false; return $m; }
|
||||
|
||||
function vv_api_record_fallback(string $fn): void {
|
||||
$f = &_vv_api_fallbacks();
|
||||
@@ -27,6 +28,7 @@ function vv_api_get_status(): array {
|
||||
$fallbacks = array_unique(_vv_api_fallbacks());
|
||||
return [
|
||||
'available' => empty($fallbacks),
|
||||
'key_missing' => _vv_api_key_missing(),
|
||||
'fallbacks' => $fallbacks,
|
||||
];
|
||||
}
|
||||
@@ -43,7 +45,12 @@ function vv_api_data(): ?array {
|
||||
$hostId = vv_detect_host();
|
||||
$vars = vv_conf_vars();
|
||||
$key = $vars[strtoupper($hostId) . '_UNRAID_API_KEY'] ?? '';
|
||||
if (!$key) { $cache = null; return null; }
|
||||
if (!$key) {
|
||||
$m = &_vv_api_key_missing();
|
||||
$m = true;
|
||||
$cache = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fields verified against live schema introspection (2026-05-29).
|
||||
// array.parities / .disks / .caches are SEPARATE lists — .disks is DATA only.
|
||||
|
||||
+142
-25
@@ -1,10 +1,6 @@
|
||||
<?php require_once dirname(__DIR__) . '/include/monitor.php'; ?>
|
||||
|
||||
<div id="vv-api-banner" style="display:none;background:#1a1200;border:1px solid #3a2800;border-radius:4px;
|
||||
padding:5px 10px;margin-bottom:8px;font-size:11px;color:#ff9800;">
|
||||
⚠ Unraid API unavailable — using local reads. Check API key in host conf.
|
||||
<span id="vv-api-fallback-list" style="color:#666;margin-left:6px;"></span>
|
||||
</div>
|
||||
<div id="vv-api-banner" style="display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;"></div>
|
||||
|
||||
<div id="vv-monitor" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;width:100%;box-sizing:border-box;">
|
||||
|
||||
@@ -15,8 +11,8 @@
|
||||
|
||||
<div class="vv-card" id="vv-ups-card" style="grid-column:span 1;">
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:6px;">
|
||||
<svg width="9" height="15" viewBox="0 0 11 18" fill="none" style="opacity:0.4;flex-shrink:0;margin-bottom:1px;">
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<svg width="9" height="14" viewBox="0 0 11 18" fill="none" style="opacity:0.4;flex-shrink:0;">
|
||||
<polygon points="9,0 2,10 6,10 3,18 11,6 6,6 8,0" fill="#ff9800"/>
|
||||
</svg>Power
|
||||
</span>
|
||||
@@ -26,39 +22,76 @@
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-cpu" style="grid-column:span 2;">
|
||||
<h3><span id="vv-cpu-title">CPU</span><a href="/Settings/CPUset" target="_blank" class="vv-card-cog" title="CPU Settings">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="3" y="3" width="6" height="6" rx="0.8"/><line x1="4.5" y1="3" x2="4.5" y2="1.2"/><line x1="7.5" y1="3" x2="7.5" y2="1.2"/><line x1="4.5" y1="9" x2="4.5" y2="10.8"/><line x1="7.5" y1="9" x2="7.5" y2="10.8"/><line x1="3" y1="4.5" x2="1.2" y2="4.5"/><line x1="3" y1="7.5" x2="1.2" y2="7.5"/><line x1="9" y1="4.5" x2="10.8" y2="4.5"/><line x1="9" y1="7.5" x2="10.8" y2="7.5"/></svg></span>
|
||||
<span id="vv-cpu-title">CPU</span>
|
||||
</span>
|
||||
<a href="/Settings/CPUset" target="_blank" class="vv-card-cog" title="CPU Settings">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-cpu-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-memory" style="grid-column:span 2;">
|
||||
<h3>Memory<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.6" y="2.5" width="12.8" height="3" rx="0.5"/><line x1="3" y1="2.5" x2="3" y2="0.8"/><line x1="5.5" y1="2.5" x2="5.5" y2="0.8"/><line x1="8" y1="2.5" x2="8" y2="0.8"/><line x1="10.5" y1="2.5" x2="10.5" y2="0.8"/><line x1="3" y1="5.5" x2="3" y2="7.2"/><line x1="5.5" y1="5.5" x2="5.5" y2="7.2"/><line x1="8" y1="5.5" x2="8" y2="7.2"/><line x1="10.5" y1="5.5" x2="10.5" y2="7.2"/></svg></span>
|
||||
Memory
|
||||
</span>
|
||||
<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-memory-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-network" style="grid-column:span 2;">
|
||||
<h3>Network<a href="/Settings/NetworkSettings" target="_blank" class="vv-card-cog" title="Network Settings">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="13" height="12" viewBox="0 0 13 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="4.5" y="0.5" width="4" height="3" rx="0.7"/><rect x="0.5" y="8" width="4" height="3" rx="0.7"/><rect x="8.5" y="8" width="4" height="3" rx="0.7"/><line x1="6.5" y1="3.5" x2="6.5" y2="6"/><line x1="6.5" y1="6" x2="2.5" y2="6"/><line x1="2.5" y1="6" x2="2.5" y2="8"/><line x1="6.5" y1="6" x2="10.5" y2="6"/><line x1="10.5" y1="6" x2="10.5" y2="8"/></svg></span>
|
||||
Network
|
||||
</span>
|
||||
<a href="/Settings/NetworkSettings" target="_blank" class="vv-card-cog" title="Network Settings">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-network-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Scripts | Fallback | Partner | Containers & VMs -->
|
||||
<div class="vv-card" id="vv-scripts-card" style="grid-column:span 1;">
|
||||
<h3>Scripts<a href="/Apps/plugin_userscripts" target="_blank" class="vv-card-cog" title="User Scripts">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="10" height="10" rx="1.5"/><line x1="1" y1="4.5" x2="11" y2="4.5"/><polyline points="3.5,7 5,8 3.5,9"/><line x1="6" y1="9" x2="8.5" y2="9"/></svg></span>
|
||||
Scripts
|
||||
</span>
|
||||
<a href="/Apps/plugin_userscripts" target="_blank" class="vv-card-cog" title="User Scripts">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-scripts-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-fallback" style="grid-column:span 1;">
|
||||
<h3>Fallback</h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M5 0.8L9.2 2.8V6.2C9.2 9 5 11.2 5 11.2C5 11.2 0.8 9 0.8 6.2V2.8Z"/></svg></span>
|
||||
Fallback
|
||||
</span>
|
||||
</h3>
|
||||
<div id="vv-fallback-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-partner" style="grid-column:span 2;">
|
||||
<h3>Partner</h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.5" y="1.5" width="4" height="7" rx="0.8"/><rect x="9.5" y="1.5" width="4" height="7" rx="0.8"/><line x1="4.5" y1="5" x2="9.5" y2="5"/><circle cx="7" cy="5" r="1" fill="#888" stroke="none"/></svg></span>
|
||||
Partner
|
||||
</span>
|
||||
</h3>
|
||||
<div id="vv-partner-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-docker-folders" style="grid-column:span 4;">
|
||||
<h3>
|
||||
<span>Containers & VMs <span id="vv-docker-count" style="font-size:10px;color:#4a4a4a;font-weight:400;text-transform:none;letter-spacing:0;margin-left:2px;"></span></span>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="0.5" width="12" height="2.5" rx="0.5"/><rect x="1" y="3.8" width="12" height="2.5" rx="0.5"/><rect x="1" y="7" width="12" height="2.5" rx="0.5"/></svg></span>
|
||||
Containers & VMs <span id="vv-docker-count" style="font-size:10px;color:#4a4a4a;font-weight:400;text-transform:none;letter-spacing:0;margin-left:2px;"></span>
|
||||
</span>
|
||||
<a href="/Docker" target="_blank" class="vv-card-cog" title="Docker">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-docker-folders-body">Loading...</div>
|
||||
@@ -66,38 +99,77 @@
|
||||
|
||||
<!-- Row 3: GPU | Transcode | Streams -->
|
||||
<div class="vv-card" id="vv-gpu-card" style="grid-column:span 2;">
|
||||
<h3>GPU<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
|
||||
GPU
|
||||
</span>
|
||||
<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-gpu-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-transcode" style="grid-column:span 2;">
|
||||
<h3>Transcode</h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.5" y="1" width="4" height="10" rx="0.8"/><line x1="0.5" y1="3.5" x2="2.3" y2="3.5"/><line x1="0.5" y1="8.5" x2="2.3" y2="8.5"/><line x1="2.7" y1="3.5" x2="4.5" y2="3.5"/><line x1="2.7" y1="8.5" x2="4.5" y2="8.5"/><line x1="6" y1="6" x2="9.5" y2="6"/><polyline points="8.2,4.3 10.2,6 8.2,7.7"/><rect x="11" y="1" width="2.5" height="10" rx="0.5"/></svg></span>
|
||||
Transcode
|
||||
</span>
|
||||
</h3>
|
||||
<div id="vv-transcode-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-streams" style="grid-column:span 4;">
|
||||
<h3>Streams</h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polygon points="1.5,1 1.5,11 10,6"/></svg></span>
|
||||
Streams
|
||||
</span>
|
||||
</h3>
|
||||
<div id="vv-streams-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 4: Watchdog | Parity | Pools | Array -->
|
||||
<div class="vv-card" id="vv-watchdog-card" style="grid-column:span 1;">
|
||||
<h3>Watchdog</h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="9" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><path d="M1 4C3.2 1 5 0.5 7 0.5C9 0.5 10.8 1 13 4C10.8 7 9 7.5 7 7.5C5 7.5 3.2 7 1 4Z"/><circle cx="7" cy="4" r="1.5"/></svg></span>
|
||||
Watchdog
|
||||
</span>
|
||||
</h3>
|
||||
<div id="vv-watchdog-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-parity-card" style="grid-column:span 1;">
|
||||
<h3>Parity<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><polyline points="3.5,6 5.5,8 8.5,4"/></svg></span>
|
||||
Parity
|
||||
</span>
|
||||
<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-parity-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-storage-card" style="grid-column:3/span 2;">
|
||||
<h3>Pools<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><ellipse cx="6" cy="3" rx="4.5" ry="1.5"/><line x1="1.5" y1="3" x2="1.5" y2="9"/><line x1="10.5" y1="3" x2="10.5" y2="9"/><ellipse cx="6" cy="9" rx="4.5" ry="1.5"/></svg></span>
|
||||
Pools
|
||||
</span>
|
||||
<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-storage-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-array-card" style="grid-column:5/span 4;">
|
||||
<h3><span id="vv-array-title">Array</span><a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a></h3>
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.7" y="0.7" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="4.5" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="8.3" width="12.6" height="3" rx="0.8"/><circle cx="11.5" cy="2.2" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="6" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="9.8" r="0.8" fill="#888" stroke="none"/></svg></span>
|
||||
<span id="vv-array-title">Array</span>
|
||||
</span>
|
||||
<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-array-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
@@ -467,10 +539,21 @@ function vvPollMonitor() {
|
||||
const apiStatus = d._api_status ?? {};
|
||||
const banner = document.getElementById('vv-api-banner');
|
||||
if (banner) {
|
||||
const hasFallbacks = apiStatus.fallbacks && apiStatus.fallbacks.length > 0;
|
||||
banner.style.display = hasFallbacks ? '' : 'none';
|
||||
const listEl = document.getElementById('vv-api-fallback-list');
|
||||
if (listEl && hasFallbacks) listEl.textContent = '(' + apiStatus.fallbacks.join(', ') + ')';
|
||||
const fallbacks = apiStatus.fallbacks ?? [];
|
||||
if (fallbacks.length === 0) {
|
||||
banner.style.display = 'none';
|
||||
} else if (apiStatus.key_missing) {
|
||||
// Key not configured — informational, not alarming
|
||||
Object.assign(banner.style, {display:'', background:'#111', border:'1px solid #2a2a2a', color:'#555'});
|
||||
banner.textContent = 'API key not configured — add HOST' +
|
||||
(typeof vvLocalHostConf !== 'undefined' ? vvLocalHostConf.replace(/\D/g,'') : '1') +
|
||||
'_UNRAID_API_KEY in Scheduler → host conf to enable enhanced monitoring.';
|
||||
} else {
|
||||
// Key present but API unreachable — genuine problem
|
||||
Object.assign(banner.style, {display:'', background:'#1a1200', border:'1px solid #3a2800', color:'#ff9800'});
|
||||
banner.innerHTML = '⚠ Unraid API unreachable — using local reads. Check API key in host conf.' +
|
||||
(fallbacks.length ? ' <span style="color:#666">(' + fallbacks.join(', ') + ')</span>' : '');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Thresholds (from dynamix.cfg via backend) ────────────────────────────
|
||||
@@ -579,6 +662,8 @@ function vvPollMonitor() {
|
||||
<span style="color:#444;">Uptime</span>
|
||||
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
|
||||
</div>${verWarn}`;
|
||||
} else if (rs && rs.no_api_key) {
|
||||
statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API key not configured — complete Onboard to enable</div>`;
|
||||
} else if (rs && !rs.available) {
|
||||
statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API unreachable</div>`;
|
||||
}
|
||||
@@ -596,6 +681,16 @@ function vvPollMonitor() {
|
||||
</div>`;
|
||||
});
|
||||
document.getElementById('vv-partner-body').innerHTML = ptHtml;
|
||||
(function() {
|
||||
const c = document.getElementById('vv-partner');
|
||||
if (!c) return;
|
||||
const remotes = ptHosts.filter(h => !h.is_me);
|
||||
c.classList.remove('vv-accent-ok','vv-accent-warn');
|
||||
if (remotes.length) {
|
||||
if (remotes.some(h => h.online)) c.classList.add('vv-accent-ok');
|
||||
else c.classList.add('vv-accent-warn');
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Fallback ──────────────────────────────────────────────────────────────
|
||||
const fb = d.fallback ?? {};
|
||||
@@ -636,6 +731,14 @@ function vvPollMonitor() {
|
||||
fbHtml += `<div style="font-size:10px;color:#333;">No containers active</div>`;
|
||||
}
|
||||
document.getElementById('vv-fallback-body').innerHTML = fbHtml;
|
||||
(function() {
|
||||
const c = document.getElementById('vv-fallback');
|
||||
if (!c) return;
|
||||
c.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');
|
||||
if (state === 'NORMAL') c.classList.add('vv-accent-ok');
|
||||
else if (state === 'FAILOVER') c.classList.add('vv-accent-err');
|
||||
else if (state !== 'UNKNOWN') c.classList.add('vv-accent-warn');
|
||||
})();
|
||||
|
||||
// ── CPU ─────────────────────────────────────────────────────────────────
|
||||
const cpu = d.cpu ?? {};
|
||||
@@ -951,6 +1054,13 @@ function vvPollMonitor() {
|
||||
}
|
||||
|
||||
el.innerHTML = html;
|
||||
const wdCard = document.getElementById('vv-watchdog-card');
|
||||
if (wdCard) {
|
||||
wdCard.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');
|
||||
if (healthy) wdCard.classList.add('vv-accent-ok');
|
||||
else if (reboots > 0 || oom > 0 || daemonHit) wdCard.classList.add('vv-accent-err');
|
||||
else wdCard.classList.add('vv-accent-warn');
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -990,6 +1100,13 @@ function vvPollMonitor() {
|
||||
} else {
|
||||
document.getElementById('vv-array-body').innerHTML = '<p style="color:#555;font-style:italic;font-size:12px;">No array disks</p>';
|
||||
}
|
||||
(function() {
|
||||
const c = document.getElementById('vv-array-card');
|
||||
if (!c) return;
|
||||
c.classList.remove('vv-accent-ok','vv-accent-err');
|
||||
if (sys.array_state === 'STARTED') c.classList.add('vv-accent-ok');
|
||||
else if (sys.array_state && sys.array_state !== 'UNKNOWN') c.classList.add('vv-accent-err');
|
||||
})();
|
||||
|
||||
// ── GPU ─────────────────────────────────────────────────────────────────
|
||||
const gpu = d.gpu ?? {};
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
.vv-pt-action-btn.info { background:#1a2a3a; color:#7ab; border:1px solid #2e4a6b; }
|
||||
.vv-pt-action-btn:disabled { opacity:0.4; cursor:default; }
|
||||
.vv-pt-transfer-note { font-size:10px; color:#555; margin-top:6px; font-family:monospace; }
|
||||
.vv-pt-onboard-highlight {
|
||||
box-shadow: 0 0 0 2px #4a8, 0 0 12px rgba(100,200,120,.25) !important;
|
||||
animation: vvOnboardPulse 1.4s ease-in-out 4;
|
||||
}
|
||||
@keyframes vvOnboardPulse {
|
||||
0%,100% { box-shadow: 0 0 0 2px #4a8, 0 0 8px rgba(100,200,120,.2); }
|
||||
50% { box-shadow: 0 0 0 4px #4a8, 0 0 18px rgba(100,200,120,.45); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;">
|
||||
@@ -199,11 +207,15 @@ function _renderActions(nodes, cfg) {
|
||||
const remoteOpts = remotes.map(n =>
|
||||
`<option value="${n.slot}">${n.id} — ${n.hostname}</option>`).join('');
|
||||
|
||||
const hasPartner = remotes.length > 0 && remotes.some(n => n.hostname);
|
||||
|
||||
let html = '<div class="vv-pt-actions">';
|
||||
|
||||
// Onboard
|
||||
// Onboard — disabled until a partner hostname is configured in master.conf
|
||||
html += `<button class="vv-pt-action-btn run" onclick="vvPtOnboard(this)"
|
||||
title="Run partnership_onboard.sh — auto-detects role (run on mirror first, then owner)">
|
||||
${!hasPartner ? 'disabled title="Add partner hostname to master.conf first (Scheduler → master.conf)"' :
|
||||
'title="Run partnership_onboard.sh — auto-detects role (run on mirror first, then owner)"'}
|
||||
${!hasPartner ? 'style="opacity:.35;cursor:default;"' : ''}>
|
||||
▶ Onboard
|
||||
</button>`;
|
||||
|
||||
@@ -215,6 +227,18 @@ function _renderActions(nodes, cfg) {
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Contextual note under actions
|
||||
if (!hasPartner) {
|
||||
html += `<div style="font-size:11px;color:#444;margin-top:8px;">
|
||||
No partner hostname configured. Edit master.conf (Scheduler tab) and set HOST2.
|
||||
</div>`;
|
||||
} else if (!cfg.enabled) {
|
||||
html += `<div style="font-size:11px;color:#555;margin-top:8px;">
|
||||
Onboard will generate your SSH key, exchange it with the partner, and establish the partnership.
|
||||
Run on the mirror (HOST2) first, then on the owner (HOST1).
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Transfer — show manual command, too destructive to one-click
|
||||
if (cfg.enabled && isOwner) {
|
||||
const confirmStr = 'i-understand-this-transfers-ownership';
|
||||
@@ -249,6 +273,15 @@ function _render(data) {
|
||||
// Actions
|
||||
document.getElementById('vv-pt-actions-body').innerHTML = _renderActions(nodes, cfg);
|
||||
|
||||
// Highlight Onboard button when arriving from first-run wizard
|
||||
if (new URLSearchParams(location.search).get('vv_onboard') === '1') {
|
||||
const onboardBtn = document.querySelector('#vv-pt-actions-body .vv-pt-action-btn.run');
|
||||
if (onboardBtn) {
|
||||
onboardBtn.classList.add('vv-pt-onboard-highlight');
|
||||
setTimeout(() => onboardBtn.scrollIntoView({behavior: 'smooth', block: 'center'}), 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamp
|
||||
const ts = data.ts ? new Date(data.ts * 1000).toLocaleString([],
|
||||
{month:'numeric',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
// Setup mode — auto-open a conf file and force the editing sequence
|
||||
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
|
||||
? $_GET['vv_setup'] : '';
|
||||
$_vv_my_host = vv_detect_host();
|
||||
$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';
|
||||
|
||||
$tree = vv_job_tree();
|
||||
$customs = vv_custom_scripts();
|
||||
$tools = vv_tools_scripts();
|
||||
@@ -416,18 +423,6 @@ $runningScripts = array_unique($runningScripts);
|
||||
style="display:none;margin-left:auto;">↻ Git Pull</button>
|
||||
</div>
|
||||
|
||||
<!-- Plugin settings row (Advanced mode only) -->
|
||||
<div class="vv-nb-settings vv-adv-only" style="display:none">
|
||||
<span style="color:#555;font-size:11px;white-space:nowrap;">Scripts dir:</span>
|
||||
<input type="text" id="vv-scripts-dir" value="<?= htmlspecialchars(SCRIPTS_DIR) ?>"
|
||||
style="flex:1;min-width:180px;background:#111;border:1px solid #333;color:#aaa;
|
||||
padding:2px 6px;border-radius:3px;font-family:monospace;font-size:11px;">
|
||||
<button type="button" onclick="vvSaveSettings()"
|
||||
style="padding:2px 10px;background:#252525;border:1px solid #444;color:#aaa;
|
||||
border-radius:3px;cursor:pointer;font-size:11px;white-space:nowrap;">Save</button>
|
||||
<span id="vv-settings-status" style="font-size:11px;color:#666;"></span>
|
||||
</div>
|
||||
|
||||
<!-- ── How do I use this (pinned) ── -->
|
||||
<div class="vv-sug-block vv-info-block" id="vv-how-to-use" data-save-key="how-to-use">
|
||||
<div class="vv-sug-header" onclick="vvToggleSug(this)">
|
||||
@@ -953,6 +948,17 @@ let vvSelectedRow = null;
|
||||
let vvCurrentSiId = null;
|
||||
let vvCurrentSiHdr = null;
|
||||
|
||||
// Setup mode — injected by PHP when navigating from the first-run wizard
|
||||
let vvSetupConf = <?= json_encode($vv_setup_conf) ?>;
|
||||
const vvLocalHostConf = <?= json_encode($_vv_local_host_conf) ?>;
|
||||
|
||||
if (vvSetupConf) {
|
||||
// Auto-open the setup conf file once the page is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => vvLoadRawConf(vvSetupConf), 150);
|
||||
});
|
||||
}
|
||||
|
||||
function vvEscHtml(s) {
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
@@ -966,16 +972,6 @@ function vvPost(url, data) {
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
function vvSaveSettings() {
|
||||
const dir = document.getElementById('vv-scripts-dir').value.trim();
|
||||
const status = document.getElementById('vv-settings-status');
|
||||
if (!dir) { status.textContent = '✗ Path required'; return; }
|
||||
status.textContent = 'Saving…';
|
||||
vvPost('/plugins/varaverk/api/settings.php', {scripts_dir: dir})
|
||||
.then(d => { status.textContent = d.ok ? '✓ Saved — reload to apply' : '✗ ' + (d.error ?? 'Error'); })
|
||||
.catch(() => { status.textContent = '✗ Request failed'; });
|
||||
}
|
||||
|
||||
function vvFitRight() {
|
||||
const right = document.getElementById('vv-sched-right');
|
||||
if (!right.classList.contains('vv-panel-visible')) return;
|
||||
@@ -2607,7 +2603,7 @@ function vvShowRawConfMode(title) {
|
||||
|
||||
function vvSaveRawConf() {
|
||||
if (!vvRawConfFile) return;
|
||||
if (!confirm('Save changes to "' + vvRawConfFile + '"?')) return;
|
||||
if (!vvSetupConf && !confirm('Save changes to "' + vvRawConfFile + '"?')) return;
|
||||
const content = document.getElementById('vv-editor-body').value;
|
||||
const btn = document.getElementById('vv-save-rawconf-btn');
|
||||
btn.disabled = true;
|
||||
@@ -2615,13 +2611,42 @@ function vvSaveRawConf() {
|
||||
vvPost('/plugins/varaverk/api/rawconf.php', {file: vvRawConfFile, content})
|
||||
.then(d => {
|
||||
btn.disabled = false;
|
||||
if (d.ok) {
|
||||
btn.textContent = '✓ Saved';
|
||||
setTimeout(() => { btn.textContent = 'Save Conf'; }, 2500);
|
||||
} else {
|
||||
if (!d.ok) {
|
||||
btn.textContent = 'Save Conf';
|
||||
alert('Save failed: ' + (d.error ?? 'Unknown error'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup mode: forced editing sequence
|
||||
if (vvSetupConf) {
|
||||
if (vvRawConfFile === 'master.conf') {
|
||||
// master.conf saved → open host conf next
|
||||
btn.textContent = '✓ Saved — opening ' + vvLocalHostConf + '…';
|
||||
vvSetupConf = vvLocalHostConf;
|
||||
history.replaceState(null, '', location.pathname + '?tab=scheduler&vv_setup=' + encodeURIComponent(vvLocalHostConf));
|
||||
setTimeout(() => vvLoadRawConf(vvLocalHostConf), 400);
|
||||
} else {
|
||||
// Host conf saved → go to Partnership so SSH keys get set up
|
||||
vvSetupConf = '';
|
||||
btn.textContent = '✓ Done — opening Partnership…';
|
||||
setTimeout(() => { window.location.href = location.pathname + '?tab=partnership&vv_onboard=1'; }, 800);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal save: show push status
|
||||
// ready:false = partner not set up yet (pre-onboard) — treat as silent, not an error
|
||||
const push = d.push ?? [];
|
||||
const synced = push.filter(p => p.ok);
|
||||
const realFailed = push.filter(p => !p.ok && p.ready !== false);
|
||||
if (realFailed.length > 0) {
|
||||
btn.textContent = '✓ Saved · push failed: ' + realFailed.map(p => p.host).join(', ');
|
||||
} else if (synced.length > 0) {
|
||||
btn.textContent = '✓ Saved · synced to ' + synced.map(p => p.host).join(', ');
|
||||
} else {
|
||||
btn.textContent = '✓ Saved';
|
||||
}
|
||||
setTimeout(() => { btn.textContent = 'Save Conf'; }, 3500);
|
||||
})
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Save Conf'; });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
// First-run setup wizard.
|
||||
// HOST1 path: blank master.conf → fill hostnames → write master.conf + host1.conf → scheduler.
|
||||
// HOST2 path: state file present → pull master.conf from HOST1 → fill host2.conf → scheduler.
|
||||
|
||||
$detectedHostname = trim(shell_exec('hostname -s') ?: '');
|
||||
|
||||
// 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">
|
||||
|
||||
<style>
|
||||
#vv-setup {
|
||||
max-width: 560px;
|
||||
margin: 48px auto 0;
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
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 .vv-setup-sub { font-size: 12px; color: #555; margin-bottom: 32px; }
|
||||
#vv-setup .vv-setup-field { margin-bottom: 20px; }
|
||||
#vv-setup label { display: block; font-size: 11px; color: #888; margin-bottom: 6px; 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-setup input[type=text]:focus { outline: none; border-color: #555; }
|
||||
#vv-setup .vv-setup-hint { font-size: 11px; color: #555; margin-top: 5px; }
|
||||
#vv-setup .vv-setup-role { display: flex; gap: 10px; margin-bottom: 24px; }
|
||||
#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; }
|
||||
#vv-setup .vv-setup-role-btn.active { border-color: #555; color: #ccc; background: #222; }
|
||||
#vv-setup .vv-setup-conditional { display: none; }
|
||||
#vv-setup .vv-setup-conditional.visible { display: block; }
|
||||
#vv-setup hr.vv-setup-divider { border: none; border-top: 1px solid #222; margin: 24px 0; }
|
||||
#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-setup-btn:hover { border-color: #666; color: #eee; }
|
||||
#vv-setup-btn:disabled { opacity: .45; cursor: default; }
|
||||
#vv-setup-status { margin-top: 12px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }
|
||||
#vv-setup-status.ok { color: #4a8; }
|
||||
#vv-setup-status.err { color: #a44; }
|
||||
.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; }
|
||||
.vv-setup-info-box strong { color: #aaa; }
|
||||
</style>
|
||||
|
||||
<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 — loading…'; status.className = 'ok';
|
||||
setTimeout(() => { window.location.href = '?tab=scheduler&vv_setup=' + encodeURIComponent(<?= json_encode($myHostId . '.conf') ?>); }, 600);
|
||||
} 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 — loading…'; status.className = 'ok';
|
||||
setTimeout(() => { window.location.href = d.redirect ?? '?tab=scheduler'; }, 600);
|
||||
} 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>
|
||||
<div class="vv-setup-sub">Set up your server identity before the plugin can start.</div>
|
||||
|
||||
<div class="vv-setup-field">
|
||||
<label>This server's hostname</label>
|
||||
<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>
|
||||
|
||||
<hr class="vv-setup-divider">
|
||||
|
||||
<label style="margin-bottom:10px;display:block;">Server role</label>
|
||||
<div class="vv-setup-role">
|
||||
<div class="vv-setup-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
||||
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first to be set up</span>
|
||||
</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>
|
||||
</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 — loading…'; status.className = 'ok';
|
||||
setTimeout(() => { window.location.href = d.redirect ?? '?tab=scheduler&vv_setup=master.conf'; }, 600);
|
||||
} 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; ?>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,223 @@
|
||||
<?xml version='1.0' standalone='yes'?>
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "varaverk">
|
||||
<!ENTITY author "gmer4lfe">
|
||||
<!ENTITY version "2026.05.30">
|
||||
<!ENTITY launch "varaverk/monitor">
|
||||
<!ENTITY github "https://github.com/FailedProxy/Varaverk">
|
||||
<!ENTITY branch "main">
|
||||
<!ENTITY cfgdir "/boot/config/plugins/varaverk">
|
||||
<!ENTITY plugdir "/usr/local/emhttp/plugins/varaverk">
|
||||
]>
|
||||
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;"
|
||||
support="https://github.com/FailedProxy/Varaverk/issues"
|
||||
icon="/plugins/varaverk/icons/varaverk.png">
|
||||
|
||||
<CHANGES>
|
||||
###2026.05.30
|
||||
- First-run setup wizard: auto-detects hostname, creates master.conf + host conf from templates
|
||||
- Scheduler setup mode: after wizard, master.conf and host conf open sequentially with forced save flow
|
||||
- Partnership tab: Onboard button highlighted on arrival from wizard; disabled until partner is configured
|
||||
- HOST2 install paths: state-file pull, master.conf push detection, conf-only flow
|
||||
- Onboard Step 9: master.conf automatically pushed to all listed hosts on onboard completion
|
||||
- Graceful pre-onboard state: neutral banners instead of error warnings before SSH is configured
|
||||
- GitHub link in tab bar and Settings page; Community Apps support URL
|
||||
|
||||
###2026.05.28
|
||||
- Initial release: Monitor, Scheduler, Docker, Watchdog, Partnership, Fallback, Arrs tabs
|
||||
- Mutual container fallback with tiered escalation and strike-confirmed handback
|
||||
- Partnership lifecycle: onboard, offboard, transfer
|
||||
- Rsync profile system with per-share container stops and writeback
|
||||
- Watchdog: Tier 1 (explicit) + Tier 2 (global scan) container monitoring
|
||||
- master.conf push-on-save to all configured partners via SSH
|
||||
</CHANGES>
|
||||
|
||||
<!--
|
||||
── Install / Update ────────────────────────────────────────────────────────────
|
||||
Runs on first install and on every plugin update.
|
||||
Clones the repo on fresh install; pulls latest on update.
|
||||
Array must be started — appdata must be available.
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Name="&cfgdir;/install.sh" Run="/bin/bash" Method="install update">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
PLUGIN="varaverk"
|
||||
GITHUB="https://github.com/FailedProxy/Varaverk"
|
||||
BRANCH="main"
|
||||
CFG_DIR="/boot/config/plugins/$PLUGIN"
|
||||
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
||||
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
||||
LOG="$CFG_DIR/install.log"
|
||||
|
||||
mkdir -p "$CFG_DIR"
|
||||
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
|
||||
log "=== Varaverk install/update ==="
|
||||
|
||||
# Unraid only
|
||||
if [[ ! -f /etc/unraid-version ]]; then
|
||||
log "ERROR: not running on Unraid — aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read SCRIPTS_DIR from cfg if present
|
||||
if [[ -f "$CFG_FILE" ]]; then
|
||||
_sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
||||
[[ -n "$_sd" ]] && SCRIPTS_DIR="$_sd"
|
||||
fi
|
||||
SCRIPTS_DIR="${SCRIPTS_DIR:-/mnt/user/appdata/Varaverk}"
|
||||
CONF_DIR="$SCRIPTS_DIR/Configurations"
|
||||
|
||||
# Array must be started — appdata must be available
|
||||
if ! df --output=fstype /mnt/user 2>/dev/null | grep -q 'shfs'; then
|
||||
log "Array not started — cannot install to appdata"
|
||||
log "Start the array then reinstall, or wait for next boot"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Clone or update repo
|
||||
if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then
|
||||
log "Cloning $GITHUB ($BRANCH) → $SCRIPTS_DIR"
|
||||
if ! git clone --branch "$BRANCH" "$GITHUB" "$SCRIPTS_DIR" >> "$LOG" 2>&1; then
|
||||
log "ERROR: git clone failed — check internet access and GitHub URL"
|
||||
exit 1
|
||||
fi
|
||||
log "Clone complete"
|
||||
else
|
||||
log "Updating repo in $SCRIPTS_DIR"
|
||||
git -C "$SCRIPTS_DIR" pull --ff-only origin "$BRANCH" >> "$LOG" 2>&1 \
|
||||
&& log "Pull complete" \
|
||||
|| log "WARNING: git pull failed — repo may have local modifications"
|
||||
fi
|
||||
|
||||
# Sanity check — Plugin/unraid must exist after clone
|
||||
if [[ ! -d "$SCRIPTS_DIR/Plugin/unraid" ]]; then
|
||||
log "ERROR: Plugin/unraid not found in cloned repo — unexpected repo structure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Symlink web files
|
||||
if [[ -L "$WEB_DIR" ]]; then
|
||||
rm "$WEB_DIR"
|
||||
elif [[ -d "$WEB_DIR" ]]; then
|
||||
log "WARNING: $WEB_DIR is a real directory — not replacing (remove manually if needed)"
|
||||
fi
|
||||
if [[ ! -e "$WEB_DIR" ]]; then
|
||||
ln -s "$SCRIPTS_DIR/Plugin/unraid" "$WEB_DIR"
|
||||
log "Symlinked: $WEB_DIR → $SCRIPTS_DIR/Plugin/unraid"
|
||||
fi
|
||||
|
||||
# Write varaverk.cfg if not present
|
||||
if [[ ! -f "$CFG_FILE" ]]; then
|
||||
printf 'SCRIPTS_DIR="%s"\n' "$SCRIPTS_DIR" > "$CFG_FILE"
|
||||
log "Created varaverk.cfg (SCRIPTS_DIR=$SCRIPTS_DIR)"
|
||||
fi
|
||||
|
||||
# Create Configurations dir if git clone didn't
|
||||
mkdir -p "$CONF_DIR"
|
||||
|
||||
# Bootstrap master.conf from template on first install
|
||||
if [[ ! -f "$CONF_DIR/master.conf" ]]; then
|
||||
if [[ -f "$SCRIPTS_DIR/Configurations/master.conf.template" ]]; then
|
||||
cp "$SCRIPTS_DIR/Configurations/master.conf.template" "$CONF_DIR/master.conf"
|
||||
log "Created master.conf from template — fill in HOST1 and HOST2 to continue"
|
||||
else
|
||||
log "WARNING: master.conf.template not found — master.conf not created"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Install complete — open Varaverk in Unraid to complete setup"
|
||||
log "→ Plugins → Varaverk (or navigate to Settings → Utilities → Varaverk)"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
── Boot (every boot) ───────────────────────────────────────────────────────────
|
||||
Fast idempotent check — recreates the web symlink if it disappeared.
|
||||
No git operations. Safe to run before the array is fully started.
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Name="&cfgdir;/boot.sh" Run="/bin/bash">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
PLUGIN="varaverk"
|
||||
CFG_FILE="/boot/config/plugins/$PLUGIN/varaverk.cfg"
|
||||
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
||||
|
||||
# Read SCRIPTS_DIR
|
||||
if [[ -f "$CFG_FILE" ]]; then
|
||||
_sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
||||
fi
|
||||
SCRIPTS_DIR="${_sd:-/mnt/user/appdata/Varaverk}"
|
||||
|
||||
# Only act if source exists (array started) and symlink is missing
|
||||
if [[ -d "$SCRIPTS_DIR/Plugin/unraid" ]] && [[ ! -e "$WEB_DIR" ]]; then
|
||||
[[ -L "$WEB_DIR" ]] && rm "$WEB_DIR"
|
||||
ln -s "$SCRIPTS_DIR/Plugin/unraid" "$WEB_DIR"
|
||||
fi
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
── Remove ──────────────────────────────────────────────────────────────────────
|
||||
Removes the web symlink. Scripts and conf in appdata are left intact.
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Name="&cfgdir;/remove.sh" Run="/bin/bash" Method="remove">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
PLUGIN="varaverk"
|
||||
CFG_DIR="/boot/config/plugins/$PLUGIN"
|
||||
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
||||
CRON_FILE="$CFG_DIR/varaverk.cron"
|
||||
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
||||
|
||||
log() { echo "[Varaverk remove] $*"; }
|
||||
|
||||
# Read SCRIPTS_DIR so we can locate running scripts
|
||||
if [[ -f "$CFG_FILE" ]]; then
|
||||
_sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
||||
fi
|
||||
SCRIPTS_DIR="${_sd:-/mnt/user/appdata/Varaverk}"
|
||||
|
||||
# ── Stop continuous background scripts ───────────────────────────────────────
|
||||
# fallback.sh manages its own lock — use --stop for clean shutdown
|
||||
if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
|
||||
bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && \
|
||||
log "fallback.sh stopped" || true
|
||||
fi
|
||||
|
||||
# Kill any remaining Varaverk background processes (watchdog orchestrator, etc.)
|
||||
pkill -f "run_job.sh" 2>/dev/null || true
|
||||
pkill -f "watchdog_orchestrator.sh" 2>/dev/null || true
|
||||
|
||||
# ── Remove cron entries ───────────────────────────────────────────────────────
|
||||
if [[ -f "$CRON_FILE" ]]; then
|
||||
rm -f "$CRON_FILE"
|
||||
/usr/local/sbin/update_cron 2>/dev/null || true
|
||||
log "Cron entries removed"
|
||||
fi
|
||||
# Remove legacy direct cron file if it exists
|
||||
rm -f /etc/cron.d/varaverk
|
||||
|
||||
# ── Remove web symlink ────────────────────────────────────────────────────────
|
||||
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR" && log "Web symlink removed"
|
||||
|
||||
# ── Remove plugin config from flash ──────────────────────────────────────────
|
||||
rm -rf "$CFG_DIR"
|
||||
log "Plugin config removed from flash"
|
||||
|
||||
log "Done. Scripts and conf in $SCRIPTS_DIR are preserved."
|
||||
log "To fully remove: delete $SCRIPTS_DIR manually."
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
</PLUGIN>
|
||||
Reference in New Issue
Block a user