feat(metablogizer): Add KISS one-click features matching Streamlit

- Add upload_and_create_site: one-click deploy with auto HAProxy setup
- Add unpublish_site: remove HAProxy vhost while preserving content
- Add set_auth_required: toggle authentication requirement per site
- Add get_sites_exposure_status: exposure/cert status for all sites
- Simplify dashboard to KISS UI pattern with status badges
- Action buttons: Share, Upload, Expose/Unpublish, Lock/Unlock, Delete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-21 10:23:18 +01:00
parent 397d7e2f74
commit 5a1276590e
4 changed files with 789 additions and 691 deletions

View File

@ -139,6 +139,29 @@ var callEmancipateStatus = rpc.declare({
params: ['job_id']
});
var callUploadAndCreateSite = rpc.declare({
object: 'luci.metablogizer',
method: 'upload_and_create_site',
params: ['name', 'domain', 'content', 'is_zip']
});
var callUnpublishSite = rpc.declare({
object: 'luci.metablogizer',
method: 'unpublish_site',
params: ['id']
});
var callSetAuthRequired = rpc.declare({
object: 'luci.metablogizer',
method: 'set_auth_required',
params: ['id', 'auth_required']
});
var callGetSitesExposureStatus = rpc.declare({
object: 'luci.metablogizer',
method: 'get_sites_exposure_status'
});
return baseclass.extend({
getStatus: function() {
return callStatus();
@ -269,16 +292,35 @@ return baseclass.extend({
return callEmancipateStatus(jobId);
},
uploadAndCreateSite: function(name, domain, content, isZip) {
return callUploadAndCreateSite(name, domain, content || '', isZip ? '1' : '0');
},
unpublishSite: function(id) {
return callUnpublishSite(id);
},
setAuthRequired: function(id, authRequired) {
return callSetAuthRequired(id, authRequired ? '1' : '0');
},
getSitesExposureStatus: function() {
return callGetSitesExposureStatus().then(function(res) {
return res.sites || [];
});
},
getDashboardData: function() {
var self = this;
return Promise.all([
self.getStatus(),
self.listSites()
self.listSites(),
self.getSitesExposureStatus()
]).then(function(results) {
return {
status: results[0] || {},
sites: results[1] || [],
hosting: {}
exposure: results[2] || []
};
});
}

View File

@ -1602,6 +1602,329 @@ EOF
json_dump
}
# One-click upload and create site
# Accepts: name, domain, content (base64), is_zip
method_upload_and_create_site() {
local tmpinput="/tmp/rpcd_mb_upload_create_$$.json"
cat > "$tmpinput"
local name domain content is_zip
name=$(jsonfilter -i "$tmpinput" -e '@.name' 2>/dev/null)
domain=$(jsonfilter -i "$tmpinput" -e '@.domain' 2>/dev/null)
content=$(jsonfilter -i "$tmpinput" -e '@.content' 2>/dev/null)
is_zip=$(jsonfilter -i "$tmpinput" -e '@.is_zip' 2>/dev/null)
rm -f "$tmpinput"
if [ -z "$name" ] || [ -z "$domain" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Name and domain are required"
json_dump
return
fi
# Sanitize name
local section_id="site_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
# Check if site already exists
if uci -q get "$UCI_CONFIG.$section_id" >/dev/null 2>&1; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Site with this name already exists"
json_dump
return
fi
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
# 1. Create site directory
mkdir -p "$SITES_ROOT/$name"
# 2. Decode and save content
umask 022
if [ "$is_zip" = "1" ] && [ -n "$content" ]; then
# Handle ZIP upload
local tmpzip="/tmp/metablog_upload_$$.zip"
echo "$content" | base64 -d > "$tmpzip" 2>/dev/null
unzip -o "$tmpzip" -d "$SITES_ROOT/$name" >/dev/null 2>&1
rm -f "$tmpzip"
elif [ -n "$content" ]; then
# Single file - assume index.html
echo "$content" | base64 -d > "$SITES_ROOT/$name/index.html" 2>/dev/null
fi
# 3. Fix permissions
fix_permissions "$SITES_ROOT/$name"
# 4. Create default index if none exists
if [ ! -f "$SITES_ROOT/$name/index.html" ]; then
cat > "$SITES_ROOT/$name/index.html" <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$name</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex; justify-content: center; align-items: center;
min-height: 100vh; margin: 0; background: #f5f5f5; }
.container { text-align: center; padding: 2rem; }
h1 { color: #333; }
</style>
</head>
<body>
<div class="container">
<h1>$name</h1>
<p>Site published with MetaBlogizer</p>
</div>
</body>
</html>
EOF
chmod 644 "$SITES_ROOT/$name/index.html"
fi
# 5. Get next port and create uhttpd instance
local port=$(get_next_port)
local server_address=$(uci -q get network.lan.ipaddr || echo "192.168.255.1")
uci set "uhttpd.metablog_${section_id}=uhttpd"
uci set "uhttpd.metablog_${section_id}.listen_http=0.0.0.0:$port"
uci set "uhttpd.metablog_${section_id}.home=$SITES_ROOT/$name"
uci set "uhttpd.metablog_${section_id}.index_page=index.html"
uci set "uhttpd.metablog_${section_id}.error_page=/index.html"
uci commit uhttpd
/etc/init.d/uhttpd reload 2>/dev/null
# 6. Create UCI site config
uci set "$UCI_CONFIG.$section_id=site"
uci set "$UCI_CONFIG.$section_id.name=$name"
uci set "$UCI_CONFIG.$section_id.domain=$domain"
uci set "$UCI_CONFIG.$section_id.ssl=1"
uci set "$UCI_CONFIG.$section_id.enabled=1"
uci set "$UCI_CONFIG.$section_id.port=$port"
uci set "$UCI_CONFIG.$section_id.runtime=uhttpd"
# 7. Create HAProxy backend if available
if haproxy_available; then
local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
uci set "haproxy.$backend_name=backend"
uci set "haproxy.$backend_name.name=$backend_name"
uci set "haproxy.$backend_name.mode=http"
uci set "haproxy.$backend_name.balance=roundrobin"
uci set "haproxy.$backend_name.enabled=1"
local server_name="${backend_name}_srv"
uci set "haproxy.$server_name=server"
uci set "haproxy.$server_name.backend=$backend_name"
uci set "haproxy.$server_name.name=srv"
uci set "haproxy.$server_name.address=$server_address"
uci set "haproxy.$server_name.port=$port"
uci set "haproxy.$server_name.weight=100"
uci set "haproxy.$server_name.check=1"
uci set "haproxy.$server_name.enabled=1"
uci commit haproxy
reload_haproxy
fi
uci commit "$UCI_CONFIG"
json_init
json_add_boolean "success" 1
json_add_string "id" "$section_id"
json_add_string "name" "$name"
json_add_string "domain" "$domain"
json_add_int "port" "$port"
json_add_string "url" "https://$domain"
json_dump
}
# Unpublish/revoke site exposure (remove HAProxy vhost but keep site)
method_unpublish_site() {
local id
read -r input
json_load "$input"
json_get_var id id
if [ -z "$id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Missing site id"
json_dump
return
fi
local name domain
name=$(get_uci "$id" name "")
domain=$(get_uci "$id" domain "")
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Site not found"
json_dump
return
fi
# Remove HAProxy vhost (keep backend for local access)
if uci -q get haproxy >/dev/null 2>&1; then
local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
uci delete "haproxy.$vhost_id" 2>/dev/null
# Remove cert entry if exists
uci delete "haproxy.cert_$vhost_id" 2>/dev/null
uci commit haproxy
reload_haproxy
fi
# Mark as unpublished in UCI
uci set "$UCI_CONFIG.$id.emancipated=0"
uci commit "$UCI_CONFIG"
json_init
json_add_boolean "success" 1
json_add_string "message" "Site unpublished"
json_dump
}
# Set authentication requirement for a site
method_set_auth_required() {
local id auth_required
read -r input
json_load "$input"
json_get_var id id
json_get_var auth_required auth_required
if [ -z "$id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Missing site id"
json_dump
return
fi
local name domain
name=$(get_uci "$id" name "")
domain=$(get_uci "$id" domain "")
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Site not found"
json_dump
return
fi
# Update UCI config
uci set "$UCI_CONFIG.$id.auth_required=$auth_required"
uci commit "$UCI_CONFIG"
# If site has HAProxy vhost, update it
if uci -q get haproxy >/dev/null 2>&1 && [ -n "$domain" ]; then
local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
if uci -q get "haproxy.$vhost_id" >/dev/null 2>&1; then
uci set "haproxy.$vhost_id.auth_required=$auth_required"
uci commit haproxy
reload_haproxy
fi
fi
json_init
json_add_boolean "success" 1
json_add_string "auth_required" "$auth_required"
json_dump
}
# Get exposure status for all sites (cert info, emancipation state)
method_get_sites_exposure_status() {
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
json_init
json_add_array "sites"
config_load "$UCI_CONFIG"
config_foreach _add_site_exposure_status site
json_close_array
json_dump
}
_add_site_exposure_status() {
local section="$1"
local name domain ssl enabled emancipated auth_required port
config_get name "$section" name ""
config_get domain "$section" domain ""
config_get ssl "$section" ssl "1"
config_get enabled "$section" enabled "1"
config_get emancipated "$section" emancipated "0"
config_get auth_required "$section" auth_required "0"
config_get port "$section" port ""
[ -z "$name" ] && return
json_add_object
json_add_string "id" "$section"
json_add_string "name" "$name"
json_add_string "domain" "$domain"
json_add_boolean "enabled" "$enabled"
json_add_boolean "emancipated" "$emancipated"
json_add_boolean "auth_required" "$auth_required"
[ -n "$port" ] && json_add_int "port" "$port"
# Check if HAProxy vhost exists
local vhost_exists=0
if [ -n "$domain" ]; then
local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
if uci -q get "haproxy.$vhost_id" >/dev/null 2>&1; then
vhost_exists=1
fi
fi
json_add_boolean "vhost_exists" "$vhost_exists"
# Quick certificate check - just check if file exists
# Full expiry check is expensive, use get_hosting_status for that
if [ -n "$domain" ] && [ "$ssl" = "1" ]; then
local cert_file=""
if [ -f "/srv/lxc/haproxy/rootfs/srv/haproxy/certs/${domain}.pem" ]; then
cert_file="/srv/lxc/haproxy/rootfs/srv/haproxy/certs/${domain}.pem"
elif [ -f "/etc/acme/${domain}_ecc/fullchain.cer" ]; then
cert_file="/etc/acme/${domain}_ecc/fullchain.cer"
fi
if [ -n "$cert_file" ]; then
json_add_string "cert_status" "valid"
else
json_add_string "cert_status" "missing"
fi
else
json_add_string "cert_status" "none"
fi
# Backend running check
local backend_running="0"
if [ -n "$port" ]; then
local hex_port=$(printf '%04X' "$port" 2>/dev/null)
if grep -qi ":${hex_port}" /proc/net/tcp 2>/dev/null; then
backend_running="1"
fi
fi
json_add_boolean "backend_running" "$backend_running"
# Has content
local has_content="0"
if [ -d "$SITES_ROOT/$name" ] && [ -f "$SITES_ROOT/$name/index.html" ]; then
has_content="1"
fi
json_add_boolean "has_content" "$has_content"
json_close_object
}
# Emancipate site - KISS ULTIME MODE (DNS + Vortex + HAProxy + SSL)
# Runs asynchronously to avoid XHR timeout - use emancipate_status to poll
method_emancipate() {
@ -2042,7 +2365,11 @@ case "$1" in
"import_vhost": { "instance": "string", "name": "string", "domain": "string" },
"sync_config": {},
"emancipate": { "id": "string" },
"emancipate_status": { "job_id": "string" }
"emancipate_status": { "job_id": "string" },
"upload_and_create_site": { "name": "string", "domain": "string", "content": "string", "is_zip": "string" },
"unpublish_site": { "id": "string" },
"set_auth_required": { "id": "string", "auth_required": "string" },
"get_sites_exposure_status": {}
}
EOF
;;
@ -2073,6 +2400,10 @@ EOF
sync_config) method_sync_config ;;
emancipate) method_emancipate ;;
emancipate_status) method_emancipate_status ;;
upload_and_create_site) method_upload_and_create_site ;;
unpublish_site) method_unpublish_site ;;
set_auth_required) method_set_auth_required ;;
get_sites_exposure_status) method_get_sites_exposure_status ;;
*) echo '{"error": "unknown method"}' ;;
esac
;;

View File

@ -12,7 +12,8 @@
"get_hosting_status",
"check_site_health",
"get_tor_status",
"discover_vhosts"
"discover_vhosts",
"get_sites_exposure_status"
],
"file": ["read", "list", "stat"]
},
@ -39,7 +40,10 @@
"import_vhost",
"sync_config",
"emancipate",
"emancipate_status"
"emancipate_status",
"upload_and_create_site",
"unpublish_site",
"set_auth_required"
],
"luci.haproxy": [
"create_backend",