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) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-07 15:18:35 +01:00
parent fddbf8ccf7
commit 4c41c38b3b
3 changed files with 80 additions and 5 deletions

View File

@ -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,

View File

@ -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<serde_json::Value>,
) -> Result<serde_json::Value> {
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<serde_json::Value>,
) -> Result<serde_json::Value> {
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),
}
}
}

View File

@ -145,7 +145,7 @@
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="vpnConnected ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">VPN</span></div>
<span class="text-sm font-medium" :class="vpnConnected ? 'text-orange-400' : 'text-white/40'">{{ vpnConnected ? 'Connected' : 'Not configured' }}</span>
<span class="text-sm font-medium" :class="vpnConnected ? 'text-orange-400' : 'text-white/40'">{{ vpnConnected ? (vpnStatus.provider || 'Connected') : 'Not configured' }}</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="systemStats.bitcoinAvailable ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">Bitcoin</span></div>
@ -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