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:
parent
397d7e2f74
commit
5a1276590e
@ -139,6 +139,29 @@ var callEmancipateStatus = rpc.declare({
|
|||||||
params: ['job_id']
|
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({
|
return baseclass.extend({
|
||||||
getStatus: function() {
|
getStatus: function() {
|
||||||
return callStatus();
|
return callStatus();
|
||||||
@ -269,16 +292,35 @@ return baseclass.extend({
|
|||||||
return callEmancipateStatus(jobId);
|
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() {
|
getDashboardData: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
self.getStatus(),
|
self.getStatus(),
|
||||||
self.listSites()
|
self.listSites(),
|
||||||
|
self.getSitesExposureStatus()
|
||||||
]).then(function(results) {
|
]).then(function(results) {
|
||||||
return {
|
return {
|
||||||
status: results[0] || {},
|
status: results[0] || {},
|
||||||
sites: results[1] || [],
|
sites: results[1] || [],
|
||||||
hosting: {}
|
exposure: results[2] || []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1602,6 +1602,329 @@ EOF
|
|||||||
json_dump
|
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)
|
# Emancipate site - KISS ULTIME MODE (DNS + Vortex + HAProxy + SSL)
|
||||||
# Runs asynchronously to avoid XHR timeout - use emancipate_status to poll
|
# Runs asynchronously to avoid XHR timeout - use emancipate_status to poll
|
||||||
method_emancipate() {
|
method_emancipate() {
|
||||||
@ -2042,7 +2365,11 @@ case "$1" in
|
|||||||
"import_vhost": { "instance": "string", "name": "string", "domain": "string" },
|
"import_vhost": { "instance": "string", "name": "string", "domain": "string" },
|
||||||
"sync_config": {},
|
"sync_config": {},
|
||||||
"emancipate": { "id": "string" },
|
"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
|
EOF
|
||||||
;;
|
;;
|
||||||
@ -2073,6 +2400,10 @@ EOF
|
|||||||
sync_config) method_sync_config ;;
|
sync_config) method_sync_config ;;
|
||||||
emancipate) method_emancipate ;;
|
emancipate) method_emancipate ;;
|
||||||
emancipate_status) method_emancipate_status ;;
|
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"}' ;;
|
*) echo '{"error": "unknown method"}' ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -12,7 +12,8 @@
|
|||||||
"get_hosting_status",
|
"get_hosting_status",
|
||||||
"check_site_health",
|
"check_site_health",
|
||||||
"get_tor_status",
|
"get_tor_status",
|
||||||
"discover_vhosts"
|
"discover_vhosts",
|
||||||
|
"get_sites_exposure_status"
|
||||||
],
|
],
|
||||||
"file": ["read", "list", "stat"]
|
"file": ["read", "list", "stat"]
|
||||||
},
|
},
|
||||||
@ -39,7 +40,10 @@
|
|||||||
"import_vhost",
|
"import_vhost",
|
||||||
"sync_config",
|
"sync_config",
|
||||||
"emancipate",
|
"emancipate",
|
||||||
"emancipate_status"
|
"emancipate_status",
|
||||||
|
"upload_and_create_site",
|
||||||
|
"unpublish_site",
|
||||||
|
"set_auth_required"
|
||||||
],
|
],
|
||||||
"luci.haproxy": [
|
"luci.haproxy": [
|
||||||
"create_backend",
|
"create_backend",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user