audit echo vs log across all scripts — outcomes always visible, verbose for per-item loops
This commit is contained in:
@@ -211,7 +211,7 @@ for container in "${RUNNING[@]}"; do
|
||||
-f '{{.State.Running}}' "$container" 2>/dev/null || echo "unknown")
|
||||
|
||||
if [[ "$STATE" != "true" ]]; then
|
||||
log "$ICON_DONE $container stopped in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
echo "$ICON_DONE $container stopped in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
STOPPED+=("$container")
|
||||
success=true
|
||||
break
|
||||
|
||||
@@ -273,7 +273,7 @@ for container in "${ORDERED_RESTART[@]}"; do
|
||||
if retry_docker docker restart "$container"; then
|
||||
[[ "${RESTART_VERIFY_WAIT:-3}" -gt 0 ]] && sleep "${RESTART_VERIFY_WAIT:-3}"
|
||||
if verify_running "$container"; then
|
||||
log "$ICON_STARTED $container restarted and running in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
echo "$ICON_STARTED $container restarted and running in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
RESTARTED+=("$container")
|
||||
LAST_RESTARTED="$container"
|
||||
else
|
||||
|
||||
@@ -394,7 +394,7 @@ if [[ ${#UPDATED[@]} -gt 0 ]]; then
|
||||
fi
|
||||
log "$ICON_SYNC Rebuilding $container from template on new image..."
|
||||
if platform_rebuild_container "$container"; then
|
||||
log "$ICON_DONE $container rebuilt ✅"
|
||||
echo "$ICON_DONE $container rebuilt ✅"
|
||||
REBUILT+=("$container")
|
||||
else
|
||||
error "Failed to rebuild $container — will be picked up by docker_daily_restart.sh"
|
||||
|
||||
@@ -238,7 +238,7 @@ for container in "${ORDERED_RESTART[@]}"; do
|
||||
else
|
||||
if retry_docker docker restart "$container"; then
|
||||
if verify_running "$container"; then
|
||||
log "$ICON_STARTED $container restarted and running in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
echo "$ICON_STARTED $container restarted and running in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
RESTARTED+=("$container")
|
||||
LAST_RESTARTED="$container"
|
||||
else
|
||||
|
||||
@@ -218,7 +218,7 @@ if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
|
||||
sleep 10
|
||||
_ELAPSED=$(( _ELAPSED + 10 ))
|
||||
if _slskd_is_connected; then
|
||||
log "slskd reconnected after ${_ELAPSED}s ✅"
|
||||
echo "slskd reconnected after ${_ELAPSED}s ✅"
|
||||
SLSKD_CONNECTED=true
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -395,7 +395,7 @@ local_start() {
|
||||
post_status=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
|
||||
"$container" 2>/dev/null)
|
||||
if [[ "$post_status" == "true" ]]; then
|
||||
log "$ICON_STARTED $container started and running ✅"
|
||||
echo "$ICON_STARTED $container started and running ✅"
|
||||
return 0
|
||||
else
|
||||
error "$container started but crashed immediately"
|
||||
@@ -428,7 +428,7 @@ local_stop() {
|
||||
return 0
|
||||
fi
|
||||
timeout "$DOCKER_TIMEOUT" docker stop "$container" >/dev/null 2>&1 && \
|
||||
log "$ICON_STOPPED $container stopped" || \
|
||||
echo "$ICON_STOPPED $container stopped" || \
|
||||
error "Failed to stop $container locally"
|
||||
}
|
||||
|
||||
@@ -460,7 +460,7 @@ remote_start() {
|
||||
"timeout $DOCKER_TIMEOUT docker inspect -f '{{.State.Running}}' \
|
||||
$container 2>/dev/null" 2>/dev/null)
|
||||
if [[ "$post_status" == "true" ]]; then
|
||||
log "$ICON_STARTED $container started on $REMOTE_SERVER_NAME ✅"
|
||||
echo "$ICON_STARTED $container started on $REMOTE_SERVER_NAME ✅"
|
||||
return 0
|
||||
else
|
||||
error "$container started on $REMOTE_SERVER_NAME but crashed immediately"
|
||||
@@ -497,7 +497,7 @@ remote_stop() {
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" -o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
root@"$REMOTE_SERVER" \
|
||||
"timeout $DOCKER_TIMEOUT docker stop $container" >/dev/null 2>&1 && \
|
||||
log "$ICON_STOPPED $container stopped on $REMOTE_SERVER_NAME" || \
|
||||
echo "$ICON_STOPPED $container stopped on $REMOTE_SERVER_NAME" || \
|
||||
error "Failed to stop $container on $REMOTE_SERVER_NAME"
|
||||
}
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||||
NEW_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||
if [[ "$NEW_STATE" == "FALLBACK" ]]; then
|
||||
log "State changed to FALLBACK — outage detected correctly ✅"
|
||||
echo "State changed to FALLBACK — outage detected correctly ✅"
|
||||
phase_pass "Fallback Detection"
|
||||
else
|
||||
error "State is $NEW_STATE — expected FALLBACK after ${FALLBACK_TEST_BLOCK_WAIT}s"
|
||||
@@ -364,7 +364,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
|
||||
"$container" 2>/dev/null)
|
||||
if [[ "$STATUS" == "true" ]]; then
|
||||
log "$ICON_RUNNING $container is running locally ✅"
|
||||
echo "$ICON_RUNNING $container is running locally ✅"
|
||||
else
|
||||
error "$ICON_NOT_RUNNING $container is NOT running locally"
|
||||
CONTAINERS_OK=false
|
||||
@@ -394,7 +394,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
|
||||
sleep 3
|
||||
if ping_remote; then
|
||||
log "$REMOTE_SERVER_NAME is reachable again ✅"
|
||||
echo "$REMOTE_SERVER_NAME is reachable again ✅"
|
||||
phase_pass "Restore Connectivity"
|
||||
else
|
||||
error "$REMOTE_SERVER_NAME still unreachable after removing iptables rule"
|
||||
@@ -420,7 +420,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||||
FINAL_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||
if [[ "$FINAL_STATE" == "NORMAL" ]]; then
|
||||
log "State returned to NORMAL — handback completed ✅"
|
||||
echo "State returned to NORMAL — handback completed ✅"
|
||||
phase_pass "Handback"
|
||||
else
|
||||
error "State is $FINAL_STATE — expected NORMAL after ${FALLBACK_TEST_HANDBACK_WAIT}s"
|
||||
@@ -450,7 +450,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
|
||||
"$container" 2>/dev/null)
|
||||
if [[ "$STATUS" != "true" ]]; then
|
||||
log "$ICON_NOT_RUNNING $container stopped locally — handed back ✅"
|
||||
echo "$ICON_NOT_RUNNING $container stopped locally — handed back ✅"
|
||||
else
|
||||
error "$ICON_RUNNING $container still running locally — handback may have failed"
|
||||
HANDBACK_OK=false
|
||||
|
||||
+1
-1
@@ -739,7 +739,7 @@ _enforce_monitored() {
|
||||
"${url}/api/${api_ver}/${bulk_endpoint}" 2>/dev/null)
|
||||
|
||||
if [[ "$http_code" == "200" || "$http_code" == "202" ]]; then
|
||||
log "${arr_type^}: re-monitored $count items ✅"
|
||||
echo "${arr_type^}: re-monitored $count items ✅"
|
||||
else
|
||||
warn "${arr_type^}: bulk re-monitor failed (HTTP $http_code)"
|
||||
fi
|
||||
|
||||
@@ -296,7 +296,7 @@ process_arr() {
|
||||
' 2>/dev/null)
|
||||
|
||||
if [[ -z "$problem_items" ]]; then
|
||||
log "$arr_name — clean ✅ no failed imports or stalled downloads"
|
||||
echo "$arr_name — clean ✅ no failed imports or stalled downloads"
|
||||
ARR_SUMMARIES+=("$arr_name: clean ✅")
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -208,7 +208,7 @@ for FOLDER in "${CLEAN_FOLDERS[@]}"; do
|
||||
FILE_COUNT=$("${CMD[@]}" 2>/dev/null | wc -l)
|
||||
|
||||
if [[ "$FILE_COUNT" -eq 0 ]]; then
|
||||
log "$FOLDER_NAME — clean ✅"
|
||||
echo "$FOLDER_NAME — clean ✅"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
@@ -223,7 +223,7 @@ for FOLDER in "${CLEAN_FOLDERS[@]}"; do
|
||||
else
|
||||
CLEAN_CMD=("${CMD[@]}" -exec rm -f {} +)
|
||||
if "${CLEAN_CMD[@]}" 2>/dev/null; then
|
||||
log "$FOLDER_NAME — $FILE_COUNT file(s) removed"
|
||||
echo "$FOLDER_NAME — $FILE_COUNT file(s) removed"
|
||||
TOTAL_REMOVED=$(( TOTAL_REMOVED + FILE_COUNT ))
|
||||
else
|
||||
error "$FOLDER_NAME — cleanup failed"
|
||||
|
||||
@@ -147,7 +147,7 @@ DROPPED=$(echo "$MOVIES" | jq '[.[] | select(.status == "deleted")] | length')
|
||||
echo " $TOTAL movies total — $DROPPED dropped from TMDb"
|
||||
|
||||
if [[ "$DROPPED" -eq 0 ]]; then
|
||||
log "No TMDb-removed movies found — nothing to do"
|
||||
echo "No TMDb-removed movies found — nothing to do"
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY RADARR TMDB REMOVED SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
@@ -198,7 +198,7 @@ while IFS=$'\t' read -r id title year tmdb_id has_file file_size; do
|
||||
CURL_EXIT=$?
|
||||
|
||||
if [[ "$CURL_EXIT" -eq 0 ]]; then
|
||||
log " Removed from Radarr ✅"
|
||||
echo " Removed from Radarr ✅"
|
||||
REMOVED+=("$title")
|
||||
if [[ "$DELETE_PARAM" == "true" ]]; then
|
||||
(( FILES_DELETED++ ))
|
||||
|
||||
@@ -146,7 +146,7 @@ DROPPED=$(echo "$SERIES" | jq '[.[] | select(.status == "deleted")] | length')
|
||||
echo " $TOTAL series total — $DROPPED dropped from TVDB"
|
||||
|
||||
if [[ "$DROPPED" -eq 0 ]]; then
|
||||
log "No TVDB-removed series found — nothing to do"
|
||||
echo "No TVDB-removed series found — nothing to do"
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY SONARR TVDB REMOVED SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
@@ -200,7 +200,7 @@ while IFS=$'\t' read -r id title year tvdb_id episode_file_count size_on_disk; d
|
||||
CURL_EXIT=$?
|
||||
|
||||
if [[ "$CURL_EXIT" -eq 0 ]]; then
|
||||
log " Removed from Sonarr ✅"
|
||||
echo " Removed from Sonarr ✅"
|
||||
REMOVED+=("$title")
|
||||
if [[ "$DELETE_PARAM" == "true" ]]; then
|
||||
(( FILES_DELETED++ ))
|
||||
|
||||
@@ -177,14 +177,14 @@ resolve_remote_ip
|
||||
|
||||
# Connectivity — no point making 100+ SSH calls if remote is unreachable
|
||||
check_connectivity
|
||||
log "Connectivity to $REMOTE_SERVER_NAME ✅"
|
||||
echo "Connectivity to $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
# Version parity — mismatched unRAID could cause md5sum path differences
|
||||
check_os_version_parity || {
|
||||
warn "Version parity check failed — proceeding with caution"
|
||||
warn "Checksum results may be unreliable if md5sum path changed between versions"
|
||||
}
|
||||
log "Version parity with $REMOTE_SERVER_NAME ✅"
|
||||
echo "Version parity with $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
# Remote array — if array is down all files appear "missing" = false alarm
|
||||
if ! check_remote_array; then
|
||||
@@ -194,7 +194,7 @@ if ! check_remote_array; then
|
||||
"Backup Verify" "warning"
|
||||
exit 1
|
||||
fi
|
||||
log "Remote array mounted on $REMOTE_SERVER_NAME ✅"
|
||||
echo "Remote array mounted on $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
echo "Pre-flight passed ✅"
|
||||
|
||||
@@ -291,7 +291,7 @@ for share in "${VERIFY_SHARES[@]}"; do
|
||||
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH"
|
||||
SHARES_WITH_ISSUES+=("$SHARE_NAME")
|
||||
else
|
||||
log "$SHARE_NAME — all $SHARE_MATCH files match ✅"
|
||||
echo "$SHARE_NAME — all $SHARE_MATCH files match ✅"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -186,7 +186,7 @@ SYSTEM_INFO=$(emby_api "System/Info" 2>/dev/null) || {
|
||||
|
||||
SERVER_NAME=$(echo "$SYSTEM_INFO" | jq -r '.ServerName // "Unknown"' 2>/dev/null)
|
||||
SERVER_VERSION=$(echo "$SYSTEM_INFO" | jq -r '.Version // "Unknown"' 2>/dev/null)
|
||||
log "$ICON_EMBY Connected to: $SERVER_NAME (v$SERVER_VERSION) ✅"
|
||||
echo "$ICON_EMBY Connected to: $SERVER_NAME (v$SERVER_VERSION) ✅"
|
||||
|
||||
# ── Active Sessions ───────────────────────────────────────────────────────────────────────────
|
||||
echo "━━━ $ICON_EMBY Active Sessions ━━━"
|
||||
|
||||
@@ -185,7 +185,7 @@ for relative_path in "${ARRAY_START_SCRIPTS[@]}"; do
|
||||
wait "$PID"
|
||||
EXIT_CODE=$?
|
||||
if [[ "$EXIT_CODE" -eq 0 ]]; then
|
||||
log "$SCRIPT_NAME — completed (one-shot) ✅"
|
||||
echo "$SCRIPT_NAME — completed (one-shot) ✅"
|
||||
(( LAUNCHED++ ))
|
||||
else
|
||||
error "$SCRIPT_NAME — exited with code $EXIT_CODE"
|
||||
|
||||
@@ -140,7 +140,7 @@ for entry in "${ARRAY_STOP_SCRIPTS[@]}"; do
|
||||
fi
|
||||
|
||||
if bash "$script_path" "${extra_args[@]}"; then
|
||||
log "$script_name — done ✅"
|
||||
echo "$script_name — done ✅"
|
||||
PASSED+=("$script_name")
|
||||
else
|
||||
warn "$script_name — failed (exit $?) — continuing to next step"
|
||||
|
||||
@@ -153,7 +153,7 @@ else
|
||||
|
||||
if [[ "$RSYNC_EXIT" -eq 0 ]]; then
|
||||
PASS+=("$SHARE_NAME")
|
||||
log "$SHARE_NAME — done in $SHARE_DUR ✅"
|
||||
echo "$SHARE_NAME — done in $SHARE_DUR ✅"
|
||||
RSYNC_OK=true
|
||||
else
|
||||
FAIL+=("$SHARE_NAME")
|
||||
|
||||
@@ -172,7 +172,7 @@ run_job() {
|
||||
log "Running: $script_name ${extra_args[*]}"
|
||||
# shellcheck disable=SC2086
|
||||
if bash "$script_path" "${extra_args[@]}" $extra_dry; then
|
||||
log "$script_name — done ✅"
|
||||
echo "$script_name — done ✅"
|
||||
JOB_PASS+=("$script_name ${extra_args[*]}")
|
||||
else
|
||||
error "$script_name — failed (exit $?)"
|
||||
@@ -271,7 +271,7 @@ else
|
||||
case "$RSYNC_EXIT" in
|
||||
0)
|
||||
PASS+=("$SHARE_NAME")
|
||||
log "$SHARE_NAME — done ✅"
|
||||
echo "$SHARE_NAME — done ✅"
|
||||
;;
|
||||
1)
|
||||
FAIL+=("$SHARE_NAME:temp-warn")
|
||||
|
||||
@@ -100,7 +100,7 @@ run_job() {
|
||||
log "Running: $script_name ${extra_args[*]}"
|
||||
# shellcheck disable=SC2086
|
||||
if bash "$script_path" "${extra_args[@]}" $extra_dry; then
|
||||
log "$script_name — done ✅"
|
||||
echo "$script_name — done ✅"
|
||||
JOB_PASS+=("$script_name ${extra_args[*]}")
|
||||
else
|
||||
warn "$script_name — failed (exit $?)"
|
||||
@@ -164,7 +164,7 @@ else
|
||||
_conf_args=("--pull-only")
|
||||
[[ "$DRY_RUN" == true ]] && _conf_args+=("--dry-run")
|
||||
if bash "$CONF_SYNC_SCRIPT" "${_conf_args[@]}"; then
|
||||
log "Partner conf cache refreshed ✅"
|
||||
echo "Partner conf cache refreshed ✅"
|
||||
JOB_PASS+=("conf_sync.sh --pull-only")
|
||||
else
|
||||
warn "Partner conf pull failed — cache may be stale"
|
||||
@@ -244,7 +244,7 @@ else
|
||||
case "$RSYNC_EXIT" in
|
||||
0)
|
||||
PASS+=("$SHARE_NAME")
|
||||
log "$SHARE_NAME — done ✅"
|
||||
echo "$SHARE_NAME — done ✅"
|
||||
;;
|
||||
1)
|
||||
FAIL+=("$SHARE_NAME:temp-warn")
|
||||
|
||||
@@ -233,7 +233,7 @@ for entry in "${MONTHLY_MAINTENANCE_SCRIPTS[@]}"; do
|
||||
[[ "$VERBOSE" == true ]] && local_args+=("--log")
|
||||
|
||||
if bash "$script_path" "${extra_args[@]}" "${local_args[@]}"; then
|
||||
log "$script_name — done ✅"
|
||||
echo "$script_name — done ✅"
|
||||
PASSED+=("$script_name")
|
||||
else
|
||||
warn "$script_name — failed (exit $?) — continuing to next step"
|
||||
|
||||
@@ -58,7 +58,7 @@ run_job() {
|
||||
|
||||
log "Running: $script_name ${extra_args[*]}"
|
||||
if bash "$script_path" "${extra_args[@]}" $extra_dry $extra_log; then
|
||||
log "$script_name — done ✅"
|
||||
echo "$script_name — done ✅"
|
||||
JOB_PASS+=("$script_name")
|
||||
else
|
||||
error "$script_name — failed (exit $?)"
|
||||
|
||||
@@ -119,7 +119,7 @@ run_job() {
|
||||
log "Running: $script_name ${extra_args[*]}"
|
||||
# shellcheck disable=SC2086
|
||||
if bash "$script_path" "${extra_args[@]}" $extra_dry; then
|
||||
log "$script_name — done ✅"
|
||||
echo "$script_name — done ✅"
|
||||
JOB_PASS+=("$script_name ${extra_args[*]}")
|
||||
else
|
||||
error "$script_name — failed (exit $?)"
|
||||
@@ -259,7 +259,7 @@ if [[ "$WEEKLY_SYNC_UPDATES_REMOTE" == true ]]; then
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
root@"$REMOTE_SERVER" \
|
||||
"docker pull $IMAGE" >/dev/null 2>&1; then
|
||||
log "$c — remote image updated ✅"
|
||||
echo "$c — remote image updated ✅"
|
||||
else
|
||||
warn "$c — remote pull failed, will start on existing image"
|
||||
fi
|
||||
@@ -304,7 +304,7 @@ else
|
||||
|
||||
if [[ "$EXIT_CODE" -eq 0 ]]; then
|
||||
PASS+=("$JOB_NAME")
|
||||
log "$JOB_NAME — done in $JOB_DUR ✅"
|
||||
echo "$JOB_NAME — done in $JOB_DUR ✅"
|
||||
else
|
||||
FAIL+=("$JOB_NAME")
|
||||
error "$JOB_NAME — failed after $JOB_DUR (exit $EXIT_CODE)"
|
||||
@@ -343,13 +343,13 @@ else
|
||||
if [[ -n "${_weekly_needs_rebuild[$_c]:-}" ]]; then
|
||||
log "Rebuilding $_c on new image..."
|
||||
if platform_rebuild_container "$_c"; then
|
||||
log "$_c rebuilt on new image ✅"
|
||||
echo "$_c rebuilt on new image ✅"
|
||||
else
|
||||
warn "$_c rebuild failed — falling back to docker start"
|
||||
docker start "$_c" >/dev/null 2>&1 || error "Failed to start $_c"
|
||||
fi
|
||||
else
|
||||
docker start "$_c" >/dev/null 2>&1 && log "$_c started" || error "Failed to start $_c"
|
||||
docker start "$_c" >/dev/null 2>&1 && echo "$_c started" || error "Failed to start $_c"
|
||||
fi
|
||||
done
|
||||
unset _c _d _needs_delay
|
||||
|
||||
@@ -205,7 +205,7 @@ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
"$GITEA_API/user" 2>/dev/null)
|
||||
|
||||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||
log "API token valid ✅"
|
||||
echo "API token valid ✅"
|
||||
elif [[ "$HTTP_CODE" == "401" ]]; then
|
||||
error "API token rejected (HTTP 401) — token may be expired or have wrong scope"
|
||||
error "Regenerate the token in Gitea: Settings → Applications → Generate Token → scope: write:user"
|
||||
@@ -436,7 +436,7 @@ PYEOF
|
||||
echo " IdentityFile $GITEA_SSH_KEY"
|
||||
} >> "$SSH_CONFIG"
|
||||
chmod 600 "$SSH_CONFIG"
|
||||
log "SSH config entry added: Host gitea-${MY_ID,,} ✅"
|
||||
echo "SSH config entry added: Host gitea-${MY_ID,,} ✅"
|
||||
fi
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
@@ -90,7 +90,7 @@ if [[ "$DIRECTION" == "h1" || "$DIRECTION" == "both" ]]; then
|
||||
"sed -i \"|${KEY_BLOB}|d\" /root/.ssh/authorized_keys 2>/dev/null
|
||||
sed -i \"/^${MIRROR_ID}_PHASE\|^${MIRROR_ID}_KEY_READY/d\" $(platform_setup_db_path) 2>/dev/null
|
||||
echo ok" 2>/dev/null | grep -q ok && {
|
||||
log "HOST1 key removed from $MIRROR authorized_keys ✅"
|
||||
echo "HOST1 key removed from $MIRROR authorized_keys ✅"
|
||||
H1_DONE=true
|
||||
} || warn "Could not SSH to $MIRROR — remove HOST1 key there manually"
|
||||
fi
|
||||
@@ -102,7 +102,7 @@ if [[ "$DIRECTION" == "h1" || "$DIRECTION" == "both" ]]; then
|
||||
elif [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete: $SSH_KEY and ${SSH_KEY}.pub"
|
||||
else
|
||||
rm -f "$SSH_KEY" "$SSH_KEY_PUB" && log "Local key pair deleted ✅" || \
|
||||
rm -f "$SSH_KEY" "$SSH_KEY_PUB" && echo "Local key pair deleted ✅" || \
|
||||
warn "Failed to delete local key — check permissions"
|
||||
fi
|
||||
|
||||
@@ -110,7 +110,7 @@ if [[ "$DIRECTION" == "h1" || "$DIRECTION" == "both" ]]; then
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
sed -i "/^${MIRROR_ID}_PHASE/d; /^${MIRROR_ID}_KEY_READY/d" "$STATE_FILE"
|
||||
log "Phase flags cleared from local setup.db ✅"
|
||||
echo "Phase flags cleared from local setup.db ✅"
|
||||
else
|
||||
warn "DRY RUN — would clear ${MIRROR_ID}_PHASE* from setup.db"
|
||||
fi
|
||||
@@ -133,7 +133,7 @@ if [[ "$DIRECTION" == "h2" || "$DIRECTION" == "both" ]]; then
|
||||
H2_DONE=true
|
||||
else
|
||||
sed -i "/${MIRROR_SHORT}/Id" "$AUTH_KEYS" && {
|
||||
log "$MIRROR key removed from HOST1 authorized_keys ✅"
|
||||
echo "$MIRROR key removed from HOST1 authorized_keys ✅"
|
||||
H2_DONE=true
|
||||
} || warn "Failed to remove $MIRROR key from HOST1 authorized_keys"
|
||||
fi
|
||||
|
||||
@@ -357,7 +357,7 @@ push_state_to_remote() {
|
||||
fi
|
||||
timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
"$local_file" "root@${remote_ip}:${local_file}" 2>/dev/null && \
|
||||
log "State file pushed to remote ✅" || \
|
||||
echo "State file pushed to remote ✅" || \
|
||||
warn "Could not push state file to remote — will propagate on next sync"
|
||||
}
|
||||
|
||||
@@ -466,7 +466,7 @@ do_ssh_key_revocation() {
|
||||
> /root/.ssh/authorized_keys.tmp 2>/dev/null \
|
||||
&& mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys \
|
||||
&& echo removed" 2>/dev/null | grep -q removed; then
|
||||
log "Our pubkey revoked from $REMOTE_SERVER_NAME ✅"
|
||||
echo "Our pubkey revoked from $REMOTE_SERVER_NAME ✅"
|
||||
SSH_REVOKE_REMOTE_OK=true
|
||||
else
|
||||
warn "Remote revocation failed — revoke manually on $REMOTE_SERVER_NAME:"
|
||||
@@ -491,7 +491,7 @@ do_ssh_key_revocation() {
|
||||
if grep -v "@${REMOTE_SERVER_NAME}" /root/.ssh/authorized_keys \
|
||||
> /root/.ssh/authorized_keys.tmp 2>/dev/null && \
|
||||
mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys; then
|
||||
log "$REMOTE_SERVER_NAME pubkey revoked locally ✅"
|
||||
echo "$REMOTE_SERVER_NAME pubkey revoked locally ✅"
|
||||
SSH_REVOKE_LOCAL_OK=true
|
||||
else
|
||||
warn "Failed to update local authorized_keys — remove @${REMOTE_SERVER_NAME} entry manually"
|
||||
@@ -568,7 +568,7 @@ start_own_stack() {
|
||||
continue
|
||||
fi
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker start "$container" >/dev/null 2>&1; then
|
||||
log "$container started ✅"
|
||||
echo "$container started ✅"
|
||||
else
|
||||
warn "$container failed to start — check manually"
|
||||
fi
|
||||
@@ -610,7 +610,7 @@ cleanup_partner_containers() {
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1 || true
|
||||
_PM_TRAP_STOPPED+=("$container")
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1 && \
|
||||
log "$container removed ✅" || warn "$container rm failed"
|
||||
echo "$container removed ✅" || warn "$container rm failed"
|
||||
else
|
||||
log "$container not found — skipping"
|
||||
fi
|
||||
@@ -623,7 +623,7 @@ cleanup_partner_containers() {
|
||||
warn " DRY RUN — would rm -rf $path"
|
||||
continue
|
||||
fi
|
||||
rm -rf "$path" && log " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
||||
rm -rf "$path" && echo " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
||||
done <<< "$all_appdata_paths"
|
||||
}
|
||||
|
||||
@@ -667,7 +667,7 @@ cleanup_owner_containers_on_mirror() {
|
||||
"docker stop '$container' >/dev/null 2>&1
|
||||
docker rm '$container' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
|
||||
grep -q removed && \
|
||||
log "$container removed from $MIRROR ✅" || \
|
||||
echo "$container removed from $MIRROR ✅" || \
|
||||
warn "Failed to remove $container from $MIRROR"
|
||||
|
||||
# Delete appdata on remote after container removal
|
||||
@@ -676,7 +676,7 @@ cleanup_owner_containers_on_mirror() {
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
|
||||
"rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
|
||||
log " Appdata removed on $MIRROR: $path ✅" || \
|
||||
echo " Appdata removed on $MIRROR: $path ✅" || \
|
||||
warn " Failed to remove appdata on $MIRROR: $path"
|
||||
done <<< "$appdata_paths"
|
||||
done <<< "$container_list"
|
||||
@@ -710,7 +710,7 @@ start_mirror_own_stack() {
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
|
||||
"docker start '$container' >/dev/null 2>&1 && echo started" 2>/dev/null | \
|
||||
grep -q started && \
|
||||
log "$container started on $MIRROR ✅" || \
|
||||
echo "$container started on $MIRROR ✅" || \
|
||||
warn "$container failed to start on $MIRROR — check manually"
|
||||
done
|
||||
}
|
||||
@@ -801,7 +801,7 @@ provision_emby_admin() {
|
||||
"${emby_url}/Users/${user_id}/Password" 2>/dev/null)
|
||||
|
||||
if [[ "$pw_code" == "200" ]] || [[ "$pw_code" == "204" ]]; then
|
||||
log "Emby admin '$username' created (id: $user_id) ✅"
|
||||
echo "Emby admin '$username' created (id: $user_id) ✅"
|
||||
else
|
||||
warn "User created but password set failed (HTTP $pw_code) — set password manually"
|
||||
fi
|
||||
@@ -812,7 +812,7 @@ provision_emby_admin() {
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"IsAdministrator": true, "IsDisabled": false}' \
|
||||
"${emby_url}/Users/${user_id}/Policy" 2>/dev/null && \
|
||||
log "$username granted admin policy ✅" || \
|
||||
echo "$username granted admin policy ✅" || \
|
||||
warn "Could not set admin policy — grant manually in Emby dashboard"
|
||||
}
|
||||
|
||||
@@ -867,7 +867,7 @@ revoke_emby_admin() {
|
||||
"${emby_url}/Users/${user_id}" 2>/dev/null)
|
||||
|
||||
if [[ "$del_code" == "200" ]] || [[ "$del_code" == "204" ]] || [[ "$del_code" == "404" ]]; then
|
||||
log "Emby admin '$username' removed ✅"
|
||||
echo "Emby admin '$username' removed ✅"
|
||||
else
|
||||
warn "Failed to delete Emby user '$username' (HTTP $del_code) — remove manually"
|
||||
fi
|
||||
@@ -922,7 +922,7 @@ update_master_conf() {
|
||||
return 1
|
||||
fi
|
||||
if sed -i "s|^[[:space:]]*${key}=.*| ${key}=${value}|" "$conf" 2>/dev/null; then
|
||||
log "master.conf updated: ${key}=${value}"
|
||||
echo "master.conf updated: ${key}=${value}"
|
||||
return 0
|
||||
else
|
||||
error "Failed to update master.conf: ${key}=${value}"
|
||||
@@ -1168,7 +1168,7 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
else
|
||||
echo "PARTNERSHIP_ENABLED=true" >> "$_master_conf"
|
||||
fi
|
||||
log "PARTNERSHIP_ENABLED=true in master.conf ✅"
|
||||
echo "PARTNERSHIP_ENABLED=true in master.conf ✅"
|
||||
platform_push_conf | while IFS= read -r line; do log "$line"; done
|
||||
else
|
||||
warn "DRY RUN — would set PARTNERSHIP_ENABLED=true in master.conf and push"
|
||||
@@ -1184,7 +1184,7 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
echo "${flag_key}=true" >> "$local_state_file"
|
||||
fi
|
||||
platform_push_setup_state
|
||||
log "${MY_ID}_LOCAL_DONE=true written to setup.db ✅"
|
||||
echo "${MY_ID}_LOCAL_DONE=true written to setup.db ✅"
|
||||
else
|
||||
warn "DRY RUN — would write ${MY_ID}_LOCAL_DONE=true"
|
||||
fi
|
||||
@@ -1259,7 +1259,7 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
container="${entry%%|*}"
|
||||
port="${entry##*|}"
|
||||
if curl -sf --max-time 10 "http://${OWNER_IP}:${port}/" >/dev/null 2>&1; then
|
||||
log "$container reachable at http://${OWNER_IP}:${port}/ ✅"
|
||||
echo "$container reachable at http://${OWNER_IP}:${port}/ ✅"
|
||||
else
|
||||
warn "$container not reachable at http://${OWNER_IP}:${port}/ — may not be running"
|
||||
fi
|
||||
@@ -1438,7 +1438,7 @@ if false; then
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$NEW_MIRROR_IP" \
|
||||
"sed -i 's|^[[:space:]]*PARTNERSHIP_OWNER_HOST=.*| PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\"|' \
|
||||
'$SCRIPT_DIR/../master.conf'" 2>/dev/null && \
|
||||
log "Remote master.conf updated ✅" || \
|
||||
echo "Remote master.conf updated ✅" || \
|
||||
error "Failed to update remote master.conf — update manually"
|
||||
else
|
||||
warn "DRY RUN — would set PARTNERSHIP_OWNER_HOST=$NEW_OWNER_ID on both servers"
|
||||
|
||||
@@ -205,7 +205,7 @@ revoke_local_emby_admin() {
|
||||
"${emby_url}/Users/${user_id}" 2>/dev/null)
|
||||
|
||||
if [[ "$del_code" == "200" ]] || [[ "$del_code" == "204" ]] || [[ "$del_code" == "404" ]]; then
|
||||
log "Emby admin '$username' removed ✅"
|
||||
echo "Emby admin '$username' removed ✅"
|
||||
else
|
||||
warn "Failed to delete Emby user '$username' (HTTP $del_code) — remove manually"
|
||||
fi
|
||||
@@ -243,7 +243,7 @@ if [[ "$AM_MIRROR" == true ]]; then
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || true
|
||||
log "Rsync stopped ✅"
|
||||
echo "Rsync stopped ✅"
|
||||
else
|
||||
warn "DRY RUN — would stop rsync"
|
||||
fi
|
||||
@@ -379,7 +379,7 @@ echo "━━━ $ICON_STOP Step 1/10 — Stop Rsync ━━━"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
bash "$SCRIPTS_ROOT/Rsync/rsync_stop.sh" --rsync-only 2>/dev/null || STEP_STOP_OK=false
|
||||
log "Rsync stopped ✅"
|
||||
echo "Rsync stopped ✅"
|
||||
else
|
||||
warn "DRY RUN — would stop rsync"
|
||||
fi
|
||||
@@ -474,7 +474,7 @@ NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
write_state_file "$LOCAL_STATE_FILE" \
|
||||
"INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
|
||||
log "Local state: INACTIVE ✅"
|
||||
echo "Local state: INACTIVE ✅"
|
||||
add_to_blocklist "$MIRROR" "$REASON"
|
||||
[[ "$MIRROR_REACHABLE" == true ]] && \
|
||||
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||
|
||||
@@ -278,7 +278,7 @@ stop_mirror_stack() {
|
||||
"docker stop '$container' 2>/dev/null
|
||||
docker rm '$container' 2>/dev/null && echo removed" 2>/dev/null | \
|
||||
grep -q removed && \
|
||||
log " $container removed ✅" || \
|
||||
echo " $container removed ✅" || \
|
||||
log " $container not found on $MIRROR — skipping"
|
||||
done
|
||||
}
|
||||
@@ -295,7 +295,7 @@ if [[ "$AM_MIRROR" == true ]]; then
|
||||
if [[ "$SKIP_SSH" == true ]]; then
|
||||
warn "Skipping SSH setup (--skip-ssh)"
|
||||
elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
|
||||
log "SSH key ready ✅"
|
||||
echo "SSH key ready ✅"
|
||||
else
|
||||
error "SSH key setup failed"
|
||||
exit 1
|
||||
@@ -323,7 +323,7 @@ if [[ "$AM_MIRROR" == true ]]; then
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
|
||||
"nohup bash '${OWNER_SCRIPTS_DIR}/Partnership/partnership_onboard.sh' --phase2-only > /tmp/vv_phase2_onboard.log 2>&1 & echo triggered" \
|
||||
2>/dev/null | grep -q triggered; then
|
||||
log "Phase 2 triggered on $OWNER ✅"
|
||||
echo "Phase 2 triggered on $OWNER ✅"
|
||||
log "Watch progress on $OWNER: tail -f /tmp/vv_phase2_onboard.log"
|
||||
PHASE2_TRIGGERED=true
|
||||
else
|
||||
@@ -387,12 +387,12 @@ elif [[ "$PHASE1_ONLY" == true ]]; then
|
||||
# hanging for a password prompt with no TTY.
|
||||
if timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" exit 0 2>/dev/null; then
|
||||
log "SSH to $MIRROR already works ✅ — skipping key install"
|
||||
echo "SSH to $MIRROR already works ✅ — skipping key install"
|
||||
STEP_SSH_OK=true
|
||||
else
|
||||
# Key not yet on HOST2 — try ssh_setup.sh (works interactively, may fail in background)
|
||||
if bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
|
||||
log "SSH keys ready ✅"
|
||||
echo "SSH keys ready ✅"
|
||||
STEP_SSH_OK=true
|
||||
else
|
||||
# Soft-fail: generate key locally if not present, then tell user to install manually
|
||||
@@ -423,7 +423,7 @@ elif [[ "$PHASE1_ONLY" == true ]]; then
|
||||
fi
|
||||
fi
|
||||
elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
|
||||
log "SSH keys ready ✅"
|
||||
echo "SSH keys ready ✅"
|
||||
STEP_SSH_OK=true
|
||||
else
|
||||
error "SSH key setup failed — aborting"
|
||||
@@ -472,7 +472,7 @@ if [[ "$PHASE1_ONLY" == true ]]; then
|
||||
[[ -n "$push_output" ]] && echo "$push_output"
|
||||
platform_push_setup_state
|
||||
if [[ $push_rc -eq 0 ]]; then
|
||||
log "Conf push complete ✅"
|
||||
echo "Conf push complete ✅"
|
||||
CONF_PUSH_OK=true
|
||||
else
|
||||
warn "Conf push had failures — retry via Scheduler → master.conf → Save Conf"
|
||||
@@ -514,7 +514,7 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||
elif timeout 60 ssh -i "$MIRROR_SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
|
||||
"bash '$_net_script'" 2>/dev/null; then
|
||||
log "Docker network ready on $MIRROR ✅"
|
||||
echo "Docker network ready on $MIRROR ✅"
|
||||
STEP_NETWORK_OK=true
|
||||
else
|
||||
warn "docker_network_connect.sh failed on $MIRROR — containers may fail if network is missing"
|
||||
|
||||
@@ -296,7 +296,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
-o StrictHostKeyChecking=no \
|
||||
"$SCRIPTS_ROOT/Configurations/master.conf" \
|
||||
"root@${MIRROR_IP}:${_REMOTE_SD}/Configurations/master.conf" 2>/dev/null && \
|
||||
log "master.conf pushed to $NEW_OWNER ✅" || \
|
||||
echo "master.conf pushed to $NEW_OWNER ✅" || \
|
||||
error "Failed to push master.conf to $NEW_OWNER — set PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\" manually"
|
||||
else
|
||||
warn "DRY RUN — would set PARTNERSHIP_OWNER_HOST=$NEW_OWNER_ID on both servers"
|
||||
|
||||
@@ -141,7 +141,7 @@ update_conf_key_path() {
|
||||
|
||||
if grep -q "^[[:space:]]*${KEY_CONF_VAR}=" "$HOST_CONF" 2>/dev/null; then
|
||||
sed -i "s|^[[:space:]]*${KEY_CONF_VAR}=.*| ${KEY_CONF_VAR}=\"${SSH_KEY_PATH}\"|" "$HOST_CONF" && \
|
||||
log "${KEY_CONF_VAR} updated in $(basename "$HOST_CONF") ✅" || \
|
||||
echo "${KEY_CONF_VAR} updated in $(basename "$HOST_CONF") ✅" || \
|
||||
warn "Failed to update ${KEY_CONF_VAR} in $(basename "$HOST_CONF") — update manually"
|
||||
else
|
||||
warn "${KEY_CONF_VAR} not found in $(basename "$HOST_CONF") — add manually:"
|
||||
@@ -237,9 +237,9 @@ if [[ "$MODE" == "validate" ]]; then
|
||||
STRIKES=$(read_strikes)
|
||||
if [[ "$STRIKES" -gt 0 ]]; then
|
||||
write_strike_file 0 "" "$NOW"
|
||||
log "SSH validate — auth restored to $REMOTE_SERVER_NAME ✅ (strikes reset)"
|
||||
echo "SSH validate — auth restored to $REMOTE_SERVER_NAME ✅ (strikes reset)"
|
||||
else
|
||||
log "SSH validate — $REMOTE_SERVER_NAME SSH auth OK ✅"
|
||||
echo "SSH validate — $REMOTE_SERVER_NAME SSH auth OK ✅"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
@@ -301,7 +301,7 @@ else
|
||||
warn "Generating ed25519 keypair: $SSH_KEY_PATH"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
ssh-keygen -t ed25519 -N "" -f "$SSH_KEY_PATH" -C "${SSH_KEY_NAME}@${LOCAL_SERVER_NAME}" && \
|
||||
log "Keypair generated ✅" || {
|
||||
echo "Keypair generated ✅" || {
|
||||
error "Failed to generate keypair"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ wait_for_container_healthy() {
|
||||
|
||||
case "$status" in
|
||||
healthy|true)
|
||||
log " $name ready ✅"
|
||||
echo " $name ready ✅"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
@@ -159,7 +159,7 @@ deploy_container_from_xml() {
|
||||
timeout 120 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||
"bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
|
||||
grep -q "deployed:${name}"; then
|
||||
log " $name deployed ✅"
|
||||
echo " $name deployed ✅"
|
||||
rm -f "$tmp_script"
|
||||
return 0
|
||||
else
|
||||
@@ -253,7 +253,7 @@ cleanup_deployed_stack_on_remote() {
|
||||
"docker stop '$cname' >/dev/null 2>&1
|
||||
docker rm '$cname' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
|
||||
grep -q removed && \
|
||||
log " $cname removed from $MIRROR ✅" || \
|
||||
echo " $cname removed from $MIRROR ✅" || \
|
||||
log " $cname not found on $MIRROR — skipping"
|
||||
|
||||
while IFS= read -r path; do
|
||||
@@ -261,7 +261,7 @@ cleanup_deployed_stack_on_remote() {
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||
"rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
|
||||
log " Appdata removed on $MIRROR: $path ✅" || \
|
||||
echo " Appdata removed on $MIRROR: $path ✅" || \
|
||||
warn " Failed to remove appdata on $MIRROR: $path"
|
||||
done <<< "$appdata_paths"
|
||||
done
|
||||
@@ -331,14 +331,14 @@ cleanup_deployed_stack_locally() {
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$cname" >/dev/null 2>&1 || true
|
||||
_PM_TRAP_STOPPED+=("$cname")
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$cname" >/dev/null 2>&1 && \
|
||||
log " $cname removed ✅" || warn " $cname rm failed"
|
||||
echo " $cname removed ✅" || warn " $cname rm failed"
|
||||
else
|
||||
log " $cname not found locally — skipping"
|
||||
fi
|
||||
|
||||
while IFS= read -r path; do
|
||||
[[ -z "$path" ]] && continue
|
||||
rm -rf "$path" && log " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
||||
rm -rf "$path" && echo " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
||||
done <<< "$appdata_paths"
|
||||
done
|
||||
}
|
||||
@@ -372,7 +372,7 @@ reconfigure_webui() {
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||
"sed -i 's|<WebUI>.*</WebUI>|<WebUI>http://${target_ip}:${port}/</WebUI>|g' '$template'" \
|
||||
2>/dev/null && \
|
||||
log "$container → http://${target_ip}:${port}/ ✅" || {
|
||||
echo "$container → http://${target_ip}:${port}/ ✅" || {
|
||||
error "Failed to reconfigure $container WebUI on $label"
|
||||
return 1
|
||||
}
|
||||
@@ -408,7 +408,7 @@ reconfigure_local_webuis() {
|
||||
|
||||
sed -i "s|<WebUI>.*</WebUI>|<WebUI>http://${target_ip}:${port}/</WebUI>|g" \
|
||||
"$template" 2>/dev/null && \
|
||||
log "$container → http://${target_ip}:${port}/ ✅" || \
|
||||
echo "$container → http://${target_ip}:${port}/ ✅" || \
|
||||
{ error "Failed to reconfigure $container"; (( failures++ )); }
|
||||
done
|
||||
return $failures
|
||||
|
||||
@@ -135,7 +135,7 @@ MOVER_PID=$(platform_get_mover_pid)
|
||||
MOVER_START=$(ps -o lstart= -p "$MOVER_PID" 2>/dev/null | xargs)
|
||||
MOVER_ELAPSED=$(ps -o etimes= -p "$MOVER_PID" 2>/dev/null | tr -d ' ')
|
||||
warn "Mover is running (PID $MOVER_PID) — stopping in ${MOVER_STOP_TIMEOUT}s"
|
||||
log "$ICON_TIME Mover started: ${MOVER_START:-unknown} — running for $(format_duration "${MOVER_ELAPSED:-0}")"
|
||||
echo "$ICON_TIME Mover started: ${MOVER_START:-unknown} — running for $(format_duration "${MOVER_ELAPSED:-0}")"
|
||||
|
||||
# ── Warn users via wall ───────────────────────────────────────────────────────────────────────
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
|
||||
@@ -196,7 +196,7 @@ if ! sed -i "s/^pm\.max_children.*/pm.max_children = $PHP_MAX_CHILDREN/" "$PHP_C
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Config updated"
|
||||
echo "Config updated"
|
||||
|
||||
# ── Restart PHP-FPM ──────────────────────────────────────────────────────────────────────────
|
||||
log "Restarting PHP-FPM..."
|
||||
@@ -223,7 +223,7 @@ if [[ "${APPLIED_VAL:-0}" -ne "$PHP_MAX_CHILDREN" ]]; then
|
||||
warn "Config reads pm.max_children = ${APPLIED_VAL:-unknown} — expected $PHP_MAX_CHILDREN"
|
||||
warn "Check $PHP_CONF manually"
|
||||
else
|
||||
log "Verified: pm.max_children = $APPLIED_VAL ✅"
|
||||
echo "Verified: pm.max_children = $APPLIED_VAL ✅"
|
||||
fi
|
||||
|
||||
FPM_WORKERS=$(pgrep -fc "php-fpm" 2>/dev/null || echo "?")
|
||||
|
||||
@@ -155,7 +155,7 @@ PUSHSCRIPT
|
||||
if timeout 10 ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no "root@${partner_ip}" \
|
||||
"bash '${remote}'; rc=\$?; rm -f '${remote}'; exit \$rc" 2>/dev/null | grep -q ok; then
|
||||
log "Key pushed to $partner_host ✅"
|
||||
echo "Key pushed to $partner_host ✅"
|
||||
else
|
||||
warn "Key push to $partner_host failed — they can create their own copy"
|
||||
fi
|
||||
|
||||
@@ -154,7 +154,7 @@ if ! mountpoint -q /mnt/user; then
|
||||
"Recreate Shares" "warning"
|
||||
exit 1
|
||||
fi
|
||||
log "Array is started — /mnt/user is mounted ✅"
|
||||
echo "Array is started — /mnt/user is mounted ✅"
|
||||
|
||||
# Check share cfg directory exists and has files
|
||||
if [[ ! -d "$SHARE_CFG_DIR" ]]; then
|
||||
@@ -222,7 +222,7 @@ for cfg in "${CFG_FILES[@]}"; do
|
||||
warn "DRY RUN — would create: $DISK_PATH"
|
||||
(( DIRS_CREATED++ ))
|
||||
elif mkdir -p "$DISK_PATH"; then
|
||||
log "Created: $DISK_PATH ✅"
|
||||
echo "Created: $DISK_PATH ✅"
|
||||
(( DIRS_CREATED++ ))
|
||||
else
|
||||
error "Failed to create: $DISK_PATH"
|
||||
@@ -242,7 +242,7 @@ for cfg in "${CFG_FILES[@]}"; do
|
||||
warn "DRY RUN — would place marker: $MARKER_PATH"
|
||||
CREATED+=("$SHARE_NAME")
|
||||
elif touch "$MARKER_PATH" 2>/dev/null; then
|
||||
log "Marker placed: $MARKER_PATH ✅"
|
||||
echo "Marker placed: $MARKER_PATH ✅"
|
||||
CREATED+=("$SHARE_NAME")
|
||||
else
|
||||
warn "$SHARE_NAME — could not place .recovery marker"
|
||||
|
||||
+2
-2
@@ -331,7 +331,7 @@ for (( ATTEMPT=1; ATTEMPT<=RETRY_COUNT; ATTEMPT++ )); do
|
||||
awk '/Total transferred file size:/{gsub(/,/,"",$NF); gsub(/[^0-9]/,"",$NF); print $NF+0}')
|
||||
BYTES_TRANSFERRED="${BYTES_TRANSFERRED:-0}"
|
||||
|
||||
log "$ICON_DONE Rsync complete — $BYTES_TRANSFERRED bytes transferred"
|
||||
echo "$ICON_DONE Rsync complete — $BYTES_TRANSFERRED bytes transferred"
|
||||
RSYNC_SUCCESS=true
|
||||
break
|
||||
else
|
||||
@@ -402,7 +402,7 @@ if [[ ${#REMOTE_RESTART_CONTAINERS[@]} -gt 0 ]]; then
|
||||
|
||||
timeout 15 ssh -i "$SSH_KEY" -o ConnectTimeout=10 root@"$REMOTE_SERVER" \
|
||||
"docker restart $container" >/dev/null 2>&1 && \
|
||||
log "$ICON_STARTED $container restarted on $REMOTE_SERVER_NAME ✅" || \
|
||||
echo "$ICON_STARTED $container restarted on $REMOTE_SERVER_NAME ✅" || \
|
||||
warn "Failed to restart $container on $REMOTE_SERVER_NAME"
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -72,7 +72,7 @@ if [[ "$PUSH_ONLY" == false ]] && [[ "$PULL_ONLY" == false ]]; then
|
||||
warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/"
|
||||
else
|
||||
cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \
|
||||
log "Own conf cached ✅" || warn "Failed to cache own conf"
|
||||
echo "Own conf cached ✅" || warn "Failed to cache own conf"
|
||||
fi
|
||||
else
|
||||
warn "Own conf not found: $MY_CONF"
|
||||
@@ -107,7 +107,7 @@ for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
"root@${partner_ip}:${remote_conf}" \
|
||||
"$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then
|
||||
log "Pulled ${partner_slot}.conf from $partner_host ✅"
|
||||
echo "Pulled ${partner_slot}.conf from $partner_host ✅"
|
||||
(( PULLED++ ))
|
||||
else
|
||||
warn "Could not pull ${partner_slot}.conf from $partner_host"
|
||||
@@ -134,7 +134,7 @@ for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
"$MY_CONF" \
|
||||
"root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then
|
||||
log "Pushed ${MY_ID,,}.conf to $partner_host ✅"
|
||||
echo "Pushed ${MY_ID,,}.conf to $partner_host ✅"
|
||||
(( PUSHED++ ))
|
||||
else
|
||||
warn "Could not push to $partner_host"
|
||||
|
||||
@@ -131,7 +131,7 @@ if [[ -z "$REMOTE_SERVER" ]]; then
|
||||
warn "$REMOTE_SERVER_NAME — cannot resolve Tailscale IP, remote operations will be skipped"
|
||||
elif timeout "$SSH_TIMEOUT" ping -c1 -W3 "$REMOTE_SERVER" &>/dev/null; then
|
||||
REMOTE_REACHABLE=true
|
||||
log "$REMOTE_SERVER_NAME reachable ✅"
|
||||
echo "$REMOTE_SERVER_NAME reachable ✅"
|
||||
else
|
||||
warn "$REMOTE_SERVER_NAME unreachable — remote operations will be skipped"
|
||||
fi
|
||||
|
||||
@@ -247,7 +247,7 @@ elif [[ "$DRY_RUN" == true ]]; then
|
||||
bash "$ARRAY_STOP_SCRIPT" --dry-run
|
||||
else
|
||||
if bash "$ARRAY_STOP_SCRIPT"; then
|
||||
log "Array stop complete ✅"
|
||||
echo "Array stop complete ✅"
|
||||
else
|
||||
warn "array_stopping.sh reported failures — proceeding with reboot"
|
||||
fi
|
||||
@@ -309,7 +309,7 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would sync filesystem buffers"
|
||||
else
|
||||
sync
|
||||
log "Filesystem buffers flushed ✅"
|
||||
echo "Filesystem buffers flushed ✅"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
|
||||
@@ -184,7 +184,7 @@ for share_path in "${PARSED_ARGS[@]}"; do
|
||||
-exec chmod "${PERMISSIONS_FILE_MODE:-664}" {} + 2>/dev/null || CHMOD_FILE_OK=false
|
||||
|
||||
if [[ "$CHOWN_OK" == true && "$CHMOD_DIR_OK" == true && "$CHMOD_FILE_OK" == true ]]; then
|
||||
log "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"
|
||||
echo "$ICON_UNLOCKED $(basename "$share_path") — permissions applied ✅"
|
||||
PASS+=("$(basename "$share_path")")
|
||||
else
|
||||
error "$(basename "$share_path") — repair failed"
|
||||
|
||||
@@ -175,7 +175,7 @@ case "$STATUS" in
|
||||
log "Stopping $CONTAINER_NAME for clean export..."
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if timeout "$DOCKER_TIMEOUT" docker stop "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
log "$CONTAINER_NAME stopped ✅"
|
||||
echo "$CONTAINER_NAME stopped ✅"
|
||||
else
|
||||
error "Failed to stop $CONTAINER_NAME — aborting export"
|
||||
exit 1
|
||||
@@ -185,7 +185,7 @@ case "$STATUS" in
|
||||
fi
|
||||
;;
|
||||
false)
|
||||
log "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"
|
||||
echo "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"
|
||||
;;
|
||||
"")
|
||||
error "$CONTAINER_NAME not found — check container name"
|
||||
@@ -224,7 +224,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
||||
log "Verifying archive..."
|
||||
if tar --test-label -f "$ARCHIVE_PATH" 2>/dev/null || \
|
||||
tar -tzf "$ARCHIVE_PATH" >/dev/null 2>&1; then
|
||||
log "Archive verified ✅"
|
||||
echo "Archive verified ✅"
|
||||
ARCHIVE_VERIFIED=true
|
||||
else
|
||||
error "Archive verification FAILED — archive may be corrupt"
|
||||
@@ -260,7 +260,7 @@ if [[ "$CONTAINER_WAS_RUNNING" == true ]]; then
|
||||
POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \
|
||||
'{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)
|
||||
if [[ "$POST_STATUS" == "true" ]]; then
|
||||
log "$CONTAINER_NAME restarted and running ✅"
|
||||
echo "$CONTAINER_NAME restarted and running ✅"
|
||||
RESTART_OK=true
|
||||
else
|
||||
error "$CONTAINER_NAME started but crashed immediately — check container logs"
|
||||
@@ -277,7 +277,7 @@ if [[ "$CONTAINER_WAS_RUNNING" == true ]]; then
|
||||
RESTART_OK=true
|
||||
fi
|
||||
else
|
||||
log "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"
|
||||
echo "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"
|
||||
RESTART_OK=true
|
||||
fi
|
||||
|
||||
|
||||
@@ -110,13 +110,13 @@ if [[ "$ALL_MODE" == true ]]; then
|
||||
STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)
|
||||
|
||||
if [[ "$STOPPED_COUNT" -eq 0 ]]; then
|
||||
log "No stopped containers"
|
||||
echo "No stopped containers"
|
||||
elif [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
|
||||
docker ps -a --filter "status=exited" --filter "status=created" \
|
||||
--format " {{.Names}} {{.Image}} {{.Status}}" 2>/dev/null
|
||||
else
|
||||
log "Removing $STOPPED_COUNT stopped container(s)..."
|
||||
echo "Removing $STOPPED_COUNT stopped container(s)..."
|
||||
docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
|
||||
success "Removed $STOPPED_COUNT stopped container(s)"
|
||||
fi
|
||||
|
||||
@@ -200,7 +200,7 @@ case "$STATUS" in
|
||||
warn "Stopping $EMBY_CONTAINER — active sessions will be interrupted"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if timeout "$DOCKER_TIMEOUT" docker stop "$EMBY_CONTAINER" >/dev/null 2>&1; then
|
||||
log "$EMBY_CONTAINER stopped ✅"
|
||||
echo "$EMBY_CONTAINER stopped ✅"
|
||||
sleep 3 # let file handles release
|
||||
else
|
||||
error "Failed to stop $EMBY_CONTAINER — aborting"
|
||||
@@ -263,7 +263,7 @@ for db_rel in "${DB_FILES[@]}"; do
|
||||
warn "Uncommitted WAL data — will be merged when Emby next starts cleanly"
|
||||
PASS_DBS+=("$db_name (WAL — see warning)")
|
||||
else
|
||||
log "$db_name exists but is empty — no pending transactions ✅"
|
||||
echo "$db_name exists but is empty — no pending transactions ✅"
|
||||
PASS_DBS+=("$db_name")
|
||||
fi
|
||||
continue
|
||||
@@ -282,7 +282,7 @@ for db_rel in "${DB_FILES[@]}"; do
|
||||
error "$db_name — sqlite3 could not open database (locked or corrupt)"
|
||||
FAIL_DBS+=("$db_name")
|
||||
elif [[ "$RESULT" == "ok" ]]; then
|
||||
log "$db_name — integrity check passed ✅"
|
||||
echo "$db_name — integrity check passed ✅"
|
||||
PASS_DBS+=("$db_name")
|
||||
else
|
||||
error "$db_name — CORRUPTION DETECTED"
|
||||
@@ -311,7 +311,7 @@ if [[ "$EMBY_WAS_RUNNING" == true ]]; then
|
||||
POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \
|
||||
'{{.State.Running}}' "$EMBY_CONTAINER" 2>/dev/null)
|
||||
if [[ "$POST_STATUS" == "true" ]]; then
|
||||
log "$EMBY_CONTAINER restarted and running ✅"
|
||||
echo "$EMBY_CONTAINER restarted and running ✅"
|
||||
RESTART_OK=true
|
||||
else
|
||||
error "$EMBY_CONTAINER started but crashed — database may be corrupt"
|
||||
@@ -329,7 +329,7 @@ if [[ "$EMBY_WAS_RUNNING" == true ]]; then
|
||||
RESTART_OK=true
|
||||
fi
|
||||
else
|
||||
log "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"
|
||||
echo "$EMBY_CONTAINER was not running — leaving stopped (state respected) ✅"
|
||||
RESTART_OK=true
|
||||
fi
|
||||
|
||||
@@ -351,7 +351,7 @@ echo " $ICON_SUCCESS Passed: ${#PASS_DBS[@]}"
|
||||
[[ ${#MISSING_DBS[@]} -gt 0 ]] && echo " Skipped: ${#MISSING_DBS[@]} (not found)"
|
||||
echo ""
|
||||
|
||||
[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do log " $ICON_SUCCESS $db"; done
|
||||
[[ ${#PASS_DBS[@]} -gt 0 ]] && for db in "${PASS_DBS[@]}"; do echo " $ICON_SUCCESS $db"; done
|
||||
[[ ${#FAIL_DBS[@]} -gt 0 ]] && for db in "${FAIL_DBS[@]}"; do echo " $ICON_ERROR $db"; done
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -219,7 +219,7 @@ fi
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_SUMMARY Artists to add (${#MISSING[@]}) ━━━"
|
||||
for a in "${MISSING[@]}"; do log " $a"; done
|
||||
for a in "${MISSING[@]}"; do echo " $a"; done
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN complete — run without --dry-run to add these artists"
|
||||
|
||||
@@ -177,7 +177,7 @@ if ! mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Ramdisk is mounted ✅"
|
||||
echo "Ramdisk is mounted ✅"
|
||||
|
||||
# Warn if transcode_manager is running — it may flip symlink back on next cycle
|
||||
if pgrep -f "transcode_manager.sh" >/dev/null 2>&1; then
|
||||
@@ -199,10 +199,10 @@ if [[ ! -d "$TRANSCODE_SSD" ]]; then
|
||||
error "Cannot safely redirect symlink — aborting"
|
||||
exit 1
|
||||
}
|
||||
log "SSD fallback created ✅"
|
||||
echo "SSD fallback created ✅"
|
||||
fi
|
||||
else
|
||||
log "SSD fallback exists: $TRANSCODE_SSD ✅"
|
||||
echo "SSD fallback exists: $TRANSCODE_SSD ✅"
|
||||
fi
|
||||
|
||||
START=$(date +%s)
|
||||
@@ -253,7 +253,7 @@ if [[ "$FILE_COUNT" -gt 0 ]]; then
|
||||
done
|
||||
fi
|
||||
else
|
||||
log "No active files on ramdisk ✅"
|
||||
echo "No active files on ramdisk ✅"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
@@ -303,7 +303,7 @@ last_flip_time=$NOW
|
||||
flip_count_hour=0
|
||||
flip_hour_start=$NOW
|
||||
EOF
|
||||
log "State file updated: current_target=$TRANSCODE_SSD"
|
||||
echo "State file updated: current_target=$TRANSCODE_SSD"
|
||||
else
|
||||
warn "Skipping state file update — stop had errors"
|
||||
fi
|
||||
|
||||
@@ -203,7 +203,7 @@ for pool in "${POOLS_TO_SCRUB[@]}"; do
|
||||
warn "DRY RUN — would scrub: $pool"
|
||||
STARTED+=("$pool")
|
||||
elif zpool scrub "$pool" 2>/dev/null; then
|
||||
log "$pool scrub started ✅"
|
||||
echo "$pool scrub started ✅"
|
||||
STARTED+=("$pool")
|
||||
else
|
||||
error "Failed to start scrub on $pool"
|
||||
@@ -231,7 +231,7 @@ fi
|
||||
echo ""
|
||||
echo "━━━ $ICON_TIME Waiting for Scrubs to Complete ━━━"
|
||||
log "Polling every 60 seconds — scrubs may take hours on large pools"
|
||||
log "Safe to interrupt — scrubs continue in background if script is stopped"
|
||||
echo "Safe to interrupt — scrubs continue in background if script is stopped"
|
||||
|
||||
while [[ "$SCRUB_RUNNING" == true ]]; do
|
||||
sleep 60
|
||||
|
||||
@@ -226,7 +226,7 @@ else
|
||||
else
|
||||
log "Creating SSD fallback directory: $TRANSCODE_SSD"
|
||||
if mkdir -p "$TRANSCODE_SSD"; then
|
||||
log "SSD fallback created: $TRANSCODE_SSD ✅"
|
||||
echo "SSD fallback created: $TRANSCODE_SSD ✅"
|
||||
else
|
||||
error "Failed to create SSD fallback: $TRANSCODE_SSD"
|
||||
SETUP_SUCCESS=false
|
||||
@@ -270,7 +270,7 @@ else
|
||||
}
|
||||
fi
|
||||
|
||||
[[ "$SETUP_SUCCESS" == true ]] && log "Symlink: $TRANSCODE_LINK → $RAMDISK_PATH ✅"
|
||||
[[ "$SETUP_SUCCESS" == true ]] && echo "Symlink: $TRANSCODE_LINK → $RAMDISK_PATH ✅"
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
@@ -291,7 +291,7 @@ else
|
||||
log "transcoding-temp already exists on ramdisk"
|
||||
else
|
||||
if mkdir -p "$TRANSCODE_TEMP_DIR"; then
|
||||
log "Created transcoding-temp on ramdisk ✅"
|
||||
echo "Created transcoding-temp on ramdisk ✅"
|
||||
else
|
||||
error "Failed to create transcoding-temp on ramdisk"
|
||||
SETUP_SUCCESS=false
|
||||
|
||||
@@ -272,7 +272,7 @@ if [[ -n "$NPM_URL" ]]; then
|
||||
if curl -sf --max-time "${NETWORK_WATCHDOG_NPM_TIMEOUT:-10}" "$NPM_URL" >/dev/null 2>&1; then
|
||||
log "$ICON_SUCCESS NPM proxy reachable — $NPM_URL"
|
||||
if [[ "$NPM_STRIKES" -gt 0 ]]; then
|
||||
log "NPM strikes cleared (was $NPM_STRIKES)"
|
||||
echo "NPM strikes cleared (was $NPM_STRIKES)"
|
||||
set_strikes "npm" 0 "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -561,7 +561,7 @@ elif [[ "$TARGET_LEVEL" -lt "$CURRENT_LEVEL" ]]; then
|
||||
if [[ "$NEW_LEVEL" -gt 0 ]]; then
|
||||
warn "De-escalated to level $NEW_LEVEL (${LEVEL_NAMES[$NEW_LEVEL]}) — ${RW_RECOVER_CYCLES:-3} more cycles to fully clear"
|
||||
else
|
||||
log "All pressure cleared — system at normal operation ✅"
|
||||
echo "All pressure cleared — system at normal operation ✅"
|
||||
notify "Resource Manager: pressure resolved on $(hostname) ($MY_ID) — system back to normal" \
|
||||
"Resource Manager" "normal"
|
||||
fi
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"claudeAiOauth":{"accessToken":"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","refreshToken":"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","expiresAt":1781475960207,"scopes":["user:file_upload","user:inference","user:mcp_servers","user:profile","user:sessions:claude_code"],"subscriptionType":"pro","rateLimitTier":"default_claude_ai"}}
|
||||
@@ -0,0 +1 @@
|
||||
2026-06-14T15:34:48.524Z
|
||||
@@ -0,0 +1 @@
|
||||
{"timestamp":"2026-06-13T13:53:50.240Z","path":"native","outcome":"success","status":"success","version_from":"2.1.176","version_to":"2.1.177","error_code":null}
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"numStartups": 22,
|
||||
"installMethod": "native",
|
||||
"autoUpdates": false,
|
||||
"hasSeenTasksHint": true,
|
||||
"tipsHistory": {
|
||||
"fotw-campaign-upsell": 13,
|
||||
"new-user-warmup": 6,
|
||||
"plan-mode-for-complex-tasks": 22,
|
||||
"memory-command": 16,
|
||||
"theme-command": 21,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 11,
|
||||
"enter-to-steer-in-relatime": 21,
|
||||
"todo-list": 21,
|
||||
"ide-upsell-external-terminal": 19,
|
||||
"install-github-app": 22,
|
||||
"install-slack-app": 22,
|
||||
"drag-and-drop-images": 14,
|
||||
"double-esc-code-restore": 14,
|
||||
"continue": 14,
|
||||
"shift-tab": 15,
|
||||
"image-paste": 4,
|
||||
"web-app": 19,
|
||||
"color-when-multi-clauding": 6,
|
||||
"custom-agents": 21,
|
||||
"remote-control": 21,
|
||||
"voice-mode": 16,
|
||||
"goal-command-nudge": 16,
|
||||
"guest-passes": 22,
|
||||
"feedback-command": 22,
|
||||
"frontend-design-plugin": 6,
|
||||
"permissions": 22,
|
||||
"rename-conversation": 11,
|
||||
"custom-commands": 11,
|
||||
"c4e-remote-sessions": 18,
|
||||
"subagent-fanout-nudge": 18,
|
||||
"no-flicker": 19
|
||||
},
|
||||
"promptQueueUseCount": 43,
|
||||
"cachedGrowthBookFeatures": {
|
||||
"tengu_slate_kestrel": true,
|
||||
"tengu_bridge_repl_v2": true,
|
||||
"tengu_basalt_meadow": true,
|
||||
"tengu_sage_compass2": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_kairos_loop_dynamic": true,
|
||||
"tengu_sepia_cormorant": [],
|
||||
"tengu_amber_heron": false,
|
||||
"tengu_log_datadog_events": true,
|
||||
"tengu-fable-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_quiet_slate_wren": false,
|
||||
"tengu_birch_compass": true,
|
||||
"tengu_bramble_lintel": 7,
|
||||
"tengu_malort_pedway": {
|
||||
"enabled": true,
|
||||
"pixelValidation": false,
|
||||
"clipboardPasteMultiline": true,
|
||||
"screenshotFilter": true,
|
||||
"mouseAnimation": true,
|
||||
"hideBeforeAction": true,
|
||||
"autoTargetDisplay": false,
|
||||
"coordinateMode": "pixels"
|
||||
},
|
||||
"tengu_lilac_loom": {},
|
||||
"tengu_sub_nomdrep_q7k": true,
|
||||
"tengu_lantern_spool": false,
|
||||
"tengu_hawthorn_steeple": false,
|
||||
"tengu_version_config": {
|
||||
"minVersion": "1.0.24"
|
||||
},
|
||||
"tengu_auto_notice_once": true,
|
||||
"tengu_sparrow_ledger": false,
|
||||
"tengu_loggia_carousel": false,
|
||||
"tengu_ccr_bridge": true,
|
||||
"tengu_basalt_sundial": false,
|
||||
"tengu_mcp_stateless_skip_init": true,
|
||||
"tengu_lapis_anchor": "off",
|
||||
"tengu_sage_compass": {},
|
||||
"tengu_kairos_cron": true,
|
||||
"tengu_kairos_loop_prompt": true,
|
||||
"tengu_jade_anvil_4": false,
|
||||
"tengu_skills_dashboard_enabled": false,
|
||||
"tengu_sedge_lantern_holdback": false,
|
||||
"tengu_dunwich_bell": false,
|
||||
"tengu_desktop_upsell": {
|
||||
"enable_shortcut_tip": true,
|
||||
"enable_startup_dialog": false
|
||||
},
|
||||
"tengu_code_diff_cli": true,
|
||||
"tengu_anchor_tide": true,
|
||||
"tengu_garnet_finch": false,
|
||||
"tengu_cobalt_heron": true,
|
||||
"tengu_ccr_v2_send_events_cli": true,
|
||||
"tengu_onyx_plover": {
|
||||
"enabled": false,
|
||||
"minHours": 24,
|
||||
"minSessions": 3,
|
||||
"remoteEnabled": false
|
||||
},
|
||||
"tengu_react_vulnerability_warning": false,
|
||||
"tengu_prompt_cache_1h_config": {
|
||||
"allowlist": [
|
||||
"repl_main_thread*",
|
||||
"sdk",
|
||||
"auto_mode",
|
||||
"rolling_compact",
|
||||
"memdir_relevance",
|
||||
"agent_classifier",
|
||||
"prompt_suggestion",
|
||||
"away_summary",
|
||||
"extract_memories",
|
||||
"compact"
|
||||
]
|
||||
},
|
||||
"tengu_timber_lark": "copy_a",
|
||||
"tengu_ladder_mq7": false,
|
||||
"tengu_birthday_hat": false,
|
||||
"tengu_prompt_cache_diagnostics": true,
|
||||
"tengu_worktree_mode": true,
|
||||
"tengu_willow_refresh_ttl_hours": 0,
|
||||
"tengu_pewter_kestrel": {
|
||||
"global": 50000,
|
||||
"Bash": 30000,
|
||||
"PowerShell": 30000,
|
||||
"Grep": 20000,
|
||||
"Snip": 1000,
|
||||
"StrReplaceBasedEditTool": 30000,
|
||||
"BashSearchTool": 20000
|
||||
},
|
||||
"tengu_slate_finch": true,
|
||||
"tengu_workflows_enabled": true,
|
||||
"tengu_permission_friction": true,
|
||||
"tengu_marble_lark": false,
|
||||
"tengu_copper_fox": false,
|
||||
"tengu_bridge_repl_v2_config": {
|
||||
"init_retry_max_attempts": 3,
|
||||
"init_retry_base_delay_ms": 500,
|
||||
"init_retry_jitter_fraction": 0.25,
|
||||
"init_retry_max_delay_ms": 4000,
|
||||
"http_timeout_ms": 10000,
|
||||
"uuid_dedup_buffer_size": 2000,
|
||||
"heartbeat_interval_ms": 20000,
|
||||
"heartbeat_jitter_fraction": 0.1,
|
||||
"token_refresh_buffer_ms": 600000,
|
||||
"teardown_archive_timeout_ms": 1500,
|
||||
"connect_timeout_ms": 15000,
|
||||
"min_version": "2.1.70",
|
||||
"should_show_app_upgrade_message": false
|
||||
},
|
||||
"tengu_marble_whisper": true,
|
||||
"tengu_maple_sundial": false,
|
||||
"tengu_velvet_cascade": {},
|
||||
"tengu_passport_quail": false,
|
||||
"tengu_ember_latch": true,
|
||||
"tengu_vscode_onboarding": false,
|
||||
"tengu_fennel_kite_model": "",
|
||||
"tengu_nimble_amber_prose": false,
|
||||
"tengu_bridge_poll_interval_ms": 0,
|
||||
"tengu_cobalt_wren": false,
|
||||
"tengu_harbor_permissions": true,
|
||||
"tengu_orchid_trellis": false,
|
||||
"tengu_ccr_bridge_multi_session": true,
|
||||
"tengu_bad_survey_transcript_ask_config": {
|
||||
"probability": 1
|
||||
},
|
||||
"tengu_good_survey_transcript_ask_config": {
|
||||
"probability": 0.5
|
||||
},
|
||||
"tengu_amber_sentinel": true,
|
||||
"tengu_crimson_vector": false,
|
||||
"tengu_drift_lantern": false,
|
||||
"tengu_kestrel_arch": "OFF",
|
||||
"tengu_read_dedup_killswitch": false,
|
||||
"tengu_saffron_lattice": {
|
||||
"enabled": false,
|
||||
"planLimitsEndDate": "2026-06-22T10:00:00Z",
|
||||
"hideRateLimitsDescription": true
|
||||
},
|
||||
"tengu_cloth_snorkel": false,
|
||||
"tengu_system_prompt_global_cache": true,
|
||||
"tengu_slate_moth": true,
|
||||
"tengu_bridge_poll_interval_config": {
|
||||
"poll_interval_ms_not_at_capacity": 2000,
|
||||
"poll_interval_ms_at_capacity": 600000,
|
||||
"heartbeat_interval_ms": 0,
|
||||
"multisession_poll_interval_ms_not_at_capacity": 5000,
|
||||
"multisession_poll_interval_ms_at_capacity": 60000,
|
||||
"multisession_poll_interval_ms_partial_capacity": 5000,
|
||||
"non_exclusive_heartbeat_interval_ms": 180000,
|
||||
"session_keepalive_interval_ms": 0,
|
||||
"session_keepalive_interval_v2_ms": 0
|
||||
},
|
||||
"tengu_gouda_loop": true,
|
||||
"tengu_otk_slot_v1": false,
|
||||
"tengu_pewter_lark": "off",
|
||||
"tengu_walnut_prism": false,
|
||||
"tengu_immediate_model_command": false,
|
||||
"tengu_pewter_summit": true,
|
||||
"tengu_fg_left_arrow_agents": true,
|
||||
"tengu_willow_sentinel_ttl_hours": 1,
|
||||
"tengu_pewter_lantern": false,
|
||||
"tengu_desktop_upsell_v2": {
|
||||
"enabled": false
|
||||
},
|
||||
"tengu_vellum_siding": false,
|
||||
"tengu_vscode_feedback_survey": true,
|
||||
"tengu_mcp_singleton_unwrap": true,
|
||||
"tengu_coral_fern": false,
|
||||
"tengu_trace_lantern": false,
|
||||
"tengu_review_bughunter_config": {
|
||||
"fleet_size": 5,
|
||||
"max_duration_minutes": 10,
|
||||
"agent_timeout_seconds": 600,
|
||||
"total_wallclock_minutes": 22,
|
||||
"model": "claude-opus-4-7",
|
||||
"cost_note": "$5-$25",
|
||||
"duration_note": "~5-10 min",
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_basalt_spur": false,
|
||||
"tengu_crystal_beam": {
|
||||
"budgetTokens": 0
|
||||
},
|
||||
"tengu_hawthorn_window": 200000,
|
||||
"tengu_flint_harbor_share": false,
|
||||
"tengu_bridge_attestation_enforce": false,
|
||||
"tengu_compass_dial": true,
|
||||
"tengu_moss_anchor": false,
|
||||
"tengu_willow_census_ttl_hours": 24,
|
||||
"tengu_compact_cache_prefix": true,
|
||||
"tengu_cedar_hollow_7m": {},
|
||||
"tengu_prompt_suggestion": true,
|
||||
"tengu_crimson_echo": {},
|
||||
"tengu_cork_m4q": true,
|
||||
"tengu_classifier_summary_llm_emit": true,
|
||||
"tengu_tide_elm": "off",
|
||||
"tengu_ccr_bundle_seed_enabled": true,
|
||||
"tengu_copper_wren": false,
|
||||
"tengu_ember_trail": "0",
|
||||
"tengu_gha_plugin_code_review": false,
|
||||
"tengu_keybinding_customization_release": true,
|
||||
"tengu_kairos_cron_durable": false,
|
||||
"tengu_canary": {},
|
||||
"tengu_mocha_barista": true,
|
||||
"tengu_negative_interaction_transcript_ask_config": {
|
||||
"probability": 0
|
||||
},
|
||||
"tengu_steady_lantern": false,
|
||||
"tengu_malformed_tool_use_clean_retry": false,
|
||||
"tengu_agent_list_attach": false,
|
||||
"tengu_ultraplan_timeout_seconds": 5400,
|
||||
"tengu_hazel_osprey_floor": 75000,
|
||||
"tengu_brick_follow": false,
|
||||
"tengu_slate_ribbon": true,
|
||||
"tengu_slate_siskin": {
|
||||
"enabled": false,
|
||||
"timeoutMs": 8000,
|
||||
"throttleMs": 30000,
|
||||
"summaryLineThreshold": 5
|
||||
},
|
||||
"tengu_amber_rokovoko": 0.2,
|
||||
"tengu_penguin_mode_promo": {
|
||||
"discountPercent": 0,
|
||||
"endDate": "Feb 16"
|
||||
},
|
||||
"tengu_slate_harrier": "off",
|
||||
"tengu_lapis_thicket": false,
|
||||
"tengu_harbor_willow": false,
|
||||
"tengu_amber_anchor": false,
|
||||
"tengu_tussock_oriole": false,
|
||||
"tengu_tern_alloy": "copy_a",
|
||||
"tengu_fgts": true,
|
||||
"tengu_vellum_lantern": false,
|
||||
"tengu_saffron_anchor": true,
|
||||
"tengu_miraculo_the_bard": false,
|
||||
"tengu_red_coaster": false,
|
||||
"tengu_cobalt_compass": true,
|
||||
"tengu_plum_vx3": true,
|
||||
"tengu_mcp_subagent_prompt": true,
|
||||
"tengu_mcp_local_oauth_blocked_hosts": {
|
||||
"hosts": [
|
||||
"microsoft365.mcp.claude.com",
|
||||
"gmail.mcp.claude.com",
|
||||
"gcal.mcp.claude.com"
|
||||
]
|
||||
},
|
||||
"tengu_byte_stream_idle_timeout_ms": 180000,
|
||||
"tengu_umber_petrel": false,
|
||||
"tengu_prism_ledger": false,
|
||||
"tengu_ccr_bundle_max_bytes": 104857600,
|
||||
"tengu_amber_sextant": true,
|
||||
"tengu_pewter_ledger": "OFF",
|
||||
"tengu_amber_flint": true,
|
||||
"tengu_disable_bypass_permissions_mode": false,
|
||||
"tengu_walrus_canteen": false,
|
||||
"tengu_ashen_kelp": true,
|
||||
"tengu_plugin_official_mkt_git_fallback": true,
|
||||
"tengu_max_version_config": {},
|
||||
"tengu_cobalt_lantern": true,
|
||||
"tengu_ultraplan_prompt_identifier": "visual_plan",
|
||||
"tengu_swann_brevity": "focused",
|
||||
"tengu_hazel_osprey": false,
|
||||
"tengu_slate_meadow": true,
|
||||
"tengu_amber_redwood2": "",
|
||||
"tengu_frond_boric": {},
|
||||
"tengu_slate_thimble": false,
|
||||
"tengu_slate_nexus": true,
|
||||
"tengu_chert_bezel": true,
|
||||
"tengu_streaming_tool_execution2": true,
|
||||
"tengu_event_watchdog_default_on": false,
|
||||
"tengu_auto_mode_config": {
|
||||
"enabled": "enabled",
|
||||
"twoStageClassifier": true
|
||||
},
|
||||
"tengu_grey_step2": {
|
||||
"enabled": true,
|
||||
"dialogTitle": "We recommend medium effort for Opus",
|
||||
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
|
||||
},
|
||||
"tengu_dune_wren": false,
|
||||
"tengu_cedar_lantern": true,
|
||||
"tengu_velvet_moth": 0.2,
|
||||
"tengu_harbor_ledger": [
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "discord"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "telegram"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "fakechat"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "imessage"
|
||||
}
|
||||
],
|
||||
"tengu_harbor": true,
|
||||
"tengu_amber_lynx": false,
|
||||
"tengu_doorbell_agave": false,
|
||||
"tengu_maple_tide": false,
|
||||
"tengu_fennel_kite": false,
|
||||
"tengu_collage_kaleidoscope": true,
|
||||
"tengu_file_write_optimization": true,
|
||||
"tengu_startup_notice": "",
|
||||
"tengu_mcp_retry_failed_remote": false,
|
||||
"tengu_session_memory": false,
|
||||
"tengu_flint_harbor_prompt": {
|
||||
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
|
||||
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
|
||||
"windowDays": 30
|
||||
},
|
||||
"tengu_slim_subagent_claudemd": true,
|
||||
"tengu_tangerine_ladder_boost": true,
|
||||
"tengu_chair_sermon": false,
|
||||
"tengu_gypsum_kite": true,
|
||||
"tengu_quartz_heron": false,
|
||||
"tengu_xterm_atlas_reset": true,
|
||||
"tengu-model-error-overrides": {
|
||||
"claude-fable-5": {
|
||||
"block": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access"
|
||||
}
|
||||
},
|
||||
"tengu_orchid_mantis_v2": true,
|
||||
"tengu-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_feedback_survey_config": {
|
||||
"minTimeBeforeFeedbackMs": 600000,
|
||||
"minTimeBetweenFeedbackMs": 43200000,
|
||||
"minTimeBetweenGlobalFeedbackMs": 43200000,
|
||||
"minUserTurnsBeforeFeedback": 5,
|
||||
"minUserTurnsBetweenFeedback": 25,
|
||||
"hideThanksAfterMs": 3000,
|
||||
"onForModels": [
|
||||
"*"
|
||||
],
|
||||
"probability": 0.05
|
||||
},
|
||||
"tengu_cork_lantern": false,
|
||||
"tengu_mint_lanes": false,
|
||||
"tengu_bridge_attestation_enforce_config": {
|
||||
"accept_level": "VERIFIED_BY_GATE",
|
||||
"accept_statuses": []
|
||||
},
|
||||
"tengu_marble_sandcastle": false,
|
||||
"tengu_bg_attach_stall_ms": 5000,
|
||||
"tengu_workout2": true,
|
||||
"tengu_orford_ness": false,
|
||||
"tengu_porch_bell_9f": "",
|
||||
"tengu_auto_mode_default_on": false,
|
||||
"tengu_birch_kettle": false,
|
||||
"tengu_classifier_summary_heuristic_emit": true,
|
||||
"tengu_cobalt_thicket": false,
|
||||
"tengu_destructive_command_warning": false,
|
||||
"tengu_cinder_plover": "",
|
||||
"tengu_cedar_halo": false,
|
||||
"tengu_sotto_voce": true,
|
||||
"tengu_sepia_moth": false,
|
||||
"tengu_cedar_sundial": false,
|
||||
"tengu_penguins_enabled": true,
|
||||
"tengu_quiet_basalt_echo": false,
|
||||
"tengu_ochre_hollow": true,
|
||||
"tengu_coral_beacon": true,
|
||||
"tengu_copper_thistle": false,
|
||||
"tengu_1p_event_batch_config": {
|
||||
"scheduledDelayMillis": 10000,
|
||||
"maxExportBatchSize": 400,
|
||||
"maxQueueSize": 8192,
|
||||
"path": "/api/event_logging/v2/batch"
|
||||
},
|
||||
"tengu_amber_wren": {
|
||||
"targetedRangeNudge": true,
|
||||
"maxTokens": 25000
|
||||
},
|
||||
"tengu_amber_prism": true,
|
||||
"tengu_cobalt_plinth": false,
|
||||
"tengu_silent_harbor": false,
|
||||
"tengu_chomp_inflection": true,
|
||||
"tengu_mcp_elicitation": true,
|
||||
"tengu_sm_config": {
|
||||
"minimumMessageTokensToInit": 150000,
|
||||
"minimumTokensBetweenUpdate": 40000,
|
||||
"toolCallsBetweenUpdates": 10
|
||||
},
|
||||
"tengu_bridge_min_version": {
|
||||
"minVersion": "2.1.70"
|
||||
},
|
||||
"tengu_kairos_input_needed_push": true,
|
||||
"tengu_quiet_harbor": false,
|
||||
"tengu_slate_wren": false,
|
||||
"tengu_tool_search_unsupported_models": [
|
||||
"claude-3-5-haiku",
|
||||
"claude-3-haiku"
|
||||
],
|
||||
"tengu_native_cursor": true,
|
||||
"tengu_orchid_mantis": false,
|
||||
"tengu_amber_lark": true,
|
||||
"tengu_shale_finch": true,
|
||||
"tengu_cedar_plume": false,
|
||||
"tengu_kairos_push_notifications": true,
|
||||
"tengu_marble_whisper2": true,
|
||||
"tengu_lichen_compass": false,
|
||||
"tengu_c4w_usage_limit_notifications_enabled": true,
|
||||
"tengu_scarf_coffee": false,
|
||||
"tengu_copper_bridge": true,
|
||||
"tengu_tool_pear": false,
|
||||
"tengu_claudeai_mcp_connectors": true,
|
||||
"tengu_ccr_post_turn_summary": false,
|
||||
"tengu_sedge_lantern": true,
|
||||
"tengu_feature_template": false,
|
||||
"tengu_harbor_prism": true,
|
||||
"tengu_cedar_inlet": "step",
|
||||
"tengu_flax_grouse": false,
|
||||
"tengu_event_sampling_config": {},
|
||||
"tengu_herring_clock": false,
|
||||
"tengu_quartz_vireo": "",
|
||||
"tengu_team_discovery": false,
|
||||
"tengu_gleaming_fair": true,
|
||||
"tengu_marble_anvil": true,
|
||||
"tengu_classifier_disabled_surfaces": "",
|
||||
"tengu_pewter_brook": false,
|
||||
"tengu_vscode_review_upsell": false,
|
||||
"claude_code_skills_dashboard_enabled_cli": false,
|
||||
"tengu_post_compact_survey": false,
|
||||
"tengu_reactive_compact_remote": false,
|
||||
"tengu_idle_amber_finch": false,
|
||||
"tengu_noreread_q7m_velvet": false,
|
||||
"tengu_ultraplan_config": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_scratch": false,
|
||||
"tengu_alder_compass": false,
|
||||
"tengu_olive_hinge": "",
|
||||
"tengu_shining_fractals": false,
|
||||
"tengu_maple_pier": false,
|
||||
"tengu_sessions_elevated_auth_enforcement": true,
|
||||
"tengu_turtle_carbon": true,
|
||||
"tengu_billiard_aviary": false,
|
||||
"tengu_cinder_almanac": true,
|
||||
"tengu_osprey_lantern": false,
|
||||
"tengu-top-of-feed-tip": {
|
||||
"tip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"color": "warning"
|
||||
},
|
||||
"tengu_cobalt_raccoon": true,
|
||||
"tengu_loud_sugary_rock": false,
|
||||
"tengu_willow_mode": "hint_v2",
|
||||
"tengu_blue_coaster": false,
|
||||
"tengu_snippet_save": false,
|
||||
"tengu_amber_lattice": {
|
||||
"plugins": [
|
||||
"security-guidance",
|
||||
"code-review",
|
||||
"commit-commands",
|
||||
"code-simplifier",
|
||||
"hookify",
|
||||
"feature-dev",
|
||||
"frontend-design",
|
||||
"pr-review-toolkit",
|
||||
"skill-creator",
|
||||
"plugin-dev",
|
||||
"agent-sdk-dev",
|
||||
"mcp-server-dev",
|
||||
"claude-code-setup",
|
||||
"claude-md-management",
|
||||
"playground",
|
||||
"ralph-loop",
|
||||
"explanatory-output-style",
|
||||
"learning-output-style",
|
||||
"clangd-lsp",
|
||||
"csharp-lsp",
|
||||
"gopls-lsp",
|
||||
"jdtls-lsp",
|
||||
"kotlin-lsp",
|
||||
"lua-lsp",
|
||||
"php-lsp",
|
||||
"pyright-lsp",
|
||||
"ruby-lsp",
|
||||
"rust-analyzer-lsp",
|
||||
"swift-lsp",
|
||||
"typescript-lsp"
|
||||
]
|
||||
},
|
||||
"tengu_slate_harbor_experiment": false,
|
||||
"tengu_velvet_ibis": {},
|
||||
"tengu_bridge_requires_action_details": true,
|
||||
"tengu_lapis_finch": true,
|
||||
"tengu_satin_quoll": {},
|
||||
"tengu_moth_copse": false,
|
||||
"tengu_silk_hinge": false,
|
||||
"tengu_surreal_dali": true,
|
||||
"tengu_cobalt_ridge": true,
|
||||
"tengu_flint_harbor": false,
|
||||
"tengu_plank_river_frost": "user_intent",
|
||||
"tengu_velvet_mallet_haiku": false,
|
||||
"tengu_velvet_mallet": false,
|
||||
"tengu_velvet_mallet_haiku_4_5": false,
|
||||
"tengu_velvet_hammer_falcon": false,
|
||||
"tengu_loud_sugary_rock2": false,
|
||||
"tengu_velvet_hammer_sonnet_4_5": false,
|
||||
"tengu_velvet_hammer_sonnet": false,
|
||||
"tengu_tab_read_sep": false,
|
||||
"tengu_quill_harbor": "acceptEdits",
|
||||
"tengu_velvet_hammer": false,
|
||||
"tengu_velvet_hammer_opus": false,
|
||||
"tengu_c4e_slash_upsell": true,
|
||||
"tengu_velvet_hammer_haiku_4_5": false,
|
||||
"tengu_feature_claudified_template": false,
|
||||
"tengu_slate_quill": true,
|
||||
"tengu_ax_screen_reader": false,
|
||||
"tengu_windows_credman": false,
|
||||
"tengu_basalt_tern": false,
|
||||
"tengu_velvet_mallet_opus": false,
|
||||
"tengu_velvet_hammer_haiku": false,
|
||||
"tengu_velvet_static": true,
|
||||
"tengu_velvet_mallet_sonnet": false,
|
||||
"tengu_soft_slate_nudge": "baseline",
|
||||
"tengu_lantern_hearth": "off",
|
||||
"tengu_velvet_mallet_falcon": false,
|
||||
"tengu_velvet_mallet_sonnet_4_5": false
|
||||
},
|
||||
"firstStartTime": "2026-06-05T19:39:28.542Z",
|
||||
"opusProMigrationComplete": true,
|
||||
"sonnet1m45MigrationComplete": true,
|
||||
"seenNotifications": {},
|
||||
"migrationVersion": 13,
|
||||
"userID": "9d89994d486a4884b8cf33372d8a4cd61ebf7d34009e9d3cbce9db24e2e971a4",
|
||||
"changelogLastFetched": 1781361371930,
|
||||
"autoUpdatesProtectedForNative": true,
|
||||
"claudeCodeFirstTokenDate": "2026-04-11T19:03:48.223040Z",
|
||||
"hasCompletedOnboarding": true,
|
||||
"lastOnboardingVersion": "2.1.165",
|
||||
"groveConfigCache": {
|
||||
"09792e21-2287-4348-b4d4-34cddbbfabc5": {
|
||||
"grove_enabled": true,
|
||||
"timestamp": 1781406640065
|
||||
}
|
||||
},
|
||||
"cachedExperimentFeatures": [
|
||||
"tengu_amber_prism",
|
||||
"tengu_basalt_spur",
|
||||
"tengu_cedar_inlet",
|
||||
"tengu_coral_beacon",
|
||||
"tengu_flint_harbor",
|
||||
"tengu_mcp_subagent_prompt",
|
||||
"tengu_ochre_hollow",
|
||||
"tengu_orchid_mantis_v2",
|
||||
"tengu_plank_river_frost",
|
||||
"tengu_read_dedup_killswitch"
|
||||
],
|
||||
"cachedGrowthBookFeaturesAt": 1781406639973,
|
||||
"lastReleaseNotesSeen": "2.1.177",
|
||||
"projects": {
|
||||
"/root": {
|
||||
"allowedTools": [],
|
||||
"mcpContextUris": [],
|
||||
"mcpServers": {},
|
||||
"enabledMcpjsonServers": [],
|
||||
"disabledMcpjsonServers": [],
|
||||
"hasTrustDialogAccepted": false,
|
||||
"projectOnboardingSeenCount": 3,
|
||||
"hasClaudeMdExternalIncludesApproved": false,
|
||||
"hasClaudeMdExternalIncludesWarningShown": false,
|
||||
"exampleFiles": [],
|
||||
"lastGracefulShutdown": false,
|
||||
"lastVersionBase": "2.1.177",
|
||||
"lastCost": 1.0676417999999999,
|
||||
"lastAPIDuration": 276732,
|
||||
"lastAPIDurationWithoutRetries": 276675,
|
||||
"lastToolDuration": 9607,
|
||||
"lastDuration": 2130140,
|
||||
"lastLinesAdded": 29,
|
||||
"lastLinesRemoved": 15,
|
||||
"lastTotalInputTokens": 4397,
|
||||
"lastTotalOutputTokens": 16093,
|
||||
"lastTotalCacheCreationInputTokens": 53595,
|
||||
"lastTotalCacheReadInputTokens": 1642666,
|
||||
"lastTotalWebSearchRequests": 0,
|
||||
"lastFpsAverage": 1.82,
|
||||
"lastFpsLow1Pct": 313.42,
|
||||
"lastModelUsage": {
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"inputTokens": 572,
|
||||
"outputTokens": 17,
|
||||
"cacheReadInputTokens": 0,
|
||||
"cacheCreationInputTokens": 0,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 0.000657
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
"inputTokens": 3825,
|
||||
"outputTokens": 16076,
|
||||
"cacheReadInputTokens": 1642666,
|
||||
"cacheCreationInputTokens": 53595,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 1.0669847999999997
|
||||
}
|
||||
},
|
||||
"lastSessionId": "96cf6b2d-d6a0-405b-81e5-95c657e1922a",
|
||||
"lastSessionMetrics": {
|
||||
"frame_duration_ms_count": 16776,
|
||||
"frame_duration_ms_min": 0.11423300000024028,
|
||||
"frame_duration_ms_max": 21.985366000095382,
|
||||
"frame_duration_ms_avg": 0.7730811968292047,
|
||||
"frame_duration_ms_p50": 0.5600509999203496,
|
||||
"frame_duration_ms_p95": 1.786581499991007,
|
||||
"frame_duration_ms_p99": 4.282493569953367,
|
||||
"pre_tool_hook_duration_ms_count": 108,
|
||||
"pre_tool_hook_duration_ms_min": 0,
|
||||
"pre_tool_hook_duration_ms_max": 15,
|
||||
"pre_tool_hook_duration_ms_avg": 0.24074074074074073,
|
||||
"pre_tool_hook_duration_ms_p50": 0,
|
||||
"pre_tool_hook_duration_ms_p95": 1,
|
||||
"pre_tool_hook_duration_ms_p99": 4.789999999999978,
|
||||
"hook_duration_ms_count": 40,
|
||||
"hook_duration_ms_min": 0,
|
||||
"hook_duration_ms_max": 8,
|
||||
"hook_duration_ms_avg": 0.35,
|
||||
"hook_duration_ms_p50": 0,
|
||||
"hook_duration_ms_p95": 1,
|
||||
"hook_duration_ms_p99": 5.269999999999996
|
||||
},
|
||||
"hasCompletedProjectOnboarding": true
|
||||
}
|
||||
},
|
||||
"routineFiredWatermark": "2026-06-05T19:47:09.178Z",
|
||||
"penguinModeOrgEnabled": true,
|
||||
"closedIssuesLastChecked": 1781406639965,
|
||||
"passesEligibilityCache": {
|
||||
"4bb43199-0efc-4d5c-b552-79865cb0361b": {
|
||||
"eligible": true,
|
||||
"referral_code_details": {
|
||||
"code": "BeGGjphr1g",
|
||||
"campaign": "claude_code_guest_pass_a47c",
|
||||
"referral_link": "https://claude.ai/referral/BeGGjphr1g"
|
||||
},
|
||||
"referrer_reward": {
|
||||
"amount_minor_units": 1000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"remaining_passes": 3,
|
||||
"limit": 3,
|
||||
"share_link": "https://claude.ai/referral/BeGGjphr1g",
|
||||
"terms_url": "https://support.claude.com/en/articles/12875061-claude-code-guest-passes",
|
||||
"timestamp": 1781406640514
|
||||
}
|
||||
},
|
||||
"cachedExtraUsageDisabledReason": "out_of_credits",
|
||||
"passesUpsellSeenCount": 3,
|
||||
"hasVisitedPasses": false,
|
||||
"passesLastSeenRemaining": 3,
|
||||
"officialMarketplaceAutoInstallAttempted": true,
|
||||
"officialMarketplaceAutoInstalled": true,
|
||||
"tipLifetimeShownCounts": {
|
||||
"fotw-campaign-upsell": 6,
|
||||
"new-user-warmup": 2,
|
||||
"plan-mode-for-complex-tasks": 5,
|
||||
"memory-command": 2,
|
||||
"theme-command": 2,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 3,
|
||||
"enter-to-steer-in-relatime": 2,
|
||||
"todo-list": 2,
|
||||
"ide-upsell-external-terminal": 5,
|
||||
"install-github-app": 3,
|
||||
"install-slack-app": 3,
|
||||
"drag-and-drop-images": 2,
|
||||
"double-esc-code-restore": 2,
|
||||
"continue": 2,
|
||||
"shift-tab": 2,
|
||||
"image-paste": 1,
|
||||
"web-app": 2,
|
||||
"color-when-multi-clauding": 1,
|
||||
"custom-agents": 2,
|
||||
"remote-control": 2,
|
||||
"voice-mode": 2,
|
||||
"goal-command-nudge": 4,
|
||||
"guest-passes": 6,
|
||||
"feedback-command": 2,
|
||||
"frontend-design-plugin": 1,
|
||||
"permissions": 2,
|
||||
"rename-conversation": 1,
|
||||
"custom-commands": 1,
|
||||
"c4e-remote-sessions": 1,
|
||||
"subagent-fanout-nudge": 1,
|
||||
"no-flicker": 1
|
||||
},
|
||||
"feedbackSurveyState": {
|
||||
"lastShownTime": 1781411703066
|
||||
},
|
||||
"hasUsedBackslashReturn": true,
|
||||
"agentLastUsed": {
|
||||
"bg": 1780696781055
|
||||
},
|
||||
"remoteControlUpsellSeenCount": 3,
|
||||
"fullscreenUpsellSeenCount": 3,
|
||||
"lastShownEmergencyTip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"oauthAccount": {
|
||||
"accountUuid": "09792e21-2287-4348-b4d4-34cddbbfabc5",
|
||||
"emailAddress": "gmer4lfe@gmail.com",
|
||||
"organizationUuid": "4bb43199-0efc-4d5c-b552-79865cb0361b",
|
||||
"hasExtraUsageEnabled": true,
|
||||
"billingType": "stripe_subscription",
|
||||
"accountCreatedAt": "2026-04-03T21:52:35.642439Z",
|
||||
"subscriptionCreatedAt": "2026-04-11T13:14:49.905923Z",
|
||||
"ccOnboardingFlags": {},
|
||||
"claudeCodeTrialEndsAt": null,
|
||||
"claudeCodeTrialDurationDays": null,
|
||||
"seatTier": null,
|
||||
"displayName": "Gmer4Lfe",
|
||||
"organizationRole": "admin",
|
||||
"workspaceRole": null,
|
||||
"organizationName": "gmer4lfe@gmail.com's Organization",
|
||||
"organizationType": "claude_pro",
|
||||
"organizationRateLimitTier": "default_claude_ai",
|
||||
"userRateLimitTier": null
|
||||
},
|
||||
"clientDataCache": {
|
||||
"cedar_lagoon": {
|
||||
"claude-fable": true,
|
||||
"claude-mythos": true
|
||||
},
|
||||
"pewter_owl_tool": true,
|
||||
"pewter_owl_model": "claude-fable"
|
||||
},
|
||||
"additionalModelOptionsCache": [
|
||||
{
|
||||
"value": "claude-fable-5[1m]",
|
||||
"label": "Fable (disabled)",
|
||||
"description": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"additionalModelCostsCache": {}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"numStartups": 22,
|
||||
"installMethod": "native",
|
||||
"autoUpdates": false,
|
||||
"hasSeenTasksHint": true,
|
||||
"tipsHistory": {
|
||||
"fotw-campaign-upsell": 13,
|
||||
"new-user-warmup": 6,
|
||||
"plan-mode-for-complex-tasks": 22,
|
||||
"memory-command": 16,
|
||||
"theme-command": 21,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 11,
|
||||
"enter-to-steer-in-relatime": 21,
|
||||
"todo-list": 21,
|
||||
"ide-upsell-external-terminal": 19,
|
||||
"install-github-app": 22,
|
||||
"install-slack-app": 22,
|
||||
"drag-and-drop-images": 14,
|
||||
"double-esc-code-restore": 14,
|
||||
"continue": 14,
|
||||
"shift-tab": 15,
|
||||
"image-paste": 4,
|
||||
"web-app": 19,
|
||||
"color-when-multi-clauding": 6,
|
||||
"custom-agents": 21,
|
||||
"remote-control": 21,
|
||||
"voice-mode": 16,
|
||||
"goal-command-nudge": 16,
|
||||
"guest-passes": 22,
|
||||
"feedback-command": 22,
|
||||
"frontend-design-plugin": 6,
|
||||
"permissions": 22,
|
||||
"rename-conversation": 11,
|
||||
"custom-commands": 11,
|
||||
"c4e-remote-sessions": 18,
|
||||
"subagent-fanout-nudge": 18,
|
||||
"no-flicker": 19
|
||||
},
|
||||
"promptQueueUseCount": 44,
|
||||
"cachedGrowthBookFeatures": {
|
||||
"tengu_slate_kestrel": true,
|
||||
"tengu_bridge_repl_v2": true,
|
||||
"tengu_basalt_meadow": true,
|
||||
"tengu_sage_compass2": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_kairos_loop_dynamic": true,
|
||||
"tengu_sepia_cormorant": [],
|
||||
"tengu_amber_heron": false,
|
||||
"tengu_log_datadog_events": true,
|
||||
"tengu-fable-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_quiet_slate_wren": false,
|
||||
"tengu_birch_compass": true,
|
||||
"tengu_bramble_lintel": 7,
|
||||
"tengu_malort_pedway": {
|
||||
"enabled": true,
|
||||
"pixelValidation": false,
|
||||
"clipboardPasteMultiline": true,
|
||||
"screenshotFilter": true,
|
||||
"mouseAnimation": true,
|
||||
"hideBeforeAction": true,
|
||||
"autoTargetDisplay": false,
|
||||
"coordinateMode": "pixels"
|
||||
},
|
||||
"tengu_lilac_loom": {},
|
||||
"tengu_sub_nomdrep_q7k": true,
|
||||
"tengu_lantern_spool": false,
|
||||
"tengu_hawthorn_steeple": false,
|
||||
"tengu_version_config": {
|
||||
"minVersion": "1.0.24"
|
||||
},
|
||||
"tengu_auto_notice_once": true,
|
||||
"tengu_sparrow_ledger": false,
|
||||
"tengu_loggia_carousel": false,
|
||||
"tengu_ccr_bridge": true,
|
||||
"tengu_basalt_sundial": false,
|
||||
"tengu_mcp_stateless_skip_init": true,
|
||||
"tengu_lapis_anchor": "off",
|
||||
"tengu_sage_compass": {},
|
||||
"tengu_kairos_cron": true,
|
||||
"tengu_kairos_loop_prompt": true,
|
||||
"tengu_jade_anvil_4": false,
|
||||
"tengu_skills_dashboard_enabled": false,
|
||||
"tengu_sedge_lantern_holdback": false,
|
||||
"tengu_dunwich_bell": false,
|
||||
"tengu_desktop_upsell": {
|
||||
"enable_shortcut_tip": true,
|
||||
"enable_startup_dialog": false
|
||||
},
|
||||
"tengu_code_diff_cli": true,
|
||||
"tengu_anchor_tide": true,
|
||||
"tengu_garnet_finch": false,
|
||||
"tengu_cobalt_heron": true,
|
||||
"tengu_ccr_v2_send_events_cli": true,
|
||||
"tengu_onyx_plover": {
|
||||
"enabled": false,
|
||||
"minHours": 24,
|
||||
"minSessions": 3,
|
||||
"remoteEnabled": false
|
||||
},
|
||||
"tengu_react_vulnerability_warning": false,
|
||||
"tengu_prompt_cache_1h_config": {
|
||||
"allowlist": [
|
||||
"repl_main_thread*",
|
||||
"sdk",
|
||||
"auto_mode",
|
||||
"rolling_compact",
|
||||
"memdir_relevance",
|
||||
"agent_classifier",
|
||||
"prompt_suggestion",
|
||||
"away_summary",
|
||||
"extract_memories",
|
||||
"compact"
|
||||
]
|
||||
},
|
||||
"tengu_timber_lark": "copy_a",
|
||||
"tengu_ladder_mq7": false,
|
||||
"tengu_birthday_hat": false,
|
||||
"tengu_prompt_cache_diagnostics": true,
|
||||
"tengu_worktree_mode": true,
|
||||
"tengu_willow_refresh_ttl_hours": 0,
|
||||
"tengu_pewter_kestrel": {
|
||||
"global": 50000,
|
||||
"Bash": 30000,
|
||||
"PowerShell": 30000,
|
||||
"Grep": 20000,
|
||||
"Snip": 1000,
|
||||
"StrReplaceBasedEditTool": 30000,
|
||||
"BashSearchTool": 20000
|
||||
},
|
||||
"tengu_slate_finch": true,
|
||||
"tengu_workflows_enabled": true,
|
||||
"tengu_permission_friction": true,
|
||||
"tengu_marble_lark": false,
|
||||
"tengu_copper_fox": false,
|
||||
"tengu_bridge_repl_v2_config": {
|
||||
"init_retry_max_attempts": 3,
|
||||
"init_retry_base_delay_ms": 500,
|
||||
"init_retry_jitter_fraction": 0.25,
|
||||
"init_retry_max_delay_ms": 4000,
|
||||
"http_timeout_ms": 10000,
|
||||
"uuid_dedup_buffer_size": 2000,
|
||||
"heartbeat_interval_ms": 20000,
|
||||
"heartbeat_jitter_fraction": 0.1,
|
||||
"token_refresh_buffer_ms": 600000,
|
||||
"teardown_archive_timeout_ms": 1500,
|
||||
"connect_timeout_ms": 15000,
|
||||
"min_version": "2.1.70",
|
||||
"should_show_app_upgrade_message": false
|
||||
},
|
||||
"tengu_marble_whisper": true,
|
||||
"tengu_maple_sundial": false,
|
||||
"tengu_velvet_cascade": {},
|
||||
"tengu_passport_quail": false,
|
||||
"tengu_ember_latch": true,
|
||||
"tengu_vscode_onboarding": false,
|
||||
"tengu_fennel_kite_model": "",
|
||||
"tengu_nimble_amber_prose": false,
|
||||
"tengu_bridge_poll_interval_ms": 0,
|
||||
"tengu_cobalt_wren": false,
|
||||
"tengu_harbor_permissions": true,
|
||||
"tengu_orchid_trellis": false,
|
||||
"tengu_ccr_bridge_multi_session": true,
|
||||
"tengu_bad_survey_transcript_ask_config": {
|
||||
"probability": 1
|
||||
},
|
||||
"tengu_good_survey_transcript_ask_config": {
|
||||
"probability": 0.5
|
||||
},
|
||||
"tengu_amber_sentinel": true,
|
||||
"tengu_crimson_vector": false,
|
||||
"tengu_drift_lantern": false,
|
||||
"tengu_kestrel_arch": "OFF",
|
||||
"tengu_read_dedup_killswitch": false,
|
||||
"tengu_saffron_lattice": {
|
||||
"enabled": false,
|
||||
"planLimitsEndDate": "2026-06-22T10:00:00Z",
|
||||
"hideRateLimitsDescription": true
|
||||
},
|
||||
"tengu_cloth_snorkel": false,
|
||||
"tengu_system_prompt_global_cache": true,
|
||||
"tengu_slate_moth": true,
|
||||
"tengu_bridge_poll_interval_config": {
|
||||
"poll_interval_ms_not_at_capacity": 2000,
|
||||
"poll_interval_ms_at_capacity": 600000,
|
||||
"heartbeat_interval_ms": 0,
|
||||
"multisession_poll_interval_ms_not_at_capacity": 5000,
|
||||
"multisession_poll_interval_ms_at_capacity": 60000,
|
||||
"multisession_poll_interval_ms_partial_capacity": 5000,
|
||||
"non_exclusive_heartbeat_interval_ms": 180000,
|
||||
"session_keepalive_interval_ms": 0,
|
||||
"session_keepalive_interval_v2_ms": 0
|
||||
},
|
||||
"tengu_gouda_loop": true,
|
||||
"tengu_otk_slot_v1": false,
|
||||
"tengu_pewter_lark": "off",
|
||||
"tengu_walnut_prism": false,
|
||||
"tengu_immediate_model_command": false,
|
||||
"tengu_pewter_summit": true,
|
||||
"tengu_fg_left_arrow_agents": true,
|
||||
"tengu_willow_sentinel_ttl_hours": 1,
|
||||
"tengu_pewter_lantern": false,
|
||||
"tengu_desktop_upsell_v2": {
|
||||
"enabled": false
|
||||
},
|
||||
"tengu_vellum_siding": false,
|
||||
"tengu_vscode_feedback_survey": true,
|
||||
"tengu_mcp_singleton_unwrap": true,
|
||||
"tengu_coral_fern": false,
|
||||
"tengu_trace_lantern": false,
|
||||
"tengu_review_bughunter_config": {
|
||||
"fleet_size": 5,
|
||||
"max_duration_minutes": 10,
|
||||
"agent_timeout_seconds": 600,
|
||||
"total_wallclock_minutes": 22,
|
||||
"model": "claude-opus-4-7",
|
||||
"cost_note": "$5-$25",
|
||||
"duration_note": "~5-10 min",
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_basalt_spur": false,
|
||||
"tengu_crystal_beam": {
|
||||
"budgetTokens": 0
|
||||
},
|
||||
"tengu_hawthorn_window": 200000,
|
||||
"tengu_flint_harbor_share": false,
|
||||
"tengu_bridge_attestation_enforce": false,
|
||||
"tengu_compass_dial": true,
|
||||
"tengu_moss_anchor": false,
|
||||
"tengu_willow_census_ttl_hours": 24,
|
||||
"tengu_compact_cache_prefix": true,
|
||||
"tengu_cedar_hollow_7m": {},
|
||||
"tengu_prompt_suggestion": true,
|
||||
"tengu_crimson_echo": {},
|
||||
"tengu_cork_m4q": true,
|
||||
"tengu_classifier_summary_llm_emit": true,
|
||||
"tengu_tide_elm": "off",
|
||||
"tengu_ccr_bundle_seed_enabled": true,
|
||||
"tengu_copper_wren": false,
|
||||
"tengu_ember_trail": "0",
|
||||
"tengu_gha_plugin_code_review": false,
|
||||
"tengu_keybinding_customization_release": true,
|
||||
"tengu_kairos_cron_durable": false,
|
||||
"tengu_canary": {},
|
||||
"tengu_mocha_barista": true,
|
||||
"tengu_negative_interaction_transcript_ask_config": {
|
||||
"probability": 0
|
||||
},
|
||||
"tengu_steady_lantern": false,
|
||||
"tengu_malformed_tool_use_clean_retry": false,
|
||||
"tengu_agent_list_attach": false,
|
||||
"tengu_ultraplan_timeout_seconds": 5400,
|
||||
"tengu_hazel_osprey_floor": 75000,
|
||||
"tengu_brick_follow": false,
|
||||
"tengu_slate_ribbon": true,
|
||||
"tengu_slate_siskin": {
|
||||
"enabled": false,
|
||||
"timeoutMs": 8000,
|
||||
"throttleMs": 30000,
|
||||
"summaryLineThreshold": 5
|
||||
},
|
||||
"tengu_amber_rokovoko": 0.2,
|
||||
"tengu_penguin_mode_promo": {
|
||||
"discountPercent": 0,
|
||||
"endDate": "Feb 16"
|
||||
},
|
||||
"tengu_slate_harrier": "off",
|
||||
"tengu_lapis_thicket": false,
|
||||
"tengu_harbor_willow": false,
|
||||
"tengu_amber_anchor": false,
|
||||
"tengu_tussock_oriole": false,
|
||||
"tengu_tern_alloy": "copy_a",
|
||||
"tengu_fgts": true,
|
||||
"tengu_vellum_lantern": false,
|
||||
"tengu_saffron_anchor": true,
|
||||
"tengu_miraculo_the_bard": false,
|
||||
"tengu_red_coaster": false,
|
||||
"tengu_cobalt_compass": true,
|
||||
"tengu_plum_vx3": true,
|
||||
"tengu_mcp_subagent_prompt": true,
|
||||
"tengu_mcp_local_oauth_blocked_hosts": {
|
||||
"hosts": [
|
||||
"microsoft365.mcp.claude.com",
|
||||
"gmail.mcp.claude.com",
|
||||
"gcal.mcp.claude.com"
|
||||
]
|
||||
},
|
||||
"tengu_byte_stream_idle_timeout_ms": 180000,
|
||||
"tengu_umber_petrel": false,
|
||||
"tengu_prism_ledger": false,
|
||||
"tengu_ccr_bundle_max_bytes": 104857600,
|
||||
"tengu_amber_sextant": true,
|
||||
"tengu_pewter_ledger": "OFF",
|
||||
"tengu_amber_flint": true,
|
||||
"tengu_disable_bypass_permissions_mode": false,
|
||||
"tengu_walrus_canteen": false,
|
||||
"tengu_ashen_kelp": true,
|
||||
"tengu_plugin_official_mkt_git_fallback": true,
|
||||
"tengu_max_version_config": {},
|
||||
"tengu_cobalt_lantern": true,
|
||||
"tengu_ultraplan_prompt_identifier": "visual_plan",
|
||||
"tengu_swann_brevity": "focused",
|
||||
"tengu_hazel_osprey": false,
|
||||
"tengu_slate_meadow": true,
|
||||
"tengu_amber_redwood2": "",
|
||||
"tengu_frond_boric": {},
|
||||
"tengu_slate_thimble": false,
|
||||
"tengu_slate_nexus": true,
|
||||
"tengu_chert_bezel": true,
|
||||
"tengu_streaming_tool_execution2": true,
|
||||
"tengu_event_watchdog_default_on": false,
|
||||
"tengu_auto_mode_config": {
|
||||
"enabled": "enabled",
|
||||
"twoStageClassifier": true
|
||||
},
|
||||
"tengu_grey_step2": {
|
||||
"enabled": true,
|
||||
"dialogTitle": "We recommend medium effort for Opus",
|
||||
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
|
||||
},
|
||||
"tengu_dune_wren": false,
|
||||
"tengu_cedar_lantern": true,
|
||||
"tengu_velvet_moth": 0.2,
|
||||
"tengu_harbor_ledger": [
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "discord"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "telegram"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "fakechat"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "imessage"
|
||||
}
|
||||
],
|
||||
"tengu_harbor": true,
|
||||
"tengu_amber_lynx": false,
|
||||
"tengu_doorbell_agave": false,
|
||||
"tengu_maple_tide": false,
|
||||
"tengu_fennel_kite": false,
|
||||
"tengu_collage_kaleidoscope": true,
|
||||
"tengu_file_write_optimization": true,
|
||||
"tengu_startup_notice": "",
|
||||
"tengu_mcp_retry_failed_remote": false,
|
||||
"tengu_session_memory": false,
|
||||
"tengu_flint_harbor_prompt": {
|
||||
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
|
||||
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
|
||||
"windowDays": 30
|
||||
},
|
||||
"tengu_slim_subagent_claudemd": true,
|
||||
"tengu_tangerine_ladder_boost": true,
|
||||
"tengu_chair_sermon": false,
|
||||
"tengu_gypsum_kite": true,
|
||||
"tengu_quartz_heron": false,
|
||||
"tengu_xterm_atlas_reset": true,
|
||||
"tengu-model-error-overrides": {
|
||||
"claude-fable-5": {
|
||||
"block": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access"
|
||||
}
|
||||
},
|
||||
"tengu_orchid_mantis_v2": true,
|
||||
"tengu-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_feedback_survey_config": {
|
||||
"minTimeBeforeFeedbackMs": 600000,
|
||||
"minTimeBetweenFeedbackMs": 43200000,
|
||||
"minTimeBetweenGlobalFeedbackMs": 43200000,
|
||||
"minUserTurnsBeforeFeedback": 5,
|
||||
"minUserTurnsBetweenFeedback": 25,
|
||||
"hideThanksAfterMs": 3000,
|
||||
"onForModels": [
|
||||
"*"
|
||||
],
|
||||
"probability": 0.05
|
||||
},
|
||||
"tengu_cork_lantern": false,
|
||||
"tengu_mint_lanes": false,
|
||||
"tengu_bridge_attestation_enforce_config": {
|
||||
"accept_level": "VERIFIED_BY_GATE",
|
||||
"accept_statuses": []
|
||||
},
|
||||
"tengu_marble_sandcastle": false,
|
||||
"tengu_bg_attach_stall_ms": 5000,
|
||||
"tengu_workout2": true,
|
||||
"tengu_orford_ness": false,
|
||||
"tengu_porch_bell_9f": "",
|
||||
"tengu_auto_mode_default_on": false,
|
||||
"tengu_birch_kettle": false,
|
||||
"tengu_classifier_summary_heuristic_emit": true,
|
||||
"tengu_cobalt_thicket": false,
|
||||
"tengu_destructive_command_warning": false,
|
||||
"tengu_cinder_plover": "",
|
||||
"tengu_cedar_halo": false,
|
||||
"tengu_sotto_voce": true,
|
||||
"tengu_sepia_moth": false,
|
||||
"tengu_cedar_sundial": false,
|
||||
"tengu_penguins_enabled": true,
|
||||
"tengu_quiet_basalt_echo": false,
|
||||
"tengu_ochre_hollow": true,
|
||||
"tengu_coral_beacon": true,
|
||||
"tengu_copper_thistle": false,
|
||||
"tengu_1p_event_batch_config": {
|
||||
"scheduledDelayMillis": 10000,
|
||||
"maxExportBatchSize": 400,
|
||||
"maxQueueSize": 8192,
|
||||
"path": "/api/event_logging/v2/batch"
|
||||
},
|
||||
"tengu_amber_wren": {
|
||||
"targetedRangeNudge": true,
|
||||
"maxTokens": 25000
|
||||
},
|
||||
"tengu_amber_prism": true,
|
||||
"tengu_cobalt_plinth": false,
|
||||
"tengu_silent_harbor": false,
|
||||
"tengu_chomp_inflection": true,
|
||||
"tengu_mcp_elicitation": true,
|
||||
"tengu_sm_config": {
|
||||
"minimumMessageTokensToInit": 150000,
|
||||
"minimumTokensBetweenUpdate": 40000,
|
||||
"toolCallsBetweenUpdates": 10
|
||||
},
|
||||
"tengu_bridge_min_version": {
|
||||
"minVersion": "2.1.70"
|
||||
},
|
||||
"tengu_kairos_input_needed_push": true,
|
||||
"tengu_quiet_harbor": false,
|
||||
"tengu_slate_wren": false,
|
||||
"tengu_tool_search_unsupported_models": [
|
||||
"claude-3-5-haiku",
|
||||
"claude-3-haiku"
|
||||
],
|
||||
"tengu_native_cursor": true,
|
||||
"tengu_orchid_mantis": false,
|
||||
"tengu_amber_lark": true,
|
||||
"tengu_shale_finch": true,
|
||||
"tengu_cedar_plume": false,
|
||||
"tengu_kairos_push_notifications": true,
|
||||
"tengu_marble_whisper2": true,
|
||||
"tengu_lichen_compass": false,
|
||||
"tengu_c4w_usage_limit_notifications_enabled": true,
|
||||
"tengu_scarf_coffee": false,
|
||||
"tengu_copper_bridge": true,
|
||||
"tengu_tool_pear": false,
|
||||
"tengu_claudeai_mcp_connectors": true,
|
||||
"tengu_ccr_post_turn_summary": false,
|
||||
"tengu_sedge_lantern": true,
|
||||
"tengu_feature_template": false,
|
||||
"tengu_harbor_prism": true,
|
||||
"tengu_cedar_inlet": "step",
|
||||
"tengu_flax_grouse": false,
|
||||
"tengu_event_sampling_config": {},
|
||||
"tengu_herring_clock": false,
|
||||
"tengu_quartz_vireo": "",
|
||||
"tengu_team_discovery": false,
|
||||
"tengu_gleaming_fair": true,
|
||||
"tengu_marble_anvil": true,
|
||||
"tengu_classifier_disabled_surfaces": "",
|
||||
"tengu_pewter_brook": false,
|
||||
"tengu_vscode_review_upsell": false,
|
||||
"claude_code_skills_dashboard_enabled_cli": false,
|
||||
"tengu_post_compact_survey": false,
|
||||
"tengu_reactive_compact_remote": false,
|
||||
"tengu_idle_amber_finch": false,
|
||||
"tengu_noreread_q7m_velvet": false,
|
||||
"tengu_ultraplan_config": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_scratch": false,
|
||||
"tengu_alder_compass": false,
|
||||
"tengu_olive_hinge": "",
|
||||
"tengu_shining_fractals": false,
|
||||
"tengu_maple_pier": false,
|
||||
"tengu_sessions_elevated_auth_enforcement": true,
|
||||
"tengu_turtle_carbon": true,
|
||||
"tengu_billiard_aviary": false,
|
||||
"tengu_cinder_almanac": true,
|
||||
"tengu_osprey_lantern": false,
|
||||
"tengu-top-of-feed-tip": {
|
||||
"tip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"color": "warning"
|
||||
},
|
||||
"tengu_cobalt_raccoon": true,
|
||||
"tengu_loud_sugary_rock": false,
|
||||
"tengu_willow_mode": "hint_v2",
|
||||
"tengu_blue_coaster": false,
|
||||
"tengu_snippet_save": false,
|
||||
"tengu_amber_lattice": {
|
||||
"plugins": [
|
||||
"security-guidance",
|
||||
"code-review",
|
||||
"commit-commands",
|
||||
"code-simplifier",
|
||||
"hookify",
|
||||
"feature-dev",
|
||||
"frontend-design",
|
||||
"pr-review-toolkit",
|
||||
"skill-creator",
|
||||
"plugin-dev",
|
||||
"agent-sdk-dev",
|
||||
"mcp-server-dev",
|
||||
"claude-code-setup",
|
||||
"claude-md-management",
|
||||
"playground",
|
||||
"ralph-loop",
|
||||
"explanatory-output-style",
|
||||
"learning-output-style",
|
||||
"clangd-lsp",
|
||||
"csharp-lsp",
|
||||
"gopls-lsp",
|
||||
"jdtls-lsp",
|
||||
"kotlin-lsp",
|
||||
"lua-lsp",
|
||||
"php-lsp",
|
||||
"pyright-lsp",
|
||||
"ruby-lsp",
|
||||
"rust-analyzer-lsp",
|
||||
"swift-lsp",
|
||||
"typescript-lsp"
|
||||
]
|
||||
},
|
||||
"tengu_slate_harbor_experiment": false,
|
||||
"tengu_velvet_ibis": {},
|
||||
"tengu_bridge_requires_action_details": true,
|
||||
"tengu_lapis_finch": true,
|
||||
"tengu_satin_quoll": {},
|
||||
"tengu_moth_copse": false,
|
||||
"tengu_silk_hinge": false,
|
||||
"tengu_surreal_dali": true,
|
||||
"tengu_cobalt_ridge": true,
|
||||
"tengu_flint_harbor": false,
|
||||
"tengu_plank_river_frost": "user_intent",
|
||||
"tengu_velvet_mallet_haiku": false,
|
||||
"tengu_velvet_mallet": false,
|
||||
"tengu_velvet_mallet_haiku_4_5": false,
|
||||
"tengu_velvet_hammer_falcon": false,
|
||||
"tengu_loud_sugary_rock2": false,
|
||||
"tengu_velvet_hammer_sonnet_4_5": false,
|
||||
"tengu_velvet_hammer_sonnet": false,
|
||||
"tengu_tab_read_sep": false,
|
||||
"tengu_quill_harbor": "acceptEdits",
|
||||
"tengu_velvet_hammer": false,
|
||||
"tengu_velvet_hammer_opus": false,
|
||||
"tengu_c4e_slash_upsell": true,
|
||||
"tengu_velvet_hammer_haiku_4_5": false,
|
||||
"tengu_feature_claudified_template": false,
|
||||
"tengu_slate_quill": true,
|
||||
"tengu_ax_screen_reader": false,
|
||||
"tengu_windows_credman": false,
|
||||
"tengu_basalt_tern": false,
|
||||
"tengu_velvet_mallet_opus": false,
|
||||
"tengu_velvet_hammer_haiku": false,
|
||||
"tengu_velvet_static": true,
|
||||
"tengu_velvet_mallet_sonnet": false,
|
||||
"tengu_soft_slate_nudge": "baseline",
|
||||
"tengu_lantern_hearth": "off",
|
||||
"tengu_velvet_mallet_falcon": false,
|
||||
"tengu_velvet_mallet_sonnet_4_5": false
|
||||
},
|
||||
"firstStartTime": "2026-06-05T19:39:28.542Z",
|
||||
"opusProMigrationComplete": true,
|
||||
"sonnet1m45MigrationComplete": true,
|
||||
"seenNotifications": {},
|
||||
"migrationVersion": 13,
|
||||
"userID": "9d89994d486a4884b8cf33372d8a4cd61ebf7d34009e9d3cbce9db24e2e971a4",
|
||||
"changelogLastFetched": 1781361371930,
|
||||
"autoUpdatesProtectedForNative": true,
|
||||
"claudeCodeFirstTokenDate": "2026-04-11T19:03:48.223040Z",
|
||||
"hasCompletedOnboarding": true,
|
||||
"lastOnboardingVersion": "2.1.165",
|
||||
"groveConfigCache": {
|
||||
"09792e21-2287-4348-b4d4-34cddbbfabc5": {
|
||||
"grove_enabled": true,
|
||||
"timestamp": 1781406640065
|
||||
}
|
||||
},
|
||||
"cachedExperimentFeatures": [
|
||||
"tengu_amber_prism",
|
||||
"tengu_basalt_spur",
|
||||
"tengu_cedar_inlet",
|
||||
"tengu_coral_beacon",
|
||||
"tengu_flint_harbor",
|
||||
"tengu_mcp_subagent_prompt",
|
||||
"tengu_ochre_hollow",
|
||||
"tengu_orchid_mantis_v2",
|
||||
"tengu_plank_river_frost",
|
||||
"tengu_read_dedup_killswitch"
|
||||
],
|
||||
"cachedGrowthBookFeaturesAt": 1781406639973,
|
||||
"lastReleaseNotesSeen": "2.1.177",
|
||||
"projects": {
|
||||
"/root": {
|
||||
"allowedTools": [],
|
||||
"mcpContextUris": [],
|
||||
"mcpServers": {},
|
||||
"enabledMcpjsonServers": [],
|
||||
"disabledMcpjsonServers": [],
|
||||
"hasTrustDialogAccepted": false,
|
||||
"projectOnboardingSeenCount": 3,
|
||||
"hasClaudeMdExternalIncludesApproved": false,
|
||||
"hasClaudeMdExternalIncludesWarningShown": false,
|
||||
"exampleFiles": [],
|
||||
"lastGracefulShutdown": false,
|
||||
"lastVersionBase": "2.1.177",
|
||||
"lastCost": 1.0676417999999999,
|
||||
"lastAPIDuration": 276732,
|
||||
"lastAPIDurationWithoutRetries": 276675,
|
||||
"lastToolDuration": 9607,
|
||||
"lastDuration": 2130140,
|
||||
"lastLinesAdded": 29,
|
||||
"lastLinesRemoved": 15,
|
||||
"lastTotalInputTokens": 4397,
|
||||
"lastTotalOutputTokens": 16093,
|
||||
"lastTotalCacheCreationInputTokens": 53595,
|
||||
"lastTotalCacheReadInputTokens": 1642666,
|
||||
"lastTotalWebSearchRequests": 0,
|
||||
"lastFpsAverage": 1.82,
|
||||
"lastFpsLow1Pct": 313.42,
|
||||
"lastModelUsage": {
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"inputTokens": 572,
|
||||
"outputTokens": 17,
|
||||
"cacheReadInputTokens": 0,
|
||||
"cacheCreationInputTokens": 0,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 0.000657
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
"inputTokens": 3825,
|
||||
"outputTokens": 16076,
|
||||
"cacheReadInputTokens": 1642666,
|
||||
"cacheCreationInputTokens": 53595,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 1.0669847999999997
|
||||
}
|
||||
},
|
||||
"lastSessionId": "96cf6b2d-d6a0-405b-81e5-95c657e1922a",
|
||||
"lastSessionMetrics": {
|
||||
"frame_duration_ms_count": 16776,
|
||||
"frame_duration_ms_min": 0.11423300000024028,
|
||||
"frame_duration_ms_max": 21.985366000095382,
|
||||
"frame_duration_ms_avg": 0.7730811968292047,
|
||||
"frame_duration_ms_p50": 0.5600509999203496,
|
||||
"frame_duration_ms_p95": 1.786581499991007,
|
||||
"frame_duration_ms_p99": 4.282493569953367,
|
||||
"pre_tool_hook_duration_ms_count": 108,
|
||||
"pre_tool_hook_duration_ms_min": 0,
|
||||
"pre_tool_hook_duration_ms_max": 15,
|
||||
"pre_tool_hook_duration_ms_avg": 0.24074074074074073,
|
||||
"pre_tool_hook_duration_ms_p50": 0,
|
||||
"pre_tool_hook_duration_ms_p95": 1,
|
||||
"pre_tool_hook_duration_ms_p99": 4.789999999999978,
|
||||
"hook_duration_ms_count": 40,
|
||||
"hook_duration_ms_min": 0,
|
||||
"hook_duration_ms_max": 8,
|
||||
"hook_duration_ms_avg": 0.35,
|
||||
"hook_duration_ms_p50": 0,
|
||||
"hook_duration_ms_p95": 1,
|
||||
"hook_duration_ms_p99": 5.269999999999996
|
||||
},
|
||||
"hasCompletedProjectOnboarding": true
|
||||
}
|
||||
},
|
||||
"routineFiredWatermark": "2026-06-05T19:47:09.178Z",
|
||||
"penguinModeOrgEnabled": true,
|
||||
"closedIssuesLastChecked": 1781406639965,
|
||||
"passesEligibilityCache": {
|
||||
"4bb43199-0efc-4d5c-b552-79865cb0361b": {
|
||||
"eligible": true,
|
||||
"referral_code_details": {
|
||||
"code": "BeGGjphr1g",
|
||||
"campaign": "claude_code_guest_pass_a47c",
|
||||
"referral_link": "https://claude.ai/referral/BeGGjphr1g"
|
||||
},
|
||||
"referrer_reward": {
|
||||
"amount_minor_units": 1000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"remaining_passes": 3,
|
||||
"limit": 3,
|
||||
"share_link": "https://claude.ai/referral/BeGGjphr1g",
|
||||
"terms_url": "https://support.claude.com/en/articles/12875061-claude-code-guest-passes",
|
||||
"timestamp": 1781406640514
|
||||
}
|
||||
},
|
||||
"cachedExtraUsageDisabledReason": "out_of_credits",
|
||||
"passesUpsellSeenCount": 3,
|
||||
"hasVisitedPasses": false,
|
||||
"passesLastSeenRemaining": 3,
|
||||
"officialMarketplaceAutoInstallAttempted": true,
|
||||
"officialMarketplaceAutoInstalled": true,
|
||||
"tipLifetimeShownCounts": {
|
||||
"fotw-campaign-upsell": 6,
|
||||
"new-user-warmup": 2,
|
||||
"plan-mode-for-complex-tasks": 5,
|
||||
"memory-command": 2,
|
||||
"theme-command": 2,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 3,
|
||||
"enter-to-steer-in-relatime": 2,
|
||||
"todo-list": 2,
|
||||
"ide-upsell-external-terminal": 5,
|
||||
"install-github-app": 3,
|
||||
"install-slack-app": 3,
|
||||
"drag-and-drop-images": 2,
|
||||
"double-esc-code-restore": 2,
|
||||
"continue": 2,
|
||||
"shift-tab": 2,
|
||||
"image-paste": 1,
|
||||
"web-app": 2,
|
||||
"color-when-multi-clauding": 1,
|
||||
"custom-agents": 2,
|
||||
"remote-control": 2,
|
||||
"voice-mode": 2,
|
||||
"goal-command-nudge": 4,
|
||||
"guest-passes": 6,
|
||||
"feedback-command": 2,
|
||||
"frontend-design-plugin": 1,
|
||||
"permissions": 2,
|
||||
"rename-conversation": 1,
|
||||
"custom-commands": 1,
|
||||
"c4e-remote-sessions": 1,
|
||||
"subagent-fanout-nudge": 1,
|
||||
"no-flicker": 1
|
||||
},
|
||||
"feedbackSurveyState": {
|
||||
"lastShownTime": 1781411703066
|
||||
},
|
||||
"hasUsedBackslashReturn": true,
|
||||
"agentLastUsed": {
|
||||
"bg": 1780696781055
|
||||
},
|
||||
"remoteControlUpsellSeenCount": 3,
|
||||
"fullscreenUpsellSeenCount": 3,
|
||||
"lastShownEmergencyTip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"oauthAccount": {
|
||||
"accountUuid": "09792e21-2287-4348-b4d4-34cddbbfabc5",
|
||||
"emailAddress": "gmer4lfe@gmail.com",
|
||||
"organizationUuid": "4bb43199-0efc-4d5c-b552-79865cb0361b",
|
||||
"hasExtraUsageEnabled": true,
|
||||
"billingType": "stripe_subscription",
|
||||
"accountCreatedAt": "2026-04-03T21:52:35.642439Z",
|
||||
"subscriptionCreatedAt": "2026-04-11T13:14:49.905923Z",
|
||||
"ccOnboardingFlags": {},
|
||||
"claudeCodeTrialEndsAt": null,
|
||||
"claudeCodeTrialDurationDays": null,
|
||||
"seatTier": null,
|
||||
"displayName": "Gmer4Lfe",
|
||||
"organizationRole": "admin",
|
||||
"workspaceRole": null,
|
||||
"organizationName": "gmer4lfe@gmail.com's Organization",
|
||||
"organizationType": "claude_pro",
|
||||
"organizationRateLimitTier": "default_claude_ai",
|
||||
"userRateLimitTier": null
|
||||
},
|
||||
"clientDataCache": {
|
||||
"cedar_lagoon": {
|
||||
"claude-fable": true,
|
||||
"claude-mythos": true
|
||||
},
|
||||
"pewter_owl_tool": true,
|
||||
"pewter_owl_model": "claude-fable"
|
||||
},
|
||||
"additionalModelOptionsCache": [
|
||||
{
|
||||
"value": "claude-fable-5[1m]",
|
||||
"label": "Fable (disabled)",
|
||||
"description": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"additionalModelCostsCache": {}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"numStartups": 22,
|
||||
"installMethod": "native",
|
||||
"autoUpdates": false,
|
||||
"hasSeenTasksHint": true,
|
||||
"tipsHistory": {
|
||||
"fotw-campaign-upsell": 13,
|
||||
"new-user-warmup": 6,
|
||||
"plan-mode-for-complex-tasks": 22,
|
||||
"memory-command": 16,
|
||||
"theme-command": 21,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 11,
|
||||
"enter-to-steer-in-relatime": 21,
|
||||
"todo-list": 21,
|
||||
"ide-upsell-external-terminal": 19,
|
||||
"install-github-app": 22,
|
||||
"install-slack-app": 22,
|
||||
"drag-and-drop-images": 14,
|
||||
"double-esc-code-restore": 14,
|
||||
"continue": 14,
|
||||
"shift-tab": 15,
|
||||
"image-paste": 4,
|
||||
"web-app": 19,
|
||||
"color-when-multi-clauding": 6,
|
||||
"custom-agents": 21,
|
||||
"remote-control": 21,
|
||||
"voice-mode": 16,
|
||||
"goal-command-nudge": 16,
|
||||
"guest-passes": 22,
|
||||
"feedback-command": 22,
|
||||
"frontend-design-plugin": 6,
|
||||
"permissions": 22,
|
||||
"rename-conversation": 11,
|
||||
"custom-commands": 11,
|
||||
"c4e-remote-sessions": 18,
|
||||
"subagent-fanout-nudge": 18,
|
||||
"no-flicker": 19
|
||||
},
|
||||
"promptQueueUseCount": 44,
|
||||
"cachedGrowthBookFeatures": {
|
||||
"tengu_slate_kestrel": true,
|
||||
"tengu_bridge_repl_v2": true,
|
||||
"tengu_basalt_meadow": true,
|
||||
"tengu_sage_compass2": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_kairos_loop_dynamic": true,
|
||||
"tengu_sepia_cormorant": [],
|
||||
"tengu_amber_heron": false,
|
||||
"tengu_log_datadog_events": true,
|
||||
"tengu-fable-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_quiet_slate_wren": false,
|
||||
"tengu_birch_compass": true,
|
||||
"tengu_bramble_lintel": 7,
|
||||
"tengu_malort_pedway": {
|
||||
"enabled": true,
|
||||
"pixelValidation": false,
|
||||
"clipboardPasteMultiline": true,
|
||||
"screenshotFilter": true,
|
||||
"mouseAnimation": true,
|
||||
"hideBeforeAction": true,
|
||||
"autoTargetDisplay": false,
|
||||
"coordinateMode": "pixels"
|
||||
},
|
||||
"tengu_lilac_loom": {},
|
||||
"tengu_sub_nomdrep_q7k": true,
|
||||
"tengu_lantern_spool": false,
|
||||
"tengu_hawthorn_steeple": false,
|
||||
"tengu_version_config": {
|
||||
"minVersion": "1.0.24"
|
||||
},
|
||||
"tengu_auto_notice_once": true,
|
||||
"tengu_sparrow_ledger": false,
|
||||
"tengu_loggia_carousel": false,
|
||||
"tengu_ccr_bridge": true,
|
||||
"tengu_basalt_sundial": false,
|
||||
"tengu_mcp_stateless_skip_init": true,
|
||||
"tengu_lapis_anchor": "off",
|
||||
"tengu_sage_compass": {},
|
||||
"tengu_kairos_cron": true,
|
||||
"tengu_kairos_loop_prompt": true,
|
||||
"tengu_jade_anvil_4": false,
|
||||
"tengu_skills_dashboard_enabled": false,
|
||||
"tengu_sedge_lantern_holdback": false,
|
||||
"tengu_dunwich_bell": false,
|
||||
"tengu_desktop_upsell": {
|
||||
"enable_shortcut_tip": true,
|
||||
"enable_startup_dialog": false
|
||||
},
|
||||
"tengu_code_diff_cli": true,
|
||||
"tengu_anchor_tide": true,
|
||||
"tengu_garnet_finch": false,
|
||||
"tengu_cobalt_heron": true,
|
||||
"tengu_ccr_v2_send_events_cli": true,
|
||||
"tengu_onyx_plover": {
|
||||
"enabled": false,
|
||||
"minHours": 24,
|
||||
"minSessions": 3,
|
||||
"remoteEnabled": false
|
||||
},
|
||||
"tengu_react_vulnerability_warning": false,
|
||||
"tengu_prompt_cache_1h_config": {
|
||||
"allowlist": [
|
||||
"repl_main_thread*",
|
||||
"sdk",
|
||||
"auto_mode",
|
||||
"rolling_compact",
|
||||
"memdir_relevance",
|
||||
"agent_classifier",
|
||||
"prompt_suggestion",
|
||||
"away_summary",
|
||||
"extract_memories",
|
||||
"compact"
|
||||
]
|
||||
},
|
||||
"tengu_timber_lark": "copy_a",
|
||||
"tengu_ladder_mq7": false,
|
||||
"tengu_birthday_hat": false,
|
||||
"tengu_prompt_cache_diagnostics": true,
|
||||
"tengu_worktree_mode": true,
|
||||
"tengu_willow_refresh_ttl_hours": 0,
|
||||
"tengu_pewter_kestrel": {
|
||||
"global": 50000,
|
||||
"Bash": 30000,
|
||||
"PowerShell": 30000,
|
||||
"Grep": 20000,
|
||||
"Snip": 1000,
|
||||
"StrReplaceBasedEditTool": 30000,
|
||||
"BashSearchTool": 20000
|
||||
},
|
||||
"tengu_slate_finch": true,
|
||||
"tengu_workflows_enabled": true,
|
||||
"tengu_permission_friction": true,
|
||||
"tengu_marble_lark": false,
|
||||
"tengu_copper_fox": false,
|
||||
"tengu_bridge_repl_v2_config": {
|
||||
"init_retry_max_attempts": 3,
|
||||
"init_retry_base_delay_ms": 500,
|
||||
"init_retry_jitter_fraction": 0.25,
|
||||
"init_retry_max_delay_ms": 4000,
|
||||
"http_timeout_ms": 10000,
|
||||
"uuid_dedup_buffer_size": 2000,
|
||||
"heartbeat_interval_ms": 20000,
|
||||
"heartbeat_jitter_fraction": 0.1,
|
||||
"token_refresh_buffer_ms": 600000,
|
||||
"teardown_archive_timeout_ms": 1500,
|
||||
"connect_timeout_ms": 15000,
|
||||
"min_version": "2.1.70",
|
||||
"should_show_app_upgrade_message": false
|
||||
},
|
||||
"tengu_marble_whisper": true,
|
||||
"tengu_maple_sundial": false,
|
||||
"tengu_velvet_cascade": {},
|
||||
"tengu_passport_quail": false,
|
||||
"tengu_ember_latch": true,
|
||||
"tengu_vscode_onboarding": false,
|
||||
"tengu_fennel_kite_model": "",
|
||||
"tengu_nimble_amber_prose": false,
|
||||
"tengu_bridge_poll_interval_ms": 0,
|
||||
"tengu_cobalt_wren": false,
|
||||
"tengu_harbor_permissions": true,
|
||||
"tengu_orchid_trellis": false,
|
||||
"tengu_ccr_bridge_multi_session": true,
|
||||
"tengu_bad_survey_transcript_ask_config": {
|
||||
"probability": 1
|
||||
},
|
||||
"tengu_good_survey_transcript_ask_config": {
|
||||
"probability": 0.5
|
||||
},
|
||||
"tengu_amber_sentinel": true,
|
||||
"tengu_crimson_vector": false,
|
||||
"tengu_drift_lantern": false,
|
||||
"tengu_kestrel_arch": "OFF",
|
||||
"tengu_read_dedup_killswitch": false,
|
||||
"tengu_saffron_lattice": {
|
||||
"enabled": false,
|
||||
"planLimitsEndDate": "2026-06-22T10:00:00Z",
|
||||
"hideRateLimitsDescription": true
|
||||
},
|
||||
"tengu_cloth_snorkel": false,
|
||||
"tengu_system_prompt_global_cache": true,
|
||||
"tengu_slate_moth": true,
|
||||
"tengu_bridge_poll_interval_config": {
|
||||
"poll_interval_ms_not_at_capacity": 2000,
|
||||
"poll_interval_ms_at_capacity": 600000,
|
||||
"heartbeat_interval_ms": 0,
|
||||
"multisession_poll_interval_ms_not_at_capacity": 5000,
|
||||
"multisession_poll_interval_ms_at_capacity": 60000,
|
||||
"multisession_poll_interval_ms_partial_capacity": 5000,
|
||||
"non_exclusive_heartbeat_interval_ms": 180000,
|
||||
"session_keepalive_interval_ms": 0,
|
||||
"session_keepalive_interval_v2_ms": 0
|
||||
},
|
||||
"tengu_gouda_loop": true,
|
||||
"tengu_otk_slot_v1": false,
|
||||
"tengu_pewter_lark": "off",
|
||||
"tengu_walnut_prism": false,
|
||||
"tengu_immediate_model_command": false,
|
||||
"tengu_pewter_summit": true,
|
||||
"tengu_fg_left_arrow_agents": true,
|
||||
"tengu_willow_sentinel_ttl_hours": 1,
|
||||
"tengu_pewter_lantern": false,
|
||||
"tengu_desktop_upsell_v2": {
|
||||
"enabled": false
|
||||
},
|
||||
"tengu_vellum_siding": false,
|
||||
"tengu_vscode_feedback_survey": true,
|
||||
"tengu_mcp_singleton_unwrap": true,
|
||||
"tengu_coral_fern": false,
|
||||
"tengu_trace_lantern": false,
|
||||
"tengu_review_bughunter_config": {
|
||||
"fleet_size": 5,
|
||||
"max_duration_minutes": 10,
|
||||
"agent_timeout_seconds": 600,
|
||||
"total_wallclock_minutes": 22,
|
||||
"model": "claude-opus-4-7",
|
||||
"cost_note": "$5-$25",
|
||||
"duration_note": "~5-10 min",
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_basalt_spur": false,
|
||||
"tengu_crystal_beam": {
|
||||
"budgetTokens": 0
|
||||
},
|
||||
"tengu_hawthorn_window": 200000,
|
||||
"tengu_flint_harbor_share": false,
|
||||
"tengu_bridge_attestation_enforce": false,
|
||||
"tengu_compass_dial": true,
|
||||
"tengu_moss_anchor": false,
|
||||
"tengu_willow_census_ttl_hours": 24,
|
||||
"tengu_compact_cache_prefix": true,
|
||||
"tengu_cedar_hollow_7m": {},
|
||||
"tengu_prompt_suggestion": true,
|
||||
"tengu_crimson_echo": {},
|
||||
"tengu_cork_m4q": true,
|
||||
"tengu_classifier_summary_llm_emit": true,
|
||||
"tengu_tide_elm": "off",
|
||||
"tengu_ccr_bundle_seed_enabled": true,
|
||||
"tengu_copper_wren": false,
|
||||
"tengu_ember_trail": "0",
|
||||
"tengu_gha_plugin_code_review": false,
|
||||
"tengu_keybinding_customization_release": true,
|
||||
"tengu_kairos_cron_durable": false,
|
||||
"tengu_canary": {},
|
||||
"tengu_mocha_barista": true,
|
||||
"tengu_negative_interaction_transcript_ask_config": {
|
||||
"probability": 0
|
||||
},
|
||||
"tengu_steady_lantern": false,
|
||||
"tengu_malformed_tool_use_clean_retry": false,
|
||||
"tengu_agent_list_attach": false,
|
||||
"tengu_ultraplan_timeout_seconds": 5400,
|
||||
"tengu_hazel_osprey_floor": 75000,
|
||||
"tengu_brick_follow": false,
|
||||
"tengu_slate_ribbon": true,
|
||||
"tengu_slate_siskin": {
|
||||
"enabled": false,
|
||||
"timeoutMs": 8000,
|
||||
"throttleMs": 30000,
|
||||
"summaryLineThreshold": 5
|
||||
},
|
||||
"tengu_amber_rokovoko": 0.2,
|
||||
"tengu_penguin_mode_promo": {
|
||||
"discountPercent": 0,
|
||||
"endDate": "Feb 16"
|
||||
},
|
||||
"tengu_slate_harrier": "off",
|
||||
"tengu_lapis_thicket": false,
|
||||
"tengu_harbor_willow": false,
|
||||
"tengu_amber_anchor": false,
|
||||
"tengu_tussock_oriole": false,
|
||||
"tengu_tern_alloy": "copy_a",
|
||||
"tengu_fgts": true,
|
||||
"tengu_vellum_lantern": false,
|
||||
"tengu_saffron_anchor": true,
|
||||
"tengu_miraculo_the_bard": false,
|
||||
"tengu_red_coaster": false,
|
||||
"tengu_cobalt_compass": true,
|
||||
"tengu_plum_vx3": true,
|
||||
"tengu_mcp_subagent_prompt": true,
|
||||
"tengu_mcp_local_oauth_blocked_hosts": {
|
||||
"hosts": [
|
||||
"microsoft365.mcp.claude.com",
|
||||
"gmail.mcp.claude.com",
|
||||
"gcal.mcp.claude.com"
|
||||
]
|
||||
},
|
||||
"tengu_byte_stream_idle_timeout_ms": 180000,
|
||||
"tengu_umber_petrel": false,
|
||||
"tengu_prism_ledger": false,
|
||||
"tengu_ccr_bundle_max_bytes": 104857600,
|
||||
"tengu_amber_sextant": true,
|
||||
"tengu_pewter_ledger": "OFF",
|
||||
"tengu_amber_flint": true,
|
||||
"tengu_disable_bypass_permissions_mode": false,
|
||||
"tengu_walrus_canteen": false,
|
||||
"tengu_ashen_kelp": true,
|
||||
"tengu_plugin_official_mkt_git_fallback": true,
|
||||
"tengu_max_version_config": {},
|
||||
"tengu_cobalt_lantern": true,
|
||||
"tengu_ultraplan_prompt_identifier": "visual_plan",
|
||||
"tengu_swann_brevity": "focused",
|
||||
"tengu_hazel_osprey": false,
|
||||
"tengu_slate_meadow": true,
|
||||
"tengu_amber_redwood2": "",
|
||||
"tengu_frond_boric": {},
|
||||
"tengu_slate_thimble": false,
|
||||
"tengu_slate_nexus": true,
|
||||
"tengu_chert_bezel": true,
|
||||
"tengu_streaming_tool_execution2": true,
|
||||
"tengu_event_watchdog_default_on": false,
|
||||
"tengu_auto_mode_config": {
|
||||
"enabled": "enabled",
|
||||
"twoStageClassifier": true
|
||||
},
|
||||
"tengu_grey_step2": {
|
||||
"enabled": true,
|
||||
"dialogTitle": "We recommend medium effort for Opus",
|
||||
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
|
||||
},
|
||||
"tengu_dune_wren": false,
|
||||
"tengu_cedar_lantern": true,
|
||||
"tengu_velvet_moth": 0.2,
|
||||
"tengu_harbor_ledger": [
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "discord"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "telegram"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "fakechat"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "imessage"
|
||||
}
|
||||
],
|
||||
"tengu_harbor": true,
|
||||
"tengu_amber_lynx": false,
|
||||
"tengu_doorbell_agave": false,
|
||||
"tengu_maple_tide": false,
|
||||
"tengu_fennel_kite": false,
|
||||
"tengu_collage_kaleidoscope": true,
|
||||
"tengu_file_write_optimization": true,
|
||||
"tengu_startup_notice": "",
|
||||
"tengu_mcp_retry_failed_remote": false,
|
||||
"tengu_session_memory": false,
|
||||
"tengu_flint_harbor_prompt": {
|
||||
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
|
||||
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
|
||||
"windowDays": 30
|
||||
},
|
||||
"tengu_slim_subagent_claudemd": true,
|
||||
"tengu_tangerine_ladder_boost": true,
|
||||
"tengu_chair_sermon": false,
|
||||
"tengu_gypsum_kite": true,
|
||||
"tengu_quartz_heron": false,
|
||||
"tengu_xterm_atlas_reset": true,
|
||||
"tengu-model-error-overrides": {
|
||||
"claude-fable-5": {
|
||||
"block": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access"
|
||||
}
|
||||
},
|
||||
"tengu_orchid_mantis_v2": true,
|
||||
"tengu-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_feedback_survey_config": {
|
||||
"minTimeBeforeFeedbackMs": 600000,
|
||||
"minTimeBetweenFeedbackMs": 43200000,
|
||||
"minTimeBetweenGlobalFeedbackMs": 43200000,
|
||||
"minUserTurnsBeforeFeedback": 5,
|
||||
"minUserTurnsBetweenFeedback": 25,
|
||||
"hideThanksAfterMs": 3000,
|
||||
"onForModels": [
|
||||
"*"
|
||||
],
|
||||
"probability": 0.05
|
||||
},
|
||||
"tengu_cork_lantern": false,
|
||||
"tengu_mint_lanes": false,
|
||||
"tengu_bridge_attestation_enforce_config": {
|
||||
"accept_level": "VERIFIED_BY_GATE",
|
||||
"accept_statuses": []
|
||||
},
|
||||
"tengu_marble_sandcastle": false,
|
||||
"tengu_bg_attach_stall_ms": 5000,
|
||||
"tengu_workout2": true,
|
||||
"tengu_orford_ness": false,
|
||||
"tengu_porch_bell_9f": "",
|
||||
"tengu_auto_mode_default_on": false,
|
||||
"tengu_birch_kettle": false,
|
||||
"tengu_classifier_summary_heuristic_emit": true,
|
||||
"tengu_cobalt_thicket": false,
|
||||
"tengu_destructive_command_warning": false,
|
||||
"tengu_cinder_plover": "",
|
||||
"tengu_cedar_halo": false,
|
||||
"tengu_sotto_voce": true,
|
||||
"tengu_sepia_moth": false,
|
||||
"tengu_cedar_sundial": false,
|
||||
"tengu_penguins_enabled": true,
|
||||
"tengu_quiet_basalt_echo": false,
|
||||
"tengu_ochre_hollow": true,
|
||||
"tengu_coral_beacon": true,
|
||||
"tengu_copper_thistle": false,
|
||||
"tengu_1p_event_batch_config": {
|
||||
"scheduledDelayMillis": 10000,
|
||||
"maxExportBatchSize": 400,
|
||||
"maxQueueSize": 8192,
|
||||
"path": "/api/event_logging/v2/batch"
|
||||
},
|
||||
"tengu_amber_wren": {
|
||||
"targetedRangeNudge": true,
|
||||
"maxTokens": 25000
|
||||
},
|
||||
"tengu_amber_prism": true,
|
||||
"tengu_cobalt_plinth": false,
|
||||
"tengu_silent_harbor": false,
|
||||
"tengu_chomp_inflection": true,
|
||||
"tengu_mcp_elicitation": true,
|
||||
"tengu_sm_config": {
|
||||
"minimumMessageTokensToInit": 150000,
|
||||
"minimumTokensBetweenUpdate": 40000,
|
||||
"toolCallsBetweenUpdates": 10
|
||||
},
|
||||
"tengu_bridge_min_version": {
|
||||
"minVersion": "2.1.70"
|
||||
},
|
||||
"tengu_kairos_input_needed_push": true,
|
||||
"tengu_quiet_harbor": false,
|
||||
"tengu_slate_wren": false,
|
||||
"tengu_tool_search_unsupported_models": [
|
||||
"claude-3-5-haiku",
|
||||
"claude-3-haiku"
|
||||
],
|
||||
"tengu_native_cursor": true,
|
||||
"tengu_orchid_mantis": false,
|
||||
"tengu_amber_lark": true,
|
||||
"tengu_shale_finch": true,
|
||||
"tengu_cedar_plume": false,
|
||||
"tengu_kairos_push_notifications": true,
|
||||
"tengu_marble_whisper2": true,
|
||||
"tengu_lichen_compass": false,
|
||||
"tengu_c4w_usage_limit_notifications_enabled": true,
|
||||
"tengu_scarf_coffee": false,
|
||||
"tengu_copper_bridge": true,
|
||||
"tengu_tool_pear": false,
|
||||
"tengu_claudeai_mcp_connectors": true,
|
||||
"tengu_ccr_post_turn_summary": false,
|
||||
"tengu_sedge_lantern": true,
|
||||
"tengu_feature_template": false,
|
||||
"tengu_harbor_prism": true,
|
||||
"tengu_cedar_inlet": "step",
|
||||
"tengu_flax_grouse": false,
|
||||
"tengu_event_sampling_config": {},
|
||||
"tengu_herring_clock": false,
|
||||
"tengu_quartz_vireo": "",
|
||||
"tengu_team_discovery": false,
|
||||
"tengu_gleaming_fair": true,
|
||||
"tengu_marble_anvil": true,
|
||||
"tengu_classifier_disabled_surfaces": "",
|
||||
"tengu_pewter_brook": false,
|
||||
"tengu_vscode_review_upsell": false,
|
||||
"claude_code_skills_dashboard_enabled_cli": false,
|
||||
"tengu_post_compact_survey": false,
|
||||
"tengu_reactive_compact_remote": false,
|
||||
"tengu_idle_amber_finch": false,
|
||||
"tengu_noreread_q7m_velvet": false,
|
||||
"tengu_ultraplan_config": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_scratch": false,
|
||||
"tengu_alder_compass": false,
|
||||
"tengu_olive_hinge": "",
|
||||
"tengu_shining_fractals": false,
|
||||
"tengu_maple_pier": false,
|
||||
"tengu_sessions_elevated_auth_enforcement": true,
|
||||
"tengu_turtle_carbon": true,
|
||||
"tengu_billiard_aviary": false,
|
||||
"tengu_cinder_almanac": true,
|
||||
"tengu_osprey_lantern": false,
|
||||
"tengu-top-of-feed-tip": {
|
||||
"tip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"color": "warning"
|
||||
},
|
||||
"tengu_cobalt_raccoon": true,
|
||||
"tengu_loud_sugary_rock": false,
|
||||
"tengu_willow_mode": "hint_v2",
|
||||
"tengu_blue_coaster": false,
|
||||
"tengu_snippet_save": false,
|
||||
"tengu_amber_lattice": {
|
||||
"plugins": [
|
||||
"security-guidance",
|
||||
"code-review",
|
||||
"commit-commands",
|
||||
"code-simplifier",
|
||||
"hookify",
|
||||
"feature-dev",
|
||||
"frontend-design",
|
||||
"pr-review-toolkit",
|
||||
"skill-creator",
|
||||
"plugin-dev",
|
||||
"agent-sdk-dev",
|
||||
"mcp-server-dev",
|
||||
"claude-code-setup",
|
||||
"claude-md-management",
|
||||
"playground",
|
||||
"ralph-loop",
|
||||
"explanatory-output-style",
|
||||
"learning-output-style",
|
||||
"clangd-lsp",
|
||||
"csharp-lsp",
|
||||
"gopls-lsp",
|
||||
"jdtls-lsp",
|
||||
"kotlin-lsp",
|
||||
"lua-lsp",
|
||||
"php-lsp",
|
||||
"pyright-lsp",
|
||||
"ruby-lsp",
|
||||
"rust-analyzer-lsp",
|
||||
"swift-lsp",
|
||||
"typescript-lsp"
|
||||
]
|
||||
},
|
||||
"tengu_slate_harbor_experiment": false,
|
||||
"tengu_velvet_ibis": {},
|
||||
"tengu_bridge_requires_action_details": true,
|
||||
"tengu_lapis_finch": true,
|
||||
"tengu_satin_quoll": {},
|
||||
"tengu_moth_copse": false,
|
||||
"tengu_silk_hinge": false,
|
||||
"tengu_surreal_dali": true,
|
||||
"tengu_cobalt_ridge": true,
|
||||
"tengu_flint_harbor": false,
|
||||
"tengu_plank_river_frost": "user_intent",
|
||||
"tengu_velvet_mallet_haiku": false,
|
||||
"tengu_velvet_mallet": false,
|
||||
"tengu_velvet_mallet_haiku_4_5": false,
|
||||
"tengu_velvet_hammer_falcon": false,
|
||||
"tengu_loud_sugary_rock2": false,
|
||||
"tengu_velvet_hammer_sonnet_4_5": false,
|
||||
"tengu_velvet_hammer_sonnet": false,
|
||||
"tengu_tab_read_sep": false,
|
||||
"tengu_quill_harbor": "acceptEdits",
|
||||
"tengu_velvet_hammer": false,
|
||||
"tengu_velvet_hammer_opus": false,
|
||||
"tengu_c4e_slash_upsell": true,
|
||||
"tengu_velvet_hammer_haiku_4_5": false,
|
||||
"tengu_feature_claudified_template": false,
|
||||
"tengu_slate_quill": true,
|
||||
"tengu_ax_screen_reader": false,
|
||||
"tengu_windows_credman": false,
|
||||
"tengu_basalt_tern": false,
|
||||
"tengu_velvet_mallet_opus": false,
|
||||
"tengu_velvet_hammer_haiku": false,
|
||||
"tengu_velvet_static": true,
|
||||
"tengu_velvet_mallet_sonnet": false,
|
||||
"tengu_soft_slate_nudge": "baseline",
|
||||
"tengu_lantern_hearth": "off",
|
||||
"tengu_velvet_mallet_falcon": false,
|
||||
"tengu_velvet_mallet_sonnet_4_5": false
|
||||
},
|
||||
"firstStartTime": "2026-06-05T19:39:28.542Z",
|
||||
"opusProMigrationComplete": true,
|
||||
"sonnet1m45MigrationComplete": true,
|
||||
"seenNotifications": {},
|
||||
"migrationVersion": 13,
|
||||
"userID": "9d89994d486a4884b8cf33372d8a4cd61ebf7d34009e9d3cbce9db24e2e971a4",
|
||||
"changelogLastFetched": 1781361371930,
|
||||
"autoUpdatesProtectedForNative": true,
|
||||
"claudeCodeFirstTokenDate": "2026-04-11T19:03:48.223040Z",
|
||||
"hasCompletedOnboarding": true,
|
||||
"lastOnboardingVersion": "2.1.165",
|
||||
"groveConfigCache": {
|
||||
"09792e21-2287-4348-b4d4-34cddbbfabc5": {
|
||||
"grove_enabled": true,
|
||||
"timestamp": 1781406640065
|
||||
}
|
||||
},
|
||||
"cachedExperimentFeatures": [
|
||||
"tengu_amber_prism",
|
||||
"tengu_basalt_spur",
|
||||
"tengu_cedar_inlet",
|
||||
"tengu_coral_beacon",
|
||||
"tengu_flint_harbor",
|
||||
"tengu_mcp_subagent_prompt",
|
||||
"tengu_ochre_hollow",
|
||||
"tengu_orchid_mantis_v2",
|
||||
"tengu_plank_river_frost",
|
||||
"tengu_read_dedup_killswitch"
|
||||
],
|
||||
"cachedGrowthBookFeaturesAt": 1781406639973,
|
||||
"lastReleaseNotesSeen": "2.1.177",
|
||||
"projects": {
|
||||
"/root": {
|
||||
"allowedTools": [],
|
||||
"mcpContextUris": [],
|
||||
"mcpServers": {},
|
||||
"enabledMcpjsonServers": [],
|
||||
"disabledMcpjsonServers": [],
|
||||
"hasTrustDialogAccepted": false,
|
||||
"projectOnboardingSeenCount": 3,
|
||||
"hasClaudeMdExternalIncludesApproved": false,
|
||||
"hasClaudeMdExternalIncludesWarningShown": false,
|
||||
"exampleFiles": [],
|
||||
"lastGracefulShutdown": false,
|
||||
"lastVersionBase": "2.1.177",
|
||||
"lastCost": 1.0676417999999999,
|
||||
"lastAPIDuration": 276732,
|
||||
"lastAPIDurationWithoutRetries": 276675,
|
||||
"lastToolDuration": 9607,
|
||||
"lastDuration": 2130140,
|
||||
"lastLinesAdded": 29,
|
||||
"lastLinesRemoved": 15,
|
||||
"lastTotalInputTokens": 4397,
|
||||
"lastTotalOutputTokens": 16093,
|
||||
"lastTotalCacheCreationInputTokens": 53595,
|
||||
"lastTotalCacheReadInputTokens": 1642666,
|
||||
"lastTotalWebSearchRequests": 0,
|
||||
"lastFpsAverage": 1.82,
|
||||
"lastFpsLow1Pct": 313.42,
|
||||
"lastModelUsage": {
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"inputTokens": 572,
|
||||
"outputTokens": 17,
|
||||
"cacheReadInputTokens": 0,
|
||||
"cacheCreationInputTokens": 0,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 0.000657
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
"inputTokens": 3825,
|
||||
"outputTokens": 16076,
|
||||
"cacheReadInputTokens": 1642666,
|
||||
"cacheCreationInputTokens": 53595,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 1.0669847999999997
|
||||
}
|
||||
},
|
||||
"lastSessionId": "96cf6b2d-d6a0-405b-81e5-95c657e1922a",
|
||||
"lastSessionMetrics": {
|
||||
"frame_duration_ms_count": 16776,
|
||||
"frame_duration_ms_min": 0.11423300000024028,
|
||||
"frame_duration_ms_max": 21.985366000095382,
|
||||
"frame_duration_ms_avg": 0.7730811968292047,
|
||||
"frame_duration_ms_p50": 0.5600509999203496,
|
||||
"frame_duration_ms_p95": 1.786581499991007,
|
||||
"frame_duration_ms_p99": 4.282493569953367,
|
||||
"pre_tool_hook_duration_ms_count": 108,
|
||||
"pre_tool_hook_duration_ms_min": 0,
|
||||
"pre_tool_hook_duration_ms_max": 15,
|
||||
"pre_tool_hook_duration_ms_avg": 0.24074074074074073,
|
||||
"pre_tool_hook_duration_ms_p50": 0,
|
||||
"pre_tool_hook_duration_ms_p95": 1,
|
||||
"pre_tool_hook_duration_ms_p99": 4.789999999999978,
|
||||
"hook_duration_ms_count": 40,
|
||||
"hook_duration_ms_min": 0,
|
||||
"hook_duration_ms_max": 8,
|
||||
"hook_duration_ms_avg": 0.35,
|
||||
"hook_duration_ms_p50": 0,
|
||||
"hook_duration_ms_p95": 1,
|
||||
"hook_duration_ms_p99": 5.269999999999996
|
||||
},
|
||||
"hasCompletedProjectOnboarding": true
|
||||
}
|
||||
},
|
||||
"routineFiredWatermark": "2026-06-05T19:47:09.178Z",
|
||||
"penguinModeOrgEnabled": true,
|
||||
"closedIssuesLastChecked": 1781406639965,
|
||||
"passesEligibilityCache": {
|
||||
"4bb43199-0efc-4d5c-b552-79865cb0361b": {
|
||||
"eligible": true,
|
||||
"referral_code_details": {
|
||||
"code": "BeGGjphr1g",
|
||||
"campaign": "claude_code_guest_pass_a47c",
|
||||
"referral_link": "https://claude.ai/referral/BeGGjphr1g"
|
||||
},
|
||||
"referrer_reward": {
|
||||
"amount_minor_units": 1000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"remaining_passes": 3,
|
||||
"limit": 3,
|
||||
"share_link": "https://claude.ai/referral/BeGGjphr1g",
|
||||
"terms_url": "https://support.claude.com/en/articles/12875061-claude-code-guest-passes",
|
||||
"timestamp": 1781406640514
|
||||
}
|
||||
},
|
||||
"cachedExtraUsageDisabledReason": null,
|
||||
"passesUpsellSeenCount": 3,
|
||||
"hasVisitedPasses": false,
|
||||
"passesLastSeenRemaining": 3,
|
||||
"officialMarketplaceAutoInstallAttempted": true,
|
||||
"officialMarketplaceAutoInstalled": true,
|
||||
"tipLifetimeShownCounts": {
|
||||
"fotw-campaign-upsell": 6,
|
||||
"new-user-warmup": 2,
|
||||
"plan-mode-for-complex-tasks": 5,
|
||||
"memory-command": 2,
|
||||
"theme-command": 2,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 3,
|
||||
"enter-to-steer-in-relatime": 2,
|
||||
"todo-list": 2,
|
||||
"ide-upsell-external-terminal": 5,
|
||||
"install-github-app": 3,
|
||||
"install-slack-app": 3,
|
||||
"drag-and-drop-images": 2,
|
||||
"double-esc-code-restore": 2,
|
||||
"continue": 2,
|
||||
"shift-tab": 2,
|
||||
"image-paste": 1,
|
||||
"web-app": 2,
|
||||
"color-when-multi-clauding": 1,
|
||||
"custom-agents": 2,
|
||||
"remote-control": 2,
|
||||
"voice-mode": 2,
|
||||
"goal-command-nudge": 4,
|
||||
"guest-passes": 6,
|
||||
"feedback-command": 2,
|
||||
"frontend-design-plugin": 1,
|
||||
"permissions": 2,
|
||||
"rename-conversation": 1,
|
||||
"custom-commands": 1,
|
||||
"c4e-remote-sessions": 1,
|
||||
"subagent-fanout-nudge": 1,
|
||||
"no-flicker": 1
|
||||
},
|
||||
"feedbackSurveyState": {
|
||||
"lastShownTime": 1781411703066
|
||||
},
|
||||
"hasUsedBackslashReturn": true,
|
||||
"agentLastUsed": {
|
||||
"bg": 1780696781055
|
||||
},
|
||||
"remoteControlUpsellSeenCount": 3,
|
||||
"fullscreenUpsellSeenCount": 3,
|
||||
"lastShownEmergencyTip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"oauthAccount": {
|
||||
"accountUuid": "09792e21-2287-4348-b4d4-34cddbbfabc5",
|
||||
"emailAddress": "gmer4lfe@gmail.com",
|
||||
"organizationUuid": "4bb43199-0efc-4d5c-b552-79865cb0361b",
|
||||
"hasExtraUsageEnabled": true,
|
||||
"billingType": "stripe_subscription",
|
||||
"accountCreatedAt": "2026-04-03T21:52:35.642439Z",
|
||||
"subscriptionCreatedAt": "2026-04-11T13:14:49.905923Z",
|
||||
"ccOnboardingFlags": {},
|
||||
"claudeCodeTrialEndsAt": null,
|
||||
"claudeCodeTrialDurationDays": null,
|
||||
"seatTier": null,
|
||||
"displayName": "Gmer4Lfe",
|
||||
"organizationRole": "admin",
|
||||
"workspaceRole": null,
|
||||
"organizationName": "gmer4lfe@gmail.com's Organization",
|
||||
"organizationType": "claude_pro",
|
||||
"organizationRateLimitTier": "default_claude_ai",
|
||||
"userRateLimitTier": null
|
||||
},
|
||||
"clientDataCache": {
|
||||
"cedar_lagoon": {
|
||||
"claude-fable": true,
|
||||
"claude-mythos": true
|
||||
},
|
||||
"pewter_owl_tool": true,
|
||||
"pewter_owl_model": "claude-fable"
|
||||
},
|
||||
"additionalModelOptionsCache": [
|
||||
{
|
||||
"value": "claude-fable-5[1m]",
|
||||
"label": "Fable (disabled)",
|
||||
"description": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"additionalModelCostsCache": {}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"numStartups": 22,
|
||||
"installMethod": "native",
|
||||
"autoUpdates": false,
|
||||
"hasSeenTasksHint": true,
|
||||
"tipsHistory": {
|
||||
"fotw-campaign-upsell": 13,
|
||||
"new-user-warmup": 6,
|
||||
"plan-mode-for-complex-tasks": 22,
|
||||
"memory-command": 16,
|
||||
"theme-command": 21,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 11,
|
||||
"enter-to-steer-in-relatime": 21,
|
||||
"todo-list": 21,
|
||||
"ide-upsell-external-terminal": 19,
|
||||
"install-github-app": 22,
|
||||
"install-slack-app": 22,
|
||||
"drag-and-drop-images": 14,
|
||||
"double-esc-code-restore": 14,
|
||||
"continue": 14,
|
||||
"shift-tab": 15,
|
||||
"image-paste": 4,
|
||||
"web-app": 19,
|
||||
"color-when-multi-clauding": 6,
|
||||
"custom-agents": 21,
|
||||
"remote-control": 21,
|
||||
"voice-mode": 16,
|
||||
"goal-command-nudge": 16,
|
||||
"guest-passes": 22,
|
||||
"feedback-command": 22,
|
||||
"frontend-design-plugin": 6,
|
||||
"permissions": 22,
|
||||
"rename-conversation": 11,
|
||||
"custom-commands": 11,
|
||||
"c4e-remote-sessions": 18,
|
||||
"subagent-fanout-nudge": 18,
|
||||
"no-flicker": 19
|
||||
},
|
||||
"promptQueueUseCount": 44,
|
||||
"cachedGrowthBookFeatures": {
|
||||
"tengu_slate_kestrel": true,
|
||||
"tengu_bridge_repl_v2": true,
|
||||
"tengu_basalt_meadow": true,
|
||||
"tengu_sage_compass2": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_kairos_loop_dynamic": true,
|
||||
"tengu_sepia_cormorant": [],
|
||||
"tengu_amber_heron": false,
|
||||
"tengu_log_datadog_events": true,
|
||||
"tengu-fable-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_quiet_slate_wren": false,
|
||||
"tengu_birch_compass": true,
|
||||
"tengu_bramble_lintel": 7,
|
||||
"tengu_malort_pedway": {
|
||||
"enabled": true,
|
||||
"pixelValidation": false,
|
||||
"clipboardPasteMultiline": true,
|
||||
"screenshotFilter": true,
|
||||
"mouseAnimation": true,
|
||||
"hideBeforeAction": true,
|
||||
"autoTargetDisplay": false,
|
||||
"coordinateMode": "pixels"
|
||||
},
|
||||
"tengu_lilac_loom": {},
|
||||
"tengu_sub_nomdrep_q7k": true,
|
||||
"tengu_lantern_spool": false,
|
||||
"tengu_hawthorn_steeple": false,
|
||||
"tengu_version_config": {
|
||||
"minVersion": "1.0.24"
|
||||
},
|
||||
"tengu_auto_notice_once": true,
|
||||
"tengu_sparrow_ledger": false,
|
||||
"tengu_loggia_carousel": false,
|
||||
"tengu_ccr_bridge": true,
|
||||
"tengu_basalt_sundial": false,
|
||||
"tengu_mcp_stateless_skip_init": true,
|
||||
"tengu_lapis_anchor": "off",
|
||||
"tengu_sage_compass": {},
|
||||
"tengu_kairos_cron": true,
|
||||
"tengu_kairos_loop_prompt": true,
|
||||
"tengu_jade_anvil_4": false,
|
||||
"tengu_skills_dashboard_enabled": false,
|
||||
"tengu_sedge_lantern_holdback": false,
|
||||
"tengu_dunwich_bell": false,
|
||||
"tengu_desktop_upsell": {
|
||||
"enable_shortcut_tip": true,
|
||||
"enable_startup_dialog": false
|
||||
},
|
||||
"tengu_code_diff_cli": true,
|
||||
"tengu_anchor_tide": true,
|
||||
"tengu_garnet_finch": false,
|
||||
"tengu_cobalt_heron": true,
|
||||
"tengu_ccr_v2_send_events_cli": true,
|
||||
"tengu_onyx_plover": {
|
||||
"enabled": false,
|
||||
"minHours": 24,
|
||||
"minSessions": 3,
|
||||
"remoteEnabled": false
|
||||
},
|
||||
"tengu_react_vulnerability_warning": false,
|
||||
"tengu_prompt_cache_1h_config": {
|
||||
"allowlist": [
|
||||
"repl_main_thread*",
|
||||
"sdk",
|
||||
"auto_mode",
|
||||
"rolling_compact",
|
||||
"memdir_relevance",
|
||||
"agent_classifier",
|
||||
"prompt_suggestion",
|
||||
"away_summary",
|
||||
"extract_memories",
|
||||
"compact"
|
||||
]
|
||||
},
|
||||
"tengu_timber_lark": "copy_a",
|
||||
"tengu_ladder_mq7": false,
|
||||
"tengu_birthday_hat": false,
|
||||
"tengu_prompt_cache_diagnostics": true,
|
||||
"tengu_worktree_mode": true,
|
||||
"tengu_willow_refresh_ttl_hours": 0,
|
||||
"tengu_pewter_kestrel": {
|
||||
"global": 50000,
|
||||
"Bash": 30000,
|
||||
"PowerShell": 30000,
|
||||
"Grep": 20000,
|
||||
"Snip": 1000,
|
||||
"StrReplaceBasedEditTool": 30000,
|
||||
"BashSearchTool": 20000
|
||||
},
|
||||
"tengu_slate_finch": true,
|
||||
"tengu_workflows_enabled": true,
|
||||
"tengu_permission_friction": true,
|
||||
"tengu_marble_lark": false,
|
||||
"tengu_copper_fox": false,
|
||||
"tengu_bridge_repl_v2_config": {
|
||||
"init_retry_max_attempts": 3,
|
||||
"init_retry_base_delay_ms": 500,
|
||||
"init_retry_jitter_fraction": 0.25,
|
||||
"init_retry_max_delay_ms": 4000,
|
||||
"http_timeout_ms": 10000,
|
||||
"uuid_dedup_buffer_size": 2000,
|
||||
"heartbeat_interval_ms": 20000,
|
||||
"heartbeat_jitter_fraction": 0.1,
|
||||
"token_refresh_buffer_ms": 600000,
|
||||
"teardown_archive_timeout_ms": 1500,
|
||||
"connect_timeout_ms": 15000,
|
||||
"min_version": "2.1.70",
|
||||
"should_show_app_upgrade_message": false
|
||||
},
|
||||
"tengu_marble_whisper": true,
|
||||
"tengu_maple_sundial": false,
|
||||
"tengu_velvet_cascade": {},
|
||||
"tengu_passport_quail": false,
|
||||
"tengu_ember_latch": true,
|
||||
"tengu_vscode_onboarding": false,
|
||||
"tengu_fennel_kite_model": "",
|
||||
"tengu_nimble_amber_prose": false,
|
||||
"tengu_bridge_poll_interval_ms": 0,
|
||||
"tengu_cobalt_wren": false,
|
||||
"tengu_harbor_permissions": true,
|
||||
"tengu_orchid_trellis": false,
|
||||
"tengu_ccr_bridge_multi_session": true,
|
||||
"tengu_bad_survey_transcript_ask_config": {
|
||||
"probability": 1
|
||||
},
|
||||
"tengu_good_survey_transcript_ask_config": {
|
||||
"probability": 0.5
|
||||
},
|
||||
"tengu_amber_sentinel": true,
|
||||
"tengu_crimson_vector": false,
|
||||
"tengu_drift_lantern": false,
|
||||
"tengu_kestrel_arch": "OFF",
|
||||
"tengu_read_dedup_killswitch": false,
|
||||
"tengu_saffron_lattice": {
|
||||
"enabled": false,
|
||||
"planLimitsEndDate": "2026-06-22T10:00:00Z",
|
||||
"hideRateLimitsDescription": true
|
||||
},
|
||||
"tengu_cloth_snorkel": false,
|
||||
"tengu_system_prompt_global_cache": true,
|
||||
"tengu_slate_moth": true,
|
||||
"tengu_bridge_poll_interval_config": {
|
||||
"poll_interval_ms_not_at_capacity": 2000,
|
||||
"poll_interval_ms_at_capacity": 600000,
|
||||
"heartbeat_interval_ms": 0,
|
||||
"multisession_poll_interval_ms_not_at_capacity": 5000,
|
||||
"multisession_poll_interval_ms_at_capacity": 60000,
|
||||
"multisession_poll_interval_ms_partial_capacity": 5000,
|
||||
"non_exclusive_heartbeat_interval_ms": 180000,
|
||||
"session_keepalive_interval_ms": 0,
|
||||
"session_keepalive_interval_v2_ms": 0
|
||||
},
|
||||
"tengu_gouda_loop": true,
|
||||
"tengu_otk_slot_v1": false,
|
||||
"tengu_pewter_lark": "off",
|
||||
"tengu_walnut_prism": false,
|
||||
"tengu_immediate_model_command": false,
|
||||
"tengu_pewter_summit": true,
|
||||
"tengu_fg_left_arrow_agents": true,
|
||||
"tengu_willow_sentinel_ttl_hours": 1,
|
||||
"tengu_pewter_lantern": false,
|
||||
"tengu_desktop_upsell_v2": {
|
||||
"enabled": false
|
||||
},
|
||||
"tengu_vellum_siding": false,
|
||||
"tengu_vscode_feedback_survey": true,
|
||||
"tengu_mcp_singleton_unwrap": true,
|
||||
"tengu_coral_fern": false,
|
||||
"tengu_trace_lantern": false,
|
||||
"tengu_review_bughunter_config": {
|
||||
"fleet_size": 5,
|
||||
"max_duration_minutes": 10,
|
||||
"agent_timeout_seconds": 600,
|
||||
"total_wallclock_minutes": 22,
|
||||
"model": "claude-opus-4-7",
|
||||
"cost_note": "$5-$25",
|
||||
"duration_note": "~5-10 min",
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_basalt_spur": false,
|
||||
"tengu_crystal_beam": {
|
||||
"budgetTokens": 0
|
||||
},
|
||||
"tengu_hawthorn_window": 200000,
|
||||
"tengu_flint_harbor_share": false,
|
||||
"tengu_bridge_attestation_enforce": false,
|
||||
"tengu_compass_dial": true,
|
||||
"tengu_moss_anchor": false,
|
||||
"tengu_willow_census_ttl_hours": 24,
|
||||
"tengu_compact_cache_prefix": true,
|
||||
"tengu_cedar_hollow_7m": {},
|
||||
"tengu_prompt_suggestion": true,
|
||||
"tengu_crimson_echo": {},
|
||||
"tengu_cork_m4q": true,
|
||||
"tengu_classifier_summary_llm_emit": true,
|
||||
"tengu_tide_elm": "off",
|
||||
"tengu_ccr_bundle_seed_enabled": true,
|
||||
"tengu_copper_wren": false,
|
||||
"tengu_ember_trail": "0",
|
||||
"tengu_gha_plugin_code_review": false,
|
||||
"tengu_keybinding_customization_release": true,
|
||||
"tengu_kairos_cron_durable": false,
|
||||
"tengu_canary": {},
|
||||
"tengu_mocha_barista": true,
|
||||
"tengu_negative_interaction_transcript_ask_config": {
|
||||
"probability": 0
|
||||
},
|
||||
"tengu_steady_lantern": false,
|
||||
"tengu_malformed_tool_use_clean_retry": false,
|
||||
"tengu_agent_list_attach": false,
|
||||
"tengu_ultraplan_timeout_seconds": 5400,
|
||||
"tengu_hazel_osprey_floor": 75000,
|
||||
"tengu_brick_follow": false,
|
||||
"tengu_slate_ribbon": true,
|
||||
"tengu_slate_siskin": {
|
||||
"enabled": false,
|
||||
"timeoutMs": 8000,
|
||||
"throttleMs": 30000,
|
||||
"summaryLineThreshold": 5
|
||||
},
|
||||
"tengu_amber_rokovoko": 0.2,
|
||||
"tengu_penguin_mode_promo": {
|
||||
"discountPercent": 0,
|
||||
"endDate": "Feb 16"
|
||||
},
|
||||
"tengu_slate_harrier": "off",
|
||||
"tengu_lapis_thicket": false,
|
||||
"tengu_harbor_willow": false,
|
||||
"tengu_amber_anchor": false,
|
||||
"tengu_tussock_oriole": false,
|
||||
"tengu_tern_alloy": "copy_a",
|
||||
"tengu_fgts": true,
|
||||
"tengu_vellum_lantern": false,
|
||||
"tengu_saffron_anchor": true,
|
||||
"tengu_miraculo_the_bard": false,
|
||||
"tengu_red_coaster": false,
|
||||
"tengu_cobalt_compass": true,
|
||||
"tengu_plum_vx3": true,
|
||||
"tengu_mcp_subagent_prompt": true,
|
||||
"tengu_mcp_local_oauth_blocked_hosts": {
|
||||
"hosts": [
|
||||
"microsoft365.mcp.claude.com",
|
||||
"gmail.mcp.claude.com",
|
||||
"gcal.mcp.claude.com"
|
||||
]
|
||||
},
|
||||
"tengu_byte_stream_idle_timeout_ms": 180000,
|
||||
"tengu_umber_petrel": false,
|
||||
"tengu_prism_ledger": false,
|
||||
"tengu_ccr_bundle_max_bytes": 104857600,
|
||||
"tengu_amber_sextant": true,
|
||||
"tengu_pewter_ledger": "OFF",
|
||||
"tengu_amber_flint": true,
|
||||
"tengu_disable_bypass_permissions_mode": false,
|
||||
"tengu_walrus_canteen": false,
|
||||
"tengu_ashen_kelp": true,
|
||||
"tengu_plugin_official_mkt_git_fallback": true,
|
||||
"tengu_max_version_config": {},
|
||||
"tengu_cobalt_lantern": true,
|
||||
"tengu_ultraplan_prompt_identifier": "visual_plan",
|
||||
"tengu_swann_brevity": "focused",
|
||||
"tengu_hazel_osprey": false,
|
||||
"tengu_slate_meadow": true,
|
||||
"tengu_amber_redwood2": "",
|
||||
"tengu_frond_boric": {},
|
||||
"tengu_slate_thimble": false,
|
||||
"tengu_slate_nexus": true,
|
||||
"tengu_chert_bezel": true,
|
||||
"tengu_streaming_tool_execution2": true,
|
||||
"tengu_event_watchdog_default_on": false,
|
||||
"tengu_auto_mode_config": {
|
||||
"enabled": "enabled",
|
||||
"twoStageClassifier": true
|
||||
},
|
||||
"tengu_grey_step2": {
|
||||
"enabled": true,
|
||||
"dialogTitle": "We recommend medium effort for Opus",
|
||||
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
|
||||
},
|
||||
"tengu_dune_wren": false,
|
||||
"tengu_cedar_lantern": true,
|
||||
"tengu_velvet_moth": 0.2,
|
||||
"tengu_harbor_ledger": [
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "discord"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "telegram"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "fakechat"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "imessage"
|
||||
}
|
||||
],
|
||||
"tengu_harbor": true,
|
||||
"tengu_amber_lynx": false,
|
||||
"tengu_doorbell_agave": false,
|
||||
"tengu_maple_tide": false,
|
||||
"tengu_fennel_kite": false,
|
||||
"tengu_collage_kaleidoscope": true,
|
||||
"tengu_file_write_optimization": true,
|
||||
"tengu_startup_notice": "",
|
||||
"tengu_mcp_retry_failed_remote": false,
|
||||
"tengu_session_memory": false,
|
||||
"tengu_flint_harbor_prompt": {
|
||||
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
|
||||
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
|
||||
"windowDays": 30
|
||||
},
|
||||
"tengu_slim_subagent_claudemd": true,
|
||||
"tengu_tangerine_ladder_boost": true,
|
||||
"tengu_chair_sermon": false,
|
||||
"tengu_gypsum_kite": true,
|
||||
"tengu_quartz_heron": false,
|
||||
"tengu_xterm_atlas_reset": true,
|
||||
"tengu-model-error-overrides": {
|
||||
"claude-fable-5": {
|
||||
"block": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access"
|
||||
}
|
||||
},
|
||||
"tengu_orchid_mantis_v2": true,
|
||||
"tengu-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_feedback_survey_config": {
|
||||
"minTimeBeforeFeedbackMs": 600000,
|
||||
"minTimeBetweenFeedbackMs": 43200000,
|
||||
"minTimeBetweenGlobalFeedbackMs": 43200000,
|
||||
"minUserTurnsBeforeFeedback": 5,
|
||||
"minUserTurnsBetweenFeedback": 25,
|
||||
"hideThanksAfterMs": 3000,
|
||||
"onForModels": [
|
||||
"*"
|
||||
],
|
||||
"probability": 0.05
|
||||
},
|
||||
"tengu_cork_lantern": false,
|
||||
"tengu_mint_lanes": false,
|
||||
"tengu_bridge_attestation_enforce_config": {
|
||||
"accept_level": "VERIFIED_BY_GATE",
|
||||
"accept_statuses": []
|
||||
},
|
||||
"tengu_marble_sandcastle": false,
|
||||
"tengu_bg_attach_stall_ms": 5000,
|
||||
"tengu_workout2": true,
|
||||
"tengu_orford_ness": false,
|
||||
"tengu_porch_bell_9f": "",
|
||||
"tengu_auto_mode_default_on": false,
|
||||
"tengu_birch_kettle": false,
|
||||
"tengu_classifier_summary_heuristic_emit": true,
|
||||
"tengu_cobalt_thicket": false,
|
||||
"tengu_destructive_command_warning": false,
|
||||
"tengu_cinder_plover": "",
|
||||
"tengu_cedar_halo": false,
|
||||
"tengu_sotto_voce": true,
|
||||
"tengu_sepia_moth": false,
|
||||
"tengu_cedar_sundial": false,
|
||||
"tengu_penguins_enabled": true,
|
||||
"tengu_quiet_basalt_echo": false,
|
||||
"tengu_ochre_hollow": true,
|
||||
"tengu_coral_beacon": true,
|
||||
"tengu_copper_thistle": false,
|
||||
"tengu_1p_event_batch_config": {
|
||||
"scheduledDelayMillis": 10000,
|
||||
"maxExportBatchSize": 400,
|
||||
"maxQueueSize": 8192,
|
||||
"path": "/api/event_logging/v2/batch"
|
||||
},
|
||||
"tengu_amber_wren": {
|
||||
"targetedRangeNudge": true,
|
||||
"maxTokens": 25000
|
||||
},
|
||||
"tengu_amber_prism": true,
|
||||
"tengu_cobalt_plinth": false,
|
||||
"tengu_silent_harbor": false,
|
||||
"tengu_chomp_inflection": true,
|
||||
"tengu_mcp_elicitation": true,
|
||||
"tengu_sm_config": {
|
||||
"minimumMessageTokensToInit": 150000,
|
||||
"minimumTokensBetweenUpdate": 40000,
|
||||
"toolCallsBetweenUpdates": 10
|
||||
},
|
||||
"tengu_bridge_min_version": {
|
||||
"minVersion": "2.1.70"
|
||||
},
|
||||
"tengu_kairos_input_needed_push": true,
|
||||
"tengu_quiet_harbor": false,
|
||||
"tengu_slate_wren": false,
|
||||
"tengu_tool_search_unsupported_models": [
|
||||
"claude-3-5-haiku",
|
||||
"claude-3-haiku"
|
||||
],
|
||||
"tengu_native_cursor": true,
|
||||
"tengu_orchid_mantis": false,
|
||||
"tengu_amber_lark": true,
|
||||
"tengu_shale_finch": true,
|
||||
"tengu_cedar_plume": false,
|
||||
"tengu_kairos_push_notifications": true,
|
||||
"tengu_marble_whisper2": true,
|
||||
"tengu_lichen_compass": false,
|
||||
"tengu_c4w_usage_limit_notifications_enabled": true,
|
||||
"tengu_scarf_coffee": false,
|
||||
"tengu_copper_bridge": true,
|
||||
"tengu_tool_pear": false,
|
||||
"tengu_claudeai_mcp_connectors": true,
|
||||
"tengu_ccr_post_turn_summary": false,
|
||||
"tengu_sedge_lantern": true,
|
||||
"tengu_feature_template": false,
|
||||
"tengu_harbor_prism": true,
|
||||
"tengu_cedar_inlet": "step",
|
||||
"tengu_flax_grouse": false,
|
||||
"tengu_event_sampling_config": {},
|
||||
"tengu_herring_clock": false,
|
||||
"tengu_quartz_vireo": "",
|
||||
"tengu_team_discovery": false,
|
||||
"tengu_gleaming_fair": true,
|
||||
"tengu_marble_anvil": true,
|
||||
"tengu_classifier_disabled_surfaces": "",
|
||||
"tengu_pewter_brook": false,
|
||||
"tengu_vscode_review_upsell": false,
|
||||
"claude_code_skills_dashboard_enabled_cli": false,
|
||||
"tengu_post_compact_survey": false,
|
||||
"tengu_reactive_compact_remote": false,
|
||||
"tengu_idle_amber_finch": false,
|
||||
"tengu_noreread_q7m_velvet": false,
|
||||
"tengu_ultraplan_config": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_scratch": false,
|
||||
"tengu_alder_compass": false,
|
||||
"tengu_olive_hinge": "",
|
||||
"tengu_shining_fractals": false,
|
||||
"tengu_maple_pier": false,
|
||||
"tengu_sessions_elevated_auth_enforcement": true,
|
||||
"tengu_turtle_carbon": true,
|
||||
"tengu_billiard_aviary": false,
|
||||
"tengu_cinder_almanac": true,
|
||||
"tengu_osprey_lantern": false,
|
||||
"tengu-top-of-feed-tip": {
|
||||
"tip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"color": "warning"
|
||||
},
|
||||
"tengu_cobalt_raccoon": true,
|
||||
"tengu_loud_sugary_rock": false,
|
||||
"tengu_willow_mode": "hint_v2",
|
||||
"tengu_blue_coaster": false,
|
||||
"tengu_snippet_save": false,
|
||||
"tengu_amber_lattice": {
|
||||
"plugins": [
|
||||
"security-guidance",
|
||||
"code-review",
|
||||
"commit-commands",
|
||||
"code-simplifier",
|
||||
"hookify",
|
||||
"feature-dev",
|
||||
"frontend-design",
|
||||
"pr-review-toolkit",
|
||||
"skill-creator",
|
||||
"plugin-dev",
|
||||
"agent-sdk-dev",
|
||||
"mcp-server-dev",
|
||||
"claude-code-setup",
|
||||
"claude-md-management",
|
||||
"playground",
|
||||
"ralph-loop",
|
||||
"explanatory-output-style",
|
||||
"learning-output-style",
|
||||
"clangd-lsp",
|
||||
"csharp-lsp",
|
||||
"gopls-lsp",
|
||||
"jdtls-lsp",
|
||||
"kotlin-lsp",
|
||||
"lua-lsp",
|
||||
"php-lsp",
|
||||
"pyright-lsp",
|
||||
"ruby-lsp",
|
||||
"rust-analyzer-lsp",
|
||||
"swift-lsp",
|
||||
"typescript-lsp"
|
||||
]
|
||||
},
|
||||
"tengu_slate_harbor_experiment": false,
|
||||
"tengu_velvet_ibis": {},
|
||||
"tengu_bridge_requires_action_details": true,
|
||||
"tengu_lapis_finch": true,
|
||||
"tengu_satin_quoll": {},
|
||||
"tengu_moth_copse": false,
|
||||
"tengu_silk_hinge": false,
|
||||
"tengu_surreal_dali": true,
|
||||
"tengu_cobalt_ridge": true,
|
||||
"tengu_flint_harbor": false,
|
||||
"tengu_plank_river_frost": "user_intent",
|
||||
"tengu_velvet_mallet_haiku": false,
|
||||
"tengu_velvet_mallet": false,
|
||||
"tengu_velvet_mallet_haiku_4_5": false,
|
||||
"tengu_velvet_hammer_falcon": false,
|
||||
"tengu_loud_sugary_rock2": false,
|
||||
"tengu_velvet_hammer_sonnet_4_5": false,
|
||||
"tengu_velvet_hammer_sonnet": false,
|
||||
"tengu_tab_read_sep": false,
|
||||
"tengu_quill_harbor": "acceptEdits",
|
||||
"tengu_velvet_hammer": false,
|
||||
"tengu_velvet_hammer_opus": false,
|
||||
"tengu_c4e_slash_upsell": true,
|
||||
"tengu_velvet_hammer_haiku_4_5": false,
|
||||
"tengu_feature_claudified_template": false,
|
||||
"tengu_slate_quill": true,
|
||||
"tengu_ax_screen_reader": false,
|
||||
"tengu_windows_credman": false,
|
||||
"tengu_basalt_tern": false,
|
||||
"tengu_velvet_mallet_opus": false,
|
||||
"tengu_velvet_hammer_haiku": false,
|
||||
"tengu_velvet_static": true,
|
||||
"tengu_velvet_mallet_sonnet": false,
|
||||
"tengu_soft_slate_nudge": "baseline",
|
||||
"tengu_lantern_hearth": "off",
|
||||
"tengu_velvet_mallet_falcon": false,
|
||||
"tengu_velvet_mallet_sonnet_4_5": false
|
||||
},
|
||||
"firstStartTime": "2026-06-05T19:39:28.542Z",
|
||||
"opusProMigrationComplete": true,
|
||||
"sonnet1m45MigrationComplete": true,
|
||||
"seenNotifications": {},
|
||||
"migrationVersion": 13,
|
||||
"userID": "9d89994d486a4884b8cf33372d8a4cd61ebf7d34009e9d3cbce9db24e2e971a4",
|
||||
"changelogLastFetched": 1781361371930,
|
||||
"autoUpdatesProtectedForNative": true,
|
||||
"claudeCodeFirstTokenDate": "2026-04-11T19:03:48.223040Z",
|
||||
"hasCompletedOnboarding": true,
|
||||
"lastOnboardingVersion": "2.1.165",
|
||||
"groveConfigCache": {
|
||||
"09792e21-2287-4348-b4d4-34cddbbfabc5": {
|
||||
"grove_enabled": true,
|
||||
"timestamp": 1781406640065
|
||||
}
|
||||
},
|
||||
"cachedExperimentFeatures": [
|
||||
"tengu_amber_prism",
|
||||
"tengu_basalt_spur",
|
||||
"tengu_cedar_inlet",
|
||||
"tengu_coral_beacon",
|
||||
"tengu_flint_harbor",
|
||||
"tengu_mcp_subagent_prompt",
|
||||
"tengu_ochre_hollow",
|
||||
"tengu_orchid_mantis_v2",
|
||||
"tengu_plank_river_frost",
|
||||
"tengu_read_dedup_killswitch"
|
||||
],
|
||||
"cachedGrowthBookFeaturesAt": 1781406639973,
|
||||
"lastReleaseNotesSeen": "2.1.177",
|
||||
"projects": {
|
||||
"/root": {
|
||||
"allowedTools": [],
|
||||
"mcpContextUris": [],
|
||||
"mcpServers": {},
|
||||
"enabledMcpjsonServers": [],
|
||||
"disabledMcpjsonServers": [],
|
||||
"hasTrustDialogAccepted": false,
|
||||
"projectOnboardingSeenCount": 3,
|
||||
"hasClaudeMdExternalIncludesApproved": false,
|
||||
"hasClaudeMdExternalIncludesWarningShown": false,
|
||||
"exampleFiles": [],
|
||||
"lastGracefulShutdown": true,
|
||||
"lastVersionBase": "2.1.177",
|
||||
"lastCost": 22.33646404999996,
|
||||
"lastAPIDuration": 5145196,
|
||||
"lastAPIDurationWithoutRetries": 5144336,
|
||||
"lastToolDuration": 506403,
|
||||
"lastDuration": 11598574,
|
||||
"lastLinesAdded": 652,
|
||||
"lastLinesRemoved": 392,
|
||||
"lastTotalInputTokens": 32237,
|
||||
"lastTotalOutputTokens": 290269,
|
||||
"lastTotalCacheCreationInputTokens": 1509902,
|
||||
"lastTotalCacheReadInputTokens": 44894244,
|
||||
"lastTotalWebSearchRequests": 0,
|
||||
"lastFpsAverage": 6.03,
|
||||
"lastFpsLow1Pct": 451.66,
|
||||
"lastModelUsage": {
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"inputTokens": 21206,
|
||||
"outputTokens": 30015,
|
||||
"cacheReadInputTokens": 2624502,
|
||||
"cacheCreationInputTokens": 791709,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 1.4233674499999998
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
"inputTokens": 11031,
|
||||
"outputTokens": 260254,
|
||||
"cacheReadInputTokens": 42269742,
|
||||
"cacheCreationInputTokens": 718193,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 20.913096599999978
|
||||
}
|
||||
},
|
||||
"lastSessionId": "685e6c5b-62c1-40bd-9cfd-2c9f7e15c50f",
|
||||
"lastSessionMetrics": {
|
||||
"frame_duration_ms_count": 69975,
|
||||
"frame_duration_ms_min": 0.06756199989467859,
|
||||
"frame_duration_ms_max": 100.53212600015104,
|
||||
"frame_duration_ms_avg": 0.663704399986351,
|
||||
"frame_duration_ms_p50": 0.4998550007585436,
|
||||
"frame_duration_ms_p95": 1.5134988494683035,
|
||||
"frame_duration_ms_p99": 2.4773533696774384,
|
||||
"pre_tool_hook_duration_ms_count": 655,
|
||||
"pre_tool_hook_duration_ms_min": 0,
|
||||
"pre_tool_hook_duration_ms_max": 12,
|
||||
"pre_tool_hook_duration_ms_avg": 0.1267175572519084,
|
||||
"pre_tool_hook_duration_ms_p50": 0,
|
||||
"pre_tool_hook_duration_ms_p95": 1,
|
||||
"pre_tool_hook_duration_ms_p99": 1,
|
||||
"hook_duration_ms_count": 465,
|
||||
"hook_duration_ms_min": 0,
|
||||
"hook_duration_ms_max": 22,
|
||||
"hook_duration_ms_avg": 0.3204301075268817,
|
||||
"hook_duration_ms_p50": 0,
|
||||
"hook_duration_ms_p95": 1,
|
||||
"hook_duration_ms_p99": 8.360000000000014
|
||||
},
|
||||
"hasCompletedProjectOnboarding": true
|
||||
}
|
||||
},
|
||||
"routineFiredWatermark": "2026-06-05T19:47:09.178Z",
|
||||
"penguinModeOrgEnabled": true,
|
||||
"closedIssuesLastChecked": 1781406639965,
|
||||
"passesEligibilityCache": {
|
||||
"4bb43199-0efc-4d5c-b552-79865cb0361b": {
|
||||
"eligible": true,
|
||||
"referral_code_details": {
|
||||
"code": "BeGGjphr1g",
|
||||
"campaign": "claude_code_guest_pass_a47c",
|
||||
"referral_link": "https://claude.ai/referral/BeGGjphr1g"
|
||||
},
|
||||
"referrer_reward": {
|
||||
"amount_minor_units": 1000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"remaining_passes": 3,
|
||||
"limit": 3,
|
||||
"share_link": "https://claude.ai/referral/BeGGjphr1g",
|
||||
"terms_url": "https://support.claude.com/en/articles/12875061-claude-code-guest-passes",
|
||||
"timestamp": 1781406640514
|
||||
}
|
||||
},
|
||||
"cachedExtraUsageDisabledReason": null,
|
||||
"passesUpsellSeenCount": 3,
|
||||
"hasVisitedPasses": false,
|
||||
"passesLastSeenRemaining": 3,
|
||||
"officialMarketplaceAutoInstallAttempted": true,
|
||||
"officialMarketplaceAutoInstalled": true,
|
||||
"tipLifetimeShownCounts": {
|
||||
"fotw-campaign-upsell": 6,
|
||||
"new-user-warmup": 2,
|
||||
"plan-mode-for-complex-tasks": 5,
|
||||
"memory-command": 2,
|
||||
"theme-command": 2,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 3,
|
||||
"enter-to-steer-in-relatime": 2,
|
||||
"todo-list": 2,
|
||||
"ide-upsell-external-terminal": 5,
|
||||
"install-github-app": 3,
|
||||
"install-slack-app": 3,
|
||||
"drag-and-drop-images": 2,
|
||||
"double-esc-code-restore": 2,
|
||||
"continue": 2,
|
||||
"shift-tab": 2,
|
||||
"image-paste": 1,
|
||||
"web-app": 2,
|
||||
"color-when-multi-clauding": 1,
|
||||
"custom-agents": 2,
|
||||
"remote-control": 2,
|
||||
"voice-mode": 2,
|
||||
"goal-command-nudge": 4,
|
||||
"guest-passes": 6,
|
||||
"feedback-command": 2,
|
||||
"frontend-design-plugin": 1,
|
||||
"permissions": 2,
|
||||
"rename-conversation": 1,
|
||||
"custom-commands": 1,
|
||||
"c4e-remote-sessions": 1,
|
||||
"subagent-fanout-nudge": 1,
|
||||
"no-flicker": 1
|
||||
},
|
||||
"feedbackSurveyState": {
|
||||
"lastShownTime": 1781411703066
|
||||
},
|
||||
"hasUsedBackslashReturn": true,
|
||||
"agentLastUsed": {
|
||||
"bg": 1780696781055
|
||||
},
|
||||
"remoteControlUpsellSeenCount": 3,
|
||||
"fullscreenUpsellSeenCount": 3,
|
||||
"lastShownEmergencyTip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"oauthAccount": {
|
||||
"accountUuid": "09792e21-2287-4348-b4d4-34cddbbfabc5",
|
||||
"emailAddress": "gmer4lfe@gmail.com",
|
||||
"organizationUuid": "4bb43199-0efc-4d5c-b552-79865cb0361b",
|
||||
"hasExtraUsageEnabled": true,
|
||||
"billingType": "stripe_subscription",
|
||||
"accountCreatedAt": "2026-04-03T21:52:35.642439Z",
|
||||
"subscriptionCreatedAt": "2026-04-11T13:14:49.905923Z",
|
||||
"ccOnboardingFlags": {},
|
||||
"claudeCodeTrialEndsAt": null,
|
||||
"claudeCodeTrialDurationDays": null,
|
||||
"seatTier": null,
|
||||
"displayName": "Gmer4Lfe",
|
||||
"organizationRole": "admin",
|
||||
"workspaceRole": null,
|
||||
"organizationName": "gmer4lfe@gmail.com's Organization",
|
||||
"organizationType": "claude_pro",
|
||||
"organizationRateLimitTier": "default_claude_ai",
|
||||
"userRateLimitTier": null
|
||||
},
|
||||
"clientDataCache": {
|
||||
"cedar_lagoon": {
|
||||
"claude-fable": true,
|
||||
"claude-mythos": true
|
||||
},
|
||||
"pewter_owl_tool": true,
|
||||
"pewter_owl_model": "claude-fable"
|
||||
},
|
||||
"additionalModelOptionsCache": [
|
||||
{
|
||||
"value": "claude-fable-5[1m]",
|
||||
"label": "Fable (disabled)",
|
||||
"description": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"additionalModelCostsCache": {}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"numStartups": 23,
|
||||
"installMethod": "native",
|
||||
"autoUpdates": false,
|
||||
"hasSeenTasksHint": true,
|
||||
"tipsHistory": {
|
||||
"fotw-campaign-upsell": 13,
|
||||
"new-user-warmup": 6,
|
||||
"plan-mode-for-complex-tasks": 22,
|
||||
"memory-command": 16,
|
||||
"theme-command": 21,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 11,
|
||||
"enter-to-steer-in-relatime": 21,
|
||||
"todo-list": 21,
|
||||
"ide-upsell-external-terminal": 19,
|
||||
"install-github-app": 22,
|
||||
"install-slack-app": 22,
|
||||
"drag-and-drop-images": 14,
|
||||
"double-esc-code-restore": 14,
|
||||
"continue": 14,
|
||||
"shift-tab": 15,
|
||||
"image-paste": 4,
|
||||
"web-app": 19,
|
||||
"color-when-multi-clauding": 6,
|
||||
"custom-agents": 21,
|
||||
"remote-control": 21,
|
||||
"voice-mode": 16,
|
||||
"goal-command-nudge": 16,
|
||||
"guest-passes": 22,
|
||||
"feedback-command": 22,
|
||||
"frontend-design-plugin": 6,
|
||||
"permissions": 22,
|
||||
"rename-conversation": 11,
|
||||
"custom-commands": 11,
|
||||
"c4e-remote-sessions": 18,
|
||||
"subagent-fanout-nudge": 18,
|
||||
"no-flicker": 19
|
||||
},
|
||||
"promptQueueUseCount": 44,
|
||||
"cachedGrowthBookFeatures": {
|
||||
"tengu_flint_harbor_share": false,
|
||||
"tengu_cinder_plover": "",
|
||||
"tengu_velvet_cascade": {},
|
||||
"tengu_post_compact_survey": false,
|
||||
"tengu_flint_harbor": false,
|
||||
"tengu_sedge_lantern": true,
|
||||
"tengu_harbor": true,
|
||||
"tengu_slim_subagent_claudemd": true,
|
||||
"tengu_cedar_plume": false,
|
||||
"tengu_bridge_poll_interval_config": {
|
||||
"poll_interval_ms_not_at_capacity": 2000,
|
||||
"poll_interval_ms_at_capacity": 600000,
|
||||
"heartbeat_interval_ms": 0,
|
||||
"multisession_poll_interval_ms_not_at_capacity": 5000,
|
||||
"multisession_poll_interval_ms_at_capacity": 60000,
|
||||
"multisession_poll_interval_ms_partial_capacity": 5000,
|
||||
"non_exclusive_heartbeat_interval_ms": 180000,
|
||||
"session_keepalive_interval_ms": 0,
|
||||
"session_keepalive_interval_v2_ms": 0
|
||||
},
|
||||
"tengu_bg_attach_stall_ms": 5000,
|
||||
"tengu_quiet_harbor": false,
|
||||
"tengu_mcp_singleton_unwrap": true,
|
||||
"tengu_sage_compass": {},
|
||||
"tengu_slate_moth": true,
|
||||
"tengu_event_watchdog_default_on": false,
|
||||
"tengu_claudeai_mcp_connectors": true,
|
||||
"tengu_feedback_survey_config": {
|
||||
"minTimeBeforeFeedbackMs": 600000,
|
||||
"minTimeBetweenFeedbackMs": 43200000,
|
||||
"minTimeBetweenGlobalFeedbackMs": 43200000,
|
||||
"minUserTurnsBeforeFeedback": 5,
|
||||
"minUserTurnsBetweenFeedback": 25,
|
||||
"hideThanksAfterMs": 3000,
|
||||
"onForModels": [
|
||||
"*"
|
||||
],
|
||||
"probability": 0.05
|
||||
},
|
||||
"tengu_loggia_carousel": false,
|
||||
"tengu_file_write_optimization": true,
|
||||
"tengu_sepia_moth": false,
|
||||
"tengu_harbor_willow": false,
|
||||
"tengu_amber_sextant": true,
|
||||
"tengu_event_sampling_config": {},
|
||||
"tengu_cedar_halo": false,
|
||||
"tengu_ember_trail": "0",
|
||||
"tengu_slate_meadow": true,
|
||||
"tengu_c4w_usage_limit_notifications_enabled": true,
|
||||
"tengu_lichen_compass": false,
|
||||
"tengu_osprey_lantern": false,
|
||||
"tengu_desktop_upsell_v2": {
|
||||
"enabled": false
|
||||
},
|
||||
"tengu_ccr_bridge": true,
|
||||
"tengu_drift_lantern": false,
|
||||
"tengu_herring_clock": false,
|
||||
"tengu_sm_config": {
|
||||
"minimumMessageTokensToInit": 150000,
|
||||
"minimumTokensBetweenUpdate": 40000,
|
||||
"toolCallsBetweenUpdates": 10
|
||||
},
|
||||
"tengu_feature_template": false,
|
||||
"tengu_bridge_attestation_enforce": false,
|
||||
"tengu_nimble_amber_prose": false,
|
||||
"tengu_destructive_command_warning": false,
|
||||
"tengu_ladder_mq7": false,
|
||||
"tengu_crimson_echo": {},
|
||||
"tengu-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_scratch": false,
|
||||
"tengu_session_memory": false,
|
||||
"tengu_orchid_mantis_v2": true,
|
||||
"tengu_prompt_cache_1h_config": {
|
||||
"allowlist": [
|
||||
"repl_main_thread*",
|
||||
"sdk",
|
||||
"auto_mode",
|
||||
"rolling_compact",
|
||||
"memdir_relevance",
|
||||
"agent_classifier",
|
||||
"prompt_suggestion",
|
||||
"away_summary",
|
||||
"extract_memories",
|
||||
"compact"
|
||||
]
|
||||
},
|
||||
"tengu_amber_redwood2": "",
|
||||
"tengu_kairos_cron": true,
|
||||
"tengu_marble_anvil": true,
|
||||
"tengu_billiard_aviary": false,
|
||||
"tengu_basalt_spur": false,
|
||||
"tengu_ochre_hollow": true,
|
||||
"tengu_maple_tide": false,
|
||||
"tengu_crimson_vector": false,
|
||||
"tengu_cedar_sundial": false,
|
||||
"tengu_skills_dashboard_enabled": false,
|
||||
"tengu_red_coaster": false,
|
||||
"tengu_good_survey_transcript_ask_config": {
|
||||
"probability": 0.5
|
||||
},
|
||||
"tengu_system_prompt_global_cache": true,
|
||||
"tengu_slate_kestrel": true,
|
||||
"tengu_harbor_prism": true,
|
||||
"tengu_disable_bypass_permissions_mode": false,
|
||||
"tengu_slate_ribbon": true,
|
||||
"tengu_1p_event_batch_config": {
|
||||
"scheduledDelayMillis": 10000,
|
||||
"maxExportBatchSize": 400,
|
||||
"maxQueueSize": 8192,
|
||||
"path": "/api/event_logging/v2/batch"
|
||||
},
|
||||
"tengu_cobalt_compass": true,
|
||||
"tengu_shining_fractals": false,
|
||||
"tengu_marble_sandcastle": false,
|
||||
"tengu_pewter_summit": true,
|
||||
"tengu_slate_finch": true,
|
||||
"tengu_kairos_loop_prompt": true,
|
||||
"tengu_version_config": {
|
||||
"minVersion": "1.0.24"
|
||||
},
|
||||
"tengu_miraculo_the_bard": false,
|
||||
"tengu_copper_fox": false,
|
||||
"tengu_marble_whisper2": true,
|
||||
"tengu_orchid_mantis": false,
|
||||
"tengu_willow_sentinel_ttl_hours": 1,
|
||||
"tengu_startup_notice": "",
|
||||
"tengu_amber_flint": true,
|
||||
"tengu_kairos_loop_dynamic": true,
|
||||
"tengu_walrus_canteen": false,
|
||||
"tengu_kairos_push_notifications": true,
|
||||
"tengu_maple_sundial": false,
|
||||
"tengu_malformed_tool_use_clean_retry": false,
|
||||
"tengu_kestrel_arch": "OFF",
|
||||
"tengu_gha_plugin_code_review": false,
|
||||
"tengu_ember_latch": true,
|
||||
"tengu_plum_vx3": true,
|
||||
"tengu_bridge_repl_v2": true,
|
||||
"tengu_cobalt_thicket": false,
|
||||
"tengu_orchid_trellis": false,
|
||||
"tengu_cobalt_lantern": true,
|
||||
"tengu_cloth_snorkel": false,
|
||||
"tengu_passport_quail": false,
|
||||
"tengu_amber_sentinel": true,
|
||||
"tengu_cork_lantern": false,
|
||||
"tengu_penguins_enabled": true,
|
||||
"tengu_velvet_ibis": {},
|
||||
"tengu_snippet_save": false,
|
||||
"tengu_maple_pier": false,
|
||||
"tengu_cobalt_raccoon": true,
|
||||
"tengu_ultraplan_prompt_identifier": "visual_plan",
|
||||
"tengu_copper_bridge": true,
|
||||
"tengu_willow_refresh_ttl_hours": 0,
|
||||
"tengu_ultraplan_timeout_seconds": 5400,
|
||||
"tengu_cedar_hollow_7m": {},
|
||||
"tengu_quartz_vireo": "",
|
||||
"claude_code_skills_dashboard_enabled_cli": false,
|
||||
"tengu_cork_m4q": true,
|
||||
"tengu_mocha_barista": true,
|
||||
"tengu_harbor_ledger": [
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "discord"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "telegram"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "fakechat"
|
||||
},
|
||||
{
|
||||
"marketplace": "claude-plugins-official",
|
||||
"plugin": "imessage"
|
||||
}
|
||||
],
|
||||
"tengu-model-error-overrides": {
|
||||
"claude-fable-5": {
|
||||
"block": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access"
|
||||
}
|
||||
},
|
||||
"tengu_pewter_lantern": false,
|
||||
"tengu_mcp_stateless_skip_init": true,
|
||||
"tengu_jade_anvil_4": false,
|
||||
"tengu_mcp_retry_failed_remote": false,
|
||||
"tengu_pewter_ledger": "OFF",
|
||||
"tengu_fg_left_arrow_agents": true,
|
||||
"tengu_lantern_spool": false,
|
||||
"tengu_auto_notice_once": true,
|
||||
"tengu_shale_finch": true,
|
||||
"tengu_immediate_model_command": false,
|
||||
"tengu_ccr_v2_send_events_cli": true,
|
||||
"tengu_compact_cache_prefix": true,
|
||||
"tengu_mcp_elicitation": true,
|
||||
"tengu_harbor_permissions": true,
|
||||
"tengu_sepia_cormorant": [],
|
||||
"tengu_grey_step2": {
|
||||
"enabled": true,
|
||||
"dialogTitle": "We recommend medium effort for Opus",
|
||||
"dialogDescription": "Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed."
|
||||
},
|
||||
"tengu_quartz_heron": false,
|
||||
"tengu_ccr_bridge_multi_session": true,
|
||||
"tengu_team_discovery": false,
|
||||
"tengu_otk_slot_v1": false,
|
||||
"tengu_blue_coaster": false,
|
||||
"tengu_vscode_onboarding": false,
|
||||
"tengu_amber_lynx": false,
|
||||
"tengu_cinder_almanac": true,
|
||||
"tengu_mint_lanes": false,
|
||||
"tengu_max_version_config": {},
|
||||
"tengu_basalt_sundial": false,
|
||||
"tengu_copper_thistle": false,
|
||||
"tengu_saffron_lattice": {
|
||||
"enabled": false,
|
||||
"planLimitsEndDate": "2026-06-22T10:00:00Z",
|
||||
"hideRateLimitsDescription": true
|
||||
},
|
||||
"tengu_penguin_mode_promo": {
|
||||
"discountPercent": 0,
|
||||
"endDate": "Feb 16"
|
||||
},
|
||||
"tengu_bridge_requires_action_details": true,
|
||||
"tengu_walnut_prism": false,
|
||||
"tengu_amber_rokovoko": 0.2,
|
||||
"tengu_loud_sugary_rock": false,
|
||||
"tengu_steady_lantern": false,
|
||||
"tengu_lapis_anchor": "off",
|
||||
"tengu_amber_wren": {
|
||||
"targetedRangeNudge": true,
|
||||
"maxTokens": 25000
|
||||
},
|
||||
"tengu_ccr_post_turn_summary": false,
|
||||
"tengu_sessions_elevated_auth_enforcement": true,
|
||||
"tengu_hazel_osprey_floor": 75000,
|
||||
"tengu_flax_grouse": false,
|
||||
"tengu_dune_wren": false,
|
||||
"tengu_hawthorn_window": 200000,
|
||||
"tengu_slate_wren": false,
|
||||
"tengu_permission_friction": true,
|
||||
"tengu_amber_anchor": false,
|
||||
"tengu_fgts": true,
|
||||
"tengu_chomp_inflection": true,
|
||||
"tengu_birthday_hat": false,
|
||||
"tengu_olive_hinge": "",
|
||||
"tengu_brick_follow": false,
|
||||
"tengu_doorbell_agave": false,
|
||||
"tengu_sage_compass2": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_lilac_loom": {},
|
||||
"tengu_compass_dial": true,
|
||||
"tengu_sparrow_ledger": false,
|
||||
"tengu_pewter_brook": false,
|
||||
"tengu_prompt_cache_diagnostics": true,
|
||||
"tengu_chert_bezel": true,
|
||||
"tengu_birch_compass": true,
|
||||
"tengu_timber_lark": "copy_a",
|
||||
"tengu_coral_beacon": true,
|
||||
"tengu_worktree_mode": true,
|
||||
"tengu_turtle_carbon": true,
|
||||
"tengu_workout2": true,
|
||||
"tengu_vellum_siding": false,
|
||||
"tengu_vscode_review_upsell": false,
|
||||
"tengu_cedar_lantern": true,
|
||||
"tengu_kairos_cron_durable": false,
|
||||
"tengu_anchor_tide": true,
|
||||
"tengu_cobalt_ridge": true,
|
||||
"tengu_bridge_repl_v2_config": {
|
||||
"init_retry_max_attempts": 3,
|
||||
"init_retry_base_delay_ms": 500,
|
||||
"init_retry_jitter_fraction": 0.25,
|
||||
"init_retry_max_delay_ms": 4000,
|
||||
"http_timeout_ms": 10000,
|
||||
"uuid_dedup_buffer_size": 2000,
|
||||
"heartbeat_interval_ms": 20000,
|
||||
"heartbeat_jitter_fraction": 0.1,
|
||||
"token_refresh_buffer_ms": 600000,
|
||||
"teardown_archive_timeout_ms": 1500,
|
||||
"connect_timeout_ms": 15000,
|
||||
"min_version": "2.1.70",
|
||||
"should_show_app_upgrade_message": false
|
||||
},
|
||||
"tengu_malort_pedway": {
|
||||
"enabled": true,
|
||||
"pixelValidation": false,
|
||||
"clipboardPasteMultiline": true,
|
||||
"screenshotFilter": true,
|
||||
"mouseAnimation": true,
|
||||
"hideBeforeAction": true,
|
||||
"autoTargetDisplay": false,
|
||||
"coordinateMode": "pixels"
|
||||
},
|
||||
"tengu_tool_search_unsupported_models": [
|
||||
"claude-3-5-haiku",
|
||||
"claude-3-haiku"
|
||||
],
|
||||
"tengu_tussock_oriole": false,
|
||||
"tengu_reactive_compact_remote": false,
|
||||
"tengu_ccr_bundle_seed_enabled": true,
|
||||
"tengu_silent_harbor": false,
|
||||
"tengu_plank_river_frost": "user_intent",
|
||||
"tengu_idle_amber_finch": false,
|
||||
"tengu_xterm_atlas_reset": true,
|
||||
"tengu_flint_harbor_prompt": {
|
||||
"prompt": "You are helping a power user generate an onboarding guide for teammates who are new to Claude Code. The guide will live in the team's onboarding docs and can be pasted into Claude for an interactive walkthrough.\n\nYou're co-authoring this with them — collaborative and helpful, like a teammate who's done this before and is happy to share.\n\n## Usage data (last {{WINDOW_DAYS}} days)\n\nThis was scanned from the guide creator's local Claude Code transcripts:\n\n```json\n{{USAGE_DATA}}\n```\n\n## Your task\n\nBefore anything else — including before thinking through the classification — output exactly this line as your first visible text:\n\n> Looking at how you've used Claude over the last {{WINDOW_DAYS}} days to put together an onboarding guide for teammates new to Claude Code.\n\nThis must come before any extended thinking about session descriptors. The guide creator is staring at a blank screen until you do. Classification is step 2, not step 1.\n\nGenerate the guide immediately, then ask for revisions. Don't wait for answers first — it's easier for the guide creator to edit a concrete draft than answer abstract questions.\n\n1. **Output the acknowledgment line above.** No thinking, no classification, no tool calls before this. One line, then move on.\n\n2. **Derive the work-type breakdown.** Read the `sessionDescriptors` array — each entry describes one session via its title, any linked code reviews (`prNumbers`), and first user message. Classify each session into one of these task types:\n\n - **build_feature** — new functionality, scripts, tools, config/CI/env setup\n - **debug_fix** — investigating and fixing bugs\n - **improve_quality** — refactoring, tests, cleanup, code review\n - **analyze_data** — queries, metrics, number crunching\n - **plan_design** — architecture, approach, strategy, understanding unfamiliar code, design review\n - **prototype** — spikes, POCs, throwaway exploration\n - **write_docs** — PRDs, RFCs, READMEs, design docs, copy/doc review\n\n Categories describe the *type of task*, not the project or domain — a teammate on any project should recognize them. Review sessions belong with whatever's being reviewed: code review is improve_quality, doc review is write_docs, design review is plan_design. Most sessions fit the list; only invent a new category if it's genuinely a different type of task. Pick the top 3-5 with rough percentages. First messages alone are usually enough; titles and code-review links are enrichment. If first messages are uninformative, use tool and MCP counts as a weak hint. If there are ~0 sessions, leave the breakdown as a TODO.\n\n In the rendered guide, display categories with spaces and title case (e.g. \"Build Feature\" not \"build_feature\").\n\n3. **Gather the remaining pieces.** For repos, start with `currentRepo` and check the workspace for sibling repo directories. For MCP server setup, use each entry's `name` (and `urlOrigin` where present) to infer what the server does and how a teammate would get access. Leave the Team Tips and Get Started sections as TODO placeholders — you'll ask for these in Review and fill them in after.\n\n4. **Write the guide to `ONBOARDING.md`** following this template:\n\n```\n{{GUIDE_TEMPLATE}}\n```\n\n Fill in real numbers from the usage data (not placeholders). Use `generatedBy` for the name; if it's missing, omit the name. Ascii bar charts: `█` for filled, `░` for empty, 20 chars wide. Keep the HTML comment instruction at the bottom exactly as shown.\n\n5. **Render the guide in a code block, then close out the first turn.** You're co-authoring this guide with the guide creator — frame the follow-up as collaboration, not corrections.\n\n After the code block, add a `---` horizontal rule and a `**Review**` heading so the guide is visually separated from your questions. Under the heading, number these three questions:\n\n 1. \"I went with '[X]' for the team name — let me know if that sounds right.\" (or if you couldn't tell: \"What's the team name? I'll add it in.\")\n 2. Is there a starter task for someone new to Claude Code? (ticket or doc link — optional)\n 3. Any team tips you'd tell a new teammate that aren't already in CLAUDE.md?\n\n After they answer, update `ONBOARDING.md` with their team name, tips, and starter task. Then close with this exact line (not numbered, not paraphrased):\n\n Saved to `ONBOARDING.md`. Drop it in your team docs and channels — when a new teammate pastes it into Claude Code, they get a guided onboarding tour from there.\n\n Apply any edits they come back with to the file.",
|
||||
"guideTemplate": "# Welcome to [Team Name]\n\n## How We Use Claude\n\nBased on [name]'s usage over the last [N] days:\n\nWork Type Breakdown:\n [Category 1] [ascii bar] [N]%\n [Category 2] [ascii bar] [N]%\n [Category 3] [ascii bar] [N]%\n ...\n\nTop Skills & Commands:\n [/command] [ascii bar] [N]x/month\n ...\n\nTop MCP Servers:\n [Server] [ascii bar] [N] calls\n ...\n\n## Your Setup Checklist\n\n### Codebases\n- [ ] [repo-name] — [repo url]\n...\n\n### MCP Servers to Activate\n- [ ] [Server] — [what it's for]. [How to get access]\n...\n\n### Skills to Know About\n- [/command] — [what it does, when the team uses it]\n...\n\n## Team Tips\n\n_TODO_\n\n## Get Started\n\n_TODO_\n\n<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the\nteam uses Claude Code. You're their onboarding buddy — warm, conversational,\nnot lecture-y.\n\nOpen with a warm welcome — include the team name from the title. Then: \"Your\nteammate uses Claude Code for [list all the work types]. Let's get you started.\"\n\nCheck what's already in place against everything under Setup Checklist\n(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead\nwith what they already have. One sentence per item, all in one message.\n\nTell them you'll help with setup, cover the actionable team tips, then the\nstarter task (if there is one). Offer to start with the first unchecked item,\nget their go-ahead, then work through the rest one by one.\n\nAfter setup, walk them through the remaining sections — offer to help where you\ncan (e.g. link to channels), and just surface the purely informational bits.\n\nDon't invent sections or summaries that aren't in the guide. The stats are the\nguide creator's personal usage data — don't extrapolate them into a \"team\nworkflow\" narrative. -->",
|
||||
"windowDays": 30
|
||||
},
|
||||
"tengu_native_cursor": true,
|
||||
"tengu_pewter_lark": "off",
|
||||
"tengu_streaming_tool_execution2": true,
|
||||
"tengu_cobalt_plinth": false,
|
||||
"tengu_porch_bell_9f": "",
|
||||
"tengu_marble_whisper": true,
|
||||
"tengu_classifier_summary_heuristic_emit": true,
|
||||
"tengu_willow_mode": "hint_v2",
|
||||
"tengu_birch_kettle": false,
|
||||
"tengu_bridge_min_version": {
|
||||
"minVersion": "2.1.70"
|
||||
},
|
||||
"tengu_classifier_disabled_surfaces": "",
|
||||
"tengu_cedar_inlet": "step",
|
||||
"tengu_velvet_moth": 0.2,
|
||||
"tengu_auto_mode_default_on": false,
|
||||
"tengu_workflows_enabled": true,
|
||||
"tengu_tangerine_ladder_boost": true,
|
||||
"tengu_gypsum_kite": true,
|
||||
"tengu_gleaming_fair": true,
|
||||
"tengu_noreread_q7m_velvet": false,
|
||||
"tengu_crystal_beam": {
|
||||
"budgetTokens": 0
|
||||
},
|
||||
"tengu_quiet_slate_wren": false,
|
||||
"tengu_vscode_feedback_survey": true,
|
||||
"tengu_sotto_voce": true,
|
||||
"tengu_slate_harrier": "off",
|
||||
"tengu_tool_pear": false,
|
||||
"tengu_surreal_dali": true,
|
||||
"tengu_collage_kaleidoscope": true,
|
||||
"tengu_pewter_kestrel": {
|
||||
"global": 50000,
|
||||
"Bash": 30000,
|
||||
"PowerShell": 30000,
|
||||
"Grep": 20000,
|
||||
"Snip": 1000,
|
||||
"StrReplaceBasedEditTool": 30000,
|
||||
"BashSearchTool": 20000
|
||||
},
|
||||
"tengu_slate_thimble": false,
|
||||
"tengu_negative_interaction_transcript_ask_config": {
|
||||
"probability": 0
|
||||
},
|
||||
"tengu_moss_anchor": false,
|
||||
"tengu_mcp_local_oauth_blocked_hosts": {
|
||||
"hosts": [
|
||||
"microsoft365.mcp.claude.com",
|
||||
"gmail.mcp.claude.com",
|
||||
"gcal.mcp.claude.com"
|
||||
]
|
||||
},
|
||||
"tengu_bridge_attestation_enforce_config": {
|
||||
"accept_level": "VERIFIED_BY_GATE",
|
||||
"accept_statuses": []
|
||||
},
|
||||
"tengu_lapis_thicket": false,
|
||||
"tengu_auto_mode_config": {
|
||||
"enabled": "enabled",
|
||||
"twoStageClassifier": true
|
||||
},
|
||||
"tengu_basalt_meadow": true,
|
||||
"tengu_byte_stream_idle_timeout_ms": 180000,
|
||||
"tengu_lapis_finch": true,
|
||||
"tengu_prism_ledger": false,
|
||||
"tengu_prompt_suggestion": true,
|
||||
"tengu_react_vulnerability_warning": false,
|
||||
"tengu_amber_prism": true,
|
||||
"tengu_plugin_official_mkt_git_fallback": true,
|
||||
"tengu_cobalt_wren": false,
|
||||
"tengu_coral_fern": false,
|
||||
"tengu_log_datadog_events": true,
|
||||
"tengu_amber_heron": false,
|
||||
"tengu_saffron_anchor": true,
|
||||
"tengu_tern_alloy": "copy_a",
|
||||
"tengu_gouda_loop": true,
|
||||
"tengu_dunwich_bell": false,
|
||||
"tengu_mcp_subagent_prompt": true,
|
||||
"tengu_quiet_basalt_echo": false,
|
||||
"tengu_sedge_lantern_holdback": false,
|
||||
"tengu_garnet_finch": false,
|
||||
"tengu_chair_sermon": false,
|
||||
"tengu_umber_petrel": false,
|
||||
"tengu_bramble_lintel": 7,
|
||||
"tengu_sub_nomdrep_q7k": true,
|
||||
"tengu_swann_brevity": "focused",
|
||||
"tengu_marble_lark": false,
|
||||
"tengu_scarf_coffee": false,
|
||||
"tengu_bridge_poll_interval_ms": 0,
|
||||
"tengu_moth_copse": false,
|
||||
"tengu_bad_survey_transcript_ask_config": {
|
||||
"probability": 1
|
||||
},
|
||||
"tengu_desktop_upsell": {
|
||||
"enable_shortcut_tip": true,
|
||||
"enable_startup_dialog": false
|
||||
},
|
||||
"tengu_agent_list_attach": true,
|
||||
"tengu_amber_lark": true,
|
||||
"tengu_slate_siskin": {
|
||||
"enabled": false,
|
||||
"timeoutMs": 8000,
|
||||
"throttleMs": 30000,
|
||||
"summaryLineThreshold": 5
|
||||
},
|
||||
"tengu_tide_elm": "off",
|
||||
"tengu_alder_compass": false,
|
||||
"tengu-fable-off-switch": {
|
||||
"activated": false
|
||||
},
|
||||
"tengu_hazel_osprey": false,
|
||||
"tengu_cobalt_heron": true,
|
||||
"tengu_code_diff_cli": true,
|
||||
"tengu_trace_lantern": false,
|
||||
"tengu_silk_hinge": false,
|
||||
"tengu_amber_lattice": {
|
||||
"plugins": [
|
||||
"security-guidance",
|
||||
"code-review",
|
||||
"commit-commands",
|
||||
"code-simplifier",
|
||||
"hookify",
|
||||
"feature-dev",
|
||||
"frontend-design",
|
||||
"pr-review-toolkit",
|
||||
"skill-creator",
|
||||
"plugin-dev",
|
||||
"agent-sdk-dev",
|
||||
"mcp-server-dev",
|
||||
"claude-code-setup",
|
||||
"claude-md-management",
|
||||
"playground",
|
||||
"ralph-loop",
|
||||
"explanatory-output-style",
|
||||
"learning-output-style",
|
||||
"clangd-lsp",
|
||||
"csharp-lsp",
|
||||
"gopls-lsp",
|
||||
"jdtls-lsp",
|
||||
"kotlin-lsp",
|
||||
"lua-lsp",
|
||||
"php-lsp",
|
||||
"pyright-lsp",
|
||||
"ruby-lsp",
|
||||
"rust-analyzer-lsp",
|
||||
"swift-lsp",
|
||||
"typescript-lsp"
|
||||
]
|
||||
},
|
||||
"tengu_willow_census_ttl_hours": 24,
|
||||
"tengu_fennel_kite": false,
|
||||
"tengu_orford_ness": false,
|
||||
"tengu_read_dedup_killswitch": false,
|
||||
"tengu_onyx_plover": {
|
||||
"enabled": false,
|
||||
"minHours": 24,
|
||||
"minSessions": 3,
|
||||
"remoteEnabled": false
|
||||
},
|
||||
"tengu_kairos_input_needed_push": true,
|
||||
"tengu_fennel_kite_model": "",
|
||||
"tengu_ccr_bundle_max_bytes": 104857600,
|
||||
"tengu-top-of-feed-tip": {
|
||||
"tip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"color": "warning"
|
||||
},
|
||||
"tengu_copper_wren": false,
|
||||
"tengu_frond_boric": {},
|
||||
"tengu_satin_quoll": {},
|
||||
"tengu_hawthorn_steeple": false,
|
||||
"tengu_review_bughunter_config": {
|
||||
"fleet_size": 5,
|
||||
"max_duration_minutes": 10,
|
||||
"agent_timeout_seconds": 600,
|
||||
"total_wallclock_minutes": 22,
|
||||
"model": "claude-opus-4-7",
|
||||
"cost_note": "$5-$25",
|
||||
"duration_note": "~5-10 min",
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_slate_nexus": true,
|
||||
"tengu_keybinding_customization_release": true,
|
||||
"tengu_canary": {},
|
||||
"tengu_classifier_summary_llm_emit": true,
|
||||
"tengu_ultraplan_config": {
|
||||
"enabled": true
|
||||
},
|
||||
"tengu_vellum_lantern": false,
|
||||
"tengu_slate_harbor_experiment": false,
|
||||
"tengu_ashen_kelp": true,
|
||||
"tengu_soft_slate_nudge": "baseline",
|
||||
"tengu_velvet_hammer_haiku_4_5": false,
|
||||
"tengu_velvet_hammer_haiku": false,
|
||||
"tengu_velvet_static": true,
|
||||
"tengu_velvet_mallet_opus": false,
|
||||
"tengu_velvet_hammer_sonnet_4_5": false,
|
||||
"tengu_c4e_slash_upsell": true,
|
||||
"tengu_velvet_mallet_sonnet": false,
|
||||
"tengu_loud_sugary_rock2": false,
|
||||
"tengu_windows_credman": false,
|
||||
"tengu_velvet_hammer": false,
|
||||
"tengu_lantern_hearth": "off",
|
||||
"tengu_velvet_mallet_falcon": false,
|
||||
"tengu_ax_screen_reader": false,
|
||||
"tengu_velvet_mallet": false,
|
||||
"tengu_velvet_hammer_sonnet": false,
|
||||
"tengu_velvet_mallet_haiku": false,
|
||||
"tengu_velvet_mallet_sonnet_4_5": false,
|
||||
"tengu_velvet_hammer_opus": false,
|
||||
"tengu_velvet_mallet_haiku_4_5": false,
|
||||
"tengu_tab_read_sep": false,
|
||||
"tengu_velvet_hammer_falcon": false,
|
||||
"tengu_feature_claudified_template": false,
|
||||
"tengu_quill_harbor": "acceptEdits",
|
||||
"tengu_slate_quill": true,
|
||||
"tengu_basalt_tern": false
|
||||
},
|
||||
"firstStartTime": "2026-06-05T19:39:28.542Z",
|
||||
"opusProMigrationComplete": true,
|
||||
"sonnet1m45MigrationComplete": true,
|
||||
"seenNotifications": {},
|
||||
"migrationVersion": 13,
|
||||
"userID": "9d89994d486a4884b8cf33372d8a4cd61ebf7d34009e9d3cbce9db24e2e971a4",
|
||||
"changelogLastFetched": 1781361371930,
|
||||
"autoUpdatesProtectedForNative": true,
|
||||
"claudeCodeFirstTokenDate": "2026-04-11T19:03:48.223040Z",
|
||||
"hasCompletedOnboarding": true,
|
||||
"lastOnboardingVersion": "2.1.165",
|
||||
"groveConfigCache": {
|
||||
"09792e21-2287-4348-b4d4-34cddbbfabc5": {
|
||||
"grove_enabled": true,
|
||||
"timestamp": 1781406640065
|
||||
}
|
||||
},
|
||||
"cachedExperimentFeatures": [
|
||||
"tengu_amber_prism",
|
||||
"tengu_basalt_spur",
|
||||
"tengu_cedar_inlet",
|
||||
"tengu_coral_beacon",
|
||||
"tengu_flint_harbor",
|
||||
"tengu_mcp_subagent_prompt",
|
||||
"tengu_ochre_hollow",
|
||||
"tengu_orchid_mantis_v2",
|
||||
"tengu_plank_river_frost",
|
||||
"tengu_read_dedup_killswitch"
|
||||
],
|
||||
"cachedGrowthBookFeaturesAt": 1781447286954,
|
||||
"lastReleaseNotesSeen": "2.1.177",
|
||||
"projects": {
|
||||
"/root": {
|
||||
"allowedTools": [],
|
||||
"mcpContextUris": [],
|
||||
"mcpServers": {},
|
||||
"enabledMcpjsonServers": [],
|
||||
"disabledMcpjsonServers": [],
|
||||
"hasTrustDialogAccepted": false,
|
||||
"projectOnboardingSeenCount": 3,
|
||||
"hasClaudeMdExternalIncludesApproved": false,
|
||||
"hasClaudeMdExternalIncludesWarningShown": false,
|
||||
"exampleFiles": [],
|
||||
"lastGracefulShutdown": false,
|
||||
"lastVersionBase": "2.1.177",
|
||||
"lastCost": 22.33646404999996,
|
||||
"lastAPIDuration": 5145196,
|
||||
"lastAPIDurationWithoutRetries": 5144336,
|
||||
"lastToolDuration": 506403,
|
||||
"lastDuration": 11598574,
|
||||
"lastLinesAdded": 652,
|
||||
"lastLinesRemoved": 392,
|
||||
"lastTotalInputTokens": 32237,
|
||||
"lastTotalOutputTokens": 290269,
|
||||
"lastTotalCacheCreationInputTokens": 1509902,
|
||||
"lastTotalCacheReadInputTokens": 44894244,
|
||||
"lastTotalWebSearchRequests": 0,
|
||||
"lastFpsAverage": 6.03,
|
||||
"lastFpsLow1Pct": 451.66,
|
||||
"lastModelUsage": {
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"inputTokens": 21206,
|
||||
"outputTokens": 30015,
|
||||
"cacheReadInputTokens": 2624502,
|
||||
"cacheCreationInputTokens": 791709,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 1.4233674499999998
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
"inputTokens": 11031,
|
||||
"outputTokens": 260254,
|
||||
"cacheReadInputTokens": 42269742,
|
||||
"cacheCreationInputTokens": 718193,
|
||||
"webSearchRequests": 0,
|
||||
"costUSD": 20.913096599999978
|
||||
}
|
||||
},
|
||||
"lastSessionId": "685e6c5b-62c1-40bd-9cfd-2c9f7e15c50f",
|
||||
"lastSessionMetrics": {
|
||||
"frame_duration_ms_count": 69975,
|
||||
"frame_duration_ms_min": 0.06756199989467859,
|
||||
"frame_duration_ms_max": 100.53212600015104,
|
||||
"frame_duration_ms_avg": 0.663704399986351,
|
||||
"frame_duration_ms_p50": 0.4998550007585436,
|
||||
"frame_duration_ms_p95": 1.5134988494683035,
|
||||
"frame_duration_ms_p99": 2.4773533696774384,
|
||||
"pre_tool_hook_duration_ms_count": 655,
|
||||
"pre_tool_hook_duration_ms_min": 0,
|
||||
"pre_tool_hook_duration_ms_max": 12,
|
||||
"pre_tool_hook_duration_ms_avg": 0.1267175572519084,
|
||||
"pre_tool_hook_duration_ms_p50": 0,
|
||||
"pre_tool_hook_duration_ms_p95": 1,
|
||||
"pre_tool_hook_duration_ms_p99": 1,
|
||||
"hook_duration_ms_count": 465,
|
||||
"hook_duration_ms_min": 0,
|
||||
"hook_duration_ms_max": 22,
|
||||
"hook_duration_ms_avg": 0.3204301075268817,
|
||||
"hook_duration_ms_p50": 0,
|
||||
"hook_duration_ms_p95": 1,
|
||||
"hook_duration_ms_p99": 8.360000000000014
|
||||
},
|
||||
"hasCompletedProjectOnboarding": true
|
||||
}
|
||||
},
|
||||
"routineFiredWatermark": "2026-06-05T19:47:09.178Z",
|
||||
"penguinModeOrgEnabled": true,
|
||||
"closedIssuesLastChecked": 1781406639965,
|
||||
"passesEligibilityCache": {
|
||||
"4bb43199-0efc-4d5c-b552-79865cb0361b": {
|
||||
"eligible": true,
|
||||
"referral_code_details": {
|
||||
"code": "BeGGjphr1g",
|
||||
"campaign": "claude_code_guest_pass_a47c",
|
||||
"referral_link": "https://claude.ai/referral/BeGGjphr1g"
|
||||
},
|
||||
"referrer_reward": {
|
||||
"amount_minor_units": 1000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"remaining_passes": 3,
|
||||
"limit": 3,
|
||||
"share_link": "https://claude.ai/referral/BeGGjphr1g",
|
||||
"terms_url": "https://support.claude.com/en/articles/12875061-claude-code-guest-passes",
|
||||
"timestamp": 1781406640514
|
||||
}
|
||||
},
|
||||
"cachedExtraUsageDisabledReason": null,
|
||||
"passesUpsellSeenCount": 3,
|
||||
"hasVisitedPasses": false,
|
||||
"passesLastSeenRemaining": 3,
|
||||
"officialMarketplaceAutoInstallAttempted": true,
|
||||
"officialMarketplaceAutoInstalled": true,
|
||||
"tipLifetimeShownCounts": {
|
||||
"fotw-campaign-upsell": 6,
|
||||
"new-user-warmup": 2,
|
||||
"plan-mode-for-complex-tasks": 5,
|
||||
"memory-command": 2,
|
||||
"theme-command": 2,
|
||||
"colorterm-truecolor": 1,
|
||||
"status-line": 1,
|
||||
"prompt-queue": 3,
|
||||
"enter-to-steer-in-relatime": 2,
|
||||
"todo-list": 2,
|
||||
"ide-upsell-external-terminal": 5,
|
||||
"install-github-app": 3,
|
||||
"install-slack-app": 3,
|
||||
"drag-and-drop-images": 2,
|
||||
"double-esc-code-restore": 2,
|
||||
"continue": 2,
|
||||
"shift-tab": 2,
|
||||
"image-paste": 1,
|
||||
"web-app": 2,
|
||||
"color-when-multi-clauding": 1,
|
||||
"custom-agents": 2,
|
||||
"remote-control": 2,
|
||||
"voice-mode": 2,
|
||||
"goal-command-nudge": 4,
|
||||
"guest-passes": 6,
|
||||
"feedback-command": 2,
|
||||
"frontend-design-plugin": 1,
|
||||
"permissions": 2,
|
||||
"rename-conversation": 1,
|
||||
"custom-commands": 1,
|
||||
"c4e-remote-sessions": 1,
|
||||
"subagent-fanout-nudge": 1,
|
||||
"no-flicker": 1
|
||||
},
|
||||
"feedbackSurveyState": {
|
||||
"lastShownTime": 1781411703066
|
||||
},
|
||||
"hasUsedBackslashReturn": true,
|
||||
"agentLastUsed": {
|
||||
"bg": 1780696781055
|
||||
},
|
||||
"remoteControlUpsellSeenCount": 3,
|
||||
"fullscreenUpsellSeenCount": 3,
|
||||
"lastShownEmergencyTip": "Claude Fable 5 is currently unavailable. Please use Opus 4.8 or another available model. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"oauthAccount": {
|
||||
"accountUuid": "09792e21-2287-4348-b4d4-34cddbbfabc5",
|
||||
"emailAddress": "gmer4lfe@gmail.com",
|
||||
"organizationUuid": "4bb43199-0efc-4d5c-b552-79865cb0361b",
|
||||
"hasExtraUsageEnabled": true,
|
||||
"billingType": "stripe_subscription",
|
||||
"accountCreatedAt": "2026-04-03T21:52:35.642439Z",
|
||||
"subscriptionCreatedAt": "2026-04-11T13:14:49.905923Z",
|
||||
"ccOnboardingFlags": {},
|
||||
"claudeCodeTrialEndsAt": null,
|
||||
"claudeCodeTrialDurationDays": null,
|
||||
"seatTier": null,
|
||||
"displayName": "Gmer4Lfe",
|
||||
"organizationRole": "admin",
|
||||
"workspaceRole": null,
|
||||
"organizationName": "gmer4lfe@gmail.com's Organization",
|
||||
"organizationType": "claude_pro",
|
||||
"organizationRateLimitTier": "default_claude_ai",
|
||||
"userRateLimitTier": null
|
||||
},
|
||||
"clientDataCache": {
|
||||
"cedar_lagoon": {
|
||||
"claude-fable": true,
|
||||
"claude-mythos": true
|
||||
},
|
||||
"pewter_owl_tool": true,
|
||||
"pewter_owl_model": "claude-fable"
|
||||
},
|
||||
"additionalModelOptionsCache": [
|
||||
{
|
||||
"value": "claude-fable-5[1m]",
|
||||
"label": "Fable (disabled)",
|
||||
"description": "Claude Fable 5 is currently unavailable. Learn more: https://www.anthropic.com/news/fable-mythos-access",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"additionalModelCostsCache": {}
|
||||
}
|
||||
Vendored
+4385
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"proto": 1,
|
||||
"supervisorPid": 64919,
|
||||
"updatedAt": 1780720722982,
|
||||
"workers": {}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
#!/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, Unraid API key
|
||||
# EMBY container name, URL, API key
|
||||
# JELLYFIN container name, URL, API key
|
||||
# GITEA API token for SSH key registration
|
||||
# NOTIFICATIONS Discord webhook
|
||||
#
|
||||
# ── PARTNERSHIP ────────────────────────────────────────────────────────────────────────────
|
||||
# PARTNERSHIP auth containers, backup paths, emby provisioning
|
||||
#
|
||||
# ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
|
||||
# DAILY SYNC SHARES media shares this host owns and pushes
|
||||
# PERSONAL SHARES private encrypted shares for offsite backup
|
||||
# WEEKLY SYNC SHARES appdata shares synced weekly
|
||||
# INTERMEDIATE SYNC mid-day appdata propagation
|
||||
# CRITICAL SYNC SHARES appdata shares synced every 30 minutes
|
||||
# BACKUP VERIFY shares for checksum verification against remote
|
||||
# HOSTN RSYNC PROFILE host-specific appdata sync profile
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers
|
||||
# NETWORK WATCHDOG DDNS domain, NPM URL for connectivity checks
|
||||
# DOCKER NETWORK CONNECT networks and containers for array start
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for permissions script
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
|
||||
# DOWNLOADERS slskd, SABnzbd, qBittorrent credentials and URLs
|
||||
# LIDARR / SONARR / RADARR URL, API key, path map
|
||||
# ARR RECOVERY per-arr recovery toggles
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Storage mode ━━━
|
||||
# Controls where Varaverk stores scripts, conf, and state files.
|
||||
# true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct)
|
||||
# false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime)
|
||||
# Auto-detected from boot device transport on first setup.
|
||||
# To change: Settings → Storage → Migrate.
|
||||
HOSTN_STORAGE_MODE_INTERNAL=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# Containers stopped on THIS server before deploying the mirror's stack on onboard.
|
||||
HOSTN_PARTNERSHIP_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# Arr containers stopped on this server when mirror's arr stack is deployed.
|
||||
HOSTN_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# ━━━ Personal Shares ━━━
|
||||
# Private encrypted shares synced for offsite backup, independent of media shares.
|
||||
HOSTN_PERSONAL_SHARES=(
|
||||
# /mnt/user/Personal # e.g. ZFS-encrypted dataset
|
||||
)
|
||||
|
||||
# ━━━ 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"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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 — This Host's Outage Timers ━━━
|
||||
# How long THIS host must be down before each tier activates on the partner.
|
||||
HOSTN_TIER2_DELAY=240 # 4 hours
|
||||
HOSTN_TIER3_DELAY=720 # 12 hours
|
||||
HOSTN_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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# API-level health checks. Format: ["ContainerName"]="url|expected_json_key|expected_value"
|
||||
declare -A HOSTN_WATCHDOG_CONTAINER_API_CHECKS=(
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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_TMDB_API_KEY=""
|
||||
HOSTN_RADARR_MOVIES_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
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_RW_PAUSE_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
# "LidaTube"
|
||||
)
|
||||
|
||||
HOSTN_RW_STOP_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_SYS_WATCHDOG_NIC="" # e.g. eth0 — for network monitoring
|
||||
|
||||
HOSTN_SYS_WATCHDOG_CHECK_DOCKER_DAEMON=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ROOTFS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_KERNEL_OOPS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_FD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_BOOT=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_OOM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RAM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOG=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ARC=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_CPU_TEMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOAD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ZOMBIES=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_CONTAINERS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_TMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_MDSTAT=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_NETWORK=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_SSHD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RUNAWAY=false
|
||||
@@ -0,0 +1,529 @@
|
||||
#!/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, Unraid API key
|
||||
# EMBY container name, URL, API key
|
||||
# JELLYFIN container name, URL, API key
|
||||
# GITEA API token for SSH key registration
|
||||
# NOTIFICATIONS Discord webhook
|
||||
#
|
||||
# ── PARTNERSHIP ────────────────────────────────────────────────────────────────────────────
|
||||
# PARTNERSHIP auth containers, backup paths, emby provisioning
|
||||
#
|
||||
# ── RSYNC ──────────────────────────────────────────────────────────────────────────────────
|
||||
# DAILY SYNC SHARES media shares this host owns and pushes
|
||||
# PERSONAL SHARES private encrypted shares for offsite backup
|
||||
# WEEKLY SYNC SHARES appdata shares synced weekly
|
||||
# INTERMEDIATE SYNC mid-day appdata propagation
|
||||
# CRITICAL SYNC SHARES appdata shares synced every 30 minutes
|
||||
# BACKUP VERIFY shares for checksum verification against remote
|
||||
# HOSTN RSYNC PROFILE host-specific appdata sync profile
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers
|
||||
# NETWORK WATCHDOG DDNS domain, NPM URL for connectivity checks
|
||||
# DOCKER NETWORK CONNECT networks and containers for array start
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for permissions script
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── ARR STACK ──────────────────────────────────────────────────────────────────────────────
|
||||
# DOWNLOADERS slskd, SABnzbd, qBittorrent credentials and URLs
|
||||
# LIDARR / SONARR / RADARR URL, API key, path map
|
||||
# ARR RECOVERY per-arr recovery toggles
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Storage mode ━━━
|
||||
# Controls where Varaverk stores scripts, conf, and state files.
|
||||
# true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct)
|
||||
# false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime)
|
||||
# Auto-detected from boot device transport on first setup.
|
||||
# To change: Settings → Storage → Migrate.
|
||||
HOSTN_STORAGE_MODE_INTERNAL=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# Containers stopped on THIS server before deploying the mirror's stack on onboard.
|
||||
HOSTN_PARTNERSHIP_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# Arr containers stopped on this server when mirror's arr stack is deployed.
|
||||
HOSTN_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# ━━━ Personal Shares ━━━
|
||||
# Private encrypted shares synced for offsite backup, independent of media shares.
|
||||
HOSTN_PERSONAL_SHARES=(
|
||||
# /mnt/user/Personal # e.g. ZFS-encrypted dataset
|
||||
)
|
||||
|
||||
# ━━━ 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"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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 — This Host's Outage Timers ━━━
|
||||
# How long THIS host must be down before each tier activates on the partner.
|
||||
HOSTN_TIER2_DELAY=240 # 4 hours
|
||||
HOSTN_TIER3_DELAY=720 # 12 hours
|
||||
HOSTN_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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# API-level health checks. Format: ["ContainerName"]="url|expected_json_key|expected_value"
|
||||
declare -A HOSTN_WATCHDOG_CONTAINER_API_CHECKS=(
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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_TMDB_API_KEY=""
|
||||
HOSTN_RADARR_MOVIES_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
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_RW_PAUSE_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
# "LidaTube"
|
||||
)
|
||||
|
||||
HOSTN_RW_STOP_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── SYSTEM WATCHDOG ───────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_SYS_WATCHDOG_NIC="" # e.g. eth0 — for network monitoring
|
||||
|
||||
HOSTN_SYS_WATCHDOG_CHECK_DOCKER_DAEMON=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ROOTFS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_KERNEL_OOPS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_FD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_BOOT=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_OOM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RAM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOG=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ARC=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_CPU_TEMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOAD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ZOMBIES=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_CONTAINERS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_TMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_MDSTAT=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_NETWORK=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_SSHD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RUNAWAY=false
|
||||
|
||||
# ==============================================================================================
|
||||
# ── AUTH STACK ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Credentials for the Varaverk Auth Stack page (NPM, lldap, Authelia).
|
||||
|
||||
# ━━━ NginxProxyManager ━━━
|
||||
# Admin API runs on 7818 (not 81 — 81 is the partnership WebUI port).
|
||||
HOSTN_NPM_URL="http://localhost:7818"
|
||||
HOSTN_NPM_USER="" # NPM admin email
|
||||
HOSTN_NPM_PASS="" # NPM admin password
|
||||
|
||||
# ━━━ lldap ━━━
|
||||
HOSTN_LLDAP_URL="http://localhost:17170"
|
||||
HOSTN_LLDAP_USER="admin" # lldap admin username
|
||||
HOSTN_LLDAP_PASS="" # lldap admin password
|
||||
|
||||
# ━━━ Authelia ━━━
|
||||
HOSTN_AUTHELIA_CONFIG="/mnt/user/appdata/Authelia/configuration.yml"
|
||||
HOSTN_AUTHELIA_CONTAINER="Authelia"
|
||||
|
||||
# ==============================================================================================
|
||||
# ──────────────────────── End Of HOSTn Variables ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -0,0 +1,807 @@
|
||||
#!/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, Unraid API key
|
||||
# EMBY container name, URL, API key
|
||||
# JELLYFIN container name, URL, API key
|
||||
# GITEA API token for SSH key registration
|
||||
# NOTIFICATIONS Discord webhook
|
||||
#
|
||||
# ── PARTNERSHIP ────────────────────────────────────────────────────────────────────────────
|
||||
# PARTNERSHIP auth containers, backup paths, emby provisioning
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers, ignore list
|
||||
# NETWORK WATCHDOG DDNS domain, NPM URL for connectivity checks
|
||||
# DOCKER NETWORK CONNECT networks and containers for docker_network_connect.sh
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for media_shares_permissions.sh
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Storage mode ━━━
|
||||
# Controls where Varaverk stores scripts, conf, and state files.
|
||||
# true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct)
|
||||
# false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime)
|
||||
HOST1_STORAGE_MODE_INTERNAL=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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 ━━━
|
||||
# Used by the Varaverk plugin to query this server's Unraid GraphQL API.
|
||||
# Generate in Unraid: Settings → Management Access → API Keys → + New Key
|
||||
HOST1_UNRAID_API_KEY="1825c3a2e03ea5089974f4da2e171aa2d5907a1dea23cc479c33e492c8ff4dbb"
|
||||
|
||||
# ━━━ 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"
|
||||
)
|
||||
|
||||
# Containers stopped on THIS server before deploying the mirror's stack on onboard.
|
||||
# Only needed when this server parks its own stack to make room for the mirror's.
|
||||
HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# Arr containers stopped on this server when mirror's arr stack is deployed.
|
||||
HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# 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"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"]=20480 # 20GB — 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"
|
||||
["NginxProxyManager"]="http://localhost:7818"
|
||||
["Authelia"]="http://localhost:9091/api/health"
|
||||
["Authelia-Secondary"]="http://localhost:9092/api/health"
|
||||
["Lldap-Gmer4Lfe"]="http://localhost:17170"
|
||||
)
|
||||
|
||||
# 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"
|
||||
"emby-test" # broken test container (exit 127 — bad image)
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# API-level health checks — checked every cycle alongside HTTP URL checks.
|
||||
# Format: ["ContainerName"]="url|expected_json_key|expected_value"
|
||||
# Empty = no API checks for this host.
|
||||
declare -A HOST1_WATCHDOG_CONTAINER_API_CHECKS=(
|
||||
)
|
||||
|
||||
# ━━━ 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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/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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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/series"
|
||||
["/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/specials"
|
||||
["/anime-movies"]="/mnt/user/Anime_Movies-Old"
|
||||
["/ext-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
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
@@ -0,0 +1,832 @@
|
||||
#!/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, Unraid API key
|
||||
# EMBY container name, URL, API key
|
||||
# JELLYFIN container name, URL, API key
|
||||
# GITEA API token for SSH key registration
|
||||
# NOTIFICATIONS Discord webhook
|
||||
#
|
||||
# ── PARTNERSHIP ────────────────────────────────────────────────────────────────────────────
|
||||
# PARTNERSHIP auth containers, backup paths, emby provisioning
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── DOCKER ─────────────────────────────────────────────────────────────────────────────────
|
||||
# DOCKER DAILY RESTART containers restarted daily
|
||||
# DOCKER WEEKLY RESTART containers restarted weekly
|
||||
# DOCKER WATCHDOG memory limits, health URLs, required containers, ignore list
|
||||
# NETWORK WATCHDOG DDNS domain, NPM URL for connectivity checks
|
||||
# DOCKER NETWORK CONNECT networks and containers for docker_network_connect.sh
|
||||
#
|
||||
# ── MEDIA ──────────────────────────────────────────────────────────────────────────────────
|
||||
# MEDIA PERMISSIONS share list for media_shares_permissions.sh
|
||||
# MEDIA CLEANER folder lists for media_cleaner.sh
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── TRANSCODES ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRANSCODES ramdisk size, thresholds, SSD path, server array
|
||||
#
|
||||
# ── 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
|
||||
#
|
||||
# ── RESOURCE MANAGER ───────────────────────────────────────────────────────────────────────
|
||||
# RESOURCE MANAGER containers paused/stopped under memory pressure
|
||||
#
|
||||
# ── SYSTEM WATCHDOG ────────────────────────────────────────────────────────────────────────
|
||||
# SYSTEM WATCHDOG per-host check toggles and NIC configuration
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Storage mode ━━━
|
||||
# Controls where Varaverk stores scripts, conf, and state files.
|
||||
# true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct)
|
||||
# false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime)
|
||||
HOST1_STORAGE_MODE_INTERNAL=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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 ━━━
|
||||
# Used by the Varaverk plugin to query this server's Unraid GraphQL API.
|
||||
# Generate in Unraid: Settings → Management Access → API Keys → + New Key
|
||||
HOST1_UNRAID_API_KEY="1825c3a2e03ea5089974f4da2e171aa2d5907a1dea23cc479c33e492c8ff4dbb"
|
||||
|
||||
# ━━━ 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"
|
||||
)
|
||||
|
||||
# Containers stopped on THIS server before deploying the mirror's stack on onboard.
|
||||
# Only needed when this server parks its own stack to make room for the mirror's.
|
||||
HOST1_PARTNERSHIP_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# Arr containers stopped on this server when mirror's arr stack is deployed.
|
||||
HOST1_PARTNERSHIP_ARR_REPLACE_CONTAINERS=(
|
||||
)
|
||||
|
||||
# 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"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"]=20480 # 20GB — 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"
|
||||
["NginxProxyManager"]="http://localhost:7818"
|
||||
["Authelia"]="http://localhost:9091/api/health"
|
||||
["Authelia-Secondary"]="http://localhost:9092/api/health"
|
||||
["Lldap-Gmer4Lfe"]="http://localhost:17170"
|
||||
)
|
||||
|
||||
# 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"
|
||||
"emby-test" # broken test container (exit 127 — bad image)
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# API-level health checks — checked every cycle alongside HTTP URL checks.
|
||||
# Format: ["ContainerName"]="url|expected_json_key|expected_value"
|
||||
# Empty = no API checks for this host.
|
||||
declare -A HOST1_WATCHDOG_CONTAINER_API_CHECKS=(
|
||||
)
|
||||
|
||||
# ━━━ 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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/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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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/series"
|
||||
["/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/specials"
|
||||
["/anime-movies"]="/mnt/user/Anime_Movies-Old"
|
||||
["/ext-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
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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
|
||||
|
||||
# ==============================================================================================
|
||||
# ── AUTH STACK ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Credentials for the Varaverk Auth Stack page (NPM, lldap, Authelia).
|
||||
# HOST1 is the auth source of truth — these are the live production credentials.
|
||||
|
||||
# ━━━ NginxProxyManager ━━━
|
||||
# Admin API runs on 7818 (not 81 — 81 is the partnership WebUI port).
|
||||
HOST1_NPM_URL="http://localhost:7818"
|
||||
HOST1_NPM_USER="" # NPM admin email
|
||||
HOST1_NPM_PASS="" # NPM admin password
|
||||
|
||||
# ━━━ lldap ━━━
|
||||
HOST1_LLDAP_URL="http://localhost:17170"
|
||||
HOST1_LLDAP_USER="admin" # lldap admin username
|
||||
HOST1_LLDAP_PASS="" # lldap admin password
|
||||
|
||||
# ━━━ Authelia ━━━
|
||||
HOST1_AUTHELIA_CONFIG="/mnt/user/appdata/Authelia/configuration.yml"
|
||||
HOST1_AUTHELIA_CONTAINER="Authelia"
|
||||
|
||||
# ==============================================================================================
|
||||
# ──────────────────────── End Of HOST1 Variables ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: project_varaverk
|
||||
description: Varaverk — self-healing mutually-redundant two-server Unraid home media ecosystem
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: ffec43cd-13e3-4911-878f-40459f7d16a9
|
||||
---
|
||||
|
||||
**Varaverk** is a complete self-healing, self-maintaining, mutually-redundant two-server home server ecosystem. One codebase runs on both servers. No primary/standby — both servers run independently and cover each other when one goes down.
|
||||
|
||||
## The Two Servers
|
||||
|
||||
**HOST1 — unRAID-Gmer4Lfe**
|
||||
- Hardware: Threadripper 1950X, 128GB RAM, ZFS cache pools
|
||||
- Location: Primary site
|
||||
- Domain: Gmer4Lfe.com
|
||||
- Runs: Arrs (Movies, TV, Music), Auth stack (source of truth), Emby (primary)
|
||||
|
||||
**HOST2 — unRAID-Jayred365**
|
||||
- Hardware: Intel i5 10th gen, 64GB RAM
|
||||
- Location: Remote — different building, different power utility
|
||||
- Domain: Gmer4Lfe.us
|
||||
|
||||
## Architecture
|
||||
- Platform adapter layer (Plugin/unraid/adapter.sh) isolates OS-specific calls — scripts never branch on OS
|
||||
- Self-healing, not enterprise HA — goal is minimal media stack disruption
|
||||
- Tailscale for mesh networking between hosts
|
||||
|
||||
## Session State — 2026-06-13
|
||||
|
||||
**What was done this session:**
|
||||
- New Claude Code install after a reinstall. Old data was at /boot/config/claude and /boot/config/claude-bin.
|
||||
- Memory files restored from old install into current install.
|
||||
- claude_startup.sh run manually — created claude-data and claude-bin dirs under /boot/config/plugins/varaverk/, migrated all data, symlinks confirmed working.
|
||||
- Verified Varaverk is fully running from /boot — nothing in appdata. varaverk.cfg SCRIPTS_DIR, DATA_DIR, STATE_DIR, all point to /boot/config/plugins/varaverk.
|
||||
- No code changes made — session was setup/verification only.
|
||||
|
||||
**Stale note in .plg:** The ###2026.05.31 CHANGES entry says "Scripts are git-cloned to appdata on first install" — this is wrong, the actual code clones to /boot/config/plugins/varaverk. Worth fixing on next package build.
|
||||
|
||||
**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.
|
||||
|
||||
## Active To-Dos (from Notes_To-Do.md)
|
||||
- Fix fallback strike list timing: ~30s first, ~90s for 3-strike trigger — needs testing
|
||||
- Verify silent toggle switches back on good notifications
|
||||
- Rename folder Unraid_Scripts → Varaverk everywhere, update git script, all traces/scripts
|
||||
- Delete old /boot/config/claude and /boot/config/claude-bin dirs (migrated, no longer needed)
|
||||
|
||||
## Future Design Ideas
|
||||
- Shared auth stack for partner hosts to start shared services
|
||||
- When owner offboards with 2+ servers: auto-promote strongest server (by compute + bandwidth)
|
||||
- Overall setup script that pulls vars automatically (docker names, etc.)
|
||||
- App layer as king: no more direct git — app opens/edits settings, partnership deploys to servers, pushes correct host.conf
|
||||
- Web UI: on initial launch with no state file, open master.conf; lock orchs until setup complete
|
||||
- First-launch guide: owner sets up master.conf → host1.conf → Tailscale shares → onboard → host2/3 install and see state file, default to mirror mode
|
||||
|
||||
**Why:** User is building this as a personal project on Unraid. Design philosophy favors simplicity and independence over enterprise tooling.
|
||||
**How to apply:** Understand the two-server mesh model when suggesting architecture. The app layer / web UI direction is the current strategic focus — moving away from raw git/scripts toward a proper application.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: project_varaverk
|
||||
description: Varaverk — self-healing mutually-redundant two-server Unraid home media ecosystem
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: ffec43cd-13e3-4911-878f-40459f7d16a9
|
||||
---
|
||||
|
||||
**Varaverk** is a complete self-healing, self-maintaining, mutually-redundant two-server home server ecosystem. One codebase runs on both servers. No primary/standby — both servers run independently and cover each other when one goes down.
|
||||
|
||||
## The Two Servers
|
||||
|
||||
**HOST1 — unRAID-Gmer4Lfe**
|
||||
- Hardware: Threadripper 1950X, 128GB RAM, ZFS cache pools
|
||||
- Location: Primary site
|
||||
- Domain: Gmer4Lfe.com
|
||||
- Runs: Arrs (Movies, TV, Music), Auth stack (source of truth), Emby (primary)
|
||||
|
||||
**HOST2 — unRAID-Jayred365**
|
||||
- Hardware: Intel i5 10th gen, 64GB RAM
|
||||
- Location: Remote — different building, different power utility
|
||||
- Domain: Gmer4Lfe.us
|
||||
|
||||
## Architecture
|
||||
- Platform adapter layer (Plugin/unraid/adapter.sh) isolates OS-specific calls — scripts never branch on OS
|
||||
- Self-healing, not enterprise HA — goal is minimal media stack disruption
|
||||
- Tailscale for mesh networking between hosts
|
||||
|
||||
## Session State — 2026-06-13
|
||||
|
||||
**What was done this session:**
|
||||
- New Claude Code install after a reinstall. Old data was at /boot/config/claude and /boot/config/claude-bin.
|
||||
- Memory files restored from old install into current install.
|
||||
- claude_startup.sh run manually — created claude-data and claude-bin dirs under /boot/config/plugins/varaverk/, migrated all data, symlinks confirmed working.
|
||||
- Verified Varaverk is fully running from /boot — nothing in appdata. varaverk.cfg SCRIPTS_DIR, DATA_DIR, STATE_DIR, all point to /boot/config/plugins/varaverk.
|
||||
- No code changes made — session was setup/verification only.
|
||||
|
||||
**Stale note in .plg:** The ###2026.05.31 CHANGES entry says "Scripts are git-cloned to appdata on first install" — this is wrong, the actual code clones to /boot/config/plugins/varaverk. Worth fixing on next package build.
|
||||
|
||||
**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.
|
||||
|
||||
**Plugin install flow:** Plugin installs to appdata first, then during the setup wizard the user can select "normal" or set `internal_boot=true` to pin it to /boot. This is why the .plg note about appdata isn't wrong per se — it's the staging location before the wizard runs.
|
||||
|
||||
## Active To-Dos (from Notes_To-Do.md)
|
||||
- Fix fallback strike list timing: ~30s first, ~90s for 3-strike trigger — needs testing
|
||||
- Verify silent toggle switches back on good notifications
|
||||
- Rename folder Unraid_Scripts → Varaverk everywhere, update git script, all traces/scripts
|
||||
- Delete old /boot/config/claude and /boot/config/claude-bin dirs (migrated, no longer needed)
|
||||
|
||||
## Future Design Ideas
|
||||
- Shared auth stack for partner hosts to start shared services
|
||||
- When owner offboards with 2+ servers: auto-promote strongest server (by compute + bandwidth)
|
||||
- Overall setup script that pulls vars automatically (docker names, etc.)
|
||||
- App layer as king: no more direct git — app opens/edits settings, partnership deploys to servers, pushes correct host.conf
|
||||
- Web UI: on initial launch with no state file, open master.conf; lock orchs until setup complete
|
||||
- First-launch guide: owner sets up master.conf → host1.conf → Tailscale shares → onboard → host2/3 install and see state file, default to mirror mode
|
||||
|
||||
**Why:** User is building this as a personal project on Unraid. Design philosophy favors simplicity and independence over enterprise tooling.
|
||||
**How to apply:** Understand the two-server mesh model when suggesting architecture. The app layer / web UI direction is the current strategic focus — moving away from raw git/scripts toward a proper application.
|
||||
@@ -0,0 +1,664 @@
|
||||
#!/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-Gmer4Lfe"
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── AUTH STACK ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Credentials for the Varaverk Auth Stack page (NPM, lldap, Authelia).
|
||||
# Fill in when HOST2 is back online.
|
||||
|
||||
# ━━━ NginxProxyManager ━━━
|
||||
HOST2_NPM_URL="http://localhost:81"
|
||||
HOST2_NPM_USER="" # NPM admin email
|
||||
HOST2_NPM_PASS="" # NPM admin password
|
||||
|
||||
# ━━━ lldap ━━━
|
||||
HOST2_LLDAP_URL="http://localhost:17170"
|
||||
HOST2_LLDAP_USER="admin" # lldap admin username
|
||||
HOST2_LLDAP_PASS="" # lldap admin password
|
||||
|
||||
# ━━━ Authelia ━━━
|
||||
HOST2_AUTHELIA_CONFIG="/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml"
|
||||
HOST2_AUTHELIA_CONTAINER="Authelia"
|
||||
|
||||
# ==============================================================================================
|
||||
# ──────────────────────── End Of HOST2 Variables ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -0,0 +1,665 @@
|
||||
#!/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-Gmer4Lfe"
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# ==============================================================================================
|
||||
# ── AUTH STACK ────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
# Credentials for the Varaverk Auth Stack page (NPM, lldap, Authelia).
|
||||
# Credentials empty — fill in when HOST2 is back online.
|
||||
|
||||
# ━━━ NginxProxyManager ━━━
|
||||
# Admin API runs on 7818 (not 81 — 81 is the partnership WebUI port).
|
||||
HOST2_NPM_URL="http://localhost:7818"
|
||||
HOST2_NPM_USER="" # NPM admin email
|
||||
HOST2_NPM_PASS="" # NPM admin password
|
||||
|
||||
# ━━━ lldap ━━━
|
||||
HOST2_LLDAP_URL="http://localhost:17170"
|
||||
HOST2_LLDAP_USER="admin" # lldap admin username
|
||||
HOST2_LLDAP_PASS="" # lldap admin password
|
||||
|
||||
# ━━━ Authelia ━━━
|
||||
HOST2_AUTHELIA_CONFIG="/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml"
|
||||
HOST2_AUTHELIA_CONTAINER="Authelia"
|
||||
|
||||
# ==============================================================================================
|
||||
# ──────────────────────── End Of HOST2 Variables ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
// First-run setup wizard — uniform flow for all hosts.
|
||||
// Step 1: auto-detect environment + server identity form.
|
||||
// Step 2: auto-populate + guide + checklist.
|
||||
// master.conf pull (for partner servers) lives in the checklist, not here.
|
||||
|
||||
$detectedHostname = vv_get_hostname();
|
||||
?>
|
||||
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
||||
<style>
|
||||
#vv-setup {
|
||||
max-width: 580px; margin: 40px 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 4px; font-size: 17px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; }
|
||||
.vv-sub { font-size: 12px; color: #555; margin-bottom: 28px; }
|
||||
.vv-field { margin-bottom: 18px; }
|
||||
.vv-field label { display: block; font-size: 11px; color: #888; margin-bottom: 5px; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.vv-field input[type=text],
|
||||
.vv-field select {
|
||||
width: 100%; box-sizing: border-box; background: #0d0d0d;
|
||||
border: 1px solid #333; color: #ddd; padding: 7px 10px;
|
||||
border-radius: 3px; font-family: monospace; font-size: 13px;
|
||||
}
|
||||
.vv-field input:focus, .vv-field select:focus { outline: none; border-color: #555; }
|
||||
.vv-hint { font-size: 11px; color: #555; margin-top: 4px; }
|
||||
.vv-role-row { display: flex; gap: 10px; margin-bottom: 22px; }
|
||||
.vv-role-btn { flex: 1; padding: 9px 0; background: #1a1a1a; border: 1px solid #333;
|
||||
border-radius: 3px; color: #777; font-family: monospace; font-size: 12px;
|
||||
cursor: pointer; text-align: center; transition: border-color .15s, color .15s; }
|
||||
.vv-role-btn.active { border-color: #555; color: #ccc; background: #1e1e1e; }
|
||||
.vv-cond { display: none; }
|
||||
.vv-cond.show { display: block; }
|
||||
hr.vv-hr { border: none; border-top: 1px solid #1e1e1e; margin: 22px 0; }
|
||||
.vv-btn { width: 100%; padding: 10px; background: #1e1e1e; border: 1px solid #444;
|
||||
color: #ccc; font-family: monospace; font-size: 13px; border-radius: 3px;
|
||||
cursor: pointer; letter-spacing: .03em; }
|
||||
.vv-btn:hover { border-color: #666; color: #eee; }
|
||||
.vv-btn:disabled { opacity: .4; cursor: default; }
|
||||
#vv-status { margin-top: 10px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }
|
||||
#vv-status.ok { color: #4a8; }
|
||||
#vv-status.err { color: #a44; }
|
||||
|
||||
/* Detection banner */
|
||||
#vv-detect-banner {
|
||||
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||
padding: 11px 14px; margin-bottom: 22px; font-size: 12px; line-height: 1.8; color: #666;
|
||||
}
|
||||
#vv-detect-banner .vv-det-row { display: flex; gap: 8px; }
|
||||
#vv-detect-banner .vv-det-lbl { color: #555; min-width: 100px; }
|
||||
#vv-detect-banner .vv-det-val { color: #999; }
|
||||
#vv-detect-banner .loading { color: #444; font-style: italic; }
|
||||
|
||||
/* Step 2 */
|
||||
#vv-step2 { display: none; }
|
||||
.vv-guide {
|
||||
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||
padding: 13px 16px; margin-bottom: 20px; font-size: 12px; color: #666; line-height: 1.9;
|
||||
}
|
||||
.vv-guide ol { margin: 8px 0 0 16px; padding: 0; }
|
||||
.vv-guide li { margin-bottom: 3px; }
|
||||
.vv-cl-title { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }
|
||||
.vv-cl-item { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0;
|
||||
border-bottom: 1px solid #1a1a1a; font-size: 12px; }
|
||||
.vv-cl-item:last-child { border-bottom: none; }
|
||||
.vv-cl-icon { font-size: 13px; min-width: 16px; margin-top: 1px; }
|
||||
.vv-cl-body { flex: 1; }
|
||||
.vv-cl-label { color: #bbb; }
|
||||
.vv-cl-detail{ color: #555; font-size: 11px; margin-top: 2px; }
|
||||
.vv-cl-act { margin-top: 5px; }
|
||||
.vv-cl-act button { padding: 4px 10px; background: #1a1a1a; border: 1px solid #333; color: #888;
|
||||
font-family: monospace; font-size: 11px; border-radius: 2px; cursor: pointer; }
|
||||
.vv-cl-act button:hover { border-color: #555; color: #bbb; }
|
||||
.vv-cl-err { font-size: 11px; color: #a44; margin-top: 4px; }
|
||||
</style>
|
||||
|
||||
<div id="vv-setup">
|
||||
|
||||
<h1>⬡ Varaverk — First Run</h1>
|
||||
<div class="vv-sub">Set up this server before the plugin can start.</div>
|
||||
|
||||
<!-- ── Step 1: Detection + identity ──────────────────────────────────────── -->
|
||||
<div id="vv-step1">
|
||||
|
||||
<div id="vv-detect-banner"><div class="loading">Detecting environment…</div></div>
|
||||
|
||||
<div class="vv-field">
|
||||
<label>This server's hostname</label>
|
||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
|
||||
<div class="vv-hint">Must match Unraid Settings → Identification exactly (case-sensitive)</div>
|
||||
</div>
|
||||
|
||||
<hr class="vv-hr">
|
||||
<label style="display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Server role</label>
|
||||
<div class="vv-role-row">
|
||||
<div class="vv-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
||||
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first server</span>
|
||||
</div>
|
||||
<div class="vv-role-btn" id="vv-role-partner" onclick="vvSetRole('partner')">
|
||||
Partner<br><span style="color:#555;font-size:10px;">HOST2+ · joining primary</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-cond" id="vv-cond-primary">
|
||||
<div class="vv-field">
|
||||
<label>Partner's hostname <span style="color:#444;font-size:10px;">(optional — can fill in later)</span></label>
|
||||
<input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-cond" id="vv-cond-partner">
|
||||
<div class="vv-field">
|
||||
<label>Primary server's hostname <span style="color:#a44;font-size:10px;">required</span></label>
|
||||
<input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="vv-field">
|
||||
<label>Your slot</label>
|
||||
<select id="vv-partner-slot">
|
||||
<option value="host2">HOST2</option>
|
||||
<option value="host3">HOST3</option>
|
||||
<option value="host4">HOST4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#555;margin-bottom:4px;">
|
||||
SSH key and master.conf pull are handled automatically after save.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>
|
||||
<div id="vv-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 2: Populate + guide + checklist ───────────────────────────────── -->
|
||||
<div id="vv-step2">
|
||||
<hr class="vv-hr">
|
||||
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:14px;">Step 2 of 2</div>
|
||||
|
||||
<div id="vv-populate-status" style="font-size:12px;color:#555;margin-bottom:14px;">⟳ Running auto-populate…</div>
|
||||
|
||||
<div class="vv-guide">
|
||||
<strong style="color:#888;">Quick start</strong>
|
||||
<ol>
|
||||
<li>Create your Unraid API key below — needed for live monitor stats</li>
|
||||
<li>Open <strong>Scheduler → Edit host.conf</strong> — only three things need manual entry:<br>
|
||||
<span style="color:#444;">
|
||||
<code>EMBY_API_KEY</code> — Emby Dashboard → API Keys → + New Key<br>
|
||||
<code>DISCORD_WEBHOOK</code> — for notifications (optional)<br>
|
||||
<code>DAILY_SYNC_SHARES</code> — media paths to rsync nightly<br>
|
||||
Everything else was auto-populated or has working defaults
|
||||
</span></li>
|
||||
<li>If partnering: the checklist below will guide you through pulling HOST1's config and running onboard</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;">
|
||||
<button id="vv-key-btn" onclick="vvCreateKey(this)" class="vv-btn" style="flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;">
|
||||
Create API Key
|
||||
</button>
|
||||
<a href="#" onclick="vvGoScheduler(event)" style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
||||
</div>
|
||||
<div id="vv-key-status" style="font-size:12px;min-height:14px;margin-bottom:18px;"></div>
|
||||
|
||||
<hr class="vv-hr">
|
||||
<div class="vv-cl-title">Setup checklist</div>
|
||||
<div id="vv-checklist"><div style="font-size:12px;color:#444;">Loading…</div></div>
|
||||
|
||||
<div style="margin-top:18px;text-align:right;">
|
||||
<a href="#" onclick="vvGoScheduler(event)" style="font-size:12px;color:#444;text-decoration:none;">Go to Scheduler →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let _vvRedirect = '?tab=scheduler';
|
||||
|
||||
// ── Detection banner ──────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
const _ac = new AbortController();
|
||||
setTimeout(() => _ac.abort(), 6000);
|
||||
fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
|
||||
.then(r => r.json()).then(d => {
|
||||
const b = document.getElementById('vv-detect-banner');
|
||||
if (!d.ok) { b.innerHTML = '<span style="color:#555">Detection unavailable</span>'; return; }
|
||||
const modeLabel = d.mode === 'internal'
|
||||
? '<span style="color:#4a8">internal (NVMe/SSD)</span>'
|
||||
: '<span style="color:#a84">flash mode (USB boot)</span>';
|
||||
b.innerHTML =
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">OS</span><span class="vv-det-val">Unraid ' + (d.unraid_ver||'') + '</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Boot device</span><span class="vv-det-val">' + d.boot_device + ' (' + d.transport + ')</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Storage mode</span><span class="vv-det-val">' + modeLabel + '</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Scripts dir</span><span class="vv-det-val" style="color:#666">' + d.scripts_dir + '</span></div>';
|
||||
const hf = document.getElementById('vv-hostname');
|
||||
if (hf && !hf.value.trim()) hf.value = d.hostname;
|
||||
}).catch(() => {
|
||||
document.getElementById('vv-detect-banner').innerHTML = '<span style="color:#444">Detection unavailable</span>';
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Role toggle ───────────────────────────────────────────────────────────────
|
||||
let vvRole = 'primary';
|
||||
function vvSetRole(role) {
|
||||
vvRole = role;
|
||||
document.getElementById('vv-role-primary')?.classList.toggle('active', role === 'primary');
|
||||
document.getElementById('vv-role-partner')?.classList.toggle('active', role === 'partner');
|
||||
document.getElementById('vv-cond-primary')?.classList.toggle('show', role === 'primary');
|
||||
document.getElementById('vv-cond-partner')?.classList.toggle('show', role === 'partner');
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function vvSetStatus(msg, cls) {
|
||||
const s = document.getElementById('vv-status');
|
||||
s.textContent = msg; s.className = cls || '';
|
||||
}
|
||||
function vvSetBtn(text, disabled) {
|
||||
const b = document.getElementById('vv-main-btn');
|
||||
if (b) { b.textContent = text; b.disabled = disabled; }
|
||||
}
|
||||
function vvGoScheduler(e) {
|
||||
if (e) e.preventDefault();
|
||||
window.location.href = _vvRedirect || '?tab=scheduler';
|
||||
}
|
||||
|
||||
// ── Step 2 ────────────────────────────────────────────────────────────────────
|
||||
function vvShowStep2(redirect, apiKey) {
|
||||
_vvRedirect = redirect || '?tab=scheduler';
|
||||
document.getElementById('vv-step1').style.display = 'none';
|
||||
document.getElementById('vv-step2').style.display = 'block';
|
||||
if (apiKey && apiKey.ok) {
|
||||
const btn = document.getElementById('vv-key-btn');
|
||||
const status = document.getElementById('vv-key-status');
|
||||
if (btn) { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }
|
||||
if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }
|
||||
}
|
||||
vvRunPopulate();
|
||||
vvLoadChecklist();
|
||||
}
|
||||
|
||||
// ── Populate ──────────────────────────────────────────────────────────────────
|
||||
function vvRunPopulate() {
|
||||
const el = document.getElementById('vv-populate-status');
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'populate'})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
const found = (d.lines || []).filter(l => /✅|found|detected/i.test(l));
|
||||
el.textContent = found.length
|
||||
? '✓ Auto-populate: ' + found.length + ' field' + (found.length > 1 ? 's' : '') + ' detected'
|
||||
: '✓ Auto-populate ran — arr keys will fill once services are running';
|
||||
el.style.color = '#4a8';
|
||||
} else {
|
||||
el.textContent = 'Auto-populate skipped — run Tools/conf_populate.sh once your arr containers are up';
|
||||
el.style.color = '#555';
|
||||
}
|
||||
vvLoadChecklist();
|
||||
}).catch(() => {
|
||||
el.textContent = 'Auto-populate unavailable — run manually from Scheduler';
|
||||
el.style.color = '#555';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Checklist ─────────────────────────────────────────────────────────────────
|
||||
const vvActionLabels = {
|
||||
create_key: 'Create API key',
|
||||
ssh_setup: 'SSH guide →',
|
||||
run_populate: 'Run now',
|
||||
pull_master: 'Pull from HOST1',
|
||||
onboard: 'Partnership tab →',
|
||||
};
|
||||
const vvActionHref = {
|
||||
ssh_setup: '?tab=partnership',
|
||||
onboard: '?tab=partnership',
|
||||
};
|
||||
|
||||
function vvLoadChecklist() {
|
||||
fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
|
||||
.then(r => r.json()).then(d => {
|
||||
const el = document.getElementById('vv-checklist');
|
||||
if (!d.ok || !d.items) { el.innerHTML = '<span style="color:#555">Unable to load checklist</span>'; return; }
|
||||
el.innerHTML = d.items.map(item => {
|
||||
const icon = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
|
||||
const iclr = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
|
||||
let act = '';
|
||||
if (item.action) {
|
||||
const lbl = vvActionLabels[item.action] || item.action;
|
||||
const href = vvActionHref[item.action];
|
||||
if (href) {
|
||||
act = `<div class="vv-cl-act"><a href="${href}" style="font-size:11px;color:#556;">${lbl}</a></div>`;
|
||||
} else if (item.action === 'create_key') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvCreateKey(this)">${lbl}</button></div>`;
|
||||
} else if (item.action === 'run_populate') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvRunPopulateBtn(this)">${lbl}</button></div>`;
|
||||
} else if (item.action === 'pull_master') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvPullMaster(this)">${lbl}</button><div id="vv-pull-err" class="vv-cl-err"></div></div>`;
|
||||
}
|
||||
}
|
||||
return `<div class="vv-cl-item">
|
||||
<div class="vv-cl-icon" style="color:${iclr}">${icon}</div>
|
||||
<div class="vv-cl-body">
|
||||
<div class="vv-cl-label">${item.label}</div>
|
||||
<div class="vv-cl-detail">${item.detail || ''}</div>
|
||||
${act}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function vvRunPopulateBtn(btn) {
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'populate'})
|
||||
}).then(() => { btn.textContent = 'Done'; vvLoadChecklist(); })
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||
}
|
||||
|
||||
function vvPullMaster(btn) {
|
||||
btn.disabled = true; btn.textContent = '⟳ Pulling…';
|
||||
const errEl = document.getElementById('vv-pull-err');
|
||||
if (errEl) errEl.textContent = '';
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'pull'})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
btn.textContent = '✓ Done';
|
||||
setTimeout(vvLoadChecklist, 600);
|
||||
} else {
|
||||
if (errEl) errEl.textContent = d.error || 'Pull failed';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
}
|
||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||
}
|
||||
|
||||
// ── API key ───────────────────────────────────────────────────────────────────
|
||||
function vvCreateKey(btn) {
|
||||
const status = document.getElementById('vv-key-status');
|
||||
btn.disabled = true; btn.textContent = '⟳ Creating…';
|
||||
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
|
||||
.then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
status.textContent = '✓ Key created — ' + d.key_preview;
|
||||
status.style.color = '#4a8';
|
||||
btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
|
||||
vvLoadChecklist();
|
||||
} else {
|
||||
status.textContent = '✗ ' + (d.error || 'Failed');
|
||||
status.style.color = '#a44';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
}
|
||||
}).catch(e => {
|
||||
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||
function vvDoSave() {
|
||||
const hostname = document.getElementById('vv-hostname')?.value.trim();
|
||||
if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
|
||||
let host1 = '', host2 = '', mySlot = 'host1';
|
||||
if (vvRole === 'primary') {
|
||||
host1 = hostname;
|
||||
host2 = document.getElementById('vv-partner-hostname')?.value.trim() || '';
|
||||
mySlot = 'host1';
|
||||
} else {
|
||||
const primary = document.getElementById('vv-primary-hostname')?.value.trim();
|
||||
if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
|
||||
mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
|
||||
host1 = primary;
|
||||
if (mySlot === 'host2') host2 = hostname;
|
||||
}
|
||||
vvSetBtn('Saving…', true);
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) { vvShowStep2(d.redirect || '?tab=scheduler', d.api_key); }
|
||||
else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
|
||||
}).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
// First-run setup wizard — uniform flow for all hosts.
|
||||
// Step 1: auto-detect environment + server identity form.
|
||||
// Step 2: auto-populate + guide + checklist.
|
||||
// master.conf pull (for partner servers) lives in the checklist, not here.
|
||||
|
||||
$detectedHostname = vv_get_hostname();
|
||||
?>
|
||||
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
||||
<style>
|
||||
#vv-setup {
|
||||
max-width: 580px; margin: 40px 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 4px; font-size: 17px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; }
|
||||
.vv-sub { font-size: 12px; color: #555; margin-bottom: 28px; }
|
||||
.vv-field { margin-bottom: 18px; }
|
||||
.vv-field label { display: block; font-size: 11px; color: #888; margin-bottom: 5px; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.vv-field input[type=text],
|
||||
.vv-field select {
|
||||
width: 100%; box-sizing: border-box; background: #0d0d0d;
|
||||
border: 1px solid #333; color: #ddd; padding: 7px 10px;
|
||||
border-radius: 3px; font-family: monospace; font-size: 13px;
|
||||
}
|
||||
.vv-field input:focus, .vv-field select:focus { outline: none; border-color: #555; }
|
||||
.vv-hint { font-size: 11px; color: #555; margin-top: 4px; }
|
||||
.vv-role-row { display: flex; gap: 10px; margin-bottom: 22px; }
|
||||
.vv-role-btn { flex: 1; padding: 9px 0; background: #1a1a1a; border: 1px solid #333;
|
||||
border-radius: 3px; color: #777; font-family: monospace; font-size: 12px;
|
||||
cursor: pointer; text-align: center; transition: border-color .15s, color .15s; }
|
||||
.vv-role-btn.active { border-color: #555; color: #ccc; background: #1e1e1e; }
|
||||
.vv-cond { display: none; }
|
||||
.vv-cond.show { display: block; }
|
||||
hr.vv-hr { border: none; border-top: 1px solid #1e1e1e; margin: 22px 0; }
|
||||
.vv-btn { width: 100%; padding: 10px; background: #1e1e1e; border: 1px solid #444;
|
||||
color: #ccc; font-family: monospace; font-size: 13px; border-radius: 3px;
|
||||
cursor: pointer; letter-spacing: .03em; }
|
||||
.vv-btn:hover { border-color: #666; color: #eee; }
|
||||
.vv-btn:disabled { opacity: .4; cursor: default; }
|
||||
#vv-status { margin-top: 10px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }
|
||||
#vv-status.ok { color: #4a8; }
|
||||
#vv-status.err { color: #a44; }
|
||||
|
||||
/* Detection banner */
|
||||
#vv-detect-banner {
|
||||
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||
padding: 11px 14px; margin-bottom: 22px; font-size: 12px; line-height: 1.8; color: #666;
|
||||
}
|
||||
#vv-detect-banner .vv-det-row { display: flex; gap: 8px; }
|
||||
#vv-detect-banner .vv-det-lbl { color: #555; min-width: 100px; }
|
||||
#vv-detect-banner .vv-det-val { color: #999; }
|
||||
#vv-detect-banner .loading { color: #444; font-style: italic; }
|
||||
|
||||
/* Step 2 */
|
||||
#vv-step2 { display: none; }
|
||||
.vv-guide {
|
||||
background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;
|
||||
padding: 13px 16px; margin-bottom: 20px; font-size: 12px; color: #666; line-height: 1.9;
|
||||
}
|
||||
.vv-guide ol { margin: 8px 0 0 16px; padding: 0; }
|
||||
.vv-guide li { margin-bottom: 3px; }
|
||||
.vv-cl-title { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }
|
||||
.vv-cl-item { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0;
|
||||
border-bottom: 1px solid #1a1a1a; font-size: 12px; }
|
||||
.vv-cl-item:last-child { border-bottom: none; }
|
||||
.vv-cl-icon { font-size: 13px; min-width: 16px; margin-top: 1px; }
|
||||
.vv-cl-body { flex: 1; }
|
||||
.vv-cl-label { color: #bbb; }
|
||||
.vv-cl-detail{ color: #555; font-size: 11px; margin-top: 2px; }
|
||||
.vv-cl-act { margin-top: 5px; }
|
||||
.vv-cl-act button { padding: 4px 10px; background: #1a1a1a; border: 1px solid #333; color: #888;
|
||||
font-family: monospace; font-size: 11px; border-radius: 2px; cursor: pointer; }
|
||||
.vv-cl-act button:hover { border-color: #555; color: #bbb; }
|
||||
.vv-cl-err { font-size: 11px; color: #a44; margin-top: 4px; }
|
||||
</style>
|
||||
|
||||
<div id="vv-setup">
|
||||
|
||||
<h1>⬡ Varaverk — First Run</h1>
|
||||
<div class="vv-sub">Set up this server before the plugin can start.</div>
|
||||
|
||||
<!-- ── Step 1: Detection + identity ──────────────────────────────────────── -->
|
||||
<div id="vv-step1">
|
||||
|
||||
<div id="vv-detect-banner"><div class="loading">Detecting environment…</div></div>
|
||||
|
||||
<div class="vv-field">
|
||||
<label>Storage mode</label>
|
||||
<div class="vv-role-row" style="margin-bottom:4px">
|
||||
<div class="vv-role-btn" id="vv-store-flash" onclick="vvSetStorage('flash')">
|
||||
Appdata<br><span style="color:#555;font-size:10px;">USB boot · requires array</span>
|
||||
</div>
|
||||
<div class="vv-role-btn" id="vv-store-internal" onclick="vvSetStorage('internal')">
|
||||
Internal Boot<br><span style="color:#555;font-size:10px;">NVMe/SSD · no array dep</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vv-store-hint" class="vv-hint"></div>
|
||||
</div>
|
||||
|
||||
<div class="vv-field">
|
||||
<label>This server's hostname</label>
|
||||
<input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
|
||||
<div class="vv-hint">Must match Unraid Settings → Identification exactly (case-sensitive)</div>
|
||||
</div>
|
||||
|
||||
<hr class="vv-hr">
|
||||
<label style="display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Server role</label>
|
||||
<div class="vv-role-row">
|
||||
<div class="vv-role-btn active" id="vv-role-primary" onclick="vvSetRole('primary')">
|
||||
Primary<br><span style="color:#555;font-size:10px;">HOST1 · first server</span>
|
||||
</div>
|
||||
<div class="vv-role-btn" id="vv-role-partner" onclick="vvSetRole('partner')">
|
||||
Partner<br><span style="color:#555;font-size:10px;">HOST2+ · joining primary</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-cond" id="vv-cond-primary">
|
||||
<div class="vv-field">
|
||||
<label>Partner's hostname <span style="color:#444;font-size:10px;">(optional — can fill in later)</span></label>
|
||||
<input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-cond" id="vv-cond-partner">
|
||||
<div class="vv-field">
|
||||
<label>Primary server's hostname <span style="color:#a44;font-size:10px;">required</span></label>
|
||||
<input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="vv-field">
|
||||
<label>Your slot</label>
|
||||
<select id="vv-partner-slot">
|
||||
<option value="host2">HOST2</option>
|
||||
<option value="host3">HOST3</option>
|
||||
<option value="host4">HOST4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#555;margin-bottom:4px;">
|
||||
SSH key and master.conf pull are handled automatically after save.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>
|
||||
<div id="vv-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Step 2: Populate + guide + checklist ───────────────────────────────── -->
|
||||
<div id="vv-step2">
|
||||
<hr class="vv-hr">
|
||||
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:14px;">Step 2 of 2</div>
|
||||
|
||||
<div id="vv-populate-status" style="font-size:12px;color:#555;margin-bottom:14px;">⟳ Running auto-populate…</div>
|
||||
|
||||
<div class="vv-guide">
|
||||
<strong style="color:#888;">Quick start</strong>
|
||||
<ol>
|
||||
<li>Create your Unraid API key below — needed for live monitor stats</li>
|
||||
<li>Open <strong>Scheduler → Edit host.conf</strong> — only three things need manual entry:<br>
|
||||
<span style="color:#444;">
|
||||
<code>EMBY_API_KEY</code> — Emby Dashboard → API Keys → + New Key<br>
|
||||
<code>DISCORD_WEBHOOK</code> — for notifications (optional)<br>
|
||||
<code>DAILY_SYNC_SHARES</code> — media paths to rsync nightly<br>
|
||||
Everything else was auto-populated or has working defaults
|
||||
</span></li>
|
||||
<li>If partnering: the checklist below will guide you through pulling HOST1's config and running onboard</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;">
|
||||
<button id="vv-key-btn" onclick="vvCreateKey(this)" class="vv-btn" style="flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;">
|
||||
Create API Key
|
||||
</button>
|
||||
<a href="#" onclick="vvGoScheduler(event)" style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
||||
</div>
|
||||
<div id="vv-key-status" style="font-size:12px;min-height:14px;margin-bottom:18px;"></div>
|
||||
|
||||
<hr class="vv-hr">
|
||||
<div class="vv-cl-title">Setup checklist</div>
|
||||
<div id="vv-checklist"><div style="font-size:12px;color:#444;">Loading…</div></div>
|
||||
|
||||
<div style="margin-top:18px;text-align:right;">
|
||||
<a href="#" onclick="vvGoScheduler(event)" style="font-size:12px;color:#444;text-decoration:none;">Go to Scheduler →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let _vvRedirect = '?tab=scheduler';
|
||||
let _vvStorageMode = 'flash';
|
||||
let _vvCurrentDir = '';
|
||||
|
||||
function vvSetStorage(mode) {
|
||||
_vvStorageMode = mode;
|
||||
document.getElementById('vv-store-flash')?.classList.toggle('active', mode === 'flash');
|
||||
document.getElementById('vv-store-internal')?.classList.toggle('active', mode === 'internal');
|
||||
const hint = document.getElementById('vv-store-hint');
|
||||
if (hint) hint.textContent = mode === 'flash'
|
||||
? 'Scripts live in appdata — requires array to be started. Recommended for USB flash boot.'
|
||||
: 'Scripts live on /boot — available before array mounts. Requires NVMe/SSD boot.';
|
||||
}
|
||||
|
||||
// ── Detection banner ──────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
const _ac = new AbortController();
|
||||
setTimeout(() => _ac.abort(), 6000);
|
||||
fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now(), {signal: _ac.signal})
|
||||
.then(r => r.json()).then(d => {
|
||||
const b = document.getElementById('vv-detect-banner');
|
||||
if (!d.ok) { b.innerHTML = '<span style="color:#555">Detection unavailable</span>'; return; }
|
||||
_vvCurrentDir = d.scripts_dir || '';
|
||||
b.innerHTML =
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">OS</span><span class="vv-det-val">Unraid ' + (d.unraid_ver||'') + '</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Boot device</span><span class="vv-det-val">' + d.boot_device + ' (' + d.transport + ')</span></div>' +
|
||||
'<div class="vv-det-row"><span class="vv-det-lbl">Scripts dir</span><span class="vv-det-val" style="color:#666">' + d.scripts_dir + '</span></div>';
|
||||
vvSetStorage(d.mode);
|
||||
const hf = document.getElementById('vv-hostname');
|
||||
if (hf && !hf.value.trim()) hf.value = d.hostname;
|
||||
}).catch(() => {
|
||||
document.getElementById('vv-detect-banner').innerHTML = '<span style="color:#444">Detection unavailable</span>';
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Role toggle ───────────────────────────────────────────────────────────────
|
||||
let vvRole = 'primary';
|
||||
function vvSetRole(role) {
|
||||
vvRole = role;
|
||||
document.getElementById('vv-role-primary')?.classList.toggle('active', role === 'primary');
|
||||
document.getElementById('vv-role-partner')?.classList.toggle('active', role === 'partner');
|
||||
document.getElementById('vv-cond-primary')?.classList.toggle('show', role === 'primary');
|
||||
document.getElementById('vv-cond-partner')?.classList.toggle('show', role === 'partner');
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function vvSetStatus(msg, cls) {
|
||||
const s = document.getElementById('vv-status');
|
||||
s.textContent = msg; s.className = cls || '';
|
||||
}
|
||||
function vvSetBtn(text, disabled) {
|
||||
const b = document.getElementById('vv-main-btn');
|
||||
if (b) { b.textContent = text; b.disabled = disabled; }
|
||||
}
|
||||
function vvGoScheduler(e) {
|
||||
if (e) e.preventDefault();
|
||||
window.location.href = _vvRedirect || '?tab=scheduler';
|
||||
}
|
||||
|
||||
// ── Step 2 ────────────────────────────────────────────────────────────────────
|
||||
function vvShowStep2(redirect, apiKey) {
|
||||
_vvRedirect = redirect || '?tab=scheduler';
|
||||
document.getElementById('vv-step1').style.display = 'none';
|
||||
document.getElementById('vv-step2').style.display = 'block';
|
||||
if (apiKey && apiKey.ok) {
|
||||
const btn = document.getElementById('vv-key-btn');
|
||||
const status = document.getElementById('vv-key-status');
|
||||
if (btn) { btn.textContent = 'Created ✓'; btn.disabled = true; btn.style.opacity = '.6'; }
|
||||
if (status) { status.textContent = '✓ API key created automatically'; status.style.color = '#4a8'; }
|
||||
}
|
||||
vvRunPopulate();
|
||||
vvLoadChecklist();
|
||||
}
|
||||
|
||||
// ── Populate ──────────────────────────────────────────────────────────────────
|
||||
function vvRunPopulate() {
|
||||
const el = document.getElementById('vv-populate-status');
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'populate'})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
const found = (d.lines || []).filter(l => /✅|found|detected/i.test(l));
|
||||
el.textContent = found.length
|
||||
? '✓ Auto-populate: ' + found.length + ' field' + (found.length > 1 ? 's' : '') + ' detected'
|
||||
: '✓ Auto-populate ran — arr keys will fill once services are running';
|
||||
el.style.color = '#4a8';
|
||||
} else {
|
||||
el.textContent = 'Auto-populate skipped — run Tools/conf_populate.sh once your arr containers are up';
|
||||
el.style.color = '#555';
|
||||
}
|
||||
vvLoadChecklist();
|
||||
}).catch(() => {
|
||||
el.textContent = 'Auto-populate unavailable — run manually from Scheduler';
|
||||
el.style.color = '#555';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Checklist ─────────────────────────────────────────────────────────────────
|
||||
const vvActionLabels = {
|
||||
create_key: 'Create API key',
|
||||
ssh_setup: 'SSH guide →',
|
||||
run_populate: 'Run now',
|
||||
pull_master: 'Pull from HOST1',
|
||||
onboard: 'Partnership tab →',
|
||||
};
|
||||
const vvActionHref = {
|
||||
ssh_setup: '?tab=partnership',
|
||||
onboard: '?tab=partnership',
|
||||
};
|
||||
|
||||
function vvLoadChecklist() {
|
||||
fetch('/plugins/varaverk/api/checklist.php?_=' + Date.now())
|
||||
.then(r => r.json()).then(d => {
|
||||
const el = document.getElementById('vv-checklist');
|
||||
if (!d.ok || !d.items) { el.innerHTML = '<span style="color:#555">Unable to load checklist</span>'; return; }
|
||||
el.innerHTML = d.items.map(item => {
|
||||
const icon = item.ok === null ? '○' : (item.ok ? '✓' : '✗');
|
||||
const iclr = item.ok === null ? '#444' : (item.ok ? '#4a8' : '#a66');
|
||||
let act = '';
|
||||
if (item.action) {
|
||||
const lbl = vvActionLabels[item.action] || item.action;
|
||||
const href = vvActionHref[item.action];
|
||||
if (href) {
|
||||
act = `<div class="vv-cl-act"><a href="${href}" style="font-size:11px;color:#556;">${lbl}</a></div>`;
|
||||
} else if (item.action === 'create_key') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvCreateKey(this)">${lbl}</button></div>`;
|
||||
} else if (item.action === 'run_populate') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvRunPopulateBtn(this)">${lbl}</button></div>`;
|
||||
} else if (item.action === 'pull_master') {
|
||||
act = `<div class="vv-cl-act"><button onclick="vvPullMaster(this)">${lbl}</button><div id="vv-pull-err" class="vv-cl-err"></div></div>`;
|
||||
}
|
||||
}
|
||||
return `<div class="vv-cl-item">
|
||||
<div class="vv-cl-icon" style="color:${iclr}">${icon}</div>
|
||||
<div class="vv-cl-body">
|
||||
<div class="vv-cl-label">${item.label}</div>
|
||||
<div class="vv-cl-detail">${item.detail || ''}</div>
|
||||
${act}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function vvRunPopulateBtn(btn) {
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'populate'})
|
||||
}).then(() => { btn.textContent = 'Done'; vvLoadChecklist(); })
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||
}
|
||||
|
||||
function vvPullMaster(btn) {
|
||||
btn.disabled = true; btn.textContent = '⟳ Pulling…';
|
||||
const errEl = document.getElementById('vv-pull-err');
|
||||
if (errEl) errEl.textContent = '';
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'pull'})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
btn.textContent = '✓ Done';
|
||||
setTimeout(vvLoadChecklist, 600);
|
||||
} else {
|
||||
if (errEl) errEl.textContent = d.error || 'Pull failed';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
}
|
||||
}).catch(() => { btn.disabled = false; btn.textContent = 'Retry'; });
|
||||
}
|
||||
|
||||
// ── API key ───────────────────────────────────────────────────────────────────
|
||||
function vvCreateKey(btn) {
|
||||
const status = document.getElementById('vv-key-status');
|
||||
btn.disabled = true; btn.textContent = '⟳ Creating…';
|
||||
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
|
||||
.then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
status.textContent = '✓ Key created — ' + d.key_preview;
|
||||
status.style.color = '#4a8';
|
||||
btn.textContent = 'Created ✓'; btn.style.opacity = '.6';
|
||||
vvLoadChecklist();
|
||||
} else {
|
||||
status.textContent = '✗ ' + (d.error || 'Failed');
|
||||
status.style.color = '#a44';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
}
|
||||
}).catch(e => {
|
||||
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
||||
btn.disabled = false; btn.textContent = 'Retry';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||
function vvDoSave() {
|
||||
const hostname = document.getElementById('vv-hostname')?.value.trim();
|
||||
if (!hostname) { vvSetStatus('✗ Hostname is required', 'err'); return; }
|
||||
let host1 = '', host2 = '', mySlot = 'host1';
|
||||
if (vvRole === 'primary') {
|
||||
host1 = hostname;
|
||||
host2 = document.getElementById('vv-partner-hostname')?.value.trim() || '';
|
||||
mySlot = 'host1';
|
||||
} else {
|
||||
const primary = document.getElementById('vv-primary-hostname')?.value.trim();
|
||||
if (!primary) { vvSetStatus('✗ Primary hostname required', 'err'); return; }
|
||||
mySlot = document.getElementById('vv-partner-slot')?.value || 'host2';
|
||||
host1 = primary;
|
||||
if (mySlot === 'host2') host2 = hostname;
|
||||
}
|
||||
vvSetBtn('Saving…', true);
|
||||
fetch('/plugins/varaverk/api/setup.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
if (d.needs_migration) {
|
||||
const dest = d.migrate_to === 'flash' ? 'appdata' : '/boot';
|
||||
vvSetStatus('⟳ Migrating scripts to ' + dest + '…', '');
|
||||
vvDoMigration(d.migrate_to, d.redirect || '?tab=scheduler', d.api_key);
|
||||
} else {
|
||||
vvShowStep2(d.redirect || '?tab=scheduler', d.api_key);
|
||||
}
|
||||
} else { vvSetBtn('Save and continue →', false); vvSetStatus('✗ ' + (d.error||'Error'), 'err'); }
|
||||
}).catch(() => { vvSetBtn('Save and continue →', false); vvSetStatus('✗ Request failed', 'err'); });
|
||||
}
|
||||
|
||||
function vvDoMigration(to, redirect, apiKey) {
|
||||
fetch('/plugins/varaverk/api/storage.php', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: new URLSearchParams({action: 'migrate', to})
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
vvShowStep2(redirect, apiKey);
|
||||
} else {
|
||||
vvSetBtn('Save and continue →', false);
|
||||
vvSetStatus('✗ Migration failed — ' + (d.error || 'check install.log'), 'err');
|
||||
}
|
||||
}).catch(() => {
|
||||
vvSetBtn('Save and continue →', false);
|
||||
vvSetStatus('✗ Migration request failed', 'err');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
|
||||
? trim($_GET['action'] ?? '')
|
||||
: trim($_POST['action'] ?? 'save');
|
||||
|
||||
// ── GET: detect environment ────────────────────────────────────────────────────────────────────
|
||||
if ($action === 'detect') {
|
||||
$bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||
$bootDisk = $bootPart
|
||||
? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
|
||||
: '';
|
||||
$transport = $bootDisk
|
||||
? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
|
||||
: 'unknown';
|
||||
|
||||
$isUsb = ($transport === 'usb');
|
||||
|
||||
preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'hostname' => vv_get_hostname(),
|
||||
'unraid_ver' => $vm[1] ?? 'unknown',
|
||||
'transport' => $transport,
|
||||
'boot_device' => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
|
||||
'mode' => $isUsb ? 'flash' : 'internal',
|
||||
'scripts_dir' => SCRIPTS_DIR,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
|
||||
if ($action === 'ssh_generate') {
|
||||
$script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
|
||||
exit;
|
||||
}
|
||||
exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
|
||||
// Derive pubkey path from hostname
|
||||
$hostname = vv_get_hostname();
|
||||
$shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
|
||||
$pubPath = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
|
||||
$pubKey = trim(@file_get_contents($pubPath) ?: '');
|
||||
echo json_encode([
|
||||
'ok' => $rc === 0 && !empty($pubKey),
|
||||
'pubkey' => $pubKey,
|
||||
'error' => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
|
||||
if ($action === 'populate') {
|
||||
$script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
|
||||
exit;
|
||||
}
|
||||
exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
|
||||
$lines = array_values(array_filter(array_map('trim', $out)));
|
||||
echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
|
||||
|
||||
// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
|
||||
if ($action === 'pull') {
|
||||
$mySlot = trim($_POST['my_slot'] ?? '') ?: strtolower(vv_detect_host());
|
||||
$myHostname = trim($_POST['my_hostname'] ?? '') ?: vv_get_hostname();
|
||||
$host1Hostname = trim($_POST['host1_hostname'] ?? '');
|
||||
if (!$host1Hostname) {
|
||||
$masterRaw = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
|
||||
$host1Hostname = trim($_mh[1] ?? '');
|
||||
}
|
||||
if (!$host1Hostname) {
|
||||
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
|
||||
exit;
|
||||
}
|
||||
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] ?? '/boot/config/plugins/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) {
|
||||
$bootPart2 = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||
$bootDisk2 = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
|
||||
$transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
|
||||
$storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
|
||||
$conf = str_replace('HOSTN', $hostId, $template);
|
||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||
'${1}"' . $sshKey . '"', $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||
'${1}' . $storageInternal2, $conf);
|
||||
vv_write_conf_raw($confFile, $conf);
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists($sshScript)) {
|
||||
exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
|
||||
}
|
||||
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
|
||||
|
||||
$state = vv_setup_state_read();
|
||||
$state['master_conf_pulled'] = 'true';
|
||||
vv_setup_state_write($state);
|
||||
|
||||
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
|
||||
'api_key' => $apiKeyResult,
|
||||
'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';
|
||||
|
||||
// Auto-detect storage mode from boot device transport
|
||||
$bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||
$bootDisk = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
|
||||
$transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
|
||||
$storageInternal = ($transport !== 'usb') ? 'true' : 'false';
|
||||
|
||||
$conf = str_replace('HOSTN', $hostId, $template);
|
||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||
'${1}"' . $sshKeyPath . '"', $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||
'${1}' . $storageInternal, $conf);
|
||||
if (!vv_write_conf_raw($confFile, $conf)) {
|
||||
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]);
|
||||
|
||||
// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
|
||||
if (file_exists($sshScript)) {
|
||||
exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
|
||||
}
|
||||
|
||||
// Auto-create Unraid API key and write into the fresh conf
|
||||
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'host_id' => $hostId,
|
||||
'api_key' => $apiKeyResult,
|
||||
'redirect' => '?tab=scheduler&vv_setup=master.conf',
|
||||
]);
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
|
||||
$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
|
||||
? trim($_GET['action'] ?? '')
|
||||
: trim($_POST['action'] ?? 'save');
|
||||
|
||||
// ── GET: detect environment ────────────────────────────────────────────────────────────────────
|
||||
if ($action === 'detect') {
|
||||
$bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||
$bootDisk = $bootPart
|
||||
? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '')
|
||||
: '';
|
||||
$transport = $bootDisk
|
||||
? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: ''))
|
||||
: 'unknown';
|
||||
|
||||
$isUsb = ($transport === 'usb');
|
||||
|
||||
preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'hostname' => vv_get_hostname(),
|
||||
'unraid_ver' => $vm[1] ?? 'unknown',
|
||||
'transport' => $transport,
|
||||
'boot_device' => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
|
||||
'mode' => $isUsb ? 'flash' : 'internal',
|
||||
'scripts_dir' => SCRIPTS_DIR,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── GET/POST: generate local SSH keypair ──────────────────────────────────────────────────────
|
||||
if ($action === 'ssh_generate') {
|
||||
$script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']);
|
||||
exit;
|
||||
}
|
||||
exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc);
|
||||
// Derive pubkey path from hostname
|
||||
$hostname = vv_get_hostname();
|
||||
$shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname));
|
||||
$pubPath = '/root/.ssh/' . $shortName . '_rsync_automation.pub';
|
||||
$pubKey = trim(@file_get_contents($pubPath) ?: '');
|
||||
echo json_encode([
|
||||
'ok' => $rc === 0 && !empty($pubKey),
|
||||
'pubkey' => $pubKey,
|
||||
'error' => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── POST: run conf_populate.sh ─────────────────────────────────────────────────────────────────
|
||||
if ($action === 'populate') {
|
||||
$script = SCRIPTS_DIR . '/Plugin/unraid/Tools/conf_populate.sh';
|
||||
if (!file_exists($script)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']);
|
||||
exit;
|
||||
}
|
||||
exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc);
|
||||
$lines = array_values(array_filter(array_map('trim', $out)));
|
||||
echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh';
|
||||
|
||||
// ── Pull master.conf from HOST1 via SSH (wizard or checklist) ────────────────────────────────
|
||||
if ($action === 'pull') {
|
||||
$mySlot = trim($_POST['my_slot'] ?? '') ?: strtolower(vv_detect_host());
|
||||
$myHostname = trim($_POST['my_hostname'] ?? '') ?: vv_get_hostname();
|
||||
$host1Hostname = trim($_POST['host1_hostname'] ?? '');
|
||||
if (!$host1Hostname) {
|
||||
$masterRaw = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh);
|
||||
$host1Hostname = trim($_mh[1] ?? '');
|
||||
}
|
||||
if (!$host1Hostname) {
|
||||
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']);
|
||||
exit;
|
||||
}
|
||||
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] ?? '/boot/config/plugins/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) {
|
||||
$bootPart2 = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||
$bootDisk2 = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : '';
|
||||
$transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : '';
|
||||
$storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false';
|
||||
$conf = str_replace('HOSTN', $hostId, $template);
|
||||
$conf = str_replace('hostn', $hostIdLow, $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
|
||||
'${1}"' . $sshKey . '"', $conf);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||
'${1}' . $storageInternal2, $conf);
|
||||
vv_write_conf_raw($confFile, $conf);
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists($sshScript)) {
|
||||
exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
|
||||
}
|
||||
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
|
||||
|
||||
$state = vv_setup_state_read();
|
||||
$state['master_conf_pulled'] = 'true';
|
||||
vv_setup_state_write($state);
|
||||
|
||||
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
|
||||
'api_key' => $apiKeyResult,
|
||||
'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';
|
||||
|
||||
// Storage mode: use wizard selection, fall back to auto-detect from boot transport
|
||||
$smParam = trim($_POST['storage_mode'] ?? '');
|
||||
if ($smParam === 'flash') {
|
||||
$storageInternal = 'false';
|
||||
} elseif ($smParam === 'internal') {
|
||||
$storageInternal = 'true';
|
||||
} else {
|
||||
$bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: '');
|
||||
$bootDisk = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : '';
|
||||
$transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : '';
|
||||
$storageInternal = ($transport !== 'usb') ? 'true' : 'false';
|
||||
}
|
||||
|
||||
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);
|
||||
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||
'${1}' . $storageInternal, $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]);
|
||||
|
||||
// Auto-generate SSH keypair (local only — remote copy happens during onboarding)
|
||||
if (file_exists($sshScript)) {
|
||||
exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null');
|
||||
}
|
||||
|
||||
// Auto-create Unraid API key and write into the fresh conf
|
||||
$apiKeyResult = vv_auto_create_api_key($hostId, $confFile);
|
||||
|
||||
$targetDir = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk';
|
||||
$needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'host_id' => $hostId,
|
||||
'api_key' => $apiKeyResult,
|
||||
'needs_migration'=> $needsMigration,
|
||||
'migrate_to' => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null,
|
||||
'redirect' => '?tab=scheduler&vv_setup=master.conf',
|
||||
]);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
||||
<?xml version='1.0' standalone='yes'?>
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "varaverk">
|
||||
<!ENTITY author "gmer4lfe">
|
||||
<!ENTITY version "2026.05.31">
|
||||
<!ENTITY sha256 "d588470d6cc7f284601cb56039d5dfea6fb5bb1ee2c800657438a8edacafbd01">
|
||||
<!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">
|
||||
<!ENTITY pkg "varaverk-&version;-noarch-1.txz">
|
||||
]>
|
||||
<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.31
|
||||
- Packaged release: web files now ship as a .txz that Unraid reinstalls to RAM on every boot
|
||||
- Survives reboots with zero manual steps (no symlink, no go script) — fixes plugin vanishing after OS upgrades
|
||||
- Scripts are git-cloned to appdata on first install; web files stay on flash (~200KB)
|
||||
- Updates handled in-UI (git pull); the plugin no longer pulls on every boot
|
||||
|
||||
###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>
|
||||
|
||||
<!--
|
||||
── 1. Web files symlink (runs on every boot) ──────────────────────────────────
|
||||
Instead of extracting a .txz, we symlink the installed plugin web dir directly
|
||||
to the workspace on flash. Changes to Plugin/unraid/ are live instantly — no
|
||||
package build, no sync step. /boot is always mounted before this runs.
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Run="/bin/bash">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
WEB_DIR="/usr/local/emhttp/plugins/varaverk"
|
||||
SRC="/boot/config/plugins/varaverk/Plugin/unraid"
|
||||
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
|
||||
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
|
||||
ln -sf "$SRC" "$WEB_DIR"
|
||||
echo "[Varaverk] web dir symlinked → $SRC"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
── 2. Scripts bootstrap (first install only) ──────────────────────────────────
|
||||
Clones the repo directly into the plugin config dir on flash (/boot/config/plugins/varaverk).
|
||||
No array dependency — scripts live on flash (64GB NVMe) and are available at boot.
|
||||
Uses git init+fetch+reset so the clone works into the non-empty cfgdir (varaverk.cfg,
|
||||
varaverk-*.txz etc. are already there). Never auto-pulls — updates via the UI git pull.
|
||||
|
||||
Clone source priority:
|
||||
1. Gitea (internal) — reads settings from varaverk.cfg; detects container IP at runtime
|
||||
2. GitHub (public) — HTTPS fallback if Gitea is unreachable
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Run="/bin/bash" Method="install">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
PLUGIN="varaverk"
|
||||
CFG_DIR="/boot/config/plugins/$PLUGIN"
|
||||
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
||||
GITHUB="https://github.com/FailedProxy/Varaverk"
|
||||
BRANCH="main"
|
||||
LOG="$CFG_DIR/install.log"
|
||||
|
||||
mkdir -p "$CFG_DIR"
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
|
||||
# Scripts live in the plugin dir on flash — no array needed.
|
||||
SCRIPTS_DIR="$CFG_DIR"
|
||||
CONF_DIR="$SCRIPTS_DIR/Configurations"
|
||||
|
||||
# ── Boot device check ─────────────────────────────────────────────────────────
|
||||
# Warn if /boot is on a USB/removable device. Varaverk is designed for internal
|
||||
# NVMe/SSD boot — git repo + state files + data writes on USB will wear it out
|
||||
# fast and may run out of space. Install proceeds but user is warned.
|
||||
_boot_dev=$(df /boot --output=source 2>/dev/null | tail -1)
|
||||
_boot_base=$(lsblk -no pkname "$_boot_dev" 2>/dev/null || basename "${_boot_dev%[0-9p]*}")
|
||||
_removable=$(cat "/sys/block/${_boot_base}/removable" 2>/dev/null || echo "0")
|
||||
if [[ "$_removable" == "1" ]]; then
|
||||
log "WARNING: /boot is on a removable/USB device ($_boot_dev)"
|
||||
log "WARNING: Varaverk is designed for internal NVMe/SSD boot."
|
||||
log "WARNING: Running from USB risks drive wear and space exhaustion."
|
||||
log "WARNING: Strongly recommend migrating boot to an internal NVMe/SSD drive."
|
||||
fi
|
||||
unset _boot_dev _boot_base _removable
|
||||
|
||||
# Seed varaverk.cfg with defaults (SCRIPTS_DIR + Gitea settings) if not present.
|
||||
# Requires internal NVMe/SSD boot — scripts live on flash, available before array mounts.
|
||||
if [[ ! -f "$CFG_FILE" ]]; then
|
||||
cat > "$CFG_FILE" <<'CFGEOF'
|
||||
SCRIPTS_DIR="/boot/config/plugins/varaverk"
|
||||
GITEA_CONTAINER="Gitea"
|
||||
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
|
||||
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
|
||||
SSH_PORT="221"
|
||||
CFGEOF
|
||||
log "seeded varaverk.cfg"
|
||||
fi
|
||||
|
||||
# Read Gitea settings from varaverk.cfg (allows override without editing .plg).
|
||||
_read_cfg() { grep -oP "(?<=^${1}=\")[^\"]*" "$CFG_FILE" 2>/dev/null || echo "${2}"; }
|
||||
GITEA_CONTAINER=$(_read_cfg GITEA_CONTAINER "Gitea")
|
||||
GITEA_REPO_PATH=$(_read_cfg GITEA_REPO_PATH "FailedProxy/Varaverk.git")
|
||||
GITEA_SSH_KEY=$(_read_cfg GITEA_SSH_KEY "/root/.ssh/unraid_gitea")
|
||||
SSH_PORT=$(_read_cfg SSH_PORT "221")
|
||||
|
||||
# Clone on first install only; never auto-pull (updates via the UI git pull).
|
||||
if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then
|
||||
log "initialising repo in $SCRIPTS_DIR ($BRANCH)..."
|
||||
|
||||
# Locate Gitea: local container → local IP; else Tailscale; else fall back to GitHub.
|
||||
GITEA_IP=""
|
||||
if command -v docker >/dev/null 2>&1 && \
|
||||
docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
|
||||
GITEA_IP=$(hostname -I | awk '{print $1}')
|
||||
log "Gitea running locally — using $GITEA_IP"
|
||||
elif command -v tailscale >/dev/null 2>&1; then
|
||||
# Try each known peer until we find one hosting Gitea
|
||||
while IFS= read -r peer_ip; do
|
||||
if ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \
|
||||
-o ConnectTimeout=3 -o StrictHostKeyChecking=no \
|
||||
-o BatchMode=yes "git@${peer_ip}" info 2>/dev/null | grep -q "varaverk\|Gitea\|gitea"; then
|
||||
GITEA_IP="$peer_ip"
|
||||
log "Gitea found on Tailscale peer $GITEA_IP"
|
||||
break
|
||||
fi
|
||||
done < <(tailscale status --json 2>/dev/null | \
|
||||
python3 -c "import json,sys; d=json.load(sys.stdin); \
|
||||
[print(v['TailscaleIPs'][0]) for v in d.get('Peer',{}).values() \
|
||||
if v.get('TailscaleIPs')]" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# init-in-place — git clone would fail because the dir already has files.
|
||||
git -C "$SCRIPTS_DIR" init >> "$LOG" 2>&1
|
||||
|
||||
CLONED=false
|
||||
if [[ -n "$GITEA_IP" && -f "$GITEA_SSH_KEY" ]]; then
|
||||
GITEA_URL="ssh://git@${GITEA_IP}:${SSH_PORT}/${GITEA_REPO_PATH}"
|
||||
log "trying Gitea: $GITEA_URL"
|
||||
git -C "$SCRIPTS_DIR" remote add origin "$GITEA_URL" >> "$LOG" 2>&1
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT -o StrictHostKeyChecking=no" \
|
||||
GIT_TERMINAL_PROMPT=0 \
|
||||
git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
|
||||
git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
|
||||
log "scripts installed from Gitea ($GITEA_IP)"
|
||||
CLONED=true
|
||||
else
|
||||
log "Gitea fetch failed — falling back to GitHub"
|
||||
git -C "$SCRIPTS_DIR" remote remove origin >> "$LOG" 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$CLONED" == false ]]; then
|
||||
log "trying GitHub: $GITHUB"
|
||||
git -C "$SCRIPTS_DIR" remote add origin "$GITHUB" >> "$LOG" 2>&1
|
||||
if GIT_TERMINAL_PROMPT=0 \
|
||||
git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
|
||||
git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
|
||||
log "scripts installed from GitHub"
|
||||
CLONED=true
|
||||
else
|
||||
log "WARNING: both Gitea and GitHub failed — scripts not installed, retry when network is up"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "repo present — leaving scripts untouched (update from the UI)"
|
||||
fi
|
||||
|
||||
# Seed master.conf from template if absent.
|
||||
mkdir -p "$CONF_DIR"
|
||||
if [[ ! -f "$CONF_DIR/master.conf" && -f "$SCRIPTS_DIR/Deployment/conf_templates/master.conf" ]]; then
|
||||
cp "$SCRIPTS_DIR/Deployment/conf_templates/master.conf" "$CONF_DIR/master.conf"
|
||||
log "seeded master.conf from template"
|
||||
fi
|
||||
log "install step complete"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
── 3. Remove ───────────────────────────────────────────────────────────────────
|
||||
Stops background scripts, removes cron, the installed package, and flash config.
|
||||
Scripts/conf in appdata are left intact (delete manually for a full wipe).
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE 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] $*"; }
|
||||
|
||||
[[ -f "$CFG_FILE" ]] && _sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
||||
SCRIPTS_DIR="${_sd:-$CFG_DIR}"
|
||||
|
||||
# Stop continuous background scripts.
|
||||
if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
|
||||
bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && log "fallback.sh stopped" || true
|
||||
fi
|
||||
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 removed"
|
||||
fi
|
||||
rm -f /etc/cron.d/varaverk
|
||||
|
||||
# Remove the installed package (and its RAM files).
|
||||
removepkg "$PLUGIN" 2>/dev/null || true
|
||||
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
|
||||
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
|
||||
log "web files removed"
|
||||
|
||||
# Remove flash config (incl. cached .txz).
|
||||
rm -rf "$CFG_DIR"
|
||||
log "flash config removed"
|
||||
log "done — scripts/conf in $SCRIPTS_DIR preserved (delete manually for full wipe)"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
</PLUGIN>
|
||||
@@ -0,0 +1,256 @@
|
||||
<?xml version='1.0' standalone='yes'?>
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "varaverk">
|
||||
<!ENTITY author "gmer4lfe">
|
||||
<!ENTITY version "2026.05.31">
|
||||
<!ENTITY sha256 "d588470d6cc7f284601cb56039d5dfea6fb5bb1ee2c800657438a8edacafbd01">
|
||||
<!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">
|
||||
<!ENTITY pkg "varaverk-&version;-noarch-1.txz">
|
||||
]>
|
||||
<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.31
|
||||
- Packaged release: web files now ship as a .txz that Unraid reinstalls to RAM on every boot
|
||||
- Survives reboots with zero manual steps (no symlink, no go script) — fixes plugin vanishing after OS upgrades
|
||||
- Scripts are git-cloned to appdata on first install; web files stay on flash (~200KB)
|
||||
- Updates handled in-UI (git pull); the plugin no longer pulls on every boot
|
||||
|
||||
###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>
|
||||
|
||||
<!--
|
||||
── 1. Web files symlink (runs on every boot) ──────────────────────────────────
|
||||
Instead of extracting a .txz, we symlink the installed plugin web dir directly
|
||||
to the workspace on flash. Changes to Plugin/unraid/ are live instantly — no
|
||||
package build, no sync step. /boot is always mounted before this runs.
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Run="/bin/bash">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
WEB_DIR="/usr/local/emhttp/plugins/varaverk"
|
||||
SRC="/boot/config/plugins/varaverk/Plugin/unraid"
|
||||
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
|
||||
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
|
||||
ln -sf "$SRC" "$WEB_DIR"
|
||||
echo "[Varaverk] web dir symlinked → $SRC"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
── 2. Scripts bootstrap (first install only) ──────────────────────────────────
|
||||
Clones the repo directly into the plugin config dir on flash (/boot/config/plugins/varaverk).
|
||||
No array dependency — scripts live on flash (64GB NVMe) and are available at boot.
|
||||
Uses git init+fetch+reset so the clone works into the non-empty cfgdir (varaverk.cfg,
|
||||
varaverk-*.txz etc. are already there). Never auto-pulls — updates via the UI git pull.
|
||||
|
||||
Clone source priority:
|
||||
1. Gitea (internal) — reads settings from varaverk.cfg; detects container IP at runtime
|
||||
2. GitHub (public) — HTTPS fallback if Gitea is unreachable
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE Run="/bin/bash" Method="install">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
#!/bin/bash
|
||||
PLUGIN="varaverk"
|
||||
CFG_DIR="/boot/config/plugins/$PLUGIN"
|
||||
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
||||
GITHUB="https://github.com/FailedProxy/Varaverk"
|
||||
BRANCH="main"
|
||||
LOG="$CFG_DIR/install.log"
|
||||
|
||||
mkdir -p "$CFG_DIR"
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
|
||||
# Scripts live in the plugin dir on flash — no array needed.
|
||||
SCRIPTS_DIR="$CFG_DIR"
|
||||
CONF_DIR="$SCRIPTS_DIR/Configurations"
|
||||
|
||||
# ── Boot device check ─────────────────────────────────────────────────────────
|
||||
# Warn if /boot is on a USB/removable device. Varaverk is designed for internal
|
||||
# NVMe/SSD boot — git repo + state files + data writes on USB will wear it out
|
||||
# fast and may run out of space. Install proceeds but user is warned.
|
||||
_boot_dev=$(df /boot --output=source 2>/dev/null | tail -1)
|
||||
_boot_base=$(lsblk -no pkname "$_boot_dev" 2>/dev/null || basename "${_boot_dev%[0-9p]*}")
|
||||
_removable=$(cat "/sys/block/${_boot_base}/removable" 2>/dev/null || echo "0")
|
||||
if [[ "$_removable" == "1" ]]; then
|
||||
log "WARNING: /boot is on a removable/USB device ($_boot_dev)"
|
||||
log "WARNING: Varaverk is designed for internal NVMe/SSD boot."
|
||||
log "WARNING: Running from USB risks drive wear and space exhaustion."
|
||||
log "WARNING: Strongly recommend migrating boot to an internal NVMe/SSD drive."
|
||||
fi
|
||||
unset _boot_dev _boot_base _removable
|
||||
|
||||
# Seed varaverk.cfg with defaults (SCRIPTS_DIR + Gitea settings) if not present.
|
||||
# Requires internal NVMe/SSD boot — scripts live on flash, available before array mounts.
|
||||
if [[ ! -f "$CFG_FILE" ]]; then
|
||||
cat > "$CFG_FILE" <<'CFGEOF'
|
||||
SCRIPTS_DIR="/boot/config/plugins/varaverk"
|
||||
GITEA_CONTAINER="Gitea"
|
||||
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
|
||||
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
|
||||
SSH_PORT="221"
|
||||
CFGEOF
|
||||
log "seeded varaverk.cfg"
|
||||
fi
|
||||
|
||||
# Read Gitea settings from varaverk.cfg (allows override without editing .plg).
|
||||
_read_cfg() { grep -oP "(?<=^${1}=\")[^\"]*" "$CFG_FILE" 2>/dev/null || echo "${2}"; }
|
||||
GITEA_CONTAINER=$(_read_cfg GITEA_CONTAINER "Gitea")
|
||||
GITEA_REPO_PATH=$(_read_cfg GITEA_REPO_PATH "FailedProxy/Varaverk.git")
|
||||
GITEA_SSH_KEY=$(_read_cfg GITEA_SSH_KEY "/root/.ssh/unraid_gitea")
|
||||
SSH_PORT=$(_read_cfg SSH_PORT "221")
|
||||
|
||||
# Clone on first install only; never auto-pull (updates via the UI git pull).
|
||||
if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then
|
||||
log "initialising repo in $SCRIPTS_DIR ($BRANCH)..."
|
||||
|
||||
# Locate Gitea: local container → local IP; else Tailscale; else fall back to GitHub.
|
||||
GITEA_IP=""
|
||||
if command -v docker >/dev/null 2>&1 && \
|
||||
docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
|
||||
GITEA_IP=$(hostname -I | awk '{print $1}')
|
||||
log "Gitea running locally — using $GITEA_IP"
|
||||
elif command -v tailscale >/dev/null 2>&1; then
|
||||
# Try each known peer until we find one hosting Gitea
|
||||
while IFS= read -r peer_ip; do
|
||||
if ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \
|
||||
-o ConnectTimeout=3 -o StrictHostKeyChecking=no \
|
||||
-o BatchMode=yes "git@${peer_ip}" info 2>/dev/null | grep -q "varaverk\|Gitea\|gitea"; then
|
||||
GITEA_IP="$peer_ip"
|
||||
log "Gitea found on Tailscale peer $GITEA_IP"
|
||||
break
|
||||
fi
|
||||
done < <(tailscale status --json 2>/dev/null | \
|
||||
python3 -c "import json,sys; d=json.load(sys.stdin); \
|
||||
[print(v['TailscaleIPs'][0]) for v in d.get('Peer',{}).values() \
|
||||
if v.get('TailscaleIPs')]" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# init-in-place — git clone would fail because the dir already has files.
|
||||
git -C "$SCRIPTS_DIR" init >> "$LOG" 2>&1
|
||||
|
||||
CLONED=false
|
||||
if [[ -n "$GITEA_IP" && -f "$GITEA_SSH_KEY" ]]; then
|
||||
GITEA_URL="ssh://git@${GITEA_IP}:${SSH_PORT}/${GITEA_REPO_PATH}"
|
||||
log "trying Gitea: $GITEA_URL"
|
||||
git -C "$SCRIPTS_DIR" remote add origin "$GITEA_URL" >> "$LOG" 2>&1
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT -o StrictHostKeyChecking=no" \
|
||||
GIT_TERMINAL_PROMPT=0 \
|
||||
git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
|
||||
git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
|
||||
log "scripts installed from Gitea ($GITEA_IP)"
|
||||
CLONED=true
|
||||
else
|
||||
log "Gitea fetch failed — falling back to GitHub"
|
||||
git -C "$SCRIPTS_DIR" remote remove origin >> "$LOG" 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$CLONED" == false ]]; then
|
||||
log "trying GitHub: $GITHUB"
|
||||
git -C "$SCRIPTS_DIR" remote add origin "$GITHUB" >> "$LOG" 2>&1
|
||||
if GIT_TERMINAL_PROMPT=0 \
|
||||
git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
|
||||
git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
|
||||
git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
|
||||
log "scripts installed from GitHub"
|
||||
CLONED=true
|
||||
else
|
||||
log "WARNING: both Gitea and GitHub failed — scripts not installed, retry when network is up"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "repo present — leaving scripts untouched (update from the UI)"
|
||||
fi
|
||||
|
||||
# Seed master.conf from template if absent.
|
||||
mkdir -p "$CONF_DIR"
|
||||
if [[ ! -f "$CONF_DIR/master.conf" && -f "$CONF_DIR/master.conf.template" ]]; then
|
||||
cp "$CONF_DIR/master.conf.template" "$CONF_DIR/master.conf"
|
||||
log "seeded master.conf from template"
|
||||
fi
|
||||
log "install step complete"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!--
|
||||
── 3. Remove ───────────────────────────────────────────────────────────────────
|
||||
Stops background scripts, removes cron, the installed package, and flash config.
|
||||
Scripts/conf in appdata are left intact (delete manually for a full wipe).
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
-->
|
||||
<FILE 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] $*"; }
|
||||
|
||||
[[ -f "$CFG_FILE" ]] && _sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
||||
SCRIPTS_DIR="${_sd:-$CFG_DIR}"
|
||||
|
||||
# Stop continuous background scripts.
|
||||
if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
|
||||
bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && log "fallback.sh stopped" || true
|
||||
fi
|
||||
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 removed"
|
||||
fi
|
||||
rm -f /etc/cron.d/varaverk
|
||||
|
||||
# Remove the installed package (and its RAM files).
|
||||
removepkg "$PLUGIN" 2>/dev/null || true
|
||||
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
|
||||
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
|
||||
log "web files removed"
|
||||
|
||||
# Remove flash config (incl. cached .txz).
|
||||
rm -rf "$CFG_DIR"
|
||||
log "flash config removed"
|
||||
log "done — scripts/conf in $SCRIPTS_DIR preserved (delete manually for full wipe)"
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
</PLUGIN>
|
||||
@@ -0,0 +1,469 @@
|
||||
#!/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
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Storage mode ━━━
|
||||
# Controls where Varaverk stores scripts, conf, and state files.
|
||||
# true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct)
|
||||
# false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime)
|
||||
# Auto-detected from boot device transport on first setup.
|
||||
# To change: Settings → Storage → Migrate.
|
||||
HOSTN_STORAGE_MODE_INTERNAL=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
@@ -0,0 +1,484 @@
|
||||
#!/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
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
# ==============================================================================================
|
||||
# ── STORAGE MODE ──────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# ━━━ Storage mode ━━━
|
||||
# Controls where Varaverk stores scripts, conf, and state files.
|
||||
# true = internal NVMe/SSD — /boot/config/plugins/varaverk (write-safe, git-direct)
|
||||
# false = USB flash boot — /mnt/user/appdata/Varaverk (preserves flash lifetime)
|
||||
# Auto-detected from boot device transport on first setup.
|
||||
# To change: Settings → Storage → Migrate.
|
||||
HOSTN_STORAGE_MODE_INTERNAL=true
|
||||
|
||||
# ==============================================================================================
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ━━━ Personal Shares ━━━
|
||||
# Private encrypted shares synced for offsite backup, independent of media shares.
|
||||
HOSTN_PERSONAL_SHARES=(
|
||||
# /mnt/user/Personal # e.g. ZFS-encrypted dataset
|
||||
)
|
||||
|
||||
# ━━━ 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_TMDB_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_KERNEL_OOPS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_FD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_BOOT=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_OOM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RAM=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOG=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ARC=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_TEMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_LOAD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_ZOMBIES=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_CONTAINERS=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_TMP=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_MDSTAT=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_NETWORK=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_SSHD=true
|
||||
HOSTN_SYS_WATCHDOG_CHECK_RUNAWAY=false
|
||||
|
||||
HOSTN_SYS_WATCHDOG_NIC="" # e.g. eth0 — for network monitoring
|
||||
|
||||
# ==============================================================================================
|
||||
# ── RESOURCE MANAGER ──────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
HOSTN_RW_PAUSE_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
# "LidaTube"
|
||||
)
|
||||
|
||||
HOSTN_RW_STOP_CONTAINERS=(
|
||||
# "Tdarr"
|
||||
)
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ============================= conf_upgrade.sh ================================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Merges a new conf template into an existing user conf while preserving every
|
||||
# value the user has already set. Run manually when the conf schema changes
|
||||
# between versions — adds new keys, removes deprecated ones, and keeps the
|
||||
# structure of the new template exactly.
|
||||
#
|
||||
# Keys in template only → ADDED (placeholder/default — user fills in once)
|
||||
# Keys in target only → REMOVED (deprecated in new version)
|
||||
# Keys in both → KEPT (target's value always wins, template ignored)
|
||||
# Comments / blank lines → always from template (structure follows new version)
|
||||
#
|
||||
# Supports all conf variable patterns: simple scalars, indexed arrays, and
|
||||
# associative arrays (declare -A).
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Dry-run Mode
|
||||
# --dry-run prints the full change report (ADDED / REMOVED / KEPT) then exits
|
||||
# without writing anything. Always preview before applying to production confs.
|
||||
#
|
||||
# Backup Option
|
||||
# --backup writes a .bak copy of the target before overwriting. Use when
|
||||
# applying to a conf that has never been upgraded before.
|
||||
#
|
||||
# File Existence Guards
|
||||
# Both --template and --target are validated before any parsing begins.
|
||||
# Missing files abort immediately with a clear error.
|
||||
#
|
||||
# Atomic Write
|
||||
# Merged output is written to a tempfile first, then copied to the target.
|
||||
# A partial write cannot corrupt the original.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# No conf vars. All inputs are CLI flags.
|
||||
#
|
||||
# --template <file> New version conf file (source of structure and defaults)
|
||||
# --target <file> Existing user conf (source of real values — always preserved)
|
||||
# --dry-run Show what would change without writing
|
||||
# --backup Write a .bak copy of target before modifying
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# conf_upgrade.sh --template master.conf.template --target Configurations/master.conf --dry-run
|
||||
# Preview what would be added, removed, and kept — no changes written.
|
||||
#
|
||||
# conf_upgrade.sh --template master.conf.template --target Configurations/master.conf --backup
|
||||
# Apply the upgrade, writing a .bak first.
|
||||
#
|
||||
# conf_upgrade.sh --template master.conf.template --target Configurations/master.conf
|
||||
# Apply the upgrade in-place with no backup.
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ── Arguments ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEMPLATE=""
|
||||
TARGET=""
|
||||
DRY_RUN=false
|
||||
BACKUP=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--template) TEMPLATE="$2"; shift 2 ;;
|
||||
--target) TARGET="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--backup) BACKUP=true; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$TEMPLATE" ]] && { echo "Error: --template required" >&2; exit 1; }
|
||||
[[ -z "$TARGET" ]] && { echo "Error: --target required" >&2; exit 1; }
|
||||
[[ -f "$TEMPLATE" ]] || { echo "Error: template not found: $TEMPLATE" >&2; exit 1; }
|
||||
[[ -f "$TARGET" ]] || { echo "Error: target not found: $TARGET" >&2; exit 1; }
|
||||
|
||||
# ── Parse target → KEY → full definition block ───────────────────────────────────────────────
|
||||
|
||||
declare -A HOST_MAP # KEY → complete definition line(s) from user's conf
|
||||
|
||||
_parse_target() {
|
||||
local in_block=false cur_key="" cur_block="" line
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$in_block" == true ]]; then
|
||||
cur_block+="$line"$'\n'
|
||||
# Closing ) — optional trailing whitespace and comment
|
||||
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
||||
HOST_MAP["$cur_key"]="$cur_block"
|
||||
in_block=false; cur_key=""; cur_block=""
|
||||
fi
|
||||
else
|
||||
# declare -A KEY=(
|
||||
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
# KEY=(
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
# KEY=value (simple scalar)
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
||||
HOST_MAP["${BASH_REMATCH[1]}"]="$line"$'\n'
|
||||
fi
|
||||
fi
|
||||
done < "$TARGET"
|
||||
}
|
||||
|
||||
# ── Walk template — collect stats (must run in current shell so arrays persist) ──────────────
|
||||
|
||||
declare -a ADDED=() KEPT=() REMOVED=()
|
||||
declare -A TMPL_SEEN=()
|
||||
|
||||
_collect_stats() {
|
||||
local in_block=false cur_key="" line
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$in_block" == true ]]; then
|
||||
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
||||
in_block=false
|
||||
TMPL_SEEN["$cur_key"]=1
|
||||
if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then KEPT+=("$cur_key")
|
||||
else ADDED+=("$cur_key"); fi
|
||||
cur_key=""
|
||||
fi
|
||||
else
|
||||
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
||||
local k="${BASH_REMATCH[1]}"
|
||||
TMPL_SEEN["$k"]=1
|
||||
if [[ -n "${HOST_MAP[$k]+_}" ]]; then KEPT+=("$k")
|
||||
else ADDED+=("$k"); fi
|
||||
fi
|
||||
fi
|
||||
done < "$TEMPLATE"
|
||||
|
||||
for key in "${!HOST_MAP[@]}"; do
|
||||
[[ -z "${TMPL_SEEN[$key]+_}" ]] && REMOVED+=("$key")
|
||||
done
|
||||
}
|
||||
|
||||
# ── Walk template — write merged output ──────────────────────────────────────────────────────
|
||||
# Runs in a subshell (stdout redirected) — array mutations are intentionally discarded here.
|
||||
|
||||
_write_merged() {
|
||||
local in_block=false cur_key="" cur_block="" line
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$in_block" == true ]]; then
|
||||
cur_block+="$line"$'\n'
|
||||
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
||||
in_block=false
|
||||
if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then printf '%s' "${HOST_MAP[$cur_key]}"
|
||||
else printf '%s' "$cur_block"; fi
|
||||
cur_key=""; cur_block=""
|
||||
fi
|
||||
else
|
||||
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
||||
local k="${BASH_REMATCH[1]}"
|
||||
if [[ -n "${HOST_MAP[$k]+_}" ]]; then printf '%s' "${HOST_MAP[$k]}"
|
||||
else printf '%s\n' "$line"; fi
|
||||
else
|
||||
printf '%s\n' "$line"
|
||||
fi
|
||||
fi
|
||||
done < "$TEMPLATE"
|
||||
}
|
||||
|
||||
# ── Run ───────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_parse_target
|
||||
_collect_stats
|
||||
|
||||
# ── Report ────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TARGET_NAME="$(basename "$TARGET")"
|
||||
echo ""
|
||||
echo "── conf_upgrade: $TARGET_NAME ──────────────────────────────────────────"
|
||||
|
||||
if [[ ${#ADDED[@]} -gt 0 ]]; then
|
||||
echo " ADDED (new — fill in your values where needed):"
|
||||
for k in "${ADDED[@]}"; do echo " + $k"; done
|
||||
fi
|
||||
|
||||
if [[ ${#REMOVED[@]} -gt 0 ]]; then
|
||||
echo " REMOVED (deprecated — no longer in this version):"
|
||||
for k in "${REMOVED[@]}"; do echo " - $k"; done
|
||||
fi
|
||||
|
||||
echo " KEPT ${#KEPT[@]} existing vars — your values preserved"
|
||||
|
||||
if [[ ${#ADDED[@]} -eq 0 && ${#REMOVED[@]} -eq 0 ]]; then
|
||||
echo " Already up to date — no changes needed."
|
||||
echo "────────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "────────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# ── Apply ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "(dry-run — no changes written)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMPOUT="$(mktemp)"
|
||||
trap 'rm -f "$TMPOUT"' EXIT
|
||||
|
||||
_write_merged > "$TMPOUT"
|
||||
|
||||
if [[ "$BACKUP" == true ]]; then
|
||||
cp "$TARGET" "${TARGET}.bak"
|
||||
echo "Backup: ${TARGET}.bak"
|
||||
fi
|
||||
|
||||
cp "$TMPOUT" "$TARGET"
|
||||
echo "Updated: $TARGET"
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ============================= conf_upgrade.sh ================================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Merges a new conf template into an existing user conf while preserving every
|
||||
# value the user has already set. Run manually when the conf schema changes
|
||||
# between versions — adds new keys, removes deprecated ones, and keeps the
|
||||
# structure of the new template exactly.
|
||||
#
|
||||
# Keys in template only → ADDED (placeholder/default — user fills in once)
|
||||
# Keys in target only → REMOVED (deprecated in new version)
|
||||
# Keys in both → KEPT (target's value always wins, template ignored)
|
||||
# Comments / blank lines → always from template (structure follows new version)
|
||||
#
|
||||
# Supports all conf variable patterns: simple scalars, indexed arrays, and
|
||||
# associative arrays (declare -A).
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Dry-run Mode
|
||||
# --dry-run prints the full change report (ADDED / REMOVED / KEPT) then exits
|
||||
# without writing anything. Always preview before applying to production confs.
|
||||
#
|
||||
# Backup Option
|
||||
# --backup writes a .bak copy of the target before overwriting. Use when
|
||||
# applying to a conf that has never been upgraded before.
|
||||
#
|
||||
# File Existence Guards
|
||||
# Both --template and --target are validated before any parsing begins.
|
||||
# Missing files abort immediately with a clear error.
|
||||
#
|
||||
# Atomic Write
|
||||
# Merged output is written to a tempfile first, then copied to the target.
|
||||
# A partial write cannot corrupt the original.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# No conf vars. All inputs are CLI flags.
|
||||
#
|
||||
# --template <file> New version conf file (source of structure and defaults)
|
||||
# --target <file> Existing user conf (source of real values — always preserved)
|
||||
# --dry-run Show what would change without writing
|
||||
# --backup Write a .bak copy of target before modifying
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# conf_upgrade.sh --template Configurations/master.conf.template --target Configurations/master.conf --dry-run
|
||||
# Preview what would be added, removed, and kept — no changes written.
|
||||
#
|
||||
# conf_upgrade.sh --template Configurations/master.conf.template --target Configurations/master.conf --backup
|
||||
# Apply the upgrade, writing a .bak first.
|
||||
#
|
||||
# conf_upgrade.sh --template Configurations/master.conf.template --target Configurations/master.conf
|
||||
# Apply the upgrade in-place with no backup.
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ── Arguments ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEMPLATE=""
|
||||
TARGET=""
|
||||
DRY_RUN=false
|
||||
BACKUP=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--template) TEMPLATE="$2"; shift 2 ;;
|
||||
--target) TARGET="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--backup) BACKUP=true; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$TEMPLATE" ]] && { echo "Error: --template required" >&2; exit 1; }
|
||||
[[ -z "$TARGET" ]] && { echo "Error: --target required" >&2; exit 1; }
|
||||
[[ -f "$TEMPLATE" ]] || { echo "Error: template not found: $TEMPLATE" >&2; exit 1; }
|
||||
[[ -f "$TARGET" ]] || { echo "Error: target not found: $TARGET" >&2; exit 1; }
|
||||
|
||||
# ── Parse target → KEY → full definition block ───────────────────────────────────────────────
|
||||
|
||||
declare -A HOST_MAP # KEY → complete definition line(s) from user's conf
|
||||
|
||||
_parse_target() {
|
||||
local in_block=false cur_key="" cur_block="" line
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$in_block" == true ]]; then
|
||||
cur_block+="$line"$'\n'
|
||||
# Closing ) — optional trailing whitespace and comment
|
||||
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
||||
HOST_MAP["$cur_key"]="$cur_block"
|
||||
in_block=false; cur_key=""; cur_block=""
|
||||
fi
|
||||
else
|
||||
# declare -A KEY=(
|
||||
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
# KEY=(
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
# KEY=value (simple scalar)
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
||||
HOST_MAP["${BASH_REMATCH[1]}"]="$line"$'\n'
|
||||
fi
|
||||
fi
|
||||
done < "$TARGET"
|
||||
}
|
||||
|
||||
# ── Walk template — collect stats (must run in current shell so arrays persist) ──────────────
|
||||
|
||||
declare -a ADDED=() KEPT=() REMOVED=()
|
||||
declare -A TMPL_SEEN=()
|
||||
|
||||
_collect_stats() {
|
||||
local in_block=false cur_key="" line
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$in_block" == true ]]; then
|
||||
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
||||
in_block=false
|
||||
TMPL_SEEN["$cur_key"]=1
|
||||
if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then KEPT+=("$cur_key")
|
||||
else ADDED+=("$cur_key"); fi
|
||||
cur_key=""
|
||||
fi
|
||||
else
|
||||
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
||||
local k="${BASH_REMATCH[1]}"
|
||||
TMPL_SEEN["$k"]=1
|
||||
if [[ -n "${HOST_MAP[$k]+_}" ]]; then KEPT+=("$k")
|
||||
else ADDED+=("$k"); fi
|
||||
fi
|
||||
fi
|
||||
done < "$TEMPLATE"
|
||||
|
||||
for key in "${!HOST_MAP[@]}"; do
|
||||
[[ -z "${TMPL_SEEN[$key]+_}" ]] && REMOVED+=("$key")
|
||||
done
|
||||
}
|
||||
|
||||
# ── Walk template — write merged output ──────────────────────────────────────────────────────
|
||||
# Runs in a subshell (stdout redirected) — array mutations are intentionally discarded here.
|
||||
|
||||
_write_merged() {
|
||||
local in_block=false cur_key="" cur_block="" line
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$in_block" == true ]]; then
|
||||
cur_block+="$line"$'\n'
|
||||
if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
|
||||
in_block=false
|
||||
if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then printf '%s' "${HOST_MAP[$cur_key]}"
|
||||
else printf '%s' "$cur_block"; fi
|
||||
cur_key=""; cur_block=""
|
||||
fi
|
||||
else
|
||||
if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
|
||||
cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
|
||||
elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
|
||||
local k="${BASH_REMATCH[1]}"
|
||||
if [[ -n "${HOST_MAP[$k]+_}" ]]; then printf '%s' "${HOST_MAP[$k]}"
|
||||
else printf '%s\n' "$line"; fi
|
||||
else
|
||||
printf '%s\n' "$line"
|
||||
fi
|
||||
fi
|
||||
done < "$TEMPLATE"
|
||||
}
|
||||
|
||||
# ── Run ───────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_parse_target
|
||||
_collect_stats
|
||||
|
||||
# ── Report ────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TARGET_NAME="$(basename "$TARGET")"
|
||||
echo ""
|
||||
echo "── conf_upgrade: $TARGET_NAME ──────────────────────────────────────────"
|
||||
|
||||
if [[ ${#ADDED[@]} -gt 0 ]]; then
|
||||
echo " ADDED (new — fill in your values where needed):"
|
||||
for k in "${ADDED[@]}"; do echo " + $k"; done
|
||||
fi
|
||||
|
||||
if [[ ${#REMOVED[@]} -gt 0 ]]; then
|
||||
echo " REMOVED (deprecated — no longer in this version):"
|
||||
for k in "${REMOVED[@]}"; do echo " - $k"; done
|
||||
fi
|
||||
|
||||
echo " KEPT ${#KEPT[@]} existing vars — your values preserved"
|
||||
|
||||
if [[ ${#ADDED[@]} -eq 0 && ${#REMOVED[@]} -eq 0 ]]; then
|
||||
echo " Already up to date — no changes needed."
|
||||
echo "────────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "────────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# ── Apply ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "(dry-run — no changes written)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMPOUT="$(mktemp)"
|
||||
trap 'rm -f "$TMPOUT"' EXIT
|
||||
|
||||
_write_merged > "$TMPOUT"
|
||||
|
||||
if [[ "$BACKUP" == true ]]; then
|
||||
cp "$TARGET" "${TARGET}.bak"
|
||||
echo "Backup: ${TARGET}.bak"
|
||||
fi
|
||||
|
||||
cp "$TMPOUT" "$TARGET"
|
||||
echo "Updated: $TARGET"
|
||||
@@ -0,0 +1,775 @@
|
||||
#!/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"
|
||||
|
||||
# ━━━ 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"]=20480 # 20GB — 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"
|
||||
["NginxProxyManager"]="http://localhost:7818"
|
||||
["Authelia"]="http://localhost:9091/api/health"
|
||||
["Authelia-Secondary"]="http://localhost:9092/api/health"
|
||||
["Lldap-Gmer4Lfe"]="http://localhost:17170"
|
||||
)
|
||||
|
||||
# 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"
|
||||
"emby-test" # broken test container (exit 127 — bad image)
|
||||
)
|
||||
|
||||
# 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/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/series"
|
||||
["/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/specials"
|
||||
["/anime-movies"]="/mnt/user/Anime_Movies-Old"
|
||||
["/ext-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 ──────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
HOST1_UNRAID_API_KEY="1825c3a2e03ea5089974f4da2e171aa2d5907a1dea23cc479c33e492c8ff4dbb"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+334
@@ -0,0 +1,334 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Git Pull & Execute =========================================
|
||||
# ==============================================================================================
|
||||
# Pulls the latest scripts from the Gitea repository via SSH.
|
||||
# Lives at the repo root — sources load_config.sh from the same directory.
|
||||
#
|
||||
# ── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────────────────────
|
||||
# 1. Detects which server it's running on via detect_hosts() (MY_ID)
|
||||
# 2. Configures sparse checkout to exclude other servers' credential files
|
||||
# Each server only pulls its own host*.conf — never sees peer credentials
|
||||
# 3. Pulls or clones latest scripts from Gitea
|
||||
# 4. Sets executable permissions on all .sh files
|
||||
#
|
||||
# ── SPARSE CHECKOUT ───────────────────────────────────────────────────────────────────────────
|
||||
# Sparse checkout ensures each server only receives its own host conf:
|
||||
# HOST1 pulls: master.conf + host1.conf + all scripts
|
||||
# HOST1 skips: host2.conf, host3.conf etc.
|
||||
# HOST2 pulls: master.conf + host2.conf + all scripts
|
||||
# HOST2 skips: host1.conf, host3.conf etc.
|
||||
#
|
||||
# Adding a new server:
|
||||
# Create host3.conf in the repo
|
||||
# All existing servers automatically exclude it on next pull
|
||||
# New server gets only its own conf ✅
|
||||
#
|
||||
# ── GITEA LOCATION DETECTION ──────────────────────────────────────────────────────────────────
|
||||
# Detects where Gitea is running at runtime — works through fallback:
|
||||
# Gitea local → connects via local IP
|
||||
# Gitea remote → connects via Tailscale IP
|
||||
# Both fail → falls back to GITEA_DOMAIN if configured
|
||||
#
|
||||
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
||||
# GITEA_CONTAINER — Docker container name for Gitea
|
||||
# GITEA_REPO_PATH — repo path on Gitea (e.g. Varaverk/varaverk.git)
|
||||
# GITEA_DOMAIN — public domain fallback (optional)
|
||||
# TARGET_DIR — local path to clone/pull into
|
||||
# GITEA_SSH_KEY — SSH key for Gitea authentication
|
||||
# SSH_PORT — Gitea SSH port (often 221 or 222)
|
||||
#
|
||||
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# git_pull_execute.sh — normal pull
|
||||
# git_pull_execute.sh --dry-run — preview without making changes
|
||||
# git_pull_execute.sh --log — verbose output
|
||||
# git_pull_execute.sh --status — show config and exit
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Root level script — load_config.sh is in the same directory
|
||||
source "$SCRIPT_DIR/load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
acquire_lock
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
error "Docker command not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# detect_hosts() sets MY_ID — needed for sparse checkout configuration
|
||||
detect_hosts
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Locate Gitea ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Locate Gitea ━━━"
|
||||
|
||||
if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
|
||||
# Gitea is running on this server — use local IP
|
||||
GITEA_IP=$(hostname -I | awk '{print $1}')
|
||||
log "Gitea running locally — connecting via $GITEA_IP"
|
||||
else
|
||||
# Gitea not running locally — find it on the remote server via Tailscale
|
||||
log "Gitea not running locally — checking remote server"
|
||||
GITEA_IP=$(resolve_tailscale_ip "${REMOTE_SERVER_NAME}")
|
||||
if [[ -n "$GITEA_IP" ]]; then
|
||||
echo " Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP"
|
||||
elif [[ -n "${GITEA_DOMAIN:-}" ]]; then
|
||||
warn "Tailscale resolution failed — falling back to $GITEA_DOMAIN"
|
||||
GITEA_IP="$GITEA_DOMAIN"
|
||||
else
|
||||
error "Cannot find Gitea — local: not running, Tailscale: failed, domain: not configured"
|
||||
notify "Git pull failed on $(hostname) — cannot locate Gitea container" "Git Sync" "alert"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
REPO_SSH="git@${GITEA_IP}:${GITEA_REPO_PATH}"
|
||||
|
||||
require_var REPO_SSH
|
||||
require_var TARGET_DIR
|
||||
require_var GITEA_SSH_KEY
|
||||
require_var SSH_PORT
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_NET Repo: $REPO_SSH"
|
||||
echo "$ICON_GEAR Target: $TARGET_DIR"
|
||||
echo "$ICON_GEAR SSH Key: $GITEA_SSH_KEY"
|
||||
echo "$ICON_GEAR SSH Port: $SSH_PORT"
|
||||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_HOST Remote ID: $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||||
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Sparse Checkout Configuration ━━━
|
||||
# ==============================================================================================
|
||||
# Build the list of host*.conf files that belong to OTHER servers.
|
||||
# This server pulls everything EXCEPT those files.
|
||||
# MY_ID is set by detect_hosts() — e.g. "HOST1"
|
||||
|
||||
configure_sparse_checkout() {
|
||||
local repo_dir="$1"
|
||||
|
||||
log "Configuring sparse checkout for $MY_ID..."
|
||||
|
||||
# Enable sparse checkout
|
||||
git -C "$repo_dir" config core.sparseCheckout true 2>/dev/null
|
||||
|
||||
# Build exclusion list — all host*.conf files except MY_ID's
|
||||
local sparse_file="$repo_dir/.git/info/sparse-checkout"
|
||||
mkdir -p "$(dirname "$sparse_file")"
|
||||
|
||||
# Start with: pull everything
|
||||
echo "/*" > "$sparse_file"
|
||||
|
||||
# Exclude each other server's conf file
|
||||
# Find all host*.conf files present in the repo
|
||||
local excluded=0
|
||||
for conf_file in "$repo_dir"/host*.conf; do
|
||||
[[ -f "$conf_file" ]] || continue
|
||||
local conf_name
|
||||
conf_name=$(basename "$conf_file")
|
||||
|
||||
# Determine which HOST ID owns this conf by grepping its hostname var
|
||||
# Pattern: HOST1="unRAID-..." or HOST2="unRAID-..."
|
||||
local conf_host_id
|
||||
conf_host_id=$(grep -m1 -oP '^\s+HOST[0-9]+(?==)' "$conf_file" 2>/dev/null | tr -d ' ')
|
||||
|
||||
if [[ -z "$conf_host_id" ]]; then
|
||||
log "Cannot determine HOST ID for $conf_name — including in pull (safe default)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$conf_host_id" != "$MY_ID" ]]; then
|
||||
echo "!$conf_name" >> "$sparse_file"
|
||||
log "Sparse checkout: excluding $conf_name (belongs to $conf_host_id)"
|
||||
((excluded++))
|
||||
else
|
||||
log "Sparse checkout: including $conf_name (belongs to $MY_ID — this server)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$excluded" -gt 0 ]]; then
|
||||
echo " Sparse checkout: excluding $excluded peer conf file(s) — credentials protected"
|
||||
else
|
||||
log "Sparse checkout: no peer conf files to exclude (single server or first run)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Git Sync ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Git Sync ━━━"
|
||||
echo "$ICON_NET Repo: $REPO_SSH"
|
||||
echo "$ICON_GEAR Target: $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
START=$(date +%s)
|
||||
SYNC_SUCCESS=false
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would sync $REPO_SSH → $TARGET_DIR"
|
||||
warn "DRY RUN — would configure sparse checkout for $MY_ID"
|
||||
warn "DRY RUN — would exclude peer host*.conf files"
|
||||
SYNC_SUCCESS=true
|
||||
else
|
||||
mkdir -p "$TARGET_DIR"
|
||||
git config --global --add safe.directory "$TARGET_DIR"
|
||||
cd "$TARGET_DIR" || { error "Cannot cd into $TARGET_DIR"; exit 1; }
|
||||
|
||||
if [[ -d ".git" ]]; then
|
||||
# ── Existing repository ──────────────────────────────────────────────
|
||||
echo " Existing repository — updating"
|
||||
|
||||
# Configure sparse checkout BEFORE pull
|
||||
# Uses conf files already present from last pull to determine exclusions
|
||||
configure_sparse_checkout "$TARGET_DIR"
|
||||
|
||||
echo " Pulling latest changes..."
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git pull --ff-only; then
|
||||
echo " Git pull successful"
|
||||
SYNC_SUCCESS=true
|
||||
else
|
||||
# ff-only fails when local commits or tracked changes exist that can't
|
||||
# fast-forward. Fail loudly — never silently destroy local work.
|
||||
error "Git pull failed — local changes conflict with remote (will not force-reset)"
|
||||
notify "Git pull failed on $(hostname) — local changes conflict, manual resolve needed" "Git Sync" "alert"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
else
|
||||
# ── Fresh clone ──────────────────────────────────────────────────────
|
||||
echo " No repository found — cloning"
|
||||
|
||||
# Clone first — need the repo to exist before configuring sparse checkout
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git clone "$REPO_SSH" .; then
|
||||
echo " Clone successful"
|
||||
|
||||
# Configure sparse checkout after clone
|
||||
# Now all host*.conf files are present — can detect exclusions
|
||||
configure_sparse_checkout "$TARGET_DIR"
|
||||
|
||||
# Apply sparse checkout — removes excluded files from working tree
|
||||
echo " Applying sparse checkout..."
|
||||
git read-tree -mu HEAD
|
||||
echo " Sparse checkout applied — peer credentials removed from working tree"
|
||||
|
||||
SYNC_SUCCESS=true
|
||||
else
|
||||
error "Clone failed"
|
||||
notify "Git clone failed on $(hostname) — check Gitea connectivity" "Git Sync" "alert"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Permissions ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Permissions ━━━"
|
||||
log "Setting executable permissions on all .sh files..."
|
||||
find "$TARGET_DIR" -type f -name "*.sh" -exec chmod +x {} \;
|
||||
echo " Permissions set on .sh files"
|
||||
|
||||
# ── Flash mode: sync Plugin/ to /boot/ so the webUI picks up updates ─────
|
||||
# In flash mode SCRIPTS_DIR is in appdata — Plugin/ lives in the repo there
|
||||
# but Unraid serves PHP from /boot/. Sync after every pull to keep them in step.
|
||||
_BOOT_DIR="/boot/config/plugins/varaverk"
|
||||
if [[ "$TARGET_DIR" != "$_BOOT_DIR" ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Flash mode: sync Plugin/ → /boot/ ━━━"
|
||||
if rsync -a --delete "$TARGET_DIR/Plugin/" "$_BOOT_DIR/Plugin/" 2>/dev/null; then
|
||||
echo " Plugin/ synced to /boot/ ✅"
|
||||
else
|
||||
warn "Plugin/ sync to /boot/ failed — webUI may be stale until next pull"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Conf Upgrade ━━━
|
||||
# ==============================================================================================
|
||||
# Merges new conf structure into the live conf files after every pull.
|
||||
# New keys → added with template defaults (user fills in once).
|
||||
# Removed keys → dropped. Existing values → always preserved.
|
||||
# Silent when already up to date — no overhead on unchanged pulls.
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Conf Upgrade ━━━"
|
||||
|
||||
UPGRADE_SCRIPT="$TARGET_DIR/Deployment/conf_upgrade.sh"
|
||||
TMPL_DIR="$TARGET_DIR/Deployment/conf_templates"
|
||||
CONF_DIR="$TARGET_DIR/Configurations"
|
||||
|
||||
if [[ ! -f "$UPGRADE_SCRIPT" ]]; then
|
||||
log "conf_upgrade.sh not found — skipping (pre-deployment-folder repo)"
|
||||
elif [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would upgrade master.conf and ${MY_ID,,}.conf"
|
||||
elif [[ "$SYNC_SUCCESS" == true ]]; then
|
||||
_DRY=""
|
||||
|
||||
# master.conf
|
||||
if [[ -f "$TMPL_DIR/master.conf" && -f "$CONF_DIR/master.conf" ]]; then
|
||||
bash "$UPGRADE_SCRIPT" \
|
||||
--template "$TMPL_DIR/master.conf" \
|
||||
--target "$CONF_DIR/master.conf" \
|
||||
--backup $_DRY
|
||||
else
|
||||
warn "master.conf template or target not found — skipping"
|
||||
fi
|
||||
|
||||
# This server's host conf only — sparse checkout ensures we have it
|
||||
HOST_CONF="$CONF_DIR/${MY_ID,,}.conf"
|
||||
if [[ -f "$TMPL_DIR/host.conf.template" && -f "$HOST_CONF" ]]; then
|
||||
bash "$UPGRADE_SCRIPT" \
|
||||
--template "$TMPL_DIR/host.conf.template" \
|
||||
--target "$HOST_CONF" \
|
||||
--backup $_DRY
|
||||
else
|
||||
warn "${MY_ID,,}.conf or host.conf.template not found — skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY GIT SYNC SUMMARY ━━━━━"
|
||||
echo "$ICON_NET Repo: $REPO_SSH"
|
||||
echo "$ICON_GEAR Target: $TARGET_DIR"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_LOCK Excluded: peer host*.conf files"
|
||||
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
||||
elif [[ "$SYNC_SUCCESS" == true ]]; then
|
||||
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
|
||||
notify "Repository synced successfully on $(hostname)" "Git Sync" "normal"
|
||||
else
|
||||
echo "$ICON_ERROR Status: $ICON_ERROR FAILED"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Git Pull & Execute =========================================
|
||||
# ==============================================================================================
|
||||
# Pulls the latest scripts from the Gitea repository via SSH.
|
||||
# Lives at the repo root — sources load_config.sh from the same directory.
|
||||
#
|
||||
# ── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────────────────────
|
||||
# 1. Detects which server it's running on via detect_hosts() (MY_ID)
|
||||
# 2. Configures sparse checkout to exclude other servers' credential files
|
||||
# Each server only pulls its own host*.conf — never sees peer credentials
|
||||
# 3. Pulls or clones latest scripts from Gitea
|
||||
# 4. Sets executable permissions on all .sh files
|
||||
#
|
||||
# ── SPARSE CHECKOUT ───────────────────────────────────────────────────────────────────────────
|
||||
# Sparse checkout ensures each server only receives its own host conf:
|
||||
# HOST1 pulls: master.conf + host1.conf + all scripts
|
||||
# HOST1 skips: host2.conf, host3.conf etc.
|
||||
# HOST2 pulls: master.conf + host2.conf + all scripts
|
||||
# HOST2 skips: host1.conf, host3.conf etc.
|
||||
#
|
||||
# Adding a new server:
|
||||
# Create host3.conf in the repo
|
||||
# All existing servers automatically exclude it on next pull
|
||||
# New server gets only its own conf ✅
|
||||
#
|
||||
# ── GITEA LOCATION DETECTION ──────────────────────────────────────────────────────────────────
|
||||
# Detects where Gitea is running at runtime — works through fallback:
|
||||
# Gitea local → connects via local IP
|
||||
# Gitea remote → connects via Tailscale IP
|
||||
# Both fail → falls back to GITEA_DOMAIN if configured
|
||||
#
|
||||
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
|
||||
# GITEA_CONTAINER — Docker container name for Gitea
|
||||
# GITEA_REPO_PATH — repo path on Gitea (e.g. Varaverk/varaverk.git)
|
||||
# GITEA_DOMAIN — public domain fallback (optional)
|
||||
# TARGET_DIR — local path to clone/pull into
|
||||
# GITEA_SSH_KEY — SSH key for Gitea authentication
|
||||
# SSH_PORT — Gitea SSH port (often 221 or 222)
|
||||
#
|
||||
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
|
||||
# git_pull_execute.sh — normal pull
|
||||
# git_pull_execute.sh --dry-run — preview without making changes
|
||||
# git_pull_execute.sh --log — verbose output
|
||||
# git_pull_execute.sh --status — show config and exit
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Root level script — load_config.sh is in the same directory
|
||||
source "$SCRIPT_DIR/load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
acquire_lock
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
error "Docker command not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# detect_hosts() sets MY_ID — needed for sparse checkout configuration
|
||||
detect_hosts
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Locate Gitea ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Locate Gitea ━━━"
|
||||
|
||||
if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
|
||||
# Gitea is running on this server — use local IP
|
||||
GITEA_IP=$(hostname -I | awk '{print $1}')
|
||||
log "Gitea running locally — connecting via $GITEA_IP"
|
||||
else
|
||||
# Gitea not running locally — find it on the remote server via Tailscale
|
||||
log "Gitea not running locally — checking remote server"
|
||||
GITEA_IP=$(resolve_tailscale_ip "${REMOTE_SERVER_NAME}")
|
||||
if [[ -n "$GITEA_IP" ]]; then
|
||||
echo " Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP"
|
||||
elif [[ -n "${GITEA_DOMAIN:-}" ]]; then
|
||||
warn "Tailscale resolution failed — falling back to $GITEA_DOMAIN"
|
||||
GITEA_IP="$GITEA_DOMAIN"
|
||||
else
|
||||
error "Cannot find Gitea — local: not running, Tailscale: failed, domain: not configured"
|
||||
notify "Git pull failed on $(hostname) — cannot locate Gitea container" "Git Sync" "alert"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
REPO_SSH="git@${GITEA_IP}:${GITEA_REPO_PATH}"
|
||||
|
||||
require_var REPO_SSH
|
||||
require_var TARGET_DIR
|
||||
require_var GITEA_SSH_KEY
|
||||
require_var SSH_PORT
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_NET Repo: $REPO_SSH"
|
||||
echo "$ICON_GEAR Target: $TARGET_DIR"
|
||||
echo "$ICON_GEAR SSH Key: $GITEA_SSH_KEY"
|
||||
echo "$ICON_GEAR SSH Port: $SSH_PORT"
|
||||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_HOST Remote ID: $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||||
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Sparse Checkout Configuration ━━━
|
||||
# ==============================================================================================
|
||||
# Build the list of host*.conf files that belong to OTHER servers.
|
||||
# This server pulls everything EXCEPT those files.
|
||||
# MY_ID is set by detect_hosts() — e.g. "HOST1"
|
||||
|
||||
configure_sparse_checkout() {
|
||||
local repo_dir="$1"
|
||||
|
||||
log "Configuring sparse checkout for $MY_ID..."
|
||||
|
||||
# Enable sparse checkout
|
||||
git -C "$repo_dir" config core.sparseCheckout true 2>/dev/null
|
||||
|
||||
# Build exclusion list — all host*.conf files except MY_ID's
|
||||
local sparse_file="$repo_dir/.git/info/sparse-checkout"
|
||||
mkdir -p "$(dirname "$sparse_file")"
|
||||
|
||||
# Start with: pull everything
|
||||
echo "/*" > "$sparse_file"
|
||||
|
||||
# Exclude each other server's conf file
|
||||
# Find all host*.conf files present in the repo
|
||||
local excluded=0
|
||||
for conf_file in "$repo_dir"/host*.conf; do
|
||||
[[ -f "$conf_file" ]] || continue
|
||||
local conf_name
|
||||
conf_name=$(basename "$conf_file")
|
||||
|
||||
# Determine which HOST ID owns this conf by grepping its hostname var
|
||||
# Pattern: HOST1="unRAID-..." or HOST2="unRAID-..."
|
||||
local conf_host_id
|
||||
conf_host_id=$(grep -m1 -oP '^\s+HOST[0-9]+(?==)' "$conf_file" 2>/dev/null | tr -d ' ')
|
||||
|
||||
if [[ -z "$conf_host_id" ]]; then
|
||||
log "Cannot determine HOST ID for $conf_name — including in pull (safe default)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$conf_host_id" != "$MY_ID" ]]; then
|
||||
echo "!$conf_name" >> "$sparse_file"
|
||||
log "Sparse checkout: excluding $conf_name (belongs to $conf_host_id)"
|
||||
((excluded++))
|
||||
else
|
||||
log "Sparse checkout: including $conf_name (belongs to $MY_ID — this server)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$excluded" -gt 0 ]]; then
|
||||
echo " Sparse checkout: excluding $excluded peer conf file(s) — credentials protected"
|
||||
else
|
||||
log "Sparse checkout: no peer conf files to exclude (single server or first run)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Git Sync ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Git Sync ━━━"
|
||||
echo "$ICON_NET Repo: $REPO_SSH"
|
||||
echo "$ICON_GEAR Target: $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
START=$(date +%s)
|
||||
SYNC_SUCCESS=false
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would sync $REPO_SSH → $TARGET_DIR"
|
||||
warn "DRY RUN — would configure sparse checkout for $MY_ID"
|
||||
warn "DRY RUN — would exclude peer host*.conf files"
|
||||
SYNC_SUCCESS=true
|
||||
else
|
||||
mkdir -p "$TARGET_DIR"
|
||||
git config --global --add safe.directory "$TARGET_DIR"
|
||||
cd "$TARGET_DIR" || { error "Cannot cd into $TARGET_DIR"; exit 1; }
|
||||
|
||||
if [[ -d ".git" ]]; then
|
||||
# ── Existing repository ──────────────────────────────────────────────
|
||||
echo " Existing repository — updating"
|
||||
|
||||
# Configure sparse checkout BEFORE pull
|
||||
# Uses conf files already present from last pull to determine exclusions
|
||||
configure_sparse_checkout "$TARGET_DIR"
|
||||
|
||||
echo " Pulling latest changes..."
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git pull --ff-only; then
|
||||
echo " Git pull successful"
|
||||
SYNC_SUCCESS=true
|
||||
else
|
||||
# ff-only fails when local commits or tracked changes exist that can't
|
||||
# fast-forward. Fail loudly — never silently destroy local work.
|
||||
error "Git pull failed — local changes conflict with remote (will not force-reset)"
|
||||
notify "Git pull failed on $(hostname) — local changes conflict, manual resolve needed" "Git Sync" "alert"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
else
|
||||
# ── Fresh clone ──────────────────────────────────────────────────────
|
||||
echo " No repository found — cloning"
|
||||
|
||||
# Clone first — need the repo to exist before configuring sparse checkout
|
||||
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git clone "$REPO_SSH" .; then
|
||||
echo " Clone successful"
|
||||
|
||||
# Configure sparse checkout after clone
|
||||
# Now all host*.conf files are present — can detect exclusions
|
||||
configure_sparse_checkout "$TARGET_DIR"
|
||||
|
||||
# Apply sparse checkout — removes excluded files from working tree
|
||||
echo " Applying sparse checkout..."
|
||||
git read-tree -mu HEAD
|
||||
echo " Sparse checkout applied — peer credentials removed from working tree"
|
||||
|
||||
SYNC_SUCCESS=true
|
||||
else
|
||||
error "Clone failed"
|
||||
notify "Git clone failed on $(hostname) — check Gitea connectivity" "Git Sync" "alert"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Permissions ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Permissions ━━━"
|
||||
log "Setting executable permissions on all .sh files..."
|
||||
find "$TARGET_DIR" -type f -name "*.sh" -exec chmod +x {} \;
|
||||
echo " Permissions set on .sh files"
|
||||
|
||||
# ── Flash mode: sync Plugin/ to /boot/ so the webUI picks up updates ─────
|
||||
# In flash mode SCRIPTS_DIR is in appdata — Plugin/ lives in the repo there
|
||||
# but Unraid serves PHP from /boot/. Sync after every pull to keep them in step.
|
||||
_BOOT_DIR="/boot/config/plugins/varaverk"
|
||||
if [[ "$TARGET_DIR" != "$_BOOT_DIR" ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Flash mode: sync Plugin/ → /boot/ ━━━"
|
||||
if rsync -a --delete "$TARGET_DIR/Plugin/" "$_BOOT_DIR/Plugin/" 2>/dev/null; then
|
||||
echo " Plugin/ synced to /boot/ ✅"
|
||||
else
|
||||
warn "Plugin/ sync to /boot/ failed — webUI may be stale until next pull"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Conf Upgrade ━━━
|
||||
# ==============================================================================================
|
||||
# Merges new conf structure into the live conf files after every pull.
|
||||
# New keys → added with template defaults (user fills in once).
|
||||
# Removed keys → dropped. Existing values → always preserved.
|
||||
# Silent when already up to date — no overhead on unchanged pulls.
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Conf Upgrade ━━━"
|
||||
|
||||
UPGRADE_SCRIPT="$TARGET_DIR/Deployment/conf_upgrade.sh"
|
||||
CONF_DIR="$TARGET_DIR/Configurations"
|
||||
|
||||
if [[ ! -f "$UPGRADE_SCRIPT" ]]; then
|
||||
log "conf_upgrade.sh not found — skipping (pre-deployment-folder repo)"
|
||||
elif [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would upgrade master.conf and ${MY_ID,,}.conf"
|
||||
elif [[ "$SYNC_SUCCESS" == true ]]; then
|
||||
_DRY=""
|
||||
|
||||
# master.conf
|
||||
if [[ -f "$CONF_DIR/master.conf.template" && -f "$CONF_DIR/master.conf" ]]; then
|
||||
bash "$UPGRADE_SCRIPT" \
|
||||
--template "$CONF_DIR/master.conf.template" \
|
||||
--target "$CONF_DIR/master.conf" \
|
||||
--backup $_DRY
|
||||
else
|
||||
warn "master.conf.template or master.conf not found — skipping"
|
||||
fi
|
||||
|
||||
# This server's host conf only — sparse checkout ensures we have it
|
||||
HOST_CONF="$CONF_DIR/${MY_ID,,}.conf"
|
||||
if [[ -f "$CONF_DIR/host.conf.template" && -f "$HOST_CONF" ]]; then
|
||||
bash "$UPGRADE_SCRIPT" \
|
||||
--template "$CONF_DIR/host.conf.template" \
|
||||
--target "$HOST_CONF" \
|
||||
--backup $_DRY
|
||||
else
|
||||
warn "${MY_ID,,}.conf or host.conf.template not found — skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY GIT SYNC SUMMARY ━━━━━"
|
||||
echo "$ICON_NET Repo: $REPO_SSH"
|
||||
echo "$ICON_GEAR Target: $TARGET_DIR"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_LOCK Excluded: peer host*.conf files"
|
||||
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
||||
elif [[ "$SYNC_SUCCESS" == true ]]; then
|
||||
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
|
||||
notify "Repository synced successfully on $(hostname)" "Git Sync" "normal"
|
||||
else
|
||||
echo "$ICON_ERROR Status: $ICON_ERROR FAILED"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
+1555
File diff suppressed because it is too large
Load Diff
+1559
File diff suppressed because it is too large
Load Diff
+1475
File diff suppressed because it is too large
Load Diff
+1471
File diff suppressed because it is too large
Load Diff
+1470
File diff suppressed because it is too large
Load Diff
+1470
File diff suppressed because it is too large
Load Diff
+1470
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# 🔌 PLUGIN
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
**The Varaverk Unraid plugin — a web UI that wraps the entire script ecosystem.**
|
||||
Scheduler, Monitor, Docker management, Partnership sync, Fallback state, and Arrs —
|
||||
all surfaced inside the Unraid web interface as a first-class plugin.
|
||||
|
||||
> **Why this folder exists:** The scripts need a control surface. Managing a 50+ container
|
||||
> homelab ecosystem from terminal windows is friction. The plugin turns configuration files
|
||||
> into editable forms, cron schedules into a visual scheduler, and runtime log output into
|
||||
> a live dashboard — without duplicating any of the logic that already lives in common.sh
|
||||
> and the conf files.
|
||||
|
||||
---
|
||||
|
||||
## ━━━ THE PROBLEM THAT BUILT THIS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
The script ecosystem works well from the command line, but day-to-day operation is not
|
||||
the command line. Checking whether the nightly sync ran, adjusting a container's watchdog
|
||||
limit, confirming the partnership fallback is active — all of that requires SSH sessions,
|
||||
knowing which log files to look at, and remembering which conf variable controls what.
|
||||
|
||||
The plugin solves the visibility problem: one URL on any browser, on any device on the
|
||||
Tailscale network, shows everything running and lets you act on it. No extra tooling,
|
||||
no separate monitoring stack, no third-party dashboards.
|
||||
|
||||
---
|
||||
|
||||
## ━━━ WHAT THIS FOLDER CONTAINS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
```
|
||||
Plugin/
|
||||
├── dev_install.sh # One-time developer setup: symlinks plugin into web server
|
||||
├── Icons/ # Source icon assets (1024px master files)
|
||||
└── unraid/ # The Unraid platform adapter + plugin application
|
||||
├── adapter.sh # Platform adapter — provides platform_*() API to all scripts
|
||||
├── Varaverk.page # Main plugin entry point (Tasks menu)
|
||||
├── VaraverkSettings.page # Unraid Settings → Other Settings entry
|
||||
├── api/ # PHP API endpoints (called by JS via fetch)
|
||||
├── css/ # Plugin stylesheet
|
||||
├── event/ # Unraid event hooks (boot-time cron setup, array lifecycle)
|
||||
├── icons/ # Plugin icons served by emhttp
|
||||
├── images/ # Plugin images
|
||||
├── include/ # PHP business logic shared across pages
|
||||
├── js/ # Frontend JavaScript
|
||||
├── pages/ # Per-tab page includes (monitor, scheduler, docker, ...)
|
||||
└── run_job.sh # Script runner invoked by the Scheduler
|
||||
|
||||
# Future platform adapters follow the same structure:
|
||||
# Plugin/truenas/adapter.sh — TrueNAS adapter (future)
|
||||
# Plugin/ubuntu/adapter.sh — Ubuntu/Debian adapter (future)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ━━━ RELATIONSHIP TO THE REST OF THE REPO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
**Plugin is a wrapper, never a reimplementation.** Every setting the plugin reads or writes
|
||||
lives in `Configurations/master.conf` or `Configurations/host*.conf` — the same files the
|
||||
shell scripts read. The plugin has no separate data store. If a conf file changes outside
|
||||
the plugin (by hand, by SSH), the plugin reflects it on next load.
|
||||
|
||||
The one exception is `varaverk.cfg` on flash (`/boot/config/plugins/varaverk/varaverk.cfg`),
|
||||
which holds a single bootstrap value: `SCRIPTS_DIR`. This is the path the plugin uses to
|
||||
find the Configurations directory and all scripts. Everything else flows from there.
|
||||
|
||||
The plugin also taps `common.sh` indirectly — `include/config.php` mirrors
|
||||
`resolve_tailscale_ip()` and `detect_host()` exactly, using the same logic as common.sh
|
||||
so behaviour stays consistent without a shell dependency.
|
||||
|
||||
---
|
||||
|
||||
## ━━━ SCRIPTS IN THIS FOLDER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
| Script | Role | When It Runs |
|
||||
|--------|------|--------------|
|
||||
| `dev_install.sh` | Symlinks `Plugin/unraid/` into Unraid's web server | Once, manually, after cloning or moving the repo |
|
||||
|
||||
---
|
||||
|
||||
|
||||
## ━━━ THE PLATFORM ADAPTER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
`Plugin/unraid/adapter.sh` is the Unraid platform adapter. It is sourced automatically
|
||||
by `load_config.sh` whenever `PLATFORM=unraid` is detected (via `/etc/unraid-version`).
|
||||
|
||||
Every bash script in the ecosystem calls `platform_*()` functions instead of OS-specific
|
||||
commands directly. The adapter translates those calls into Unraid-specific implementations.
|
||||
|
||||
```
|
||||
platform_storage_healthy # is the array up and shfs mounted?
|
||||
platform_is_maintenance_running # parity check or sync in progress?
|
||||
platform_is_service_running # is a named service process alive?
|
||||
platform_restart_service # restart via rc.d (Unraid) or systemctl (future)
|
||||
platform_stop_service # stop a named service
|
||||
platform_is_mover_running # Unraid mover active?
|
||||
platform_get_mover_pid # PID of the mover process
|
||||
platform_stop_user_scripts # kill Unraid user.scripts background jobs
|
||||
platform_send_os_notification # dynamix notify (Unraid) or equivalent
|
||||
platform_get_disk_states # reads disks.ini (Unraid) or equivalent
|
||||
platform_get_temp_thresholds # reads dynamix.cfg (Unraid) or equivalent
|
||||
platform_is_service_enabled # docker.cfg / domain.cfg enabled check
|
||||
platform_require_cmd # verify a platform command exists
|
||||
```
|
||||
|
||||
**Adding a new platform:** Create `Plugin/<platform>/adapter.sh` implementing the same
|
||||
function names. `load_config.sh` detects the OS at runtime and sources the correct adapter.
|
||||
No other files need changing.
|
||||
|
||||
---
|
||||
|
||||
## ━━━ UNRAID INTEGRATION POINTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
| File | Where it appears in Unraid |
|
||||
|------|---------------------------|
|
||||
| `Varaverk.page` | Tasks menu item |
|
||||
| `VaraverkSettings.page` | Settings → Other Settings tile |
|
||||
| `event/disks_mounted/rebuild_cron` | Fires on every boot — copies `.plg`, rebuilds cron |
|
||||
| `event/disks_mounted/array_start_jobs` | Fires when array starts |
|
||||
| `event/disks_unmounting/array_stop_jobs` | Fires when array stops |
|
||||
| `/boot/config/plugins/varaverk.plg` | Registers the plugin with Unraid's plugin system (lives on flash, not in repo) |
|
||||
+1006
File diff suppressed because it is too large
Load Diff
+1001
File diff suppressed because it is too large
Load Diff
+650
@@ -0,0 +1,650 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Downloaders Reset ==========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Maintenance reset for all download clients on this server. Clears accumulated
|
||||
# state that download clients generate but never clean up themselves — stuck
|
||||
# searches, dead transfers, failed imports, stale queue entries, completed history.
|
||||
#
|
||||
# Called every 30 minutes by critical_sync_maintenance.sh via
|
||||
# CRITICAL_MAINTENANCE_SCRIPTS. Can also be run manually for ad hoc cleanup.
|
||||
# If a downloader is not configured for this host, that section skips cleanly.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL MODEL
|
||||
# ==============================================================================================
|
||||
#
|
||||
# slskd
|
||||
# Stuck searches — clears Completed/Errored searches left by Soularr crashes
|
||||
# prevents 409 Conflict on next Soularr startup
|
||||
# Dead transfers — removes completed/errored/aborted transfer records per user
|
||||
# prevents Soularr 404 loop when polling a user whose transfer is gone
|
||||
# NEVER removes InProgress or Queued transfers
|
||||
# Failed imports — purges albums Soularr downloaded but Lidarr rejected
|
||||
# Soularr moves these to failed_imports/ and never cleans them up
|
||||
#
|
||||
# SABnzbd
|
||||
# Completed history — removes completed download records older than DOWNLOADER_RETENTION_DAYS
|
||||
# Failed history — removes failed download records older than DOWNLOADER_RETENTION_DAYS
|
||||
# Stalled queue — removes Paused or Stuck queue items no longer progressing
|
||||
# active downloading items are never touched
|
||||
#
|
||||
# qBittorrent
|
||||
# Age failsafe — removes torrents older than QBIT_FAILSAFE_MIN_DAYS
|
||||
# deleteFiles=false — removes from qBit, leaves files for arrs to manage
|
||||
# optional ratio requirement via QBIT_FAILSAFE_MIN_RATIO
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Never Interrupt Active Downloads
|
||||
# Each downloader section checks for active state before any removal. slskd
|
||||
# skips users with InProgress or Queued transfers. SABnzbd only removes items
|
||||
# past the retention threshold. qBittorrent applies minimum age and optional
|
||||
# ratio requirements. In-progress work is never touched.
|
||||
#
|
||||
# Graceful Skip on Unavailability
|
||||
# If a downloader's URL is empty or the service is unreachable, that section
|
||||
# skips cleanly with a log message. The script never exits fatally on a single
|
||||
# unreachable downloader — the others still run.
|
||||
#
|
||||
# Host-Aware Configuration
|
||||
# detect_hosts() aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
|
||||
# to their unprefixed names. Downloaders not configured for this host are absent
|
||||
# from the aliased vars and skip automatically.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Active Transfer Protection
|
||||
# slskd: skips users with InProgress or Queued transfers before any removal.
|
||||
# SABnzbd: age threshold enforced before deletion.
|
||||
# qBittorrent: minimum age plus optional ratio gate before failsafe removal.
|
||||
#
|
||||
# Reachability Check
|
||||
# Each section validates its downloader URL before API calls. Missing or
|
||||
# unreachable downloaders skip without affecting other sections.
|
||||
#
|
||||
# Host Detection
|
||||
# detect_hosts() identifies which server is running the script and aliases
|
||||
# all HOST*_SLSKD_*, HOST*_SABNZBD_*, and HOST*_QBIT_* vars to the correct
|
||||
# host's values. Downloaders not configured on this host skip automatically.
|
||||
#
|
||||
# Lock Acquisition
|
||||
# acquire_lock "wait" — waits for previous run to finish since this runs every
|
||||
# 15 minutes and prior execution may still be completing.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
#
|
||||
# HOST*_SLSKD_URL / HOST*_SLSKD_API_KEY / HOST*_SLSKD_FAILED_IMPORTS_DIR
|
||||
# slskd connection and failed imports path. Aliased by detect_hosts()
|
||||
#
|
||||
# HOST*_SABNZBD_URL / HOST*_SABNZBD_API_KEY
|
||||
# SABnzbd connection details. Aliased by detect_hosts()
|
||||
#
|
||||
# HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
|
||||
# qBittorrent connection details. Aliased by detect_hosts()
|
||||
#
|
||||
# master.conf
|
||||
#
|
||||
# DOWNLOADER_RETENTION_DAYS
|
||||
# Days before SABnzbd history entries (completed or failed) are removed
|
||||
#
|
||||
# QBIT_FAILSAFE_MIN_DAYS
|
||||
# Minimum torrent age in days before failsafe removal is considered
|
||||
#
|
||||
# QBIT_FAILSAFE_MIN_RATIO
|
||||
# Minimum seeding ratio required alongside age gate (0 = age only)
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# downloaders_reset.sh
|
||||
# Run maintenance reset for all configured download clients
|
||||
#
|
||||
# downloaders_reset.sh --dry-run
|
||||
# Preview what would be removed without making any changes
|
||||
#
|
||||
# downloaders_reset.sh --status
|
||||
# Show configured downloaders, current queue depths, and retention settings
|
||||
#
|
||||
# downloaders_reset.sh --log
|
||||
# Verbose per-client per-item output
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Lock first — wait mode since this runs every 30min and previous may still be finishing
|
||||
acquire_lock "wait"
|
||||
|
||||
# detect_hosts() sets MY_ID and aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
|
||||
detect_hosts
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
CUTOFF=$(( $(date +%s) - (DOWNLOADER_RETENTION_DAYS * 86400) ))
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
|
||||
log "$ICON_GEAR Config: retention=${DOWNLOADER_RETENTION_DAYS}d qbit-age=${QBIT_FAILSAFE_MIN_DAYS}d qbit-ratio=${QBIT_FAILSAFE_MIN_RATIO}"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_GEAR slskd: ${SLSKD_URL:-not configured}"
|
||||
echo "$ICON_GEAR SABnzbd: ${SABNZBD_URL:-not configured}"
|
||||
echo "$ICON_GEAR qBittorrent: ${QBIT_URL:-not configured}"
|
||||
echo "$ICON_TIME Retention: ${DOWNLOADER_RETENTION_DAYS} days"
|
||||
echo "$ICON_GEAR qBit age: ${QBIT_FAILSAFE_MIN_DAYS} days"
|
||||
echo "$ICON_GEAR qBit ratio: ${QBIT_FAILSAFE_MIN_RATIO} (0=age only)"
|
||||
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
|
||||
# Log which downloaders are active on this host
|
||||
if [[ -z "$SLSKD_URL" ]] && [[ -z "$SABNZBD_URL" ]] && [[ -z "$QBIT_URL" ]]; then
|
||||
warn "No downloaders configured for $MY_ID — nothing to reset"
|
||||
exit 0
|
||||
fi
|
||||
[[ -n "$SLSKD_URL" ]] && log "slskd active on $MY_ID"
|
||||
[[ -n "$SABNZBD_URL" ]] && log "SABnzbd active on $MY_ID"
|
||||
[[ -n "$QBIT_URL" ]] && log "qBittorrent active on $MY_ID"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Connection Check ━━━
|
||||
# ==============================================================================================
|
||||
# slskd's internal watchdog doesn't always recover from disconnection. Check before
|
||||
# running API-dependent sections; attempt reconnect if down.
|
||||
|
||||
SLSKD_CONNECTED=false
|
||||
|
||||
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC slskd — Connection Check ━━━"
|
||||
|
||||
_slskd_is_connected() {
|
||||
local state
|
||||
state=$(curl -sf --max-time 10 \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" \
|
||||
"$SLSKD_URL/api/v0/application" 2>/dev/null | \
|
||||
jq -r '.server.isConnected // false' 2>/dev/null)
|
||||
[[ "$state" == "true" ]]
|
||||
}
|
||||
|
||||
if _slskd_is_connected; then
|
||||
log "slskd connected to Soulseek ✅"
|
||||
SLSKD_CONNECTED=true
|
||||
else
|
||||
warn "slskd disconnected — triggering reconnect"
|
||||
curl -sf --max-time 10 -X PUT \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$SLSKD_URL/api/v0/server" \
|
||||
-d '{"address":"server.slsknet.org","port":2242}' \
|
||||
>/dev/null 2>&1
|
||||
|
||||
_ELAPSED=0
|
||||
while [[ "$_ELAPSED" -lt 60 ]]; do
|
||||
sleep 10
|
||||
_ELAPSED=$(( _ELAPSED + 10 ))
|
||||
if _slskd_is_connected; then
|
||||
log "slskd reconnected after ${_ELAPSED}s ✅"
|
||||
SLSKD_CONNECTED=true
|
||||
break
|
||||
fi
|
||||
log " waiting... (${_ELAPSED}s / 60s)"
|
||||
done
|
||||
|
||||
[[ "$SLSKD_CONNECTED" != true ]] && \
|
||||
warn "slskd still disconnected after 60s — skipping API-dependent sections"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Stuck Searches ━━━
|
||||
# ==============================================================================================
|
||||
# Clears searches in Completed/Errored state left by Soularr crashes.
|
||||
# Prevents 409 Conflict error on next Soularr startup when it tries to
|
||||
# create a search with the same ID that already exists in a terminal state.
|
||||
|
||||
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]] && [[ "$SLSKD_CONNECTED" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 slskd — Stuck Searches ━━━"
|
||||
|
||||
SEARCHES=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/searches" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$SEARCHES" ]]; then
|
||||
warn "slskd not reachable — skipping searches"
|
||||
else
|
||||
IDS=$(echo "$SEARCHES" | tr '{' '\n' | \
|
||||
grep '"isComplete":true' | grep '"searchText":' | \
|
||||
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
|
||||
COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
COUNT="${COUNT//[^0-9]/}"; COUNT="${COUNT:-0}"
|
||||
|
||||
if [[ "$COUNT" -eq 0 ]]; then
|
||||
success "No stuck searches found ✅"
|
||||
else
|
||||
log "Found $COUNT stuck search(es)"
|
||||
SUCCESS=0; FAIL=0
|
||||
while IFS= read -r ID; do
|
||||
[[ -z "$ID" ]] && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete search: $ID"
|
||||
((SUCCESS++))
|
||||
continue
|
||||
fi
|
||||
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
"$SLSKD_URL/api/v0/searches/$ID" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY")
|
||||
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
||||
log "$ICON_TRASH Cleared search: $ID"
|
||||
((SUCCESS++))
|
||||
else
|
||||
error "Failed: $ID (HTTP $RESULT)"
|
||||
((FAIL++))
|
||||
fi
|
||||
done <<< "$IDS"
|
||||
success "Searches: $SUCCESS cleared, $FAIL failed"
|
||||
(( TOTAL_FAIL += FAIL ))
|
||||
(( TOTAL_PASS += SUCCESS ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Dead Transfer Records ━━━
|
||||
# ==============================================================================================
|
||||
# Removes completed/errored/aborted transfer records per user.
|
||||
# Prevents Soularr 404 loop when polling a user whose transfer no longer exists.
|
||||
# Safety: NEVER removes transfers that are InProgress or Queued — active downloads protected.
|
||||
|
||||
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]] && [[ "$SLSKD_CONNECTED" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 slskd — Dead Transfer Records ━━━"
|
||||
|
||||
TRANSFERS=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/transfers/downloads" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$TRANSFERS" ]]; then
|
||||
warn "slskd not reachable — skipping transfers"
|
||||
else
|
||||
USERNAMES=$(echo "$TRANSFERS" | grep -o '"username":"[^"]*"' | \
|
||||
sed 's/"username":"//;s/"//' | sort -u)
|
||||
|
||||
if [[ -z "$USERNAMES" ]]; then
|
||||
success "No transfer records found ✅"
|
||||
else
|
||||
USER_COUNT=$(echo "$USERNAMES" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $USER_COUNT user(s) with transfer records"
|
||||
SUCCESS=0; SKIPPED=0; FAIL=0
|
||||
while IFS= read -r USER; do
|
||||
[[ -z "$USER" ]] && continue
|
||||
|
||||
USER_DATA=$(curl -sf --max-time 10 \
|
||||
"$SLSKD_URL/api/v0/transfers/downloads/$USER" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
|
||||
|
||||
# Skip users with any active or queued transfers — never interrupt downloads
|
||||
ACTIVE=$(echo "$USER_DATA" | grep -c '"state":"InProgress"\|"state":"Queued"')
|
||||
if [[ "${ACTIVE:-0}" -gt 0 ]]; then
|
||||
log "$ICON_SKIP Skipping $USER — has active/queued transfer(s)"
|
||||
((SKIPPED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract IDs of terminal-state file transfers
|
||||
# Split at { so each file object lands on its own line, then grep for state
|
||||
FILE_IDS=$(echo "$USER_DATA" | tr '{' '\n' | \
|
||||
grep '"state":"Completed"\|"state":"Errored"\|"state":"Aborted"\|"state":"Cancelled"' | \
|
||||
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
|
||||
|
||||
if [[ -z "$FILE_IDS" ]]; then
|
||||
log "$ICON_SKIP Skipping $USER — no terminal-state transfers"
|
||||
((SKIPPED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
F_COUNT=$(echo "$FILE_IDS" | grep -c .)
|
||||
warn "DRY RUN — would clear $F_COUNT transfer(s) for: $USER"
|
||||
((SUCCESS++))
|
||||
continue
|
||||
fi
|
||||
|
||||
F_SUCCESS=0; F_FAIL=0
|
||||
while IFS= read -r FILE_ID; do
|
||||
[[ -z "$FILE_ID" ]] && continue
|
||||
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
"$SLSKD_URL/api/v0/transfers/downloads/$USER/$FILE_ID" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY")
|
||||
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
||||
((F_SUCCESS++))
|
||||
else
|
||||
((F_FAIL++))
|
||||
fi
|
||||
done <<< "$FILE_IDS"
|
||||
|
||||
log "$ICON_TRASH Cleared $F_SUCCESS transfer(s) for: $USER ($F_FAIL failed)"
|
||||
((SUCCESS += F_SUCCESS))
|
||||
((FAIL += F_FAIL))
|
||||
done <<< "$USERNAMES"
|
||||
success "Transfers: $SUCCESS cleared, $SKIPPED skipped (active/empty), $FAIL failed"
|
||||
(( TOTAL_FAIL += FAIL ))
|
||||
(( TOTAL_PASS += SUCCESS ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Purge Expired Failed Imports ━━━
|
||||
# ==============================================================================================
|
||||
# Removes albums Soularr downloaded but Lidarr rejected.
|
||||
# Soularr moves rejected albums to failed_imports/ and never cleans them up.
|
||||
# Purges directories older than DOWNLOADER_RETENTION_DAYS to prevent unbounded growth.
|
||||
|
||||
if [[ -n "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 slskd — Failed Imports (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
|
||||
|
||||
if [[ ! -d "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
|
||||
warn "Directory not found: $SLSKD_FAILED_IMPORTS_DIR — skipping"
|
||||
else
|
||||
OLD_IMPORTS=$(find "$SLSKD_FAILED_IMPORTS_DIR" \
|
||||
-mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}")
|
||||
IMPORT_COUNT=$(echo "$OLD_IMPORTS" | grep -c . 2>/dev/null || echo 0)
|
||||
IMPORT_COUNT="${IMPORT_COUNT//[^0-9]/}"; IMPORT_COUNT="${IMPORT_COUNT:-0}"
|
||||
|
||||
if [[ "$IMPORT_COUNT" -eq 0 ]]; then
|
||||
success "No expired failed imports found ✅"
|
||||
else
|
||||
log "Found $IMPORT_COUNT expired failed import(s)"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete:"
|
||||
echo "$OLD_IMPORTS"
|
||||
else
|
||||
find "$SLSKD_FAILED_IMPORTS_DIR" \
|
||||
-mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}" \
|
||||
-exec rm -rf {} \;
|
||||
success "$ICON_TRASH Purged $IMPORT_COUNT expired failed import(s)"
|
||||
(( TOTAL_PASS += IMPORT_COUNT ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ SABnzbd — Clear Completed History ━━━
|
||||
# ==============================================================================================
|
||||
# Removes completed download history older than DOWNLOADER_RETENTION_DAYS.
|
||||
# Keeps recent history for reference — only purges what's past the retention window.
|
||||
|
||||
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 SABnzbd — Completed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
|
||||
|
||||
HISTORY=$(curl -sf --max-time 15 \
|
||||
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&apikey=$SABNZBD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$HISTORY" ]]; then
|
||||
warn "SABnzbd not reachable — skipping completed history"
|
||||
else
|
||||
COMPLETED_IDS=$(echo "$HISTORY" | grep -o '"nzo_id":"[^"]*"' | \
|
||||
sed 's/"nzo_id":"//;s/"//')
|
||||
|
||||
if [[ -z "$COMPLETED_IDS" ]]; then
|
||||
success "No completed history found ✅"
|
||||
else
|
||||
HIST_TOTAL=$(echo "$COMPLETED_IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $HIST_TOTAL completed history entries"
|
||||
DELETED=0; SKIPPED=0
|
||||
while IFS= read -r NZO_ID; do
|
||||
[[ -z "$NZO_ID" ]] && continue
|
||||
JOB_TIME=$(echo "$HISTORY" | grep -A5 "$NZO_ID" | \
|
||||
grep -o '"completed":[0-9]*' | grep -o '[0-9]*' | head -1)
|
||||
[[ -z "$JOB_TIME" ]] && continue
|
||||
[[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete completed job: $NZO_ID"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
|
||||
>/dev/null
|
||||
log "$ICON_TRASH Deleted: $NZO_ID"
|
||||
((DELETED++))
|
||||
fi
|
||||
done <<< "$COMPLETED_IDS"
|
||||
success "Completed: $DELETED deleted, $SKIPPED within retention"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ SABnzbd — Clear Failed History ━━━
|
||||
# ==============================================================================================
|
||||
# Removes failed download history older than DOWNLOADER_RETENTION_DAYS.
|
||||
# Failed history is kept briefly for diagnosis but purged after the retention window.
|
||||
|
||||
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 SABnzbd — Failed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
|
||||
|
||||
FAILED_HIST=$(curl -sf --max-time 15 \
|
||||
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&failed_only=1&apikey=$SABNZBD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$FAILED_HIST" ]]; then
|
||||
warn "SABnzbd not reachable — skipping failed history"
|
||||
else
|
||||
FAILED_IDS=$(echo "$FAILED_HIST" | grep -o '"nzo_id":"[^"]*"' | \
|
||||
sed 's/"nzo_id":"//;s/"//')
|
||||
|
||||
if [[ -z "$FAILED_IDS" ]]; then
|
||||
success "No failed history found ✅"
|
||||
else
|
||||
FAILED_TOTAL=$(echo "$FAILED_IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $FAILED_TOTAL failed history entries"
|
||||
DELETED=0; SKIPPED=0
|
||||
while IFS= read -r NZO_ID; do
|
||||
[[ -z "$NZO_ID" ]] && continue
|
||||
JOB_TIME=$(echo "$FAILED_HIST" | grep -A5 "$NZO_ID" | \
|
||||
grep -o '"completed":[0-9]*' | grep -o '[0-9]*' | head -1)
|
||||
[[ -z "$JOB_TIME" ]] && continue
|
||||
[[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete failed job: $NZO_ID"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
|
||||
>/dev/null
|
||||
log "$ICON_TRASH Deleted: $NZO_ID"
|
||||
((DELETED++))
|
||||
fi
|
||||
done <<< "$FAILED_IDS"
|
||||
success "Failed: $DELETED deleted, $SKIPPED within retention"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ SABnzbd — Remove Stalled Queue Items ━━━
|
||||
# ==============================================================================================
|
||||
# Removes queue items in Paused or Stuck state that are no longer progressing.
|
||||
# Active downloading items (Downloading, Grabbing) are never touched.
|
||||
# Paused items may be intentional pauses — but in an automated environment
|
||||
# a Paused item sitting in the queue indefinitely is effectively stalled.
|
||||
|
||||
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 SABnzbd — Stalled Queue Items ━━━"
|
||||
|
||||
QUEUE=$(curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=queue&output=json&apikey=$SABNZBD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$QUEUE" ]]; then
|
||||
warn "SABnzbd not reachable — skipping queue"
|
||||
else
|
||||
STALLED_IDS=$(echo "$QUEUE" | grep -o '"nzo_id":"[^"]*"' | \
|
||||
sed 's/"nzo_id":"//;s/"//')
|
||||
|
||||
if [[ -z "$STALLED_IDS" ]]; then
|
||||
success "No stalled queue items found ✅"
|
||||
else
|
||||
QUEUE_TOTAL=$(echo "$STALLED_IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $QUEUE_TOTAL queue item(s) — checking status"
|
||||
DELETED=0; SKIPPED=0
|
||||
while IFS= read -r NZO_ID; do
|
||||
[[ -z "$NZO_ID" ]] && continue
|
||||
STATUS=$(echo "$QUEUE" | grep -A10 "$NZO_ID" | \
|
||||
grep -o '"status":"[^"]*"' | sed 's/"status":"//;s/"//')
|
||||
# Only remove Paused or Stuck items — Downloading/Grabbing are active
|
||||
if [[ "$STATUS" != "Paused" ]] && [[ "$STATUS" != "Stuck" ]]; then
|
||||
((SKIPPED++))
|
||||
continue
|
||||
fi
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would remove stalled item: $NZO_ID ($STATUS)"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=queue&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
|
||||
>/dev/null
|
||||
log "$ICON_TRASH Removed stalled ($STATUS): $NZO_ID"
|
||||
((DELETED++))
|
||||
fi
|
||||
done <<< "$STALLED_IDS"
|
||||
success "Queue: $DELETED removed, $SKIPPED active (skipped)"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ qBittorrent — Age Failsafe Cleanup ━━━
|
||||
# ==============================================================================================
|
||||
# Last-chance cleanup for torrents that have been sitting in qBit past their useful life.
|
||||
# deleteFiles=false — removes the torrent record from qBit but leaves files on disk.
|
||||
# Radarr/Sonarr manage actual files independently — this only cleans up the qBit entry.
|
||||
#
|
||||
# Safety checks before deletion:
|
||||
# Age must exceed QBIT_FAILSAFE_MIN_DAYS
|
||||
# Ratio must meet QBIT_FAILSAFE_MIN_RATIO (0 = age only, no ratio requirement)
|
||||
|
||||
if [[ -n "$QBIT_URL" ]] && [[ -n "$QBIT_USERNAME" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 qBittorrent — Failsafe (older than ${QBIT_FAILSAFE_MIN_DAYS} days) ━━━"
|
||||
[[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]] && \
|
||||
log "Ratio requirement: >= ${QBIT_FAILSAFE_MIN_RATIO}"
|
||||
|
||||
QBIT_COOKIE=$(curl -sf --max-time 10 -c - \
|
||||
"$QBIT_URL/api/v2/auth/login" \
|
||||
--data "username=$QBIT_USERNAME&password=$QBIT_PASSWORD" 2>/dev/null | \
|
||||
grep SID | awk '{print "SID="$NF}')
|
||||
|
||||
if [[ -z "$QBIT_COOKIE" ]]; then
|
||||
error "Failed to authenticate with qBittorrent — check QBIT_USERNAME/PASSWORD"
|
||||
notify "qBittorrent auth failed on $(hostname) — check credentials in host*.conf" "Downloaders Reset" "warning"
|
||||
((TOTAL_FAIL++))
|
||||
else
|
||||
TORRENTS=$(curl -sf --max-time 15 \
|
||||
"$QBIT_URL/api/v2/torrents/info" \
|
||||
-H "Cookie: $QBIT_COOKIE" 2>/dev/null)
|
||||
|
||||
NOW=$(date +%s)
|
||||
TORRENT_TOTAL=$(echo "$TORRENTS" | tr '}' '\n' | grep -c '"hash"' 2>/dev/null || echo 0)
|
||||
log "Found $TORRENT_TOTAL torrent(s) — applying age/ratio filter"
|
||||
DELETED=0; SKIPPED=0
|
||||
|
||||
while read -r TORRENT; do
|
||||
[[ -z "$TORRENT" ]] && continue
|
||||
HASH=$(echo "$TORRENT" | grep -o '"hash":"[^"]*"' | sed 's/"hash":"//;s/"//')
|
||||
NAME=$(echo "$TORRENT" | grep -o '"name":"[^"]*"' | sed 's/"name":"//;s/"//')
|
||||
ADDED=$(echo "$TORRENT" | grep -o '"added_on":[0-9]*' | grep -o '[0-9]*')
|
||||
RATIO=$(echo "$TORRENT" | grep -o '"ratio":[0-9.]*' | grep -o '[0-9.]*')
|
||||
[[ -z "$HASH" || -z "$ADDED" ]] && continue
|
||||
|
||||
AGE_DAYS=$(( (NOW - ADDED) / 86400 ))
|
||||
|
||||
# Age check — must be old enough
|
||||
[[ "$AGE_DAYS" -lt "$QBIT_FAILSAFE_MIN_DAYS" ]] && ((SKIPPED++)) && continue
|
||||
|
||||
# Ratio check — if configured
|
||||
if [[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]]; then
|
||||
RATIO_INT="${RATIO%.*}"
|
||||
MIN_RATIO_INT="${QBIT_FAILSAFE_MIN_RATIO%.*}"
|
||||
[[ "$RATIO_INT" -lt "$MIN_RATIO_INT" ]] && ((SKIPPED++)) && continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 -X POST \
|
||||
"$QBIT_URL/api/v2/torrents/delete" \
|
||||
-H "Cookie: $QBIT_COOKIE" \
|
||||
--data "hashes=$HASH&deleteFiles=false" >/dev/null
|
||||
log "$ICON_TRASH Deleted: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
|
||||
((DELETED++))
|
||||
fi
|
||||
done < <(echo "$TORRENTS" | tr '}' '\n')
|
||||
|
||||
success "qBittorrent: $DELETED deleted, $SKIPPED skipped (under threshold)"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY DOWNLOADERS RESET SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_TIME Duration: $(format_duration $(( $(date +%s) - START_TIME )))"
|
||||
echo "$ICON_SUCCESS Actions: $TOTAL_PASS"
|
||||
echo "$ICON_ERROR Failures: $TOTAL_FAIL"
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
||||
elif [[ "$TOTAL_FAIL" -gt 0 ]]; then
|
||||
echo "$ICON_ERROR Status: $TOTAL_FAIL failure(s) — check logs"
|
||||
notify "Downloaders reset completed with failures on $(hostname)" "Downloaders Reset" "warning"
|
||||
exit 1
|
||||
else
|
||||
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
+650
@@ -0,0 +1,650 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Downloaders Reset ==========================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Maintenance reset for all download clients on this server. Clears accumulated
|
||||
# state that download clients generate but never clean up themselves — stuck
|
||||
# searches, dead transfers, failed imports, stale queue entries, completed history.
|
||||
#
|
||||
# Called every 30 minutes by critical_sync_maintenance.sh via
|
||||
# CRITICAL_MAINTENANCE_SCRIPTS. Can also be run manually for ad hoc cleanup.
|
||||
# If a downloader is not configured for this host, that section skips cleanly.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL MODEL
|
||||
# ==============================================================================================
|
||||
#
|
||||
# slskd
|
||||
# Stuck searches — clears Completed/Errored searches left by Soularr crashes
|
||||
# prevents 409 Conflict on next Soularr startup
|
||||
# Dead transfers — removes completed/errored/aborted transfer records per user
|
||||
# prevents Soularr 404 loop when polling a user whose transfer is gone
|
||||
# NEVER removes InProgress or Queued transfers
|
||||
# Failed imports — purges albums Soularr downloaded but Lidarr rejected
|
||||
# Soularr moves these to failed_imports/ and never cleans them up
|
||||
#
|
||||
# SABnzbd
|
||||
# Completed history — removes completed download records older than DOWNLOADER_RETENTION_DAYS
|
||||
# Failed history — removes failed download records older than DOWNLOADER_RETENTION_DAYS
|
||||
# Stalled queue — removes Paused or Stuck queue items no longer progressing
|
||||
# active downloading items are never touched
|
||||
#
|
||||
# qBittorrent
|
||||
# Age failsafe — removes torrents older than QBIT_FAILSAFE_MIN_DAYS
|
||||
# deleteFiles=false — removes from qBit, leaves files for arrs to manage
|
||||
# optional ratio requirement via QBIT_FAILSAFE_MIN_RATIO
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Never Interrupt Active Downloads
|
||||
# Each downloader section checks for active state before any removal. slskd
|
||||
# skips users with InProgress or Queued transfers. SABnzbd only removes items
|
||||
# past the retention threshold. qBittorrent applies minimum age and optional
|
||||
# ratio requirements. In-progress work is never touched.
|
||||
#
|
||||
# Graceful Skip on Unavailability
|
||||
# If a downloader's URL is empty or the service is unreachable, that section
|
||||
# skips cleanly with a log message. The script never exits fatally on a single
|
||||
# unreachable downloader — the others still run.
|
||||
#
|
||||
# Host-Aware Configuration
|
||||
# detect_hosts() aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
|
||||
# to their unprefixed names. Downloaders not configured for this host are absent
|
||||
# from the aliased vars and skip automatically.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Active Transfer Protection
|
||||
# slskd: skips users with InProgress or Queued transfers before any removal.
|
||||
# SABnzbd: age threshold enforced before deletion.
|
||||
# qBittorrent: minimum age plus optional ratio gate before failsafe removal.
|
||||
#
|
||||
# Reachability Check
|
||||
# Each section validates its downloader URL before API calls. Missing or
|
||||
# unreachable downloaders skip without affecting other sections.
|
||||
#
|
||||
# Host Detection
|
||||
# detect_hosts() identifies which server is running the script and aliases
|
||||
# all HOST*_SLSKD_*, HOST*_SABNZBD_*, and HOST*_QBIT_* vars to the correct
|
||||
# host's values. Downloaders not configured on this host skip automatically.
|
||||
#
|
||||
# Lock Acquisition
|
||||
# acquire_lock "wait" — waits for previous run to finish since this runs every
|
||||
# 30 minutes and prior execution may still be completing.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
#
|
||||
# HOST*_SLSKD_URL / HOST*_SLSKD_API_KEY / HOST*_SLSKD_FAILED_IMPORTS_DIR
|
||||
# slskd connection and failed imports path. Aliased by detect_hosts()
|
||||
#
|
||||
# HOST*_SABNZBD_URL / HOST*_SABNZBD_API_KEY
|
||||
# SABnzbd connection details. Aliased by detect_hosts()
|
||||
#
|
||||
# HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
|
||||
# qBittorrent connection details. Aliased by detect_hosts()
|
||||
#
|
||||
# master.conf
|
||||
#
|
||||
# DOWNLOADER_RETENTION_DAYS
|
||||
# Days before SABnzbd history entries (completed or failed) are removed
|
||||
#
|
||||
# QBIT_FAILSAFE_MIN_DAYS
|
||||
# Minimum torrent age in days before failsafe removal is considered
|
||||
#
|
||||
# QBIT_FAILSAFE_MIN_RATIO
|
||||
# Minimum seeding ratio required alongside age gate (0 = age only)
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# downloaders_reset.sh
|
||||
# Run maintenance reset for all configured download clients
|
||||
#
|
||||
# downloaders_reset.sh --dry-run
|
||||
# Preview what would be removed without making any changes
|
||||
#
|
||||
# downloaders_reset.sh --status
|
||||
# Show configured downloaders, current queue depths, and retention settings
|
||||
#
|
||||
# downloaders_reset.sh --log
|
||||
# Verbose per-client per-item output
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Lock first — wait mode since this runs every 30min and previous may still be finishing
|
||||
acquire_lock "wait"
|
||||
|
||||
# detect_hosts() sets MY_ID and aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
|
||||
detect_hosts
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
CUTOFF=$(( $(date +%s) - (DOWNLOADER_RETENTION_DAYS * 86400) ))
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
|
||||
log "$ICON_GEAR Config: retention=${DOWNLOADER_RETENTION_DAYS}d qbit-age=${QBIT_FAILSAFE_MIN_DAYS}d qbit-ratio=${QBIT_FAILSAFE_MIN_RATIO}"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_GEAR slskd: ${SLSKD_URL:-not configured}"
|
||||
echo "$ICON_GEAR SABnzbd: ${SABNZBD_URL:-not configured}"
|
||||
echo "$ICON_GEAR qBittorrent: ${QBIT_URL:-not configured}"
|
||||
echo "$ICON_TIME Retention: ${DOWNLOADER_RETENTION_DAYS} days"
|
||||
echo "$ICON_GEAR qBit age: ${QBIT_FAILSAFE_MIN_DAYS} days"
|
||||
echo "$ICON_GEAR qBit ratio: ${QBIT_FAILSAFE_MIN_RATIO} (0=age only)"
|
||||
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||
|
||||
# Log which downloaders are active on this host
|
||||
if [[ -z "$SLSKD_URL" ]] && [[ -z "$SABNZBD_URL" ]] && [[ -z "$QBIT_URL" ]]; then
|
||||
warn "No downloaders configured for $MY_ID — nothing to reset"
|
||||
exit 0
|
||||
fi
|
||||
[[ -n "$SLSKD_URL" ]] && log "slskd active on $MY_ID"
|
||||
[[ -n "$SABNZBD_URL" ]] && log "SABnzbd active on $MY_ID"
|
||||
[[ -n "$QBIT_URL" ]] && log "qBittorrent active on $MY_ID"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Connection Check ━━━
|
||||
# ==============================================================================================
|
||||
# slskd's internal watchdog doesn't always recover from disconnection. Check before
|
||||
# running API-dependent sections; attempt reconnect if down.
|
||||
|
||||
SLSKD_CONNECTED=false
|
||||
|
||||
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC slskd — Connection Check ━━━"
|
||||
|
||||
_slskd_is_connected() {
|
||||
local state
|
||||
state=$(curl -sf --max-time 10 \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" \
|
||||
"$SLSKD_URL/api/v0/application" 2>/dev/null | \
|
||||
jq -r '.server.isConnected // false' 2>/dev/null)
|
||||
[[ "$state" == "true" ]]
|
||||
}
|
||||
|
||||
if _slskd_is_connected; then
|
||||
log "slskd connected to Soulseek ✅"
|
||||
SLSKD_CONNECTED=true
|
||||
else
|
||||
warn "slskd disconnected — triggering reconnect"
|
||||
curl -sf --max-time 10 -X PUT \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$SLSKD_URL/api/v0/server" \
|
||||
-d '{"address":"server.slsknet.org","port":2242}' \
|
||||
>/dev/null 2>&1
|
||||
|
||||
_ELAPSED=0
|
||||
while [[ "$_ELAPSED" -lt 60 ]]; do
|
||||
sleep 10
|
||||
_ELAPSED=$(( _ELAPSED + 10 ))
|
||||
if _slskd_is_connected; then
|
||||
log "slskd reconnected after ${_ELAPSED}s ✅"
|
||||
SLSKD_CONNECTED=true
|
||||
break
|
||||
fi
|
||||
log " waiting... (${_ELAPSED}s / 60s)"
|
||||
done
|
||||
|
||||
[[ "$SLSKD_CONNECTED" != true ]] && \
|
||||
warn "slskd still disconnected after 60s — skipping API-dependent sections"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Stuck Searches ━━━
|
||||
# ==============================================================================================
|
||||
# Clears searches in Completed/Errored state left by Soularr crashes.
|
||||
# Prevents 409 Conflict error on next Soularr startup when it tries to
|
||||
# create a search with the same ID that already exists in a terminal state.
|
||||
|
||||
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]] && [[ "$SLSKD_CONNECTED" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 slskd — Stuck Searches ━━━"
|
||||
|
||||
SEARCHES=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/searches" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$SEARCHES" ]]; then
|
||||
warn "slskd not reachable — skipping searches"
|
||||
else
|
||||
IDS=$(echo "$SEARCHES" | tr '{' '\n' | \
|
||||
grep '"isComplete":true' | grep '"searchText":' | \
|
||||
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
|
||||
COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
COUNT="${COUNT//[^0-9]/}"; COUNT="${COUNT:-0}"
|
||||
|
||||
if [[ "$COUNT" -eq 0 ]]; then
|
||||
success "No stuck searches found ✅"
|
||||
else
|
||||
log "Found $COUNT stuck search(es)"
|
||||
SUCCESS=0; FAIL=0
|
||||
while IFS= read -r ID; do
|
||||
[[ -z "$ID" ]] && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete search: $ID"
|
||||
((SUCCESS++))
|
||||
continue
|
||||
fi
|
||||
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
"$SLSKD_URL/api/v0/searches/$ID" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY")
|
||||
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
||||
log "$ICON_TRASH Cleared search: $ID"
|
||||
((SUCCESS++))
|
||||
else
|
||||
error "Failed: $ID (HTTP $RESULT)"
|
||||
((FAIL++))
|
||||
fi
|
||||
done <<< "$IDS"
|
||||
success "Searches: $SUCCESS cleared, $FAIL failed"
|
||||
(( TOTAL_FAIL += FAIL ))
|
||||
(( TOTAL_PASS += SUCCESS ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Dead Transfer Records ━━━
|
||||
# ==============================================================================================
|
||||
# Removes completed/errored/aborted transfer records per user.
|
||||
# Prevents Soularr 404 loop when polling a user whose transfer no longer exists.
|
||||
# Safety: NEVER removes transfers that are InProgress or Queued — active downloads protected.
|
||||
|
||||
if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]] && [[ "$SLSKD_CONNECTED" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 slskd — Dead Transfer Records ━━━"
|
||||
|
||||
TRANSFERS=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/transfers/downloads" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$TRANSFERS" ]]; then
|
||||
warn "slskd not reachable — skipping transfers"
|
||||
else
|
||||
USERNAMES=$(echo "$TRANSFERS" | grep -o '"username":"[^"]*"' | \
|
||||
sed 's/"username":"//;s/"//' | sort -u)
|
||||
|
||||
if [[ -z "$USERNAMES" ]]; then
|
||||
success "No transfer records found ✅"
|
||||
else
|
||||
USER_COUNT=$(echo "$USERNAMES" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $USER_COUNT user(s) with transfer records"
|
||||
SUCCESS=0; SKIPPED=0; FAIL=0
|
||||
while IFS= read -r USER; do
|
||||
[[ -z "$USER" ]] && continue
|
||||
|
||||
USER_DATA=$(curl -sf --max-time 10 \
|
||||
"$SLSKD_URL/api/v0/transfers/downloads/$USER" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)
|
||||
|
||||
# Skip users with any active or queued transfers — never interrupt downloads
|
||||
ACTIVE=$(echo "$USER_DATA" | grep -c '"state":"InProgress"\|"state":"Queued"')
|
||||
if [[ "${ACTIVE:-0}" -gt 0 ]]; then
|
||||
log "$ICON_SKIP Skipping $USER — has active/queued transfer(s)"
|
||||
((SKIPPED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract IDs of terminal-state file transfers
|
||||
# Split at { so each file object lands on its own line, then grep for state
|
||||
FILE_IDS=$(echo "$USER_DATA" | tr '{' '\n' | \
|
||||
grep '"state":"Completed"\|"state":"Errored"\|"state":"Aborted"\|"state":"Cancelled"' | \
|
||||
grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
|
||||
|
||||
if [[ -z "$FILE_IDS" ]]; then
|
||||
log "$ICON_SKIP Skipping $USER — no terminal-state transfers"
|
||||
((SKIPPED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
F_COUNT=$(echo "$FILE_IDS" | grep -c .)
|
||||
warn "DRY RUN — would clear $F_COUNT transfer(s) for: $USER"
|
||||
((SUCCESS++))
|
||||
continue
|
||||
fi
|
||||
|
||||
F_SUCCESS=0; F_FAIL=0
|
||||
while IFS= read -r FILE_ID; do
|
||||
[[ -z "$FILE_ID" ]] && continue
|
||||
RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
"$SLSKD_URL/api/v0/transfers/downloads/$USER/$FILE_ID" \
|
||||
-H "X-Api-Key: $SLSKD_API_KEY")
|
||||
if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
|
||||
((F_SUCCESS++))
|
||||
else
|
||||
((F_FAIL++))
|
||||
fi
|
||||
done <<< "$FILE_IDS"
|
||||
|
||||
log "$ICON_TRASH Cleared $F_SUCCESS transfer(s) for: $USER ($F_FAIL failed)"
|
||||
((SUCCESS += F_SUCCESS))
|
||||
((FAIL += F_FAIL))
|
||||
done <<< "$USERNAMES"
|
||||
success "Transfers: $SUCCESS cleared, $SKIPPED skipped (active/empty), $FAIL failed"
|
||||
(( TOTAL_FAIL += FAIL ))
|
||||
(( TOTAL_PASS += SUCCESS ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ slskd — Purge Expired Failed Imports ━━━
|
||||
# ==============================================================================================
|
||||
# Removes albums Soularr downloaded but Lidarr rejected.
|
||||
# Soularr moves rejected albums to failed_imports/ and never cleans them up.
|
||||
# Purges directories older than DOWNLOADER_RETENTION_DAYS to prevent unbounded growth.
|
||||
|
||||
if [[ -n "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 slskd — Failed Imports (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
|
||||
|
||||
if [[ ! -d "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
|
||||
warn "Directory not found: $SLSKD_FAILED_IMPORTS_DIR — skipping"
|
||||
else
|
||||
OLD_IMPORTS=$(find "$SLSKD_FAILED_IMPORTS_DIR" \
|
||||
-mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}")
|
||||
IMPORT_COUNT=$(echo "$OLD_IMPORTS" | grep -c . 2>/dev/null || echo 0)
|
||||
IMPORT_COUNT="${IMPORT_COUNT//[^0-9]/}"; IMPORT_COUNT="${IMPORT_COUNT:-0}"
|
||||
|
||||
if [[ "$IMPORT_COUNT" -eq 0 ]]; then
|
||||
success "No expired failed imports found ✅"
|
||||
else
|
||||
log "Found $IMPORT_COUNT expired failed import(s)"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete:"
|
||||
echo "$OLD_IMPORTS"
|
||||
else
|
||||
find "$SLSKD_FAILED_IMPORTS_DIR" \
|
||||
-mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}" \
|
||||
-exec rm -rf {} \;
|
||||
success "$ICON_TRASH Purged $IMPORT_COUNT expired failed import(s)"
|
||||
(( TOTAL_PASS += IMPORT_COUNT ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ SABnzbd — Clear Completed History ━━━
|
||||
# ==============================================================================================
|
||||
# Removes completed download history older than DOWNLOADER_RETENTION_DAYS.
|
||||
# Keeps recent history for reference — only purges what's past the retention window.
|
||||
|
||||
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 SABnzbd — Completed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
|
||||
|
||||
HISTORY=$(curl -sf --max-time 15 \
|
||||
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&apikey=$SABNZBD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$HISTORY" ]]; then
|
||||
warn "SABnzbd not reachable — skipping completed history"
|
||||
else
|
||||
COMPLETED_IDS=$(echo "$HISTORY" | grep -o '"nzo_id":"[^"]*"' | \
|
||||
sed 's/"nzo_id":"//;s/"//')
|
||||
|
||||
if [[ -z "$COMPLETED_IDS" ]]; then
|
||||
success "No completed history found ✅"
|
||||
else
|
||||
HIST_TOTAL=$(echo "$COMPLETED_IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $HIST_TOTAL completed history entries"
|
||||
DELETED=0; SKIPPED=0
|
||||
while IFS= read -r NZO_ID; do
|
||||
[[ -z "$NZO_ID" ]] && continue
|
||||
JOB_TIME=$(echo "$HISTORY" | grep -A5 "$NZO_ID" | \
|
||||
grep -o '"completed":[0-9]*' | grep -o '[0-9]*' | head -1)
|
||||
[[ -z "$JOB_TIME" ]] && continue
|
||||
[[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete completed job: $NZO_ID"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
|
||||
>/dev/null
|
||||
log "$ICON_TRASH Deleted: $NZO_ID"
|
||||
((DELETED++))
|
||||
fi
|
||||
done <<< "$COMPLETED_IDS"
|
||||
success "Completed: $DELETED deleted, $SKIPPED within retention"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ SABnzbd — Clear Failed History ━━━
|
||||
# ==============================================================================================
|
||||
# Removes failed download history older than DOWNLOADER_RETENTION_DAYS.
|
||||
# Failed history is kept briefly for diagnosis but purged after the retention window.
|
||||
|
||||
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 SABnzbd — Failed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"
|
||||
|
||||
FAILED_HIST=$(curl -sf --max-time 15 \
|
||||
"$SABNZBD_URL/api?mode=history&output=json&limit=1000&failed_only=1&apikey=$SABNZBD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$FAILED_HIST" ]]; then
|
||||
warn "SABnzbd not reachable — skipping failed history"
|
||||
else
|
||||
FAILED_IDS=$(echo "$FAILED_HIST" | grep -o '"nzo_id":"[^"]*"' | \
|
||||
sed 's/"nzo_id":"//;s/"//')
|
||||
|
||||
if [[ -z "$FAILED_IDS" ]]; then
|
||||
success "No failed history found ✅"
|
||||
else
|
||||
FAILED_TOTAL=$(echo "$FAILED_IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $FAILED_TOTAL failed history entries"
|
||||
DELETED=0; SKIPPED=0
|
||||
while IFS= read -r NZO_ID; do
|
||||
[[ -z "$NZO_ID" ]] && continue
|
||||
JOB_TIME=$(echo "$FAILED_HIST" | grep -A5 "$NZO_ID" | \
|
||||
grep -o '"completed":[0-9]*' | grep -o '[0-9]*' | head -1)
|
||||
[[ -z "$JOB_TIME" ]] && continue
|
||||
[[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete failed job: $NZO_ID"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
|
||||
>/dev/null
|
||||
log "$ICON_TRASH Deleted: $NZO_ID"
|
||||
((DELETED++))
|
||||
fi
|
||||
done <<< "$FAILED_IDS"
|
||||
success "Failed: $DELETED deleted, $SKIPPED within retention"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ SABnzbd — Remove Stalled Queue Items ━━━
|
||||
# ==============================================================================================
|
||||
# Removes queue items in Paused or Stuck state that are no longer progressing.
|
||||
# Active downloading items (Downloading, Grabbing) are never touched.
|
||||
# Paused items may be intentional pauses — but in an automated environment
|
||||
# a Paused item sitting in the queue indefinitely is effectively stalled.
|
||||
|
||||
if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 SABnzbd — Stalled Queue Items ━━━"
|
||||
|
||||
QUEUE=$(curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=queue&output=json&apikey=$SABNZBD_API_KEY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$QUEUE" ]]; then
|
||||
warn "SABnzbd not reachable — skipping queue"
|
||||
else
|
||||
STALLED_IDS=$(echo "$QUEUE" | grep -o '"nzo_id":"[^"]*"' | \
|
||||
sed 's/"nzo_id":"//;s/"//')
|
||||
|
||||
if [[ -z "$STALLED_IDS" ]]; then
|
||||
success "No stalled queue items found ✅"
|
||||
else
|
||||
QUEUE_TOTAL=$(echo "$STALLED_IDS" | grep -c . 2>/dev/null || echo 0)
|
||||
log "Found $QUEUE_TOTAL queue item(s) — checking status"
|
||||
DELETED=0; SKIPPED=0
|
||||
while IFS= read -r NZO_ID; do
|
||||
[[ -z "$NZO_ID" ]] && continue
|
||||
STATUS=$(echo "$QUEUE" | grep -A10 "$NZO_ID" | \
|
||||
grep -o '"status":"[^"]*"' | sed 's/"status":"//;s/"//')
|
||||
# Only remove Paused or Stuck items — Downloading/Grabbing are active
|
||||
if [[ "$STATUS" != "Paused" ]] && [[ "$STATUS" != "Stuck" ]]; then
|
||||
((SKIPPED++))
|
||||
continue
|
||||
fi
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would remove stalled item: $NZO_ID ($STATUS)"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 \
|
||||
"$SABNZBD_URL/api?mode=queue&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
|
||||
>/dev/null
|
||||
log "$ICON_TRASH Removed stalled ($STATUS): $NZO_ID"
|
||||
((DELETED++))
|
||||
fi
|
||||
done <<< "$STALLED_IDS"
|
||||
success "Queue: $DELETED removed, $SKIPPED active (skipped)"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ qBittorrent — Age Failsafe Cleanup ━━━
|
||||
# ==============================================================================================
|
||||
# Last-chance cleanup for torrents that have been sitting in qBit past their useful life.
|
||||
# deleteFiles=false — removes the torrent record from qBit but leaves files on disk.
|
||||
# Radarr/Sonarr manage actual files independently — this only cleans up the qBit entry.
|
||||
#
|
||||
# Safety checks before deletion:
|
||||
# Age must exceed QBIT_FAILSAFE_MIN_DAYS
|
||||
# Ratio must meet QBIT_FAILSAFE_MIN_RATIO (0 = age only, no ratio requirement)
|
||||
|
||||
if [[ -n "$QBIT_URL" ]] && [[ -n "$QBIT_USERNAME" ]]; then
|
||||
echo ""
|
||||
echo "━━━ 🔍 qBittorrent — Failsafe (older than ${QBIT_FAILSAFE_MIN_DAYS} days) ━━━"
|
||||
[[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]] && \
|
||||
log "Ratio requirement: >= ${QBIT_FAILSAFE_MIN_RATIO}"
|
||||
|
||||
QBIT_COOKIE=$(curl -sf --max-time 10 -c - \
|
||||
"$QBIT_URL/api/v2/auth/login" \
|
||||
--data "username=$QBIT_USERNAME&password=$QBIT_PASSWORD" 2>/dev/null | \
|
||||
grep SID | awk '{print "SID="$NF}')
|
||||
|
||||
if [[ -z "$QBIT_COOKIE" ]]; then
|
||||
error "Failed to authenticate with qBittorrent — check QBIT_USERNAME/PASSWORD"
|
||||
notify "qBittorrent auth failed on $(hostname) — check credentials in host*.conf" "Downloaders Reset" "warning"
|
||||
((TOTAL_FAIL++))
|
||||
else
|
||||
TORRENTS=$(curl -sf --max-time 15 \
|
||||
"$QBIT_URL/api/v2/torrents/info" \
|
||||
-H "Cookie: $QBIT_COOKIE" 2>/dev/null)
|
||||
|
||||
NOW=$(date +%s)
|
||||
TORRENT_TOTAL=$(echo "$TORRENTS" | tr '}' '\n' | grep -c '"hash"' 2>/dev/null || echo 0)
|
||||
log "Found $TORRENT_TOTAL torrent(s) — applying age/ratio filter"
|
||||
DELETED=0; SKIPPED=0
|
||||
|
||||
while read -r TORRENT; do
|
||||
[[ -z "$TORRENT" ]] && continue
|
||||
HASH=$(echo "$TORRENT" | grep -o '"hash":"[^"]*"' | sed 's/"hash":"//;s/"//')
|
||||
NAME=$(echo "$TORRENT" | grep -o '"name":"[^"]*"' | sed 's/"name":"//;s/"//')
|
||||
ADDED=$(echo "$TORRENT" | grep -o '"added_on":[0-9]*' | grep -o '[0-9]*')
|
||||
RATIO=$(echo "$TORRENT" | grep -o '"ratio":[0-9.]*' | grep -o '[0-9.]*')
|
||||
[[ -z "$HASH" || -z "$ADDED" ]] && continue
|
||||
|
||||
AGE_DAYS=$(( (NOW - ADDED) / 86400 ))
|
||||
|
||||
# Age check — must be old enough
|
||||
[[ "$AGE_DAYS" -lt "$QBIT_FAILSAFE_MIN_DAYS" ]] && ((SKIPPED++)) && continue
|
||||
|
||||
# Ratio check — if configured
|
||||
if [[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]]; then
|
||||
RATIO_INT="${RATIO%.*}"
|
||||
MIN_RATIO_INT="${QBIT_FAILSAFE_MIN_RATIO%.*}"
|
||||
[[ "$RATIO_INT" -lt "$MIN_RATIO_INT" ]] && ((SKIPPED++)) && continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
|
||||
((DELETED++))
|
||||
else
|
||||
curl -sf --max-time 10 -X POST \
|
||||
"$QBIT_URL/api/v2/torrents/delete" \
|
||||
-H "Cookie: $QBIT_COOKIE" \
|
||||
--data "hashes=$HASH&deleteFiles=false" >/dev/null
|
||||
log "$ICON_TRASH Deleted: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
|
||||
((DELETED++))
|
||||
fi
|
||||
done < <(echo "$TORRENTS" | tr '}' '\n')
|
||||
|
||||
success "qBittorrent: $DELETED deleted, $SKIPPED skipped (under threshold)"
|
||||
(( TOTAL_PASS += DELETED ))
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY DOWNLOADERS RESET SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_TIME Duration: $(format_duration $(( $(date +%s) - START_TIME )))"
|
||||
echo "$ICON_SUCCESS Actions: $TOTAL_PASS"
|
||||
echo "$ICON_ERROR Failures: $TOTAL_FAIL"
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
||||
elif [[ "$TOTAL_FAIL" -gt 0 ]]; then
|
||||
echo "$ICON_ERROR Status: $TOTAL_FAIL failure(s) — check logs"
|
||||
notify "Downloaders reset completed with failures on $(hostname)" "Downloaders Reset" "warning"
|
||||
exit 1
|
||||
else
|
||||
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ============================= Backup Verify ==================================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# rsync mirror integrity verification via independent MD5 checksums. Scheduled
|
||||
# weekly (Sunday 10am). Randomly samples BACKUP_VERIFY_SAMPLE files per share
|
||||
# above BACKUP_VERIFY_MIN_SIZE, computes checksums locally, then computes the
|
||||
# same checksums on the remote via SSH and compares.
|
||||
#
|
||||
# Per file: MATCH (checksums identical) | MISMATCH (file exists on both but
|
||||
# checksums differ — sync failure or corruption) | MISSING (file exists locally
|
||||
# but not on remote). All MISMATCHes and significant MISSINGs trigger notification.
|
||||
# rsync exit code 0 is not trusted — this script verifies actual content.
|
||||
#
|
||||
# Share list from HOST*_BACKUP_VERIFY_SHARES if defined, otherwise falls back
|
||||
# to HOST*_DAILY_SYNC_SHARES. Both aliased by detect_hosts().
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Independent Verification
|
||||
# rsync reports success when the transfer completed without network errors and
|
||||
# file sizes and modification times match. It does not detect silent corruption
|
||||
# during transfer (bitflip in transit), corruption written to storage at rest
|
||||
# (faulty drive sector), or files that matched size/mtime but had wrong content.
|
||||
# All of these produce exit code 0. This script checks whether "done" means "correct."
|
||||
#
|
||||
# Intentionally Small Sample
|
||||
# 10 files per share (default) — a spot check, not an exhaustive verify.
|
||||
# Catches systematic problems and hardware issues while running in minutes, not
|
||||
# hours. Full verification would take longer than the rsync itself.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Single Instance Lock
|
||||
# acquire_lock prevents concurrent runs producing conflicting results.
|
||||
#
|
||||
# Remote Connectivity Check
|
||||
# check_connectivity() verifies the remote Tailscale IP is reachable before
|
||||
# any SSH calls. Without this, all files show as MISSING on a network hiccup.
|
||||
#
|
||||
# Remote Array Check
|
||||
# check_remote_array() verifies /mnt/user is mounted on the remote before
|
||||
# computing checksums. Array not started = all files "missing" = false alarm.
|
||||
#
|
||||
# Version Parity
|
||||
# Refuses to run if remote unRAID version doesn't match local. A mismatch
|
||||
# may mean the remote is in an unexpected state.
|
||||
#
|
||||
# SSH Timeout
|
||||
# SSH_TIMEOUT caps all SSH calls. One hung connection does not block the run.
|
||||
#
|
||||
# Notification Validated
|
||||
# platform_require_cmd confirms the notify script is present before use.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
#
|
||||
# HOST*_BACKUP_VERIFY_SHARES
|
||||
# Shares to verify. Leave empty to use HOST*_DAILY_SYNC_SHARES automatically.
|
||||
# Aliased by detect_hosts() → BACKUP_VERIFY_SHARES.
|
||||
#
|
||||
# HOST*_DAILY_SYNC_SHARES
|
||||
# Fallback share list if BACKUP_VERIFY_SHARES is empty. Aliased by detect_hosts().
|
||||
#
|
||||
# master.conf
|
||||
#
|
||||
# BACKUP_VERIFY_SAMPLE
|
||||
# Random files checked per share per run. (default: 10)
|
||||
#
|
||||
# BACKUP_VERIFY_MIN_SIZE
|
||||
# Minimum file size to include in sample — tiny files have low corruption
|
||||
# risk and slow checksums. (default: 1M)
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# backup_verify.sh
|
||||
# Sample files from all shares and compare checksums. Notify on MISMATCH
|
||||
# or significant MISSING count. Silent when all samples match.
|
||||
#
|
||||
# backup_verify.sh --dry-run
|
||||
# Show which files would be sampled. No checksums computed, no notifications.
|
||||
#
|
||||
# backup_verify.sh --status
|
||||
# Show share list, sample size, and min file size configuration. Then exit.
|
||||
#
|
||||
# backup_verify.sh --log
|
||||
# Verbose per-file checksum comparison output during the run.
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
SSH_TIMEOUT=15
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Setup ━━━"
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
acquire_lock
|
||||
|
||||
# detect_hosts() sets MY_ID and aliases BACKUP_VERIFY_SHARES + DAILY_SYNC_SHARES
|
||||
detect_hosts
|
||||
|
||||
# Share selection — configured list or fallback to daily sync shares
|
||||
if [[ ${#BACKUP_VERIFY_SHARES[@]} -gt 0 ]]; then
|
||||
VERIFY_SHARES=("${BACKUP_VERIFY_SHARES[@]}")
|
||||
log "Using BACKUP_VERIFY_SHARES (${#VERIFY_SHARES[@]} shares)"
|
||||
else
|
||||
VERIFY_SHARES=("${DAILY_SYNC_SHARES[@]}")
|
||||
log "BACKUP_VERIFY_SHARES not set — using DAILY_SYNC_SHARES (${#VERIFY_SHARES[@]} shares)"
|
||||
fi
|
||||
|
||||
if [[ ${#VERIFY_SHARES[@]} -eq 0 ]]; then
|
||||
warn "No shares configured for $MY_ID — nothing to verify"
|
||||
warn "Check HOST*_BACKUP_VERIFY_SHARES or HOST*_DAILY_SYNC_SHARES in host*.conf"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "$ICON_GEAR Config: sample=${BACKUP_VERIFY_SAMPLE} min-size=${BACKUP_VERIFY_MIN_SIZE} ssh-timeout=${SSH_TIMEOUT}s"
|
||||
log "$ICON_GEAR Remote: $REMOTE_ID ($REMOTE_SERVER_NAME — $REMOTE_SERVER)"
|
||||
log "$ICON_GEAR Shares: ${VERIFY_SHARES[*]}"
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — showing sample selection only, no checksums computed"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_HOST Remote: $REMOTE_ID ($REMOTE_SERVER_NAME — $REMOTE_SERVER)"
|
||||
echo "$ICON_VERIFY Shares: ${#VERIFY_SHARES[@]}"
|
||||
echo "$ICON_VERIFY Sample: $BACKUP_VERIFY_SAMPLE files per share"
|
||||
echo "$ICON_VERIFY Min size: $BACKUP_VERIFY_MIN_SIZE"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo ""
|
||||
echo " Shares to verify:"
|
||||
for share in "${VERIFY_SHARES[@]}"; do
|
||||
echo " $share"
|
||||
done
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Pre-flight ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SHIELD Pre-flight ━━━"
|
||||
|
||||
resolve_remote_ip
|
||||
|
||||
# Connectivity — no point making 100+ SSH calls if remote is unreachable
|
||||
check_connectivity
|
||||
log "Connectivity to $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
# Version parity — mismatched unRAID could cause md5sum path differences
|
||||
check_unraid_version_parity || {
|
||||
warn "Version parity check failed — proceeding with caution"
|
||||
warn "Checksum results may be unreliable if md5sum path changed between versions"
|
||||
}
|
||||
log "Version parity with $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
# Remote array — if array is down all files appear "missing" = false alarm
|
||||
if ! check_remote_array; then
|
||||
error "Remote array not mounted on $REMOTE_SERVER_NAME"
|
||||
error "All files would appear as MISSING — aborting to prevent false alarm"
|
||||
notify "Backup verify aborted on $(hostname) — remote array not mounted on $REMOTE_SERVER_NAME" \
|
||||
"Backup Verify" "warning"
|
||||
exit 1
|
||||
fi
|
||||
log "Remote array mounted on $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
echo "Pre-flight passed ✅"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Backup Verification ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_VERIFY Backup Verification — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) → $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||||
echo "$ICON_VERIFY Sample: $BACKUP_VERIFY_SAMPLE files per share (min: $BACKUP_VERIFY_MIN_SIZE)"
|
||||
echo ""
|
||||
|
||||
START=$(date +%s)
|
||||
TOTAL_CHECKED=0
|
||||
TOTAL_MATCH=0
|
||||
TOTAL_MISMATCH=0
|
||||
TOTAL_MISSING=0
|
||||
SHARES_WITH_ISSUES=()
|
||||
|
||||
for share in "${VERIFY_SHARES[@]}"; do
|
||||
SHARE_NAME=$(basename "$share")
|
||||
echo "━━━ $ICON_VERIFY $SHARE_NAME ━━━"
|
||||
|
||||
if [[ ! -d "$share" ]]; then
|
||||
warn "$SHARE_NAME not found locally — skipping"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
# Sample random files above minimum size
|
||||
mapfile -t SAMPLE_FILES < <(
|
||||
find "$share" -type f -size +"$BACKUP_VERIFY_MIN_SIZE" 2>/dev/null | \
|
||||
shuf | head -n "$BACKUP_VERIFY_SAMPLE"
|
||||
)
|
||||
|
||||
if [[ ${#SAMPLE_FILES[@]} -eq 0 ]]; then
|
||||
log "$SHARE_NAME — no files found above $BACKUP_VERIFY_MIN_SIZE"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
log "$SHARE_NAME — sampled ${#SAMPLE_FILES[@]} files"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
for f in "${SAMPLE_FILES[@]}"; do
|
||||
warn "DRY RUN — would check: $(basename "$f")"
|
||||
done
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
SHARE_MATCH=0
|
||||
SHARE_MISMATCH=0
|
||||
SHARE_MISSING=0
|
||||
|
||||
for local_file in "${SAMPLE_FILES[@]}"; do
|
||||
[[ -z "$local_file" ]] && continue
|
||||
|
||||
# Local checksum
|
||||
local_md5=$(md5sum "$local_file" 2>/dev/null | awk '{print $1}')
|
||||
if [[ -z "$local_md5" ]]; then
|
||||
warn "Could not checksum locally: $(basename "$local_file") — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remote checksum via SSH — timeout protected
|
||||
remote_md5=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
-o StrictHostKeyChecking=no \
|
||||
root@"$REMOTE_SERVER" \
|
||||
"md5sum '$local_file' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)
|
||||
|
||||
(( TOTAL_CHECKED++ ))
|
||||
|
||||
if [[ -z "$remote_md5" ]]; then
|
||||
warn "$ICON_ERROR MISSING: $(basename "$local_file")"
|
||||
(( SHARE_MISSING++ ))
|
||||
(( TOTAL_MISSING++ ))
|
||||
elif [[ "$local_md5" == "$remote_md5" ]]; then
|
||||
log "MATCH: $(basename "$local_file")"
|
||||
(( SHARE_MATCH++ ))
|
||||
(( TOTAL_MATCH++ ))
|
||||
else
|
||||
error "MISMATCH: $(basename "$local_file")"
|
||||
error " local: $local_md5"
|
||||
error " remote: $remote_md5"
|
||||
(( SHARE_MISMATCH++ ))
|
||||
(( TOTAL_MISMATCH++ ))
|
||||
fi
|
||||
done
|
||||
|
||||
# Per-share result — only visible if issues found
|
||||
if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 ]]; then
|
||||
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH"
|
||||
SHARES_WITH_ISSUES+=("$SHARE_NAME")
|
||||
else
|
||||
log "$SHARE_NAME — all $SHARE_MATCH files match ✅"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo "━━━━━ $ICON_SUMMARY BACKUP VERIFY SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_HOST Remote: $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||||
echo "$ICON_VERIFY Checked: $TOTAL_CHECKED files"
|
||||
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
||||
echo ""
|
||||
|
||||
if [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
|
||||
echo "$ICON_SUCCESS Match: $TOTAL_MATCH"
|
||||
warn "Missing: $TOTAL_MISSING"
|
||||
[[ "$TOTAL_MISMATCH" -gt 0 ]] && echo "$ICON_ERROR Mismatch: $TOTAL_MISMATCH"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — no checksums computed"
|
||||
elif [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
|
||||
echo "$ICON_ERROR Status: ISSUES FOUND — ${#SHARES_WITH_ISSUES[@]} share(s) need attention: ${SHARES_WITH_ISSUES[*]}"
|
||||
notify "Backup verify FAILED on $(hostname) → $REMOTE_SERVER_NAME — mismatches: $TOTAL_MISMATCH missing: $TOTAL_MISSING — shares: ${SHARES_WITH_ISSUES[*]}" \
|
||||
"Backup Verify" "warning"
|
||||
else
|
||||
echo "$ICON_DONE Status: all $TOTAL_CHECKED files match across ${#VERIFY_SHARES[@]} shares ✅"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
[[ "$TOTAL_MISMATCH" -gt 0 ]] && exit 1
|
||||
exit 0
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ============================= Backup Verify ==================================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# rsync mirror integrity verification via independent MD5 checksums. Scheduled
|
||||
# weekly (Sunday 10am). Randomly samples BACKUP_VERIFY_SAMPLE files per share
|
||||
# above BACKUP_VERIFY_MIN_SIZE, computes checksums locally, then computes the
|
||||
# same checksums on the remote via SSH and compares.
|
||||
#
|
||||
# Per file: MATCH (checksums identical) | MISMATCH (file exists on both but
|
||||
# checksums differ — sync failure or corruption) | MISSING (file exists locally
|
||||
# but not on remote). All MISMATCHes and significant MISSINGs trigger notification.
|
||||
# rsync exit code 0 is not trusted — this script verifies actual content.
|
||||
#
|
||||
# Share list from HOST*_BACKUP_VERIFY_SHARES if defined, otherwise falls back
|
||||
# to HOST*_DAILY_SYNC_SHARES. Both aliased by detect_hosts().
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Independent Verification
|
||||
# rsync reports success when the transfer completed without network errors and
|
||||
# file sizes and modification times match. It does not detect silent corruption
|
||||
# during transfer (bitflip in transit), corruption written to storage at rest
|
||||
# (faulty drive sector), or files that matched size/mtime but had wrong content.
|
||||
# All of these produce exit code 0. This script checks whether "done" means "correct."
|
||||
#
|
||||
# Intentionally Small Sample
|
||||
# 10 files per share (default) — a spot check, not an exhaustive verify.
|
||||
# Catches systematic problems and hardware issues while running in minutes, not
|
||||
# hours. Full verification would take longer than the rsync itself.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Single Instance Lock
|
||||
# acquire_lock prevents concurrent runs producing conflicting results.
|
||||
#
|
||||
# Remote Connectivity Check
|
||||
# check_connectivity() verifies the remote Tailscale IP is reachable before
|
||||
# any SSH calls. Without this, all files show as MISSING on a network hiccup.
|
||||
#
|
||||
# Remote Array Check
|
||||
# check_remote_array() verifies /mnt/user is mounted on the remote before
|
||||
# computing checksums. Array not started = all files "missing" = false alarm.
|
||||
#
|
||||
# Version Parity
|
||||
# Refuses to run if remote unRAID version doesn't match local. A mismatch
|
||||
# may mean the remote is in an unexpected state.
|
||||
#
|
||||
# SSH Timeout
|
||||
# SSH_TIMEOUT caps all SSH calls. One hung connection does not block the run.
|
||||
#
|
||||
# Notification Validated
|
||||
# platform_require_cmd confirms the notify script is present before use.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
#
|
||||
# HOST*_BACKUP_VERIFY_SHARES
|
||||
# Shares to verify. Leave empty to use HOST*_DAILY_SYNC_SHARES automatically.
|
||||
# Aliased by detect_hosts() → BACKUP_VERIFY_SHARES.
|
||||
#
|
||||
# HOST*_DAILY_SYNC_SHARES
|
||||
# Fallback share list if BACKUP_VERIFY_SHARES is empty. Aliased by detect_hosts().
|
||||
#
|
||||
# master.conf
|
||||
#
|
||||
# BACKUP_VERIFY_SAMPLE
|
||||
# Random files checked per share per run. (default: 10)
|
||||
#
|
||||
# BACKUP_VERIFY_MIN_SIZE
|
||||
# Minimum file size to include in sample — tiny files have low corruption
|
||||
# risk and slow checksums. (default: 1M)
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# backup_verify.sh
|
||||
# Sample files from all shares and compare checksums. Notify on MISMATCH
|
||||
# or significant MISSING count. Silent when all samples match.
|
||||
#
|
||||
# backup_verify.sh --dry-run
|
||||
# Show which files would be sampled. No checksums computed, no notifications.
|
||||
#
|
||||
# backup_verify.sh --status
|
||||
# Show share list, sample size, and min file size configuration. Then exit.
|
||||
#
|
||||
# backup_verify.sh --log
|
||||
# Verbose per-file checksum comparison output during the run.
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
SSH_TIMEOUT=15
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Setup ━━━"
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
acquire_lock
|
||||
|
||||
# detect_hosts() sets MY_ID and aliases BACKUP_VERIFY_SHARES + DAILY_SYNC_SHARES
|
||||
detect_hosts
|
||||
|
||||
# Share selection — configured list or fallback to daily sync shares
|
||||
if [[ ${#BACKUP_VERIFY_SHARES[@]} -gt 0 ]]; then
|
||||
VERIFY_SHARES=("${BACKUP_VERIFY_SHARES[@]}")
|
||||
log "Using BACKUP_VERIFY_SHARES (${#VERIFY_SHARES[@]} shares)"
|
||||
else
|
||||
VERIFY_SHARES=("${DAILY_SYNC_SHARES[@]}")
|
||||
log "BACKUP_VERIFY_SHARES not set — using DAILY_SYNC_SHARES (${#VERIFY_SHARES[@]} shares)"
|
||||
fi
|
||||
|
||||
if [[ ${#VERIFY_SHARES[@]} -eq 0 ]]; then
|
||||
warn "No shares configured for $MY_ID — nothing to verify"
|
||||
warn "Check HOST*_BACKUP_VERIFY_SHARES or HOST*_DAILY_SYNC_SHARES in host*.conf"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "$ICON_GEAR Config: sample=${BACKUP_VERIFY_SAMPLE} min-size=${BACKUP_VERIFY_MIN_SIZE} ssh-timeout=${SSH_TIMEOUT}s"
|
||||
log "$ICON_GEAR Remote: $REMOTE_ID ($REMOTE_SERVER_NAME — $REMOTE_SERVER)"
|
||||
log "$ICON_GEAR Shares: ${VERIFY_SHARES[*]}"
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — showing sample selection only, no checksums computed"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_HOST Remote: $REMOTE_ID ($REMOTE_SERVER_NAME — $REMOTE_SERVER)"
|
||||
echo "$ICON_VERIFY Shares: ${#VERIFY_SHARES[@]}"
|
||||
echo "$ICON_VERIFY Sample: $BACKUP_VERIFY_SAMPLE files per share"
|
||||
echo "$ICON_VERIFY Min size: $BACKUP_VERIFY_MIN_SIZE"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo ""
|
||||
echo " Shares to verify:"
|
||||
for share in "${VERIFY_SHARES[@]}"; do
|
||||
echo " $share"
|
||||
done
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Pre-flight ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_SHIELD Pre-flight ━━━"
|
||||
|
||||
resolve_remote_ip
|
||||
|
||||
# Connectivity — no point making 100+ SSH calls if remote is unreachable
|
||||
check_connectivity
|
||||
log "Connectivity to $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
# Version parity — mismatched unRAID could cause md5sum path differences
|
||||
check_os_version_parity || {
|
||||
warn "Version parity check failed — proceeding with caution"
|
||||
warn "Checksum results may be unreliable if md5sum path changed between versions"
|
||||
}
|
||||
log "Version parity with $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
# Remote array — if array is down all files appear "missing" = false alarm
|
||||
if ! check_remote_array; then
|
||||
error "Remote array not mounted on $REMOTE_SERVER_NAME"
|
||||
error "All files would appear as MISSING — aborting to prevent false alarm"
|
||||
notify "Backup verify aborted on $(hostname) — remote array not mounted on $REMOTE_SERVER_NAME" \
|
||||
"Backup Verify" "warning"
|
||||
exit 1
|
||||
fi
|
||||
log "Remote array mounted on $REMOTE_SERVER_NAME ✅"
|
||||
|
||||
echo "Pre-flight passed ✅"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Backup Verification ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_VERIFY Backup Verification — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) → $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||||
echo "$ICON_VERIFY Sample: $BACKUP_VERIFY_SAMPLE files per share (min: $BACKUP_VERIFY_MIN_SIZE)"
|
||||
echo ""
|
||||
|
||||
START=$(date +%s)
|
||||
TOTAL_CHECKED=0
|
||||
TOTAL_MATCH=0
|
||||
TOTAL_MISMATCH=0
|
||||
TOTAL_MISSING=0
|
||||
SHARES_WITH_ISSUES=()
|
||||
|
||||
for share in "${VERIFY_SHARES[@]}"; do
|
||||
SHARE_NAME=$(basename "$share")
|
||||
echo "━━━ $ICON_VERIFY $SHARE_NAME ━━━"
|
||||
|
||||
if [[ ! -d "$share" ]]; then
|
||||
warn "$SHARE_NAME not found locally — skipping"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
# Sample random files above minimum size
|
||||
mapfile -t SAMPLE_FILES < <(
|
||||
find "$share" -type f -size +"$BACKUP_VERIFY_MIN_SIZE" 2>/dev/null | \
|
||||
shuf | head -n "$BACKUP_VERIFY_SAMPLE"
|
||||
)
|
||||
|
||||
if [[ ${#SAMPLE_FILES[@]} -eq 0 ]]; then
|
||||
log "$SHARE_NAME — no files found above $BACKUP_VERIFY_MIN_SIZE"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
log "$SHARE_NAME — sampled ${#SAMPLE_FILES[@]} files"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
for f in "${SAMPLE_FILES[@]}"; do
|
||||
warn "DRY RUN — would check: $(basename "$f")"
|
||||
done
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
SHARE_MATCH=0
|
||||
SHARE_MISMATCH=0
|
||||
SHARE_MISSING=0
|
||||
|
||||
for local_file in "${SAMPLE_FILES[@]}"; do
|
||||
[[ -z "$local_file" ]] && continue
|
||||
|
||||
# Local checksum
|
||||
local_md5=$(md5sum "$local_file" 2>/dev/null | awk '{print $1}')
|
||||
if [[ -z "$local_md5" ]]; then
|
||||
warn "Could not checksum locally: $(basename "$local_file") — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Remote checksum via SSH — timeout protected
|
||||
remote_md5=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
-o StrictHostKeyChecking=no \
|
||||
root@"$REMOTE_SERVER" \
|
||||
"md5sum '$local_file' 2>/dev/null | awk '{print \$1}'" 2>/dev/null)
|
||||
|
||||
(( TOTAL_CHECKED++ ))
|
||||
|
||||
if [[ -z "$remote_md5" ]]; then
|
||||
warn "$ICON_ERROR MISSING: $(basename "$local_file")"
|
||||
(( SHARE_MISSING++ ))
|
||||
(( TOTAL_MISSING++ ))
|
||||
elif [[ "$local_md5" == "$remote_md5" ]]; then
|
||||
log "MATCH: $(basename "$local_file")"
|
||||
(( SHARE_MATCH++ ))
|
||||
(( TOTAL_MATCH++ ))
|
||||
else
|
||||
error "MISMATCH: $(basename "$local_file")"
|
||||
error " local: $local_md5"
|
||||
error " remote: $remote_md5"
|
||||
(( SHARE_MISMATCH++ ))
|
||||
(( TOTAL_MISMATCH++ ))
|
||||
fi
|
||||
done
|
||||
|
||||
# Per-share result — only visible if issues found
|
||||
if [[ "$SHARE_MISMATCH" -gt 0 || "$SHARE_MISSING" -gt 0 ]]; then
|
||||
warn "$SHARE_NAME — match: $SHARE_MATCH missing: $SHARE_MISSING mismatch: $SHARE_MISMATCH"
|
||||
SHARES_WITH_ISSUES+=("$SHARE_NAME")
|
||||
else
|
||||
log "$SHARE_NAME — all $SHARE_MATCH files match ✅"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo "━━━━━ $ICON_SUMMARY BACKUP VERIFY SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_HOST Remote: $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||||
echo "$ICON_VERIFY Checked: $TOTAL_CHECKED files"
|
||||
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
||||
echo ""
|
||||
|
||||
if [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
|
||||
echo "$ICON_SUCCESS Match: $TOTAL_MATCH"
|
||||
warn "Missing: $TOTAL_MISSING"
|
||||
[[ "$TOTAL_MISMATCH" -gt 0 ]] && echo "$ICON_ERROR Mismatch: $TOTAL_MISMATCH"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — no checksums computed"
|
||||
elif [[ "$TOTAL_MISMATCH" -gt 0 || "$TOTAL_MISSING" -gt 0 ]]; then
|
||||
echo "$ICON_ERROR Status: ISSUES FOUND — ${#SHARES_WITH_ISSUES[@]} share(s) need attention: ${SHARES_WITH_ISSUES[*]}"
|
||||
notify "Backup verify FAILED on $(hostname) → $REMOTE_SERVER_NAME — mismatches: $TOTAL_MISMATCH missing: $TOTAL_MISSING — shares: ${SHARES_WITH_ISSUES[*]}" \
|
||||
"Backup Verify" "warning"
|
||||
else
|
||||
echo "$ICON_DONE Status: all $TOTAL_CHECKED files match across ${#VERIFY_SHARES[@]} shares ✅"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
[[ "$TOTAL_MISMATCH" -gt 0 ]] && exit 1
|
||||
exit 0
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================================
|
||||
# ================================= Docker Daily Restart =======================================
|
||||
# ==============================================================================================
|
||||
#
|
||||
# PURPOSE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Restarts configured containers every night at 1am as proactive maintenance.
|
||||
#
|
||||
# Called by daily_sync_maintenance.sh via DAILY_MAINTENANCE_SCRIPTS. Runs inside
|
||||
# the daily maintenance window — any service downtime is absorbed by a window
|
||||
# that is already happening. Also drives docker_update.sh in normal mode: the
|
||||
# same DAILY_RESTART_CONTAINERS list is used for both restarts and image pulls,
|
||||
# so there is no second list to maintain.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Proactive Maintenance
|
||||
# Daily restarts target containers known to degrade over time without
|
||||
# crossing a clear failure threshold — connection table growth, scheduler
|
||||
# state accumulation, session cache bloat. The watchdog cannot detect this
|
||||
# class of degradation. Scheduled restarts clear it before it becomes visible.
|
||||
#
|
||||
# State Respect
|
||||
# Running containers are restarted. Stopped containers are left stopped — they
|
||||
# were intentionally halted and this script has no authority to override that
|
||||
# decision. This rule is consistent across the entire ecosystem.
|
||||
#
|
||||
# Dependency-Safe Ordering
|
||||
# Restarts follow the same dependency ordering used by docker_watchdog.sh.
|
||||
# Services that other containers depend on restart first. A dependent is never
|
||||
# restarted while its dependency is still coming up.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# OPERATIONAL SAFEGUARDS
|
||||
# ==============================================================================================
|
||||
#
|
||||
# Dependency Ordering
|
||||
# Containers restart in dependency-safe order using HOST*_WATCHDOG_DEPENDENCIES.
|
||||
# CONTAINER_DELAY seconds between dependency restart and dependent restart gives
|
||||
# the dependency time to fully initialise before dependents try to connect.
|
||||
#
|
||||
# Restart Verification
|
||||
# After each restart, container state is checked after a settle period. A
|
||||
# container that starts and immediately crashes is marked failed and a
|
||||
# notification is sent — the script does not silently pass a restart that
|
||||
# did not stick.
|
||||
#
|
||||
# Timeout Protection
|
||||
# All docker commands wrapped in a 30 second timeout. A hung Docker daemon
|
||||
# cannot cause this script to hang indefinitely. Timed-out commands retry
|
||||
# per RETRY_COUNT before marking as failed.
|
||||
#
|
||||
# Lock Acquisition
|
||||
# acquire_lock() prevents concurrent execution if a previous run is still active.
|
||||
#
|
||||
# ==============================================================================================
|
||||
# CONFIGURATION
|
||||
# ==============================================================================================
|
||||
#
|
||||
# host*.conf
|
||||
#
|
||||
# HOST*_DAILY_RESTART_CONTAINERS
|
||||
# Containers restarted nightly. Also used by docker_update.sh normal mode
|
||||
# for image pulls — add a container once, it gets both. Aliased by
|
||||
# detect_hosts() → DAILY_RESTART_CONTAINERS
|
||||
#
|
||||
# HOST*_WATCHDOG_DEPENDENCIES
|
||||
# Dependency ordering shared with docker_watchdog.sh. Aliased by
|
||||
# detect_hosts() → WATCHDOG_DEPENDENCIES
|
||||
#
|
||||
# master.conf
|
||||
#
|
||||
# RETRY_COUNT
|
||||
# Retry attempts before giving up on a container
|
||||
#
|
||||
# SLEEP
|
||||
# Seconds between retry attempts
|
||||
#
|
||||
# CONTAINER_DELAY
|
||||
# Seconds to wait after restarting a dependency before starting its dependents
|
||||
#
|
||||
# ==============================================================================================
|
||||
# RUNTIME MODES
|
||||
# ==============================================================================================
|
||||
#
|
||||
# docker_daily_restart.sh
|
||||
# Restart all containers in DAILY_RESTART_CONTAINERS
|
||||
#
|
||||
# docker_daily_restart.sh --dry-run
|
||||
# Preview which containers would be restarted and which would be skipped
|
||||
#
|
||||
# docker_daily_restart.sh --status
|
||||
# Show configured restart list, container states, and dependency ordering
|
||||
#
|
||||
# docker_daily_restart.sh --log
|
||||
# Verbose per-container execution output
|
||||
#
|
||||
# ==============================================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../load_config.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Setup ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
acquire_lock
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
error "Docker command not found — check PATH or Docker installation"
|
||||
notify "Docker daily restart failed — Docker not found on $(hostname)" "Docker Daily Restart" "warning"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# detect_hosts() sets MY_ID and aliases HOST*_DAILY_RESTART_CONTAINERS → DAILY_RESTART_CONTAINERS
|
||||
detect_hosts
|
||||
|
||||
if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
|
||||
warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to restart"
|
||||
warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Status ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_CONTAINERS Containers: ${DAILY_RESTART_CONTAINERS[*]}"
|
||||
echo "$ICON_RETRY Retries: $RETRY_COUNT"
|
||||
echo "$ICON_TIME Sleep: ${SLEEP}s between retries"
|
||||
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no containers will be restarted"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
|
||||
# docker_cmd, verify_running, retry_docker — defined in common.sh
|
||||
|
||||
# Builds a dependency-safe restart order from DAILY_RESTART_CONTAINERS.
|
||||
# Containers that are dependencies of others restart first.
|
||||
# Returns ordered list in ORDERED_RESTART array.
|
||||
build_restart_order() {
|
||||
ORDERED_RESTART=()
|
||||
local remaining=("${DAILY_RESTART_CONTAINERS[@]}")
|
||||
local placed=()
|
||||
|
||||
# First pass — add dependency containers that appear in our list
|
||||
for container in "${remaining[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
local is_dependency=false
|
||||
# Check if this container is a dependency of any other in our list
|
||||
for dep_string in "${WATCHDOG_DEPENDENCIES[@]}"; do
|
||||
if [[ "$dep_string" == *"$container"* ]]; then
|
||||
is_dependency=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Also check associative array format
|
||||
for dependent in "${!WATCHDOG_DEPENDENCIES[@]}"; do
|
||||
if [[ "${WATCHDOG_DEPENDENCIES[$dependent]}" == *"$container"* ]]; then
|
||||
is_dependency=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$is_dependency" == true ]]; then
|
||||
# Check not already placed
|
||||
local already=false
|
||||
for p in "${placed[@]}"; do [[ "$p" == "$container" ]] && already=true && break; done
|
||||
if [[ "$already" == false ]]; then
|
||||
ORDERED_RESTART+=("$container")
|
||||
placed+=("$container")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Second pass — add remaining containers (dependents and independents)
|
||||
for container in "${remaining[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
local already=false
|
||||
for p in "${placed[@]}"; do [[ "$p" == "$container" ]] && already=true && break; done
|
||||
if [[ "$already" == false ]]; then
|
||||
ORDERED_RESTART+=("$container")
|
||||
placed+=("$container")
|
||||
fi
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
# Checks if a container is a dependent of the previously restarted container.
|
||||
# If so, waits CONTAINER_DELAY before restarting to allow dependency to settle.
|
||||
# Usage: check_dependency_delay "$container" "$last_restarted"
|
||||
check_dependency_delay() {
|
||||
local container="$1"
|
||||
local last="$2"
|
||||
[[ -z "$last" ]] && return
|
||||
|
||||
local deps="${WATCHDOG_DEPENDENCIES[$container]:-}"
|
||||
if [[ -n "$deps" ]] && [[ "$deps" == *"$last"* ]]; then
|
||||
echo " Waiting ${CONTAINER_DELAY}s — $container depends on $last..."
|
||||
sleep "$CONTAINER_DELAY"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Daily Restart ━━━
|
||||
# ==============================================================================================
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Daily Restart — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) — ${#DAILY_RESTART_CONTAINERS[@]} container(s)"
|
||||
log "$ICON_CONTAINERS Containers: ${DAILY_RESTART_CONTAINERS[*]}"
|
||||
log "$ICON_RETRY Retries: $RETRY_COUNT"
|
||||
log "$ICON_GEAR Config: sleep=${SLEEP}s delay=${CONTAINER_DELAY}s verify-wait=${RESTART_VERIFY_WAIT}s cmd-timeout=${DOCKER_TIMEOUT}s"
|
||||
|
||||
START=$(date +%s)
|
||||
FAILED=()
|
||||
RESTARTED=()
|
||||
SKIPPED=()
|
||||
|
||||
# Build dependency-safe restart order
|
||||
build_restart_order
|
||||
log "$ICON_GEAR Restart order: ${ORDERED_RESTART[*]}"
|
||||
|
||||
LAST_RESTARTED=""
|
||||
|
||||
for container in "${ORDERED_RESTART[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
c_start=$(date +%s)
|
||||
c_image=$(docker inspect --format '{{.Config.Image}}' "$container" 2>/dev/null || echo "unknown")
|
||||
log "━━━ $ICON_CONTAINERS $container ($c_image) ━━━"
|
||||
|
||||
if ! timeout "$DOCKER_TIMEOUT" docker inspect "$container" &>/dev/null; then
|
||||
warn "$container does not exist — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)
|
||||
|
||||
case "$STATUS" in
|
||||
true)
|
||||
log "$ICON_RUNNING $container is running — restarting..."
|
||||
|
||||
# Wait if this container depends on the last one restarted
|
||||
check_dependency_delay "$container" "$LAST_RESTARTED"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would restart $container"
|
||||
RESTARTED+=("$container")
|
||||
else
|
||||
if retry_docker docker restart "$container"; then
|
||||
# Verify container stayed running after restart
|
||||
if verify_running "$container"; then
|
||||
log "$ICON_STARTED $container restarted and running in $(format_duration $(( $(date +%s) - c_start ))) ✅"
|
||||
RESTARTED+=("$container")
|
||||
LAST_RESTARTED="$container"
|
||||
else
|
||||
error "$container restarted but crashed immediately"
|
||||
notify "$container crashed after restart on $(hostname)" "Docker Daily Restart" "warning"
|
||||
FAILED+=("$container")
|
||||
fi
|
||||
else
|
||||
error "Failed to restart $container after $RETRY_COUNT attempts"
|
||||
notify "$container failed to restart on $(hostname)" "Docker Daily Restart" "warning"
|
||||
FAILED+=("$container")
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
false)
|
||||
log "$ICON_NOT_RUNNING $container is stopped — skipping"
|
||||
SKIPPED+=("$container")
|
||||
;;
|
||||
*)
|
||||
error "Unknown status for $container: $STATUS"
|
||||
FAILED+=("$container")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
END=$(date +%s)
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Prune Old Images ━━━
|
||||
# ==============================================================================================
|
||||
# Restarts above swap containers onto new images — old images are now dangling. Prune immediately.
|
||||
echo ""
|
||||
echo "━━━ $ICON_SYNC Pruning Dangling Images — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would prune dangling images"
|
||||
PRUNED_SUMMARY="(dry run)"
|
||||
else
|
||||
PRUNED_OUTPUT=$(docker image prune -f 2>&1)
|
||||
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$PRUNED_OUTPUT" | sed 's/^/ /'
|
||||
PRUNED_SUMMARY=$(echo "$PRUNED_OUTPUT" | grep -E "^Total reclaimed" || echo "nothing reclaimed")
|
||||
fi
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
echo "━━━━━ $ICON_SUMMARY DAILY RESTART SUMMARY ━━━━━"
|
||||
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
|
||||
echo "$ICON_CONTAINERS Scope: ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipped, ${#FAILED[@]} failed"
|
||||
[[ ${#RESTARTED[@]} -gt 0 ]] && log "$ICON_STARTED Restarted: ${RESTARTED[*]}"
|
||||
[[ ${#SKIPPED[@]} -gt 0 ]] && log "$ICON_NOT_RUNNING Skipped: ${SKIPPED[*]}"
|
||||
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
|
||||
echo "$ICON_SYNC Pruned: ${PRUNED_SUMMARY:-none}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — no changes made"
|
||||
elif [[ ${#FAILED[@]} -eq 0 ]]; then
|
||||
echo "$ICON_DONE Status: ALL DONE ✅"
|
||||
notify "Daily restart complete — ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipped on $(hostname)" "Docker Daily Restart" "normal"
|
||||
else
|
||||
echo "$ICON_ERROR Status: ${#FAILED[@]} container(s) failed"
|
||||
notify "Daily restart completed with errors on $(hostname) — failed: ${FAILED[*]}" "Docker Daily Restart" "warning"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
[[ ${#FAILED[@]} -gt 0 ]] && exit 1
|
||||
exit 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user