The WebUI data snapshots (locations.json, dashboard.json, snapshots_*.json, etc.) are regenerated on every wizard/config change. Each file emitted two extra success lines via createTouch — "Touching <file>" and "Updating <file> with <user> ownership" — which spammed the output around the genuinely useful "... JSON regenerated" line. Add an optional "silent" flag to createTouch (third arg; default keeps the existing loud behaviour for interactive install flows) and pass it from every WebUI data generator/task. Touch + chown still run; only the logging is suppressed for these background regenerations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
321 lines
17 KiB
Bash
Executable File
321 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# LibrePortal WebUI Services Config Generator
|
|
# Generates apps-services.json from database
|
|
webuiGenerateAppsServicesConfig() {
|
|
local testing_mode="$1"
|
|
local output_file="${containers_dir}libreportal/frontend/data/apps/generated/apps-services.json"
|
|
local first_service=true
|
|
local service_count=0
|
|
|
|
isNotice "Generating apps-services.json from database..."
|
|
|
|
mkdir -p "$(dirname "$output_file")"
|
|
|
|
# Create temp file first, then atomic move
|
|
local temp_file="${output_file}.tmp.$$"
|
|
|
|
# Create header
|
|
cat > "$temp_file" << 'EOF'
|
|
{
|
|
"services": [
|
|
|
|
EOF
|
|
|
|
if [[ ! -f "$docker_dir/$db_file" ]]; then
|
|
echo "Warning: Database not found at $docker_dir/$db_file"
|
|
# Write empty array and close
|
|
echo " ]" >> "$temp_file"
|
|
echo "}" >> "$temp_file"
|
|
mv "$temp_file" "$output_file"
|
|
createTouch "$output_file" "$docker_install_user" "silent"
|
|
fi
|
|
|
|
# Get all installed apps
|
|
local installed_apps=$(sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE status = 1 ORDER BY name;" 2>/dev/null)
|
|
|
|
if [[ -z "$installed_apps" ]]; then
|
|
echo "No installed apps found"
|
|
# Write empty array and close
|
|
echo " ]" >> "$temp_file"
|
|
echo "}" >> "$temp_file"
|
|
mv "$temp_file" "$output_file"
|
|
createTouch "$output_file" "$docker_install_user" "silent"
|
|
fi
|
|
|
|
# Process each app
|
|
while IFS= read -r app_name; do
|
|
# Get app installation date
|
|
local install_date=$(sqlite3 "$docker_dir/$db_file" "SELECT install_date, install_time FROM apps WHERE name = '$app_name';" 2>/dev/null)
|
|
local installed_date="${install_date}|"
|
|
|
|
# Get all Docker services for this app
|
|
local docker_services=$(sqlite3 "$docker_dir/$db_file" "SELECT service_name, resource_value FROM network_resources WHERE app_name = '$app_name' AND resource_type = 'ip' AND status = 'active' ORDER BY service_name;" 2>/dev/null)
|
|
|
|
if [[ -n "$docker_services" ]]; then
|
|
while IFS='|' read -r docker_service_name docker_service_ip; do
|
|
# Get all ports for this service
|
|
local service_ports=$(sqlite3 "$docker_dir/$db_file" "SELECT service_name, resource_value FROM network_resources WHERE app_name = '$app_name' AND resource_type = 'port' AND parent_service = '$docker_service_name' AND status = 'active' ORDER BY service_name;" 2>/dev/null)
|
|
|
|
if [[ -n "$service_ports" ]]; then
|
|
while IFS='|' read -r port_service_name port_mapping; do
|
|
# Parse port mapping: external:internal:access:protocol
|
|
local external_port=$(echo "$port_mapping" | cut -d':' -f1)
|
|
local internal_port=$(echo "$port_mapping" | cut -d':' -f2)
|
|
local access_type=$(echo "$port_mapping" | cut -d':' -f3)
|
|
local protocol=$(echo "$port_mapping" | cut -d':' -f4)
|
|
|
|
# Get server IP (from system config or default)
|
|
local server_ip=""
|
|
if [[ -f "${containers_dir}libreportal/config/generated/configs.json" ]]; then
|
|
server_ip=$(grep -o '"CFG_SERVER_IP":[[:space:]]*"[^"]*"' "${containers_dir}libreportal/config/generated/configs.json" | cut -d'"' -f4)
|
|
fi
|
|
[[ -z "$server_ip" ]] && server_ip="localhost"
|
|
|
|
# Determine if Traefik managed (check if service has Traefik labels)
|
|
local traefik_managed="false"
|
|
# For now, assume public access means Traefik managed
|
|
[[ "$access_type" == "public" ]] && traefik_managed="true"
|
|
|
|
# Per-port URL path (10th col of CFG_<APP>_PORT_N).
|
|
# Set when the matching port row is found below; left
|
|
# empty for ports without a web UI (DNS, etc.).
|
|
local port_url_path=""
|
|
|
|
# Get button config from app config file
|
|
local button_enabled="false"
|
|
local button_text=""
|
|
local login_required="false"
|
|
local app_config_file="${install_containers_dir}/${app_name}/${app_name}.config"
|
|
if [[ -f "$app_config_file" ]]; then
|
|
# Look for button config in port configuration
|
|
while IFS='=' read -r var_name var_value || [[ -n "$var_name" ]]; do
|
|
[[ -z "$var_name" || "$var_name" =~ ^[[:space:]]*# ]] && continue
|
|
[[ "$var_name" =~ ^CFG_${app_name^^}_PORT_ ]] || continue
|
|
|
|
# Remove quotes from value
|
|
var_value="${var_value#\"}"
|
|
var_value="${var_value%\"}"
|
|
|
|
# 10-col: service|name|external:internal|access|protocol|login|traefik|button_enabled|button_text|url_path
|
|
# 9-col legacy: service|name|external:internal|access|protocol|login|traefik|button_enabled|button_text
|
|
# 8-col legacy (no login):
|
|
# service|name|external:internal|access|protocol|traefik|button_enabled|button_text
|
|
local field_count=$(awk -F'|' '{print NF}' <<< "$var_value")
|
|
local config_service=$(echo "$var_value" | cut -d'|' -f1)
|
|
local config_name=$(echo "$var_value" | cut -d'|' -f2)
|
|
local config_login_required="false"
|
|
local config_button_enabled
|
|
local config_button_text
|
|
local config_url_path=""
|
|
if [[ "$field_count" -ge 10 ]]; then
|
|
config_login_required=$(echo "$var_value" | cut -d'|' -f6)
|
|
config_button_enabled=$(echo "$var_value" | cut -d'|' -f8)
|
|
config_button_text=$(echo "$var_value" | cut -d'|' -f9)
|
|
config_url_path=$(echo "$var_value" | cut -d'|' -f10)
|
|
elif [[ "$field_count" -ge 9 ]]; then
|
|
config_login_required=$(echo "$var_value" | cut -d'|' -f6)
|
|
config_button_enabled=$(echo "$var_value" | cut -d'|' -f8)
|
|
config_button_text=$(echo "$var_value" | cut -d'|' -f9)
|
|
else
|
|
config_button_enabled=$(echo "$var_value" | cut -d'|' -f7)
|
|
config_button_text=$(echo "$var_value" | cut -d'|' -f8)
|
|
fi
|
|
|
|
# Construct full service name from config (appname_servicename format used in database)
|
|
local config_full_service_name="${app_name}_${config_name}"
|
|
|
|
# Case-insensitive matching for service names
|
|
if [[ "${config_service,,}" == "${docker_service_name,,}" && "${config_full_service_name,,}" == "${port_service_name,,}" ]]; then
|
|
button_enabled="$config_button_enabled"
|
|
button_text="$config_button_text"
|
|
login_required="$config_login_required"
|
|
port_url_path="$config_url_path"
|
|
fi
|
|
done < "$app_config_file"
|
|
fi
|
|
|
|
# Default button text if not set
|
|
[[ -z "$button_text" ]] && button_text="${port_service_name^} Interface"
|
|
|
|
# Multi-button support: button_text + port_url_path may
|
|
# carry comma-separated parallel arrays. The first pair
|
|
# remains the "primary" (kept in externalURL/buttonText
|
|
# for back-compat); the full list goes into a `links[]`
|
|
# array the frontend can render as multiple Open buttons.
|
|
local IFS_BAK="$IFS"
|
|
IFS=',' read -ra _btn_labels <<< "$button_text"
|
|
IFS=',' read -ra _btn_paths <<< "$port_url_path"
|
|
IFS="$IFS_BAK"
|
|
|
|
# Normalize each path: empty stays empty; non-empty
|
|
# gets a leading slash if missing so authors can write
|
|
# either "admin/" or "/admin/" interchangeably.
|
|
local _link_count=${#_btn_labels[@]}
|
|
local _i
|
|
for ((_i=0; _i<_link_count; _i++)); do
|
|
local _p="${_btn_paths[$_i]:-}"
|
|
if [[ -n "$_p" && "$_p" != /* ]]; then
|
|
_btn_paths[$_i]="/$_p"
|
|
fi
|
|
done
|
|
|
|
# Primary URL = first pair (kept for back-compat).
|
|
local _primary_path="${_btn_paths[0]:-}"
|
|
local _primary_label="${_btn_labels[0]:-$button_text}"
|
|
local external_url="http://${server_ip}:${external_port}${_primary_path}"
|
|
local internal_url="http://${docker_service_ip}:${internal_port}${_primary_path}"
|
|
|
|
# Build links[] JSON array — one entry per button.
|
|
local _links_json=""
|
|
for ((_i=0; _i<_link_count; _i++)); do
|
|
local _l="${_btn_labels[$_i]}"
|
|
local _p="${_btn_paths[$_i]:-}"
|
|
[[ $_i -gt 0 ]] && _links_json+=","
|
|
_links_json+=$'\n {'
|
|
_links_json+=$'\n '"\"label\": \"$_l\","
|
|
_links_json+=$'\n '"\"externalURL\": \"http://${server_ip}:${external_port}${_p}\","
|
|
_links_json+=$'\n '"\"internalURL\": \"http://${docker_service_ip}:${internal_port}${_p}\""
|
|
_links_json+=$'\n }'
|
|
done
|
|
|
|
# Add separator comma for non-first services
|
|
if [[ "$first_service" == false ]]; then
|
|
echo " }," >> "$temp_file"
|
|
fi
|
|
first_service=false
|
|
service_count=$((service_count + 1))
|
|
|
|
# Write JSON entry
|
|
{
|
|
echo " {"
|
|
echo " \"app\": \"$app_name\","
|
|
echo " \"installed\": \"$installed_date\","
|
|
echo " \"serviceName\": \"$docker_service_name\","
|
|
echo " \"serviceIP\": \"$docker_service_ip\","
|
|
echo " \"name\": \"$port_service_name\","
|
|
echo " \"externalPort\": $external_port,"
|
|
echo " \"internalPort\": $internal_port,"
|
|
echo " \"access\": \"$access_type\","
|
|
echo " \"protocol\": \"$protocol\","
|
|
echo " \"traefikManaged\": $traefik_managed,"
|
|
echo " \"serverIP\": \"$server_ip\","
|
|
echo " \"externalURL\": \"$external_url\","
|
|
echo " \"internalURL\": \"$internal_url\","
|
|
echo " \"links\": [${_links_json}"
|
|
echo " ],"
|
|
echo " \"buttonEnabled\": $button_enabled,"
|
|
echo " \"buttonText\": \"$_primary_label\","
|
|
echo " \"loginRequired\": $login_required"
|
|
} >> "$temp_file"
|
|
done <<< "$service_ports"
|
|
fi
|
|
done <<< "$docker_services"
|
|
fi
|
|
done <<< "$installed_apps"
|
|
|
|
# Host-installed apps live outside the Docker network_resources DB.
|
|
# Walk container configs for HOST_INSTALL=true + HOST_SERVICES, confirm
|
|
# the package is present via dpkg, then emit one synthetic service entry
|
|
# per systemd unit. Transport=systemd tells the frontend the logs come
|
|
# from a host log file (tail -F over the bind-mounted /host/var/log).
|
|
if [[ -d "$install_containers_dir" ]]; then
|
|
for app_dir in "$install_containers_dir"/*; do
|
|
[[ -d "$app_dir" ]] || continue
|
|
local host_app_name=$(basename "$app_dir")
|
|
[[ "$host_app_name" == "template" ]] && continue
|
|
local cfg_file="$app_dir/$host_app_name.config"
|
|
[[ -f "$cfg_file" ]] || continue
|
|
local app_upper="${host_app_name^^}"; app_upper="${app_upper//-/_}"
|
|
local host_install host_package host_services host_log_map
|
|
host_install=$(grep -E "^CFG_${app_upper}_HOST_INSTALL=" "$cfg_file" | cut -d'=' -f2 | tr -d '\r"')
|
|
[[ "$host_install" == "true" ]] || continue
|
|
host_package=$(grep -E "^CFG_${app_upper}_HOST_PACKAGE=" "$cfg_file" | cut -d'=' -f2 | tr -d '\r"')
|
|
host_services=$(grep -E "^CFG_${app_upper}_HOST_SERVICES=" "$cfg_file" | cut -d'=' -f2 | tr -d '\r"')
|
|
host_log_map=$(grep -E "^CFG_${app_upper}_HOST_LOG_FILES=" "$cfg_file" | cut -d'=' -f2 | tr -d '\r"')
|
|
[[ -n "$host_package" ]] || continue
|
|
dpkg-query -W -f='${Status}' "$host_package" 2>/dev/null | grep -q "install ok installed" || continue
|
|
[[ -n "$host_services" ]] || continue
|
|
|
|
IFS=',' read -ra _units <<< "$host_services"
|
|
for _unit in "${_units[@]}"; do
|
|
_unit="${_unit//[[:space:]]/}"
|
|
[[ -z "$_unit" ]] && continue
|
|
local _status="inactive"
|
|
systemctl is-active --quiet "$_unit" 2>/dev/null && _status="active"
|
|
local _display_name="${_unit%.service}"
|
|
# Pluck the matching log file for this unit from the
|
|
# comma-list of <unit>|<path> pairs. Empty = no log file
|
|
# known; backend will surface that to the client.
|
|
local _log_host="" _log_container=""
|
|
if [[ -n "$host_log_map" ]]; then
|
|
IFS=',' read -ra _pairs <<< "$host_log_map"
|
|
for _p in "${_pairs[@]}"; do
|
|
local _u="${_p%%|*}"
|
|
local _f="${_p#*|}"
|
|
if [[ "$_u" == "$_unit" ]]; then
|
|
_log_host="$_f"
|
|
# /var/log/X -> /host/var/log/X (matches the
|
|
# bind-mount in libreportal's compose).
|
|
_log_container="/host${_f}"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if [[ "$first_service" == false ]]; then
|
|
echo " }," >> "$temp_file"
|
|
fi
|
|
first_service=false
|
|
service_count=$((service_count + 1))
|
|
{
|
|
echo " {"
|
|
echo " \"app\": \"$host_app_name\","
|
|
echo " \"installed\": \"\","
|
|
echo " \"serviceName\": \"$_display_name\","
|
|
echo " \"name\": \"$_display_name\","
|
|
echo " \"transport\": \"systemd\","
|
|
echo " \"unit\": \"$_unit\","
|
|
echo " \"status\": \"$_status\","
|
|
[[ -n "$_log_container" ]] && echo " \"logFile\": \"$_log_container\","
|
|
echo " \"buttonEnabled\": false,"
|
|
echo " \"buttonText\": \"\","
|
|
echo " \"loginRequired\": false"
|
|
} >> "$temp_file"
|
|
done
|
|
done
|
|
fi
|
|
|
|
# Close last object and array
|
|
if [[ "$first_service" == false ]]; then
|
|
echo " }" >> "$temp_file"
|
|
else
|
|
# No services found, write empty entry
|
|
echo " ]" >> "$temp_file"
|
|
echo "}" >> "$temp_file"
|
|
mv "$temp_file" "$output_file"
|
|
createTouch "$output_file" "$docker_install_user" "silent"
|
|
echo "No services found to generate"
|
|
fi
|
|
|
|
echo " ]" >> "$temp_file"
|
|
echo "}" >> "$temp_file"
|
|
|
|
# Atomic move to final location
|
|
if [ $? -eq 0 ]; then
|
|
mv "$temp_file" "$output_file"
|
|
# Set proper ownership for web UI access
|
|
createTouch "$output_file" "$docker_install_user" "silent"
|
|
else
|
|
rm -f "$temp_file" 2>/dev/null
|
|
fi
|
|
|
|
# Output summary
|
|
if [[ "$testing_mode" == "test" ]]; then
|
|
echo "apps-services.json test generation completed!"
|
|
echo "Output file: $output_file"
|
|
echo "Total services generated: $service_count"
|
|
else
|
|
isSuccessful "Generation of apps-services.json completed!"
|
|
fi
|
|
}
|