feat(secubox-core): Add secubox-landing page generator

- Add secubox-landing script to generate landing pages from HAProxy vhosts
- Integrate landing command into secubox CLI
- Add boot hook to regenerate landing pages on startup
- Fix HAProxy multi-cert SNI using crt-list instead of directory mode
- Fix backend IPs from 127.0.0.1 to 192.168.255.1 for LXC compatibility
- Auto-convert localhost IPs in RPCD handler and CLI tools

Landing page features:
- Groups all services by zone with stats header
- Shows SSL certificate status per domain
- Categorizes by type: Streamlit, Blog, Admin, Media, Dev, etc.
- Regenerates at boot (30s after startup)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-08 06:15:37 +01:00
parent 95f5022082
commit ab34719f9f
13 changed files with 928 additions and 12 deletions

View File

@ -642,6 +642,13 @@ method_create_server() {
return
fi
# Auto-convert localhost/127.0.0.1 to 192.168.255.1 (HAProxy runs in LXC)
case "$address" in
127.0.0.1|localhost|127.0.1.1)
address="192.168.255.1"
;;
esac
section_id="${backend}_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
uci set "$UCI_CONFIG.$section_id=server"
@ -686,6 +693,13 @@ method_update_server() {
return
fi
# Auto-convert localhost/127.0.0.1 to 192.168.255.1 (HAProxy runs in LXC)
case "$address" in
127.0.0.1|localhost|127.0.1.1)
address="192.168.255.1"
;;
esac
# Check if this is an inline server (id format: backendname_servername)
# If so, we need to convert it to a proper server section
if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then

View File

@ -82,7 +82,7 @@ use_backend backend_${name//-/_} if host_${name//-/_}_${zone//\./_}
backend backend_${name//-/_}
mode http
option httpchk GET /
server ${name} 127.0.0.1:${backend_port} check inter 10s
server ${name} 192.168.255.1:${backend_port} check inter 10s
EOF
# Step 3: Register in UCI

View File

@ -466,7 +466,7 @@ cmd_ssl_add() {
uci set "haproxy.${backend_name}.mode=http"
uci set "haproxy.${backend_name}.balance=roundrobin"
uci set "haproxy.${backend_name}.enabled=1"
uci add_list "haproxy.${backend_name}.server=${service} 127.0.0.1:${local_port} check"
uci add_list "haproxy.${backend_name}.server=${service} 192.168.255.1:${local_port} check"
fi
# Check if vhost already exists

View File

@ -47,10 +47,8 @@ deploy() {
echo "Certificate deployed: $HAPROXY_CERTS_DIR/$domain.pem"
# Reload HAProxy if running
if [ -x /etc/init.d/haproxy ]; then
/etc/init.d/haproxy reload 2>/dev/null || true
fi
# Regenerate certs.list for multi-certificate SNI support
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
return 0
}

View File

@ -40,8 +40,54 @@ done
log_info "Certificate sync complete"
# Generate certs.list for multi-certificate SNI support
CERTS_LIST="$HAPROXY_CERTS_DIR/certs.list"
CONTAINER_CERTS_PATH="/opt/haproxy/certs"
log_info "Generating certificate list file..."
> "$CERTS_LIST"
for cert_file in "$HAPROXY_CERTS_DIR"/*.pem; do
[ -f "$cert_file" ] || continue
filename=$(basename "$cert_file")
container_cert_path="$CONTAINER_CERTS_PATH/$filename"
# Extract domain names from certificate SANs
domains=$(openssl x509 -in "$cert_file" -noout -text 2>/dev/null | \
grep -A1 "Subject Alternative Name" | \
tail -n1 | \
sed 's/DNS://g' | \
tr ',' '\n' | \
tr -d ' ')
# If no SANs, try to get CN
if [ -z "$domains" ]; then
domains=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | \
sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')
fi
if [ -n "$domains" ]; then
echo "$domains" | while read -r domain; do
[ -n "$domain" ] || continue
echo "$container_cert_path $domain" >> "$CERTS_LIST"
done
else
log_info "No domain found in certificate: $filename, adding without SNI filter"
echo "$container_cert_path" >> "$CERTS_LIST"
fi
done
# Deduplicate entries
if [ -f "$CERTS_LIST" ]; then
sort -u "$CERTS_LIST" > "$CERTS_LIST.tmp"
mv "$CERTS_LIST.tmp" "$CERTS_LIST"
count=$(wc -l < "$CERTS_LIST")
log_info "Generated certs.list with $count entries"
fi
# Reload HAProxy if running
if pgrep -x haproxy >/dev/null 2>&1 || lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING; then
if pgrep haproxy >/dev/null 2>&1 || lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING; then
log_info "Reloading HAProxy..."
/etc/init.d/haproxy reload 2>/dev/null || true
fi

View File

@ -409,6 +409,69 @@ lxc_exec() {
lxc-attach -n "$LXC_NAME" -- "$@"
}
# ===========================================
# Certificate List Generation (for multi-cert SNI)
# ===========================================
# Generate crt-list file for multi-certificate SNI support
# This is more reliable than directory mode for multi-domain setups
generate_certs_list() {
local certs_list="$CERTS_PATH/certs.list"
local container_certs_path="/opt/haproxy/certs"
log_info "Generating certificate list file..."
# Start fresh
> "$certs_list"
# Process each .pem file
for cert_file in "$CERTS_PATH"/*.pem; do
[ -f "$cert_file" ] || continue
local filename=$(basename "$cert_file")
local container_cert_path="$container_certs_path/$filename"
# Extract domain names from certificate (CN and SANs)
local domains=$(openssl x509 -in "$cert_file" -noout -text 2>/dev/null | \
grep -A1 "Subject Alternative Name" | \
tail -n1 | \
sed 's/DNS://g' | \
tr ',' '\n' | \
tr -d ' ')
# If no SANs, try to get CN
if [ -z "$domains" ]; then
domains=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | \
sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')
fi
if [ -n "$domains" ]; then
# For each domain, add an entry with SNI filter
# Format: /path/to/cert.pem [sni_filter]
echo "$domains" | while read -r domain; do
[ -n "$domain" ] || continue
# Handle wildcard certificates - the SNI filter should match the pattern
echo "$container_cert_path $domain" >> "$certs_list"
done
else
# Fallback: add cert without SNI filter (will be default)
log_warn "No domain found in certificate: $filename"
echo "$container_cert_path" >> "$certs_list"
fi
done
# Deduplicate entries (same cert may have multiple SANs)
if [ -f "$certs_list" ]; then
sort -u "$certs_list" > "$certs_list.tmp"
mv "$certs_list.tmp" "$certs_list"
local count=$(wc -l < "$certs_list")
log_info "Generated certs.list with $count entries"
else
log_warn "No certificates found in $CERTS_PATH"
fi
}
# ===========================================
# Configuration Generation
# ===========================================
@ -420,6 +483,9 @@ generate_config() {
log_info "Generating HAProxy configuration..."
# Generate certs.list for multi-certificate SNI support
generate_certs_list
# Global section
cat > "$cfg_file" << EOF
# HAProxy Configuration - Generated by SecuBox
@ -509,14 +575,28 @@ EOF
# HTTPS Frontend (if certificates exist)
# Use container path /opt/haproxy/certs/ (not host path)
local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
local CERTS_LIST_FILE="$CERTS_PATH/certs.list"
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
cat << EOF
# Use crt-list for proper multi-certificate SNI matching
# Falls back to directory mode if certs.list doesn't exist
if [ -f "$CERTS_LIST_FILE" ] && [ -s "$CERTS_LIST_FILE" ]; then
cat << EOF
frontend https-in
bind *:$https_port,[::]:$https_port ssl crt-list $CONTAINER_CERTS_PATH/certs.list alpn h2,http/1.1
mode http
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
EOF
else
# Fallback to directory mode if no certs.list
cat << EOF
frontend https-in
bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
mode http
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
EOF
fi
# Add vhost ACLs for HTTPS
config_foreach _add_vhost_acl vhost "https"

View File

@ -139,7 +139,7 @@ configure_haproxy() {
uci -q add haproxy backend
uci -q set haproxy.@backend[-1].name='jellyfin_web'
uci -q set haproxy.@backend[-1].mode='http'
uci -q add_list haproxy.@backend[-1].server="jellyfin 127.0.0.1:$port check"
uci -q add_list haproxy.@backend[-1].server="jellyfin 192.168.255.1:$port check"
# Add vhost
uci -q add haproxy vhost

View File

@ -268,7 +268,7 @@ configure_haproxy() {
uci -q add haproxy backend
uci -q set haproxy.@backend[-1].name='jitsi_web'
uci -q set haproxy.@backend[-1].mode='http'
uci -q add_list haproxy.@backend[-1].server="jitsi 127.0.0.1:$web_port check"
uci -q add_list haproxy.@backend[-1].server="jitsi 192.168.255.1:$web_port check"
# Add vhost
uci -q add haproxy vhost

View File

@ -583,7 +583,7 @@ configure_haproxy() {
uci -q add haproxy backend
uci -q set haproxy.@backend[-1].name='simplex_smp'
uci -q set haproxy.@backend[-1].mode='tcp'
uci -q add_list haproxy.@backend[-1].server="simplex-smp 127.0.0.1:$SMP_PORT check"
uci -q add_list haproxy.@backend[-1].server="simplex-smp 192.168.255.1:$SMP_PORT check"
uci commit haproxy
/etc/init.d/haproxy reload 2>/dev/null

View File

@ -228,7 +228,7 @@ if service_port and domain:
run_cmd(f'uci set haproxy.{backend_name}=backend')
run_cmd(f'uci set haproxy.{backend_name}.name="{backend_name}"')
run_cmd(f'uci set haproxy.{backend_name}.mode="http"')
run_cmd(f'uci set haproxy.{backend_name}.server="srv 127.0.0.1:{service_port} check"')
run_cmd(f'uci set haproxy.{backend_name}.server="srv 192.168.255.1:{service_port} check"')
run_cmd(f'uci set haproxy.{backend_name}.enabled="1"')
run_cmd(f'uci set haproxy.{backend_name}_vhost=vhost')

View File

@ -62,4 +62,7 @@ service_triggers() {
boot() {
# Delay start on boot to allow network to initialize
( sleep 10; start "$@"; ) &
# Regenerate landing pages after network is up
( sleep 30; /usr/sbin/secubox-landing regenerate >/dev/null 2>&1; ) &
}

View File

@ -31,6 +31,7 @@ ${BOLD}Commands:${NC}
${GREEN}device${NC} Device information and management
${GREEN}net${NC} Network management
${GREEN}diag${NC} Diagnostics and health checks
${GREEN}landing${NC} Generate landing pages from vhosts
${GREEN}ai${NC} AI copilot (optional)
${BOLD}Examples:${NC}
@ -392,6 +393,41 @@ EOF
esac
}
# Landing page commands
cmd_landing() {
case "$1" in
generate|gen)
shift
/usr/sbin/secubox-landing generate "$@"
;;
list|ls)
/usr/sbin/secubox-landing list
;;
show)
/usr/sbin/secubox-landing show "$2"
;;
regenerate|regen)
/usr/sbin/secubox-landing regenerate
;;
help|*)
cat <<EOF
${BOLD}secubox landing${NC} - Landing page generator
${BOLD}Usage:${NC}
secubox landing list List all zones and service counts
secubox landing show <zone> Show services for a zone
secubox landing generate [zone] Generate landing page(s)
secubox landing regenerate Regenerate all landing pages
${BOLD}Examples:${NC}
secubox landing list
secubox landing generate gk2.secubox.in
secubox landing regenerate
EOF
;;
esac
}
# AI commands (optional)
cmd_ai() {
# Check if AI is enabled
@ -462,6 +498,10 @@ case "$1" in
shift
cmd_diag "$@"
;;
landing)
shift
cmd_landing "$@"
;;
ai)
shift
cmd_ai "$@"

View File

@ -0,0 +1,735 @@
#!/bin/sh
#
# SecuBox Landing Page Generator
# Generates landing pages from HAProxy vhosts configuration
# Includes ALL vhosts across all zones, grouped by zone
#
VERSION="1.1.0"
SITES_DIR="/srv/metablogizer/sites"
CERTS_DIR="/etc/haproxy/certs"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[LANDING]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Get public IP
get_public_ip() {
curl -s --connect-timeout 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\n'
}
# Get node name from UCI or hostname
get_node_name() {
local name=$(uci -q get secubox.main.node_name)
[ -z "$name" ] && name=$(uci -q get system.@system[0].hostname)
[ -z "$name" ] && name="SecuBox"
echo "$name"
}
# Get ALL vhosts (all domains)
get_all_vhosts() {
uci show haproxy 2>/dev/null | grep '\.domain=' | grep -v '_wildcard_' | grep -v 'cert_' | \
sed "s/.*domain='\\([^']*\\)'/\\1/" | sort -u
}
# Extract zone from domain (last 2 parts)
get_zone_from_domain() {
local domain="$1"
echo "$domain" | awk -F. '{if(NF>2) print $(NF-1)"."$NF; else print $0}'
}
# Extract all unique zones from vhosts
get_zones() {
get_all_vhosts | while read domain; do
get_zone_from_domain "$domain"
done | sort -u
}
# Get all vhosts for a specific zone
get_vhosts_for_zone() {
local zone="$1"
get_all_vhosts | grep "\.${zone}$"
}
# Check if domain has valid certificate
has_valid_cert() {
local domain="$1"
local zone=$(get_zone_from_domain "$domain")
# Check for exact cert or wildcard
[ -f "${CERTS_DIR}/${domain}.pem" ] && return 0
[ -f "${CERTS_DIR}/_wildcard_.${zone}.pem" ] && return 0
# Check ACME certs
[ -f "/etc/acme/${domain}/fullchain.pem" ] && return 0
[ -f "/etc/acme/_wildcard_.${zone}/fullchain.pem" ] && return 0
return 1
}
# Detect service type from backend name
detect_service_type() {
local backend="$1"
local domain="$2"
case "$backend" in
streamlit_*|*streamlit*) echo "streamlit" ;;
metablog_*|*metablog*) echo "blog" ;;
luci|*luci*) echo "admin" ;;
*jellyfin*|*media*|*jitsi*) echo "media" ;;
*git*|*gitea*) echo "dev" ;;
*glances*|*status*|*monitor*|*uptime*) echo "infra" ;;
*)
# Try to guess from domain
case "$domain" in
console.*|admin.*|luci.*|c3box.*) echo "admin" ;;
git.*|dev.*|devel.*) echo "dev" ;;
blog.*|news.*|zine.*|gandalf.*|cyberzine.*) echo "blog" ;;
media.*|live.*|meet.*|jitsi.*) echo "media" ;;
status.*|glances.*|monitor.*) echo "infra" ;;
slides.*|sliders.*) echo "presentation" ;;
mail.*) echo "mail" ;;
*) echo "service" ;;
esac
;;
esac
}
# Get backend info for a vhost
get_vhost_backend() {
local domain="$1"
local backend=""
# Find the UCI section for this domain
for s in $(uci show haproxy 2>/dev/null | grep "\.domain='${domain}'" | cut -d. -f2 | cut -d= -f1); do
backend=$(uci -q get haproxy.${s}.backend)
[ -n "$backend" ] && break
done
echo "$backend"
}
# Check if vhost is enabled
is_vhost_enabled() {
local domain="$1"
for s in $(uci show haproxy 2>/dev/null | grep "\.domain='${domain}'" | cut -d. -f2 | cut -d= -f1); do
local enabled=$(uci -q get haproxy.${s}.enabled)
[ "$enabled" = "1" ] && return 0
done
return 1
}
# Generate HTML for a single service item
generate_service_html() {
local domain="$1"
local name="$2"
local desc="$3"
local type="$4"
local has_cert="$5"
local extra_class=""
local tags=""
case "$type" in
streamlit)
extra_class=" streamlit"
tags='<span class="tag">STREAMLIT</span>'
;;
admin)
extra_class=" highlight"
tags='<span class="tag">ADMIN</span>'
;;
blog)
tags='<span class="tag tag-blog">BLOG</span>'
;;
media)
tags='<span class="tag tag-media">MEDIA</span>'
;;
dev)
tags='<span class="tag tag-dev">DEV</span>'
;;
presentation)
tags='<span class="tag tag-slides">SLIDES</span>'
;;
mail)
tags='<span class="tag tag-mail">MAIL</span>'
;;
*)
tags=""
;;
esac
# Add SSL indicator
if [ "$has_cert" = "1" ]; then
tags="${tags}"'<span class="ssl-ok">🔒</span>'
fi
cat <<EOF
<a href="https://${domain}" class="item${extra_class}">
<div class="item-left">
<span class="item-name">${name}</span>
<span class="item-desc">${desc}</span>
</div>
<div class="item-right">
${tags}
<span class="domain">${domain}</span>
</div>
</a>
EOF
}
# Generate full landing page for a zone (includes ALL zones)
generate_landing_page() {
local primary_zone="$1"
local include_all="${2:-1}" # Include all zones by default
local output_dir="${SITES_DIR}/${primary_zone%%.*}"
local output_file="${output_dir}/index.html"
local node_name=$(get_node_name)
local public_ip=$(get_public_ip)
[ -z "$public_ip" ] && public_ip="N/A"
log "Generating landing page for zone: $primary_zone"
# Create output directory
mkdir -p "$output_dir"
# Collect zones to include
local zones_to_include=""
if [ "$include_all" = "1" ]; then
zones_to_include=$(get_zones)
else
zones_to_include="$primary_zone"
fi
# Count all services
local total_count=0
local cert_count=0
# Generate the HTML file header
cat > "$output_file" <<HTMLHEAD
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${node_name} - ${primary_zone}</title>
<style>
:root {
--bg: #0a0a0f;
--card-bg: #12121a;
--accent: #00d4aa;
--accent2: #ff6b6b;
--accent3: #6b8aff;
--text: #e0e0e0;
--text-dim: #808090;
--border: #2a2a3a;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 1.5rem;
}
.container { max-width: 1000px; margin: 0 auto; }
header {
text-align: center;
margin-bottom: 2rem;
padding: 1.5rem;
background: linear-gradient(135deg, #1a1a2e 0%, #0a0a0f 100%);
border-radius: 12px;
border: 1px solid var(--border);
}
h1 {
font-size: 2rem;
background: linear-gradient(90deg, var(--accent), #00a0ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.3rem;
}
.subtitle { color: var(--text-dim); font-size: 0.95rem; }
.node-info {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.node-info span {
background: var(--card-bg);
padding: 0.3rem 0.8rem;
border-radius: 6px;
font-family: monospace;
font-size: 0.8rem;
}
.stats {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: var(--accent);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-dim);
}
.zone-section {
margin-top: 2rem;
padding: 1rem;
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border);
}
.zone-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.zone-name {
font-size: 1.1rem;
color: var(--accent);
font-weight: 600;
}
.zone-count {
color: var(--text-dim);
font-size: 0.8rem;
}
.category {
margin-top: 1rem;
margin-bottom: 0.5rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--border);
color: var(--text-dim);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.list {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem 0.8rem;
text-decoration: none;
color: inherit;
transition: all 0.2s;
}
.item:hover {
border-color: var(--accent);
transform: translateX(4px);
background: rgba(0,212,170,0.05);
}
.item.streamlit:hover { border-color: var(--accent2); background: rgba(255,107,107,0.05); }
.item-left {
display: flex;
align-items: center;
gap: 0.6rem;
flex: 1;
}
.item-name {
font-weight: 600;
color: var(--accent);
min-width: 100px;
font-size: 0.9rem;
}
.item.streamlit .item-name { color: var(--accent2); }
.item-desc {
color: var(--text-dim);
font-size: 0.8rem;
}
.item-right {
display: flex;
align-items: center;
gap: 0.4rem;
}
.tag {
background: var(--accent)22;
color: var(--accent);
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-size: 0.6rem;
font-weight: 600;
font-family: monospace;
}
.item.streamlit .tag {
background: var(--accent2)22;
color: var(--accent2);
}
.tag-blog { background: #9b59b622; color: #9b59b6; }
.tag-media { background: #e74c3c22; color: #e74c3c; }
.tag-dev { background: #3498db22; color: #3498db; }
.tag-slides { background: #f39c1222; color: #f39c12; }
.tag-mail { background: #1abc9c22; color: #1abc9c; }
.ssl-ok { font-size: 0.7rem; }
.domain {
font-family: monospace;
color: var(--text-dim);
font-size: 0.7rem;
}
footer {
text-align: center;
margin-top: 2rem;
color: var(--text-dim);
font-size: 0.75rem;
}
footer a { color: var(--accent); }
.highlight {
border-color: var(--accent) !important;
background: linear-gradient(135deg, rgba(0,212,170,0.05) 0%, transparent 100%);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>${node_name}</h1>
<p class="subtitle">Services distribués sur le noeud Vortex</p>
<div class="node-info">
<span>*.${primary_zone}</span>
<span>${public_ip}</span>
<span>Mesh: Enabled</span>
</div>
HTMLHEAD
# Process each zone
local zone_html=""
for zone in $zones_to_include; do
local vhosts=$(get_vhosts_for_zone "$zone")
local zone_count=0
# Group vhosts by category
local streamlit_items=""
local blog_items=""
local admin_items=""
local media_items=""
local dev_items=""
local infra_items=""
local presentation_items=""
local mail_items=""
local service_items=""
for domain in $vhosts; do
# Skip base zone domains
[ "$domain" = "$zone" ] && continue
# Skip disabled vhosts
is_vhost_enabled "$domain" || continue
local backend=$(get_vhost_backend "$domain")
local type=$(detect_service_type "$backend" "$domain")
# Extract name from subdomain
local subdomain=$(echo "$domain" | sed "s/\\.${zone}$//")
local name=$(echo "$subdomain" | sed 's/[-_]/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
# Generate description based on type/backend
local desc=""
case "$type" in
streamlit) desc="Streamlit App" ;;
blog) desc="Blog / Site" ;;
admin) desc="Administration" ;;
media) desc="Media / Live" ;;
dev) desc="Development" ;;
infra) desc="Monitoring" ;;
presentation) desc="Slides" ;;
mail) desc="Mail Service" ;;
*) desc="Service" ;;
esac
# Check certificate
local has_cert=0
has_valid_cert "$domain" && has_cert=1
[ "$has_cert" = "1" ] && cert_count=$((cert_count + 1))
local item_html=$(generate_service_html "$domain" "$name" "$desc" "$type" "$has_cert")
zone_count=$((zone_count + 1))
total_count=$((total_count + 1))
case "$type" in
streamlit) streamlit_items="${streamlit_items}${item_html}\n" ;;
blog) blog_items="${blog_items}${item_html}\n" ;;
admin) admin_items="${admin_items}${item_html}\n" ;;
media) media_items="${media_items}${item_html}\n" ;;
dev) dev_items="${dev_items}${item_html}\n" ;;
infra) infra_items="${infra_items}${item_html}\n" ;;
presentation) presentation_items="${presentation_items}${item_html}\n" ;;
mail) mail_items="${mail_items}${item_html}\n" ;;
*) service_items="${service_items}${item_html}\n" ;;
esac
done
# Skip empty zones
[ "$zone_count" -eq 0 ] && continue
# Build zone section
zone_html="${zone_html}
<div class=\"zone-section\">
<div class=\"zone-header\">
<span class=\"zone-name\">*.${zone}</span>
<span class=\"zone-count\">${zone_count} services</span>
</div>"
# Add categories with content
[ -n "$streamlit_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Apps Streamlit</h3>
<div class=\"list\">
$(printf "%b" "$streamlit_items") </div>"
[ -n "$blog_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Sites et Blogs</h3>
<div class=\"list\">
$(printf "%b" "$blog_items") </div>"
[ -n "$media_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Médias et Live</h3>
<div class=\"list\">
$(printf "%b" "$media_items") </div>"
[ -n "$presentation_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Présentations</h3>
<div class=\"list\">
$(printf "%b" "$presentation_items") </div>"
if [ -n "$admin_items" ] || [ -n "$infra_items" ]; then
zone_html="${zone_html}
<h3 class=\"category\">Infrastructure</h3>
<div class=\"list\">"
[ -n "$admin_items" ] && zone_html="${zone_html}
$(printf "%b" "$admin_items")"
[ -n "$infra_items" ] && zone_html="${zone_html}
$(printf "%b" "$infra_items")"
zone_html="${zone_html}
</div>"
fi
[ -n "$dev_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Développement</h3>
<div class=\"list\">
$(printf "%b" "$dev_items") </div>"
[ -n "$mail_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Services Mail</h3>
<div class=\"list\">
$(printf "%b" "$mail_items") </div>"
[ -n "$service_items" ] && zone_html="${zone_html}
<h3 class=\"category\">Autres Services</h3>
<div class=\"list\">
$(printf "%b" "$service_items") </div>"
zone_html="${zone_html}
</div>"
done
# Add stats to header
local zone_count=$(echo "$zones_to_include" | wc -w)
cat >> "$output_file" <<STATS
<div class="stats">
<div class="stat">
<div class="stat-value">${total_count}</div>
<div class="stat-label">Services</div>
</div>
<div class="stat">
<div class="stat-value">${zone_count}</div>
<div class="stat-label">Zones</div>
</div>
<div class="stat">
<div class="stat-value">${cert_count}</div>
<div class="stat-label">SSL Certs</div>
</div>
</div>
</header>
STATS
# Add zone sections
printf "%s" "$zone_html" >> "$output_file"
# Footer
cat >> "$output_file" <<HTMLFOOTER
<footer>
<p>Propulsé par <a href="https://secubox.in">SecuBox</a> | Vortex DNS Mesh</p>
<p style="margin-top: 0.3rem;">Generated: $(date "+%Y-%m-%d %H:%M:%S")</p>
</footer>
</div>
</body>
</html>
HTMLFOOTER
log " Generated: $output_file (${total_count} services, ${zone_count} zones, ${cert_count} certs)"
}
# ============================================================================
# Commands
# ============================================================================
cmd_generate() {
local zone="$1"
local single_zone="${2:-}" # --single to only include that zone
if [ -n "$zone" ]; then
if [ "$single_zone" = "--single" ]; then
generate_landing_page "$zone" 0
else
generate_landing_page "$zone" 1
fi
else
# Generate for all zones
log "Generating landing pages for all zones..."
for z in $(get_zones); do
generate_landing_page "$z" 1
done
fi
log "Done!"
}
cmd_list() {
echo ""
echo "Available Zones:"
echo "================"
local total=0
for z in $(get_zones); do
local count=$(get_vhosts_for_zone "$z" | wc -l)
printf " %-30s %3d services\n" "$z" "$count"
total=$((total + count))
done
echo ""
echo "Total: $total services across $(get_zones | wc -w) zones"
echo ""
}
cmd_show() {
local zone="$1"
if [ -z "$zone" ]; then
# Show all vhosts
echo ""
echo "All Vhosts:"
echo "==========="
for domain in $(get_all_vhosts); do
local backend=$(get_vhost_backend "$domain")
local type=$(detect_service_type "$backend" "$domain")
local enabled="✓"
is_vhost_enabled "$domain" || enabled="✗"
local cert="🔒"
has_valid_cert "$domain" || cert=" "
printf " %s %s %-40s [%-10s] → %s\n" "$enabled" "$cert" "$domain" "$type" "${backend:-inline}"
done
echo ""
else
echo ""
echo "Services for zone: $zone"
echo "========================="
for domain in $(get_vhosts_for_zone "$zone"); do
local backend=$(get_vhost_backend "$domain")
local type=$(detect_service_type "$backend" "$domain")
local enabled="✓"
is_vhost_enabled "$domain" || enabled="✗"
local cert="🔒"
has_valid_cert "$domain" || cert=" "
printf " %s %s %-40s [%-10s] → %s\n" "$enabled" "$cert" "$domain" "$type" "${backend:-inline}"
done
echo ""
fi
}
cmd_regenerate_all() {
log "Regenerating all landing pages..."
for z in $(get_zones); do
generate_landing_page "$z" 1
done
log "All landing pages regenerated!"
}
show_help() {
cat <<EOF
SecuBox Landing Page Generator v${VERSION}
Usage: secubox-landing <command> [options]
Commands:
generate [zone] [--single]
Generate landing page(s)
If zone specified, generate for that zone
--single: only include services from that zone
Default: includes ALL zones on each landing
list List all detected zones and service counts
show [zone] Show vhosts (optionally filtered by zone)
Displays: enabled, SSL, domain, type, backend
regenerate Regenerate all landing pages
Examples:
secubox-landing list
secubox-landing show
secubox-landing show gk2.secubox.in
secubox-landing generate gk2.secubox.in
secubox-landing generate gk2.secubox.in --single
secubox-landing regenerate
Environment:
SITES_DIR Output directory (default: /srv/metablogizer/sites)
EOF
}
# ============================================================================
# Main
# ============================================================================
case "${1:-}" in
generate|gen)
shift
cmd_generate "$@"
;;
list|ls)
cmd_list
;;
show)
shift
cmd_show "$@"
;;
regenerate|regen)
cmd_regenerate_all
;;
help|--help|-h|'')
show_help
;;
*)
error "Unknown command: $1"
show_help >&2
exit 1
;;
esac
exit 0