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,
|
"content.indeehub-projects" => self.handle_content_indeehub_projects().await,
|
||||||
"system.settings.get" => self.handle_system_settings_get(params).await,
|
"system.settings.get" => self.handle_system_settings_get(params).await,
|
||||||
"system.settings.set" => self.handle_system_settings_set(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.get" => self.handle_system_kiosk_display_get().await,
|
||||||
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await,
|
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await,
|
||||||
"bitcoin.relay-update-settings" => {
|
"bitcoin.relay-update-settings" => {
|
||||||
|
|||||||
@@ -503,6 +503,7 @@ impl RpcHandler {
|
|||||||
"ai.permissions.set" => self.handle_ai_permissions_set(params).await,
|
"ai.permissions.set" => self.handle_ai_permissions_set(params).await,
|
||||||
"system.settings.get" => self.handle_system_settings_get(params).await,
|
"system.settings.get" => self.handle_system_settings_get(params).await,
|
||||||
"system.settings.set" => self.handle_system_settings_set(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.get" => self.handle_system_kiosk_display_get().await,
|
||||||
"system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).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
|
/// system.kiosk-display.get — Current kiosk display preset + whether this
|
||||||
/// node has a kiosk at all (no kiosk unit -> the Settings section hides).
|
/// 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(
|
pub(in crate::api::rpc) async fn handle_system_kiosk_display_get(
|
||||||
|
|||||||
@@ -26,7 +26,30 @@ async function computeFingerprint(pem: string): Promise<string> {
|
|||||||
.join(':')
|
.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 {
|
try {
|
||||||
const res = await fetch('/ca.crt', { cache: 'no-store' })
|
const res = await fetch('/ca.crt', { cache: 'no-store' })
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
@@ -48,7 +71,9 @@ onMounted(async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
onMounted(probe)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -70,9 +95,13 @@ onMounted(async () => {
|
|||||||
v-else-if="!caAvailable"
|
v-else-if="!caAvailable"
|
||||||
class="p-3 bg-white/5 border border-white/10 rounded-lg text-sm text-white/70"
|
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
|
<p class="mb-3">This node has not generated its certificate yet.</p>
|
||||||
<code class="px-1 py-0.5 bg-black/30 rounded text-xs">scripts/setup-node-ca.sh</code>
|
<button
|
||||||
on the node, then reload this page.
|
: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>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import SystemDangerZone from '@/views/settings/SystemDangerZone.vue'
|
|||||||
<InterfaceModeSection />
|
<InterfaceModeSection />
|
||||||
<KioskDisplaySection />
|
<KioskDisplaySection />
|
||||||
<ClaudeAuthSection />
|
<ClaudeAuthSection />
|
||||||
<AIDataAccessSection />
|
|
||||||
<RoutstrBudgetSection />
|
<RoutstrBudgetSection />
|
||||||
|
<AIDataAccessSection />
|
||||||
<WebhookSection />
|
<WebhookSection />
|
||||||
<TelemetrySection />
|
<TelemetrySection />
|
||||||
<NodeCertificateSection />
|
<NodeCertificateSection />
|
||||||
|
|||||||
Reference in New Issue
Block a user