feat(settings): CA generation from the UI; Routstr panel beside the API key
Demo images / Build & push demo images (push) Successful in 3m36s
Demo images / Build & push demo images (push) Successful in 3m36s
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6137762786
commit
a0bd9e53f8
@@ -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" => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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<serde_json::Value> {
|
||||
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(
|
||||
|
||||
@@ -26,7 +26,30 @@ async function computeFingerprint(pem: string): Promise<string> {
|
||||
.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)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -70,9 +95,13 @@ onMounted(async () => {
|
||||
v-else-if="!caAvailable"
|
||||
class="p-3 bg-white/5 border border-white/10 rounded-lg text-sm text-white/70"
|
||||
>
|
||||
This node has not generated a certificate authority yet. Run
|
||||
<code class="px-1 py-0.5 bg-black/30 rounded text-xs">scripts/setup-node-ca.sh</code>
|
||||
on the node, then reload this page.
|
||||
<p class="mb-3">This node has not generated its certificate yet.</p>
|
||||
<button
|
||||
:disabled="generating"
|
||||
@click="generateCa"
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-semibold disabled:opacity-60"
|
||||
>{{ generating ? 'Generating…' : 'Generate certificate' }}</button>
|
||||
<p v-if="generateError" class="mt-2 text-xs text-orange-300/90">{{ generateError }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
|
||||
@@ -16,8 +16,8 @@ import SystemDangerZone from '@/views/settings/SystemDangerZone.vue'
|
||||
<InterfaceModeSection />
|
||||
<KioskDisplaySection />
|
||||
<ClaudeAuthSection />
|
||||
<AIDataAccessSection />
|
||||
<RoutstrBudgetSection />
|
||||
<AIDataAccessSection />
|
||||
<WebhookSection />
|
||||
<TelemetrySection />
|
||||
<NodeCertificateSection />
|
||||
|
||||
Reference in New Issue
Block a user