From 4c41c38b3bf42ca90fb15b1e5e95c3751c5af3cb Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 7 Apr 2026 15:18:35 +0100 Subject: [PATCH] fix: implement Claude API key save RPC, VPN status on home page - Add system.settings.get/set RPC methods for Claude API key management - Save key to secrets/claude-api-key, restart claude-api-proxy service - Home Network card now fetches VPN status via vpn.status RPC - Shows provider name (nostr-vpn, tailscale) instead of just "Connected" Co-Authored-By: Claude Opus 4.6 (1M context) --- core/archipelago/src/api/rpc/dispatcher.rs | 2 + .../src/api/rpc/system/handlers.rs | 74 +++++++++++++++++++ neode-ui/src/views/Home.vue | 9 +-- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 373113b5..2dfbe73e 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -292,6 +292,8 @@ impl RpcHandler { "system.disk-cleanup" => self.handle_system_disk_cleanup().await, "system.reboot" => self.handle_system_reboot(params).await, "system.factory-reset" => self.handle_system_factory_reset(params).await, + "system.settings.get" => self.handle_system_settings_get(params).await, + "system.settings.set" => self.handle_system_settings_set(params).await, // Opt-in anonymous analytics "analytics.get-status" => self.handle_analytics_get_status().await, diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 309f739c..17887602 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -327,4 +327,78 @@ impl RpcHandler { Ok(serde_json::json!({ "status": "resetting" })) } + + /// system.settings.get — Read a settings value + pub(in crate::api::rpc) async fn handle_system_settings_get( + &self, + params: Option, + ) -> Result { + let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let key = params.get("key").and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing key"))?; + + match key { + "claude_api_key_set" => { + let key_file = self.config.data_dir.join("secrets/claude-api-key"); + let has_key = tokio::fs::metadata(&key_file).await.is_ok(); + Ok(serde_json::json!({ "value": has_key })) + } + _ => Ok(serde_json::json!({ "value": null })), + } + } + + /// system.settings.set — Write a settings value + pub(in crate::api::rpc) async fn handle_system_settings_set( + &self, + params: Option, + ) -> Result { + let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let key = params.get("key").and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing key"))?; + let value = params.get("value").and_then(|v| v.as_str()).unwrap_or(""); + + match key { + "claude_api_key" => { + let secrets_dir = self.config.data_dir.join("secrets"); + tokio::fs::create_dir_all(&secrets_dir).await + .context("Failed to create secrets dir")?; + let key_file = secrets_dir.join("claude-api-key"); + + if value.is_empty() { + // Remove key + tokio::fs::remove_file(&key_file).await.ok(); + info!("Claude API key removed"); + } else { + // Save key + tokio::fs::write(&key_file, value).await + .context("Failed to write API key")?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&key_file, std::fs::Permissions::from_mode(0o600)).ok(); + } + info!("Claude API key saved"); + } + + // Update the claude-api-proxy environment and restart + let env_line = format!("ANTHROPIC_API_KEY={}", value); + let env_file = self.config.data_dir.join("secrets/claude-api-proxy.env"); + tokio::fs::write(&env_file, &env_line).await.ok(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&env_file, std::fs::Permissions::from_mode(0o600)).ok(); + } + + // Restart the proxy to pick up the new key + let _ = tokio::process::Command::new("sudo") + .args(["systemctl", "restart", "claude-api-proxy"]) + .output() + .await; + + Ok(serde_json::json!({ "saved": true })) + } + _ => anyhow::bail!("Unknown setting: {}", key), + } + } } diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index 281ee271..27da971b 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -145,7 +145,7 @@
VPN
- {{ vpnConnected ? 'Connected' : 'Not configured' }} + {{ vpnConnected ? (vpnStatus.provider || 'Connected') : 'Not configured' }}
Bitcoin
@@ -311,10 +311,8 @@ const torConnected = computed(() => { const torAddr = store.data?.['server-info']?.['tor-address'] return !!torAddr && torAddr.length > 0 }) -const vpnConnected = computed(() => { - const pkg = packages.value['tailscale'] - return !!pkg && pkg.state === PackageState.Running -}) +const vpnStatus = ref<{ connected: boolean; provider: string | null }>({ connected: false, provider: null }) +const vpnConnected = computed(() => vpnStatus.value.connected || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running)) const bitcoinSyncDisplay = computed(() => { if (!systemStats.bitcoinAvailable) return 'Not running' if (systemStats.bitcoinSyncPercent >= 99.9) return 'Synced' @@ -350,6 +348,7 @@ const cloudFolderDisplay = computed(() => cloudFolderCount.value !== null ? Stri onMounted(async () => { try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ } loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 30000); checkUpdateStatus(); loadWeb5Status() + rpcClient.vpnStatus().then(s => { vpnStatus.value = s }).catch(() => {}) }) // Wallet modals