From df34698acbe00bf1356b50df89b106b3a6ebb670 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 6 Feb 2026 08:52:53 +0100 Subject: [PATCH] feat(metablogizer): Add LuCI Emancipate button with async workflow - Add Emancipate button to dashboard sites table - Implement async RPC with job polling to avoid XHR timeout - Add emancipate + emancipate_status RPCD methods - Add ACL permissions for new RPC methods - Change HAProxy reload to restart for clean state - Document RPCD ACL requirements in CLAUDE.md Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 28 +++++ .../secubox/luci-app-metablogizer/Makefile | 4 +- .../luci-static/resources/metablogizer/api.js | 20 +++ .../resources/view/metablogizer/dashboard.js | 95 ++++++++++++++ .../root/usr/libexec/rpcd/luci.metablogizer | 119 +++++++++++++++++- .../rpcd/acl.d/luci-app-metablogizer.json | 4 +- .../files/usr/sbin/metablogizerctl | 16 ++- 7 files changed, 278 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fda46b86..f857f424 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,6 +144,34 @@ ssh root@192.168.255.1 '/etc/init.d/rpcd restart' - These are not shown in standard LuCI forms but are accessible via `uci -q get` - Useful for storing client private keys, internal state, etc. +### ACL Permissions for New RPC Methods +- **CRITICAL: When adding a new RPCD method, you MUST also add it to the ACL file** +- Without ACL entry, LuCI will return `-32002: Access denied` error +- ACL files are located at: `root/usr/share/rpcd/acl.d/.json` +- Add read-only methods to the `"read"` section, write/action methods to the `"write"` section: + ```json + { + "luci-app-example": { + "read": { + "ubus": { + "luci.example": ["status", "list", "get_info"] + } + }, + "write": { + "ubus": { + "luci.example": ["create", "delete", "update", "action_method"] + } + } + } + } + ``` +- After deploying ACL changes, restart rpcd AND have user re-login to LuCI: + ```bash + scp root/usr/share/rpcd/acl.d/.json root@192.168.255.1:/usr/share/rpcd/acl.d/ + ssh root@192.168.255.1 '/etc/init.d/rpcd restart' + ``` +- User must log out and log back into LuCI to get new permissions + ## LuCI JavaScript Frontend ### RPC `expect` Field Behavior diff --git a/package/secubox/luci-app-metablogizer/Makefile b/package/secubox/luci-app-metablogizer/Makefile index c672c8b1..23854822 100644 --- a/package/secubox/luci-app-metablogizer/Makefile +++ b/package/secubox/luci-app-metablogizer/Makefile @@ -11,8 +11,8 @@ LUCI_DEPENDS:=+luci-base +git LUCI_PKGARCH:=all PKG_NAME:=luci-app-metablogizer -PKG_VERSION:=1.0.0 -PKG_RELEASE:=5 +PKG_VERSION:=1.1.0 +PKG_RELEASE:=1 PKG_MAINTAINER:=CyberMind PKG_LICENSE:=GPL-2.0 diff --git a/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/metablogizer/api.js b/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/metablogizer/api.js index 5e8a9f81..a703ed26 100644 --- a/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/metablogizer/api.js +++ b/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/metablogizer/api.js @@ -127,6 +127,18 @@ var callSyncConfig = rpc.declare({ method: 'sync_config' }); +var callEmancipate = rpc.declare({ + object: 'luci.metablogizer', + method: 'emancipate', + params: ['id'] +}); + +var callEmancipateStatus = rpc.declare({ + object: 'luci.metablogizer', + method: 'emancipate_status', + params: ['job_id'] +}); + return baseclass.extend({ getStatus: function() { return callStatus(); @@ -249,6 +261,14 @@ return baseclass.extend({ return callSyncConfig(); }, + emancipate: function(id) { + return callEmancipate(id); + }, + + emancipateStatus: function(jobId) { + return callEmancipateStatus(jobId); + }, + getDashboardData: function() { var self = this; return Promise.all([ diff --git a/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/dashboard.js b/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/dashboard.js index e65e1903..b39b189f 100644 --- a/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/dashboard.js +++ b/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/dashboard.js @@ -208,6 +208,12 @@ return view.extend({ 'title': _('Sync') }, _('Sync')) : '', ' ', + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(self, 'handleEmancipate', site), + 'title': _('KISS ULTIME MODE: DNS + SSL + Mesh') + }, site.emancipated ? '✓' : _('Emancipate')), + ' ', E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(self, 'handleDelete', site), @@ -699,6 +705,95 @@ return view.extend({ ]); }, + handleEmancipate: function(site) { + var self = this; + ui.showModal(_('Emancipate Site'), [ + E('p', {}, _('KISS ULTIME MODE will configure:')), + E('ul', {}, [ + E('li', {}, _('DNS registration (Gandi/OVH)')), + E('li', {}, _('Vortex DNS mesh publication')), + E('li', {}, _('HAProxy vhost with SSL')), + E('li', {}, _('ACME certificate issuance')) + ]), + E('p', { 'style': 'margin-top:1em' }, _('Emancipate "') + site.name + '" (' + site.domain + ')?'), + E('div', { 'class': 'right', 'style': 'margin-top:1em' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { 'class': 'cbi-button cbi-button-apply', 'click': function() { + ui.hideModal(); + self.runEmancipateAsync(site); + }}, _('Emancipate')) + ]) + ]); + }, + + runEmancipateAsync: function(site) { + var self = this; + var outputPre = E('pre', { 'style': 'max-height:300px;overflow:auto;background:#f5f5f5;padding:10px;font-size:11px;white-space:pre-wrap' }, _('Starting...')); + + ui.showModal(_('Emancipating'), [ + E('p', { 'class': 'spinning' }, _('Running KISS ULTIME MODE workflow...')), + outputPre + ]); + + api.emancipate(site.id).then(function(r) { + if (!r.success) { + ui.hideModal(); + ui.showModal(_('Emancipation Failed'), [ + E('p', { 'style': 'color:#a00' }, r.error || _('Failed to start')), + E('div', { 'class': 'right', 'style': 'margin-top:1em' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close')) + ]) + ]); + return; + } + + // Poll for completion + var jobId = r.job_id; + var pollInterval = setInterval(function() { + api.emancipateStatus(jobId).then(function(status) { + if (status.output) { + outputPre.textContent = status.output; + outputPre.scrollTop = outputPre.scrollHeight; + } + + if (status.complete) { + clearInterval(pollInterval); + ui.hideModal(); + + if (status.status === 'success') { + ui.showModal(_('Emancipation Complete'), [ + E('p', { 'style': 'color:#0a0' }, _('Site emancipated successfully!')), + E('pre', { 'style': 'max-height:300px;overflow:auto;background:#f5f5f5;padding:10px;font-size:11px;white-space:pre-wrap' }, status.output || ''), + E('div', { 'class': 'right', 'style': 'margin-top:1em' }, [ + E('button', { 'class': 'cbi-button cbi-button-action', 'click': function() { + ui.hideModal(); + window.location.reload(); + }}, _('OK')) + ]) + ]); + } else { + ui.showModal(_('Emancipation Failed'), [ + E('p', { 'style': 'color:#a00' }, _('Workflow failed')), + E('pre', { 'style': 'max-height:200px;overflow:auto;background:#fee;padding:10px;font-size:11px;white-space:pre-wrap' }, status.output || ''), + E('div', { 'class': 'right', 'style': 'margin-top:1em' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close')) + ]) + ]); + } + } + }).catch(function(e) { + clearInterval(pollInterval); + ui.hideModal(); + ui.addNotification(null, E('p', _('Poll error: ') + e.message), 'error'); + }); + }, 2000); // Poll every 2 seconds + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: ') + e.message), 'error'); + }); + }, + copyToClipboard: function(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { diff --git a/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer b/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer index c9329094..f78c766b 100755 --- a/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer +++ b/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer @@ -1599,6 +1599,119 @@ EOF json_dump } +# Emancipate site - KISS ULTIME MODE (DNS + Vortex + HAProxy + SSL) +# Runs asynchronously to avoid XHR timeout - use emancipate_status to poll +method_emancipate() { + 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 + name=$(get_uci "$id" name "") + + if [ -z "$name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Site not found" + json_dump + return + fi + + # Check if metablogizerctl exists + if [ ! -x /usr/sbin/metablogizerctl ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "metablogizerctl not installed" + json_dump + return + fi + + # Generate job ID and output file + local job_id="emancipate_${name}_$$" + local job_dir="/tmp/metablogizer_jobs" + local output_file="$job_dir/${job_id}.log" + local status_file="$job_dir/${job_id}.status" + + mkdir -p "$job_dir" + + # Run emancipate command in background + ( + echo "running" > "$status_file" + /usr/sbin/metablogizerctl emancipate "$name" > "$output_file" 2>&1 + local rc=$? + if [ $rc -eq 0 ]; then + echo "success" > "$status_file" + else + echo "failed" > "$status_file" + fi + ) & + + json_init + json_add_boolean "success" 1 + json_add_string "job_id" "$job_id" + json_add_string "status" "running" + json_add_string "site" "$name" + json_dump +} + +# Check emancipate job status +method_emancipate_status() { + local job_id + + read -r input + json_load "$input" + json_get_var job_id job_id + + if [ -z "$job_id" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Missing job_id" + json_dump + return + fi + + local job_dir="/tmp/metablogizer_jobs" + local output_file="$job_dir/${job_id}.log" + local status_file="$job_dir/${job_id}.status" + + if [ ! -f "$status_file" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Job not found" + json_dump + return + fi + + local status=$(cat "$status_file") + local output="" + [ -f "$output_file" ] && output=$(cat "$output_file") + + json_init + json_add_boolean "success" 1 + json_add_string "status" "$status" + json_add_string "output" "$output" + + # Clean up completed jobs + if [ "$status" = "success" ] || [ "$status" = "failed" ]; then + json_add_boolean "complete" 1 + # Keep files for 5 minutes then cleanup handled by caller or cron + else + json_add_boolean "complete" 0 + fi + + json_dump +} + # Enable Tor hidden service for a site method_enable_tor() { local id @@ -1924,7 +2037,9 @@ case "$1" in "get_tor_status": {}, "discover_vhosts": {}, "import_vhost": { "instance": "string", "name": "string", "domain": "string" }, - "sync_config": {} + "sync_config": {}, + "emancipate": { "id": "string" }, + "emancipate_status": { "job_id": "string" } } EOF ;; @@ -1953,6 +2068,8 @@ EOF discover_vhosts) method_discover_vhosts ;; import_vhost) method_import_vhost ;; sync_config) method_sync_config ;; + emancipate) method_emancipate ;; + emancipate_status) method_emancipate_status ;; *) echo '{"error": "unknown method"}' ;; esac ;; diff --git a/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json b/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json index 27d1e248..13b468bf 100644 --- a/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json +++ b/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json @@ -37,7 +37,9 @@ "enable_tor", "disable_tor", "import_vhost", - "sync_config" + "sync_config", + "emancipate", + "emancipate_status" ], "luci.haproxy": [ "create_backend", diff --git a/package/secubox/secubox-app-metablogizer/files/usr/sbin/metablogizerctl b/package/secubox/secubox-app-metablogizer/files/usr/sbin/metablogizerctl index c908fb44..3c7579cc 100644 --- a/package/secubox/secubox-app-metablogizer/files/usr/sbin/metablogizerctl +++ b/package/secubox/secubox-app-metablogizer/files/usr/sbin/metablogizerctl @@ -654,10 +654,18 @@ _emancipate_ssl() { _emancipate_reload() { log_info "[RELOAD] Applying HAProxy configuration" - /etc/init.d/haproxy reload 2>/dev/null || { - log_warn "[RELOAD] Reload failed, restarting..." - /etc/init.d/haproxy restart 2>/dev/null - } + # Generate fresh config + haproxyctl generate 2>/dev/null + # Always restart for clean state with new vhosts/certs + log_info "[RELOAD] Restarting HAProxy for clean state..." + /etc/init.d/haproxy restart 2>/dev/null + sleep 1 + # Verify HAProxy is running + if pgrep haproxy >/dev/null 2>&1; then + log_info "[RELOAD] HAProxy restarted successfully" + else + log_warn "[RELOAD] HAProxy may not have started properly" + fi } cmd_emancipate() {