From a0bd9e53f8d4c8e4882566b82965d5aaadf8d564 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 14 Aug 2026 09:19:05 -0400 Subject: [PATCH] feat(settings): CA generation from the UI; Routstr panel beside the API key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebUI RULE (operator, 2026-08-14): never point users at a terminal. The certificate section told users to run setup-node-ca.sh by hand — it now has a Generate button backed by system.node-ca.generate, which runs the idempotent script server-side (live-tested: generated and /ca.crt serves). Routstr budget panel moves directly under the Claude API key card. Co-Authored-By: Claude Fable 5 --- .../archipelago/src/api/rpc/assistant_chat.rs | 1 + core/archipelago/src/api/rpc/dispatcher.rs | 1 + .../src/api/rpc/system/handlers.rs | 24 ++++++++++++ .../views/settings/NodeCertificateSection.vue | 39 ++++++++++++++++--- neode-ui/src/views/settings/SystemSection.vue | 2 +- 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/core/archipelago/src/api/rpc/assistant_chat.rs b/core/archipelago/src/api/rpc/assistant_chat.rs index 60c6896e..331cfa23 100644 --- a/core/archipelago/src/api/rpc/assistant_chat.rs +++ b/core/archipelago/src/api/rpc/assistant_chat.rs @@ -344,6 +344,7 @@ impl RpcHandler { "content.indeehub-projects" => self.handle_content_indeehub_projects().await, "system.settings.get" => self.handle_system_settings_get(params).await, "system.settings.set" => self.handle_system_settings_set(params).await, + "system.node-ca.generate" => self.handle_system_node_ca_generate().await, "system.kiosk-display.get" => self.handle_system_kiosk_display_get().await, "system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await, "bitcoin.relay-update-settings" => { diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 9cfb5460..f68fc812 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -503,6 +503,7 @@ impl RpcHandler { "ai.permissions.set" => self.handle_ai_permissions_set(params).await, "system.settings.get" => self.handle_system_settings_get(params).await, "system.settings.set" => self.handle_system_settings_set(params).await, + "system.node-ca.generate" => self.handle_system_node_ca_generate().await, "system.kiosk-display.get" => self.handle_system_kiosk_display_get().await, "system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await, diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 9622c72f..9f89721b 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -1248,6 +1248,30 @@ impl RpcHandler { } } + /// system.node-ca.generate — run the node's (idempotent) CA setup so the + /// dashboard can offer certificate generation as a button. WebUI rule: + /// users must never be pointed at a terminal; the script reuses an + /// existing CA and only reissues the leaf, so re-running is safe. + pub(in crate::api::rpc) async fn handle_system_node_ca_generate( + &self, + ) -> Result { + let script = "/opt/archipelago/scripts/setup-node-ca.sh"; + if tokio::fs::metadata(script).await.is_err() { + anyhow::bail!( + "The certificate setup script is not on this node yet — it arrives with the next update." + ); + } + let status = host_sudo(&["/usr/bin/bash", script]).await?; + if !status.success() { + anyhow::bail!( + "Certificate generation failed (exit {:?}) — see the node log for detail", + status.code() + ); + } + info!("Node CA generated/reissued via dashboard"); + Ok(serde_json::json!({ "generated": true })) + } + /// system.kiosk-display.get — Current kiosk display preset + whether this /// node has a kiosk at all (no kiosk unit -> the Settings section hides). pub(in crate::api::rpc) async fn handle_system_kiosk_display_get( diff --git a/neode-ui/src/views/settings/NodeCertificateSection.vue b/neode-ui/src/views/settings/NodeCertificateSection.vue index 87a18359..2cf8a490 100644 --- a/neode-ui/src/views/settings/NodeCertificateSection.vue +++ b/neode-ui/src/views/settings/NodeCertificateSection.vue @@ -26,7 +26,30 @@ async function computeFingerprint(pem: string): Promise { .join(':') } -onMounted(async () => { +const generating = ref(false) +const generateError = ref('') + +// WebUI rule: never point a user at a terminal — the backend runs the +// (idempotent) CA setup script for us. +async function generateCa() { + generating.value = true + generateError.value = '' + try { + const { rpcClient } = await import('@/api/rpc-client') + await rpcClient.call({ method: 'system.node-ca.generate', timeout: 60000 }) + loading.value = true + await probe() + if (!caAvailable.value) { + generateError.value = 'Generation reported success but the certificate is not being served yet — try reloading in a few seconds.' + } + } catch (e: unknown) { + generateError.value = e instanceof Error ? e.message : 'Certificate generation failed' + } finally { + generating.value = false + } +} + +async function probe() { try { const res = await fetch('/ca.crt', { cache: 'no-store' }) if (!res.ok) throw new Error(`HTTP ${res.status}`) @@ -48,7 +71,9 @@ onMounted(async () => { } finally { loading.value = false } -}) +} + +onMounted(probe)