fix(fips): fall back to upstream daemon npub on legacy/dev nodes
Nodes without a seed-derived FIPS key (legacy deploys, fresh pre-onboarding installs) were reporting "Awaiting seed" in the dashboard even when the upstream fips.service was running — status.npub was None unless /data/identity/fips_key.pub existed. - fips/service.rs: new read_upstream_npub() reads /etc/fips/fips.pub (bech32 text or raw 32 bytes) from the debian package. - fips/mod.rs: FipsStatus::current() prefers the seed-derived npub, falls back to the upstream key. service_active is now TRUE if either archipelago-fips.service OR upstream fips.service is active; adds upstream_service_state to the status payload. - fips/update.rs: resolve the upstream default branch from the GitHub repo API (jmcorgan/fips is on `master`, not `main`) instead of hardcoding — future repo rename just works. - network/router.rs + api/rpc/router.rs: diagnostics gain wifi_ssid from `nmcli -t device` so the Network card can show the connected SSID. - UI: Home.vue adds a FIPS row to the Local Network card; Server.vue mounts the new FipsNetworkCard and shows SSID + FIPS Mesh rows; HomeNetworkCard.vue removed (superseded by the inline rows). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
30a7f73ead
commit
6b42bfd503
@ -100,6 +100,7 @@ impl RpcHandler {
|
|||||||
"tor_connected": diag.tor_connected,
|
"tor_connected": diag.tor_connected,
|
||||||
"dns_working": diag.dns_working,
|
"dns_working": diag.dns_working,
|
||||||
"recommendations": diag.recommendations,
|
"recommendations": diag.recommendations,
|
||||||
|
"wifi_ssid": diag.wifi_ssid,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,12 @@ pub const UPSTREAM_REPO: &str = "jmcorgan/fips";
|
|||||||
/// Default UDP port the daemon listens on.
|
/// Default UDP port the daemon listens on.
|
||||||
pub const DEFAULT_UDP_PORT: u16 = 8668;
|
pub const DEFAULT_UDP_PORT: u16 = 8668;
|
||||||
|
|
||||||
|
/// Upstream systemd unit shipped by the `fips` debian package. Archipelago
|
||||||
|
/// prefers its own supervision (`archipelago-fips.service`) but respects an
|
||||||
|
/// already-running upstream unit so legacy/dev nodes — where no seed-derived
|
||||||
|
/// key exists — still report FIPS as active in the UI.
|
||||||
|
pub const UPSTREAM_SERVICE_UNIT: &str = "fips.service";
|
||||||
|
|
||||||
/// Aggregated runtime status of the FIPS subsystem, surfaced to the dashboard.
|
/// Aggregated runtime status of the FIPS subsystem, surfaced to the dashboard.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FipsStatus {
|
pub struct FipsStatus {
|
||||||
@ -61,12 +67,18 @@ pub struct FipsStatus {
|
|||||||
/// `systemctl is-active archipelago-fips.service` result: "active",
|
/// `systemctl is-active archipelago-fips.service` result: "active",
|
||||||
/// "inactive", "failed", "masked", "unknown".
|
/// "inactive", "failed", "masked", "unknown".
|
||||||
pub service_state: String,
|
pub service_state: String,
|
||||||
/// True iff service_state == "active".
|
/// State of the upstream `fips.service` (shipped by the debian package).
|
||||||
|
pub upstream_service_state: String,
|
||||||
|
/// True if either the archipelago-managed or upstream unit is active.
|
||||||
pub service_active: bool,
|
pub service_active: bool,
|
||||||
/// Whether the seed-derived FIPS key has been materialised on disk.
|
/// Whether the seed-derived FIPS key has been materialised on disk.
|
||||||
/// The service cannot start meaningfully until this is true.
|
/// The archipelago-managed service cannot start meaningfully until
|
||||||
|
/// this is true; legacy nodes may still report FIPS active via the
|
||||||
|
/// upstream unit without this file.
|
||||||
pub key_present: bool,
|
pub key_present: bool,
|
||||||
/// Local FIPS npub (bech32), present only once the key is on disk.
|
/// Local FIPS npub (bech32). Prefers the seed-derived key when
|
||||||
|
/// present; falls back to the upstream daemon's own key on legacy
|
||||||
|
/// nodes where `/etc/fips/fips.pub` is readable.
|
||||||
pub npub: Option<String>,
|
pub npub: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,16 +92,23 @@ impl FipsStatus {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let service_state = service::unit_state(SERVICE_UNIT).await;
|
let service_state = service::unit_state(SERVICE_UNIT).await;
|
||||||
let service_active = service_state == "active";
|
let upstream_service_state = service::unit_state(UPSTREAM_SERVICE_UNIT).await;
|
||||||
|
let service_active =
|
||||||
|
service_state == "active" || upstream_service_state == "active";
|
||||||
let key_present = crate::identity::fips_key_exists(identity_dir);
|
let key_present = crate::identity::fips_key_exists(identity_dir);
|
||||||
let npub = crate::identity::fips_npub(identity_dir)
|
|
||||||
.await
|
// Prefer the seed-derived npub; otherwise read the daemon's own
|
||||||
.unwrap_or(None);
|
// key file at /etc/fips/fips.pub (world-readable per debian pkg).
|
||||||
|
let npub = match crate::identity::fips_npub(identity_dir).await {
|
||||||
|
Ok(Some(n)) => Some(n),
|
||||||
|
_ => service::read_upstream_npub().await.ok().flatten(),
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
installed,
|
installed,
|
||||||
version,
|
version,
|
||||||
service_state,
|
service_state,
|
||||||
|
upstream_service_state,
|
||||||
service_active,
|
service_active,
|
||||||
key_present,
|
key_present,
|
||||||
npub,
|
npub,
|
||||||
|
|||||||
@ -7,8 +7,11 @@
|
|||||||
//! ISO whitelists exactly these invocations.
|
//! ISO whitelists exactly these invocations.
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use nostr_sdk::ToBech32;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
use super::DAEMON_PUB_PATH;
|
||||||
|
|
||||||
/// `systemctl is-active <unit>` → "active" / "inactive" / "failed" / "masked"
|
/// `systemctl is-active <unit>` → "active" / "inactive" / "failed" / "masked"
|
||||||
/// / "unknown". Never errors; returns "unknown" on any failure.
|
/// / "unknown". Never errors; returns "unknown" on any failure.
|
||||||
pub async fn unit_state(unit: &str) -> String {
|
pub async fn unit_state(unit: &str) -> String {
|
||||||
@ -100,6 +103,31 @@ pub async fn mask(unit: &str) -> Result<()> {
|
|||||||
sudo_systemctl("mask", unit).await
|
sudo_systemctl("mask", unit).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the upstream daemon's public key at `/etc/fips/fips.pub` and return
|
||||||
|
/// it as a bech32 npub. Returns `Ok(None)` if the file doesn't exist — used
|
||||||
|
/// as a fallback on legacy/dev nodes where no seed-derived key exists.
|
||||||
|
///
|
||||||
|
/// Upstream writes the key as a bech32 string (`npub1…`); older builds may
|
||||||
|
/// have written 32 raw bytes, so we accept either form.
|
||||||
|
pub async fn read_upstream_npub() -> Result<Option<String>> {
|
||||||
|
let bytes = match tokio::fs::read(DAEMON_PUB_PATH).await {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(e) => return Err(e).context("read /etc/fips/fips.pub"),
|
||||||
|
};
|
||||||
|
if let Ok(s) = std::str::from_utf8(&bytes) {
|
||||||
|
let trimmed = s.trim();
|
||||||
|
if trimmed.starts_with("npub1") {
|
||||||
|
if let Ok(pk) = nostr_sdk::PublicKey::parse(trimmed) {
|
||||||
|
return Ok(pk.to_bech32().ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pk = nostr_sdk::PublicKey::from_slice(&bytes)
|
||||||
|
.context("parse /etc/fips/fips.pub as secp256k1 public key")?;
|
||||||
|
Ok(pk.to_bech32().ok())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
//! User-triggered FIPS upgrade from upstream `main`.
|
//! User-triggered FIPS upgrade from the upstream default branch.
|
||||||
//!
|
//!
|
||||||
//! Flow (no auto-update, no background polling — user clicks a button):
|
//! Flow (no auto-update, no background polling — user clicks a button):
|
||||||
//! 1. Query GitHub for the latest commit on `main` of jmcorgan/fips.
|
//! 1. Query GitHub for the upstream repo's default branch, then the
|
||||||
|
//! latest commit on it. (jmcorgan/fips default is `master`, not
|
||||||
|
//! `main` — we resolve it dynamically so a future rename Just Works.)
|
||||||
//! 2. Compare with the installed daemon version reported by
|
//! 2. Compare with the installed daemon version reported by
|
||||||
//! `fipsctl --version`. If identical, report "up to date".
|
//! `fipsctl --version`. If identical, report "up to date".
|
||||||
//! 3. Fetch the built .deb artefact for that commit + its SHA256.
|
//! 3. Fetch the built .deb artefact for that commit + its SHA256.
|
||||||
@ -9,10 +11,11 @@
|
|||||||
//! 5. `sudo dpkg -i` the .deb, `sudo systemctl restart` the service.
|
//! 5. `sudo dpkg -i` the .deb, `sudo systemctl restart` the service.
|
||||||
//!
|
//!
|
||||||
//! The artefact URL / SHA256 source is not yet fixed — upstream doesn't
|
//! The artefact URL / SHA256 source is not yet fixed — upstream doesn't
|
||||||
//! publish stable release assets for `main` builds. This module currently
|
//! publish stable release assets for per-commit builds. This module
|
||||||
//! implements steps 1–2 (the "is there anything newer?" query) and stubs
|
//! currently implements steps 1–2 (the "is there anything newer?" query)
|
||||||
//! out 3–5 so the RPC/UI can wire through. The apply path returns a
|
//! and stubs out 3–5 so the RPC/UI can wire through. The apply path
|
||||||
//! clear "not yet available" error until the artefact source is decided.
|
//! returns a clear "not yet available" error until the artefact source
|
||||||
|
//! is decided.
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -35,12 +38,18 @@ pub struct UpdateCheck {
|
|||||||
pub notes: String,
|
pub notes: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query GitHub for the latest commit on `main` and compare to the
|
/// Query GitHub for the latest commit on the upstream default branch and
|
||||||
/// installed version. Never errors on "no package installed" — that is
|
/// compare to the installed version. Never errors on "no package installed"
|
||||||
/// itself a valid state where an update is available (install needed).
|
/// — that is itself a valid state where an update is available.
|
||||||
pub async fn check() -> Result<UpdateCheck> {
|
pub async fn check() -> Result<UpdateCheck> {
|
||||||
let current = service::daemon_version().await.ok();
|
let current = service::daemon_version().await.ok();
|
||||||
let latest = fetch_latest_main_sha().await?;
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
|
.build()
|
||||||
|
.context("Build HTTP client")?;
|
||||||
|
let branch = fetch_default_branch(&client).await?;
|
||||||
|
let latest = fetch_head_sha(&client, &branch).await?;
|
||||||
let short = latest.chars().take(7).collect::<String>();
|
let short = latest.chars().take(7).collect::<String>();
|
||||||
|
|
||||||
let update_available = match ¤t {
|
let update_available = match ¤t {
|
||||||
@ -50,12 +59,13 @@ pub async fn check() -> Result<UpdateCheck> {
|
|||||||
|
|
||||||
let notes = if update_available {
|
let notes = if update_available {
|
||||||
format!(
|
format!(
|
||||||
"Upstream main is at {}; installed: {}",
|
"Upstream {} is at {}; installed: {}",
|
||||||
|
branch,
|
||||||
short,
|
short,
|
||||||
current.as_deref().unwrap_or("not installed")
|
current.as_deref().unwrap_or("not installed")
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("Up to date ({})", short)
|
format!("Up to date ({} @ {})", branch, short)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(UpdateCheck {
|
Ok(UpdateCheck {
|
||||||
@ -77,13 +87,26 @@ pub async fn apply() -> Result<()> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_latest_main_sha() -> Result<String> {
|
async fn fetch_default_branch(client: &reqwest::Client) -> Result<String> {
|
||||||
let url = format!("{}/repos/{}/commits/main", GITHUB_API, UPSTREAM_REPO);
|
let url = format!("{}/repos/{}", GITHUB_API, UPSTREAM_REPO);
|
||||||
let client = reqwest::Client::builder()
|
let resp = client
|
||||||
.user_agent(USER_AGENT)
|
.get(&url)
|
||||||
.timeout(std::time::Duration::from_secs(15))
|
.header("Accept", "application/vnd.github+json")
|
||||||
.build()
|
.send()
|
||||||
.context("Build HTTP client")?;
|
.await
|
||||||
|
.context("GitHub repo API")?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
anyhow::bail!("GitHub repo API returned {}", resp.status());
|
||||||
|
}
|
||||||
|
let body: serde_json::Value = resp.json().await.context("Parse repo JSON")?;
|
||||||
|
body.get("default_branch")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("GitHub repo response missing default_branch"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_head_sha(client: &reqwest::Client, branch: &str) -> Result<String> {
|
||||||
|
let url = format!("{}/repos/{}/commits/{}", GITHUB_API, UPSTREAM_REPO, branch);
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("Accept", "application/vnd.github+json")
|
.header("Accept", "application/vnd.github+json")
|
||||||
@ -91,14 +114,17 @@ async fn fetch_latest_main_sha() -> Result<String> {
|
|||||||
.await
|
.await
|
||||||
.context("GitHub commits API")?;
|
.context("GitHub commits API")?;
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
anyhow::bail!("GitHub API returned {}", resp.status());
|
anyhow::bail!(
|
||||||
|
"GitHub commits API returned {} for branch {}",
|
||||||
|
resp.status(),
|
||||||
|
branch
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let body: serde_json::Value = resp.json().await.context("Parse commits JSON")?;
|
let body: serde_json::Value = resp.json().await.context("Parse commits JSON")?;
|
||||||
let sha = body
|
body.get("sha")
|
||||||
.get("sha")
|
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or_else(|| anyhow::anyhow!("GitHub commits response missing sha field"))?;
|
.map(|s| s.to_string())
|
||||||
Ok(sha.to_string())
|
.ok_or_else(|| anyhow::anyhow!("GitHub commits response missing sha field"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -203,6 +203,35 @@ pub struct NetworkDiagnostics {
|
|||||||
pub tor_connected: bool,
|
pub tor_connected: bool,
|
||||||
pub dns_working: bool,
|
pub dns_working: bool,
|
||||||
pub recommendations: Vec<String>,
|
pub recommendations: Vec<String>,
|
||||||
|
/// SSID of the currently-active WiFi connection, or None if the node is on
|
||||||
|
/// wired / no WiFi adapter / NetworkManager isn't around.
|
||||||
|
pub wifi_ssid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask NetworkManager for the active WiFi SSID. Returns None silently if
|
||||||
|
/// nmcli is unavailable or no WiFi device is connected.
|
||||||
|
async fn active_wifi_ssid() -> Option<String> {
|
||||||
|
let out = tokio::process::Command::new("nmcli")
|
||||||
|
.args(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
// DEVICE:TYPE:STATE:CONNECTION — colons inside fields are escaped by nmcli -t
|
||||||
|
let mut parts = line.split(':');
|
||||||
|
let _dev = parts.next()?;
|
||||||
|
let typ = parts.next().unwrap_or("");
|
||||||
|
let state = parts.next().unwrap_or("");
|
||||||
|
let conn = parts.next().unwrap_or("");
|
||||||
|
if typ == "wifi" && state == "connected" && !conn.is_empty() {
|
||||||
|
return Some(conn.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a comprehensive network diagnostic check.
|
/// Run a comprehensive network diagnostic check.
|
||||||
@ -211,6 +240,7 @@ pub async fn run_diagnostics() -> Result<NetworkDiagnostics> {
|
|||||||
let upnp_available = check_upnp_available().await;
|
let upnp_available = check_upnp_available().await;
|
||||||
let tor_connected = check_tor_connectivity().await;
|
let tor_connected = check_tor_connectivity().await;
|
||||||
let dns_working = check_dns().await;
|
let dns_working = check_dns().await;
|
||||||
|
let wifi_ssid = active_wifi_ssid().await;
|
||||||
|
|
||||||
let nat_type = if wan_ip.is_some() {
|
let nat_type = if wan_ip.is_some() {
|
||||||
if upnp_available {
|
if upnp_available {
|
||||||
@ -246,6 +276,7 @@ pub async fn run_diagnostics() -> Result<NetworkDiagnostics> {
|
|||||||
tor_connected,
|
tor_connected,
|
||||||
dns_working,
|
dns_working,
|
||||||
recommendations,
|
recommendations,
|
||||||
|
wifi_ssid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -151,6 +151,10 @@
|
|||||||
<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>
|
<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>
|
||||||
<span class="text-sm font-medium" :class="systemStats.bitcoinAvailable ? 'text-orange-400' : 'text-white/40'">{{ bitcoinSyncDisplay }}</span>
|
<span class="text-sm font-medium" :class="systemStats.bitcoinAvailable ? 'text-orange-400' : 'text-white/40'">{{ bitcoinSyncDisplay }}</span>
|
||||||
</div>
|
</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="fipsDotClass"></div><span class="text-sm text-white/80">FIPS</span></div>
|
||||||
|
<span class="text-sm font-medium" :class="fipsTextClass">{{ fipsStatusLabel }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||||
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">{{ t('home.manageNetwork') }}</RouterLink>
|
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">{{ t('home.manageNetwork') }}</RouterLink>
|
||||||
@ -313,6 +317,27 @@ const torConnected = computed(() => {
|
|||||||
})
|
})
|
||||||
const vpnStatus = ref({ connected: false, provider: '' })
|
const vpnStatus = ref({ connected: false, provider: '' })
|
||||||
const vpnConnected = computed(() => vpnStatus.value.connected || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running))
|
const vpnConnected = computed(() => vpnStatus.value.connected || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running))
|
||||||
|
const fipsStatus = ref<{ installed: boolean; service_active: boolean; key_present: boolean } | null>(null)
|
||||||
|
const fipsDotClass = computed(() => {
|
||||||
|
const s = fipsStatus.value
|
||||||
|
if (!s || !s.installed) return 'bg-white/40'
|
||||||
|
if (s.service_active) return 'bg-green-400'
|
||||||
|
return 'bg-white/40'
|
||||||
|
})
|
||||||
|
const fipsTextClass = computed(() => {
|
||||||
|
const s = fipsStatus.value
|
||||||
|
if (!s || !s.installed) return 'text-white/40'
|
||||||
|
if (s.service_active) return 'text-green-400'
|
||||||
|
return 'text-white/40'
|
||||||
|
})
|
||||||
|
const fipsStatusLabel = computed(() => {
|
||||||
|
const s = fipsStatus.value
|
||||||
|
if (!s) return '…'
|
||||||
|
if (!s.installed) return 'Not installed'
|
||||||
|
if (s.service_active) return 'Active'
|
||||||
|
if (!s.key_present) return 'Awaiting seed'
|
||||||
|
return 'Inactive'
|
||||||
|
})
|
||||||
const bitcoinSyncDisplay = computed(() => {
|
const bitcoinSyncDisplay = computed(() => {
|
||||||
if (!systemStats.bitcoinAvailable) return 'Not running'
|
if (!systemStats.bitcoinAvailable) return 'Not running'
|
||||||
if (systemStats.bitcoinSyncPercent >= 99.9) return 'Synced'
|
if (systemStats.bitcoinSyncPercent >= 99.9) return 'Synced'
|
||||||
@ -349,6 +374,7 @@ onMounted(async () => {
|
|||||||
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
||||||
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 30000); checkUpdateStatus(); loadWeb5Status()
|
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 30000); checkUpdateStatus(); loadWeb5Status()
|
||||||
rpcClient.vpnStatus().then(s => { vpnStatus.value = { connected: s.connected, provider: s.provider ?? '' } }).catch(() => {})
|
rpcClient.vpnStatus().then(s => { vpnStatus.value = { connected: s.connected, provider: s.provider ?? '' } }).catch(() => {})
|
||||||
|
rpcClient.call<{ installed: boolean; service_active: boolean; key_present: boolean }>({ method: 'fips.status' }).then(s => { fipsStatus.value = s }).catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wallet modals
|
// Wallet modals
|
||||||
|
|||||||
@ -89,10 +89,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" /></svg>
|
||||||
<span class="text-white/80 text-sm">WiFi Networks</span>
|
<span class="text-white/80 text-sm">WiFi</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-white/60 text-sm">{{ networkData.wifiCount }}</span>
|
<span class="text-sm" :class="networkData.wifiSsid ? 'text-green-400' : 'text-white/40'">{{ networkData.wifiSsid || 'Not connected' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@ -126,6 +126,13 @@
|
|||||||
{{ dnsDisplayLabel }}
|
{{ dnsDisplayLabel }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||||
|
<span class="text-white/80 text-sm">FIPS Mesh</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm" :class="fipsRowTextClass">{{ fipsRowLabel }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -160,6 +167,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FIPS Mesh (full card) -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<FipsNetworkCard />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
||||||
<!-- VPN Card -->
|
<!-- VPN Card -->
|
||||||
<div class="glass-card p-6 transition-all hover:-translate-y-1">
|
<div class="glass-card p-6 transition-all hover:-translate-y-1">
|
||||||
@ -377,6 +389,7 @@ import { useAppStore } from '@/stores/app'
|
|||||||
import QuickActionsCard from './server/QuickActionsCard.vue'
|
import QuickActionsCard from './server/QuickActionsCard.vue'
|
||||||
import TorServicesCard from './server/TorServicesCard.vue'
|
import TorServicesCard from './server/TorServicesCard.vue'
|
||||||
import ServerModals from './server/ServerModals.vue'
|
import ServerModals from './server/ServerModals.vue'
|
||||||
|
import FipsNetworkCard from './server/FipsNetworkCard.vue'
|
||||||
import type { TorServiceInfo } from './server/TorServicesCard.vue'
|
import type { TorServiceInfo } from './server/TorServicesCard.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@ -401,11 +414,34 @@ const logCount = ref(0)
|
|||||||
// Network data
|
// Network data
|
||||||
const networkLoading = ref(true)
|
const networkLoading = ref(true)
|
||||||
const networkData = ref({
|
const networkData = ref({
|
||||||
wifiCount: 'N/A', torConnected: false, forwardCount: 'N/A',
|
wifiCount: 'N/A', wifiSsid: null as string | null, torConnected: false, forwardCount: 'N/A',
|
||||||
vpnConnected: false, vpnProvider: '', vpnIp: '', wgIp: '', wgPubkey: '', vpnHostname: '', vpnPeers: 0,
|
vpnConnected: false, vpnProvider: '', vpnIp: '', wgIp: '', wgPubkey: '', vpnHostname: '', vpnPeers: 0,
|
||||||
dnsProvider: 'system', dnsServers: [] as string[], dnsDoH: false,
|
dnsProvider: 'system', dnsServers: [] as string[], dnsDoH: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// FIPS status row for the Local Network card. Full FIPS card lives below.
|
||||||
|
const fipsSummary = ref<{ installed: boolean; service_active: boolean; key_present: boolean } | null>(null)
|
||||||
|
const fipsRowLabel = computed(() => {
|
||||||
|
const s = fipsSummary.value
|
||||||
|
if (!s) return '…'
|
||||||
|
if (!s.installed) return 'Not installed'
|
||||||
|
// Service-active wins even on legacy nodes with no seed-derived key.
|
||||||
|
if (s.service_active) return 'Active'
|
||||||
|
if (!s.key_present) return 'Awaiting seed'
|
||||||
|
return 'Inactive'
|
||||||
|
})
|
||||||
|
const fipsRowTextClass = computed(() => {
|
||||||
|
const s = fipsSummary.value
|
||||||
|
if (!s || !s.installed) return 'text-white/40'
|
||||||
|
if (s.service_active) return 'text-green-400'
|
||||||
|
return 'text-white/60'
|
||||||
|
})
|
||||||
|
async function loadFipsSummary() {
|
||||||
|
try {
|
||||||
|
fipsSummary.value = await rpcClient.call<{ installed: boolean; service_active: boolean; key_present: boolean }>({ method: 'fips.status' })
|
||||||
|
} catch { /* backend too old */ }
|
||||||
|
}
|
||||||
|
|
||||||
async function loadNetworkData() {
|
async function loadNetworkData() {
|
||||||
networkLoading.value = true
|
networkLoading.value = true
|
||||||
try {
|
try {
|
||||||
@ -415,7 +451,7 @@ async function loadNetworkData() {
|
|||||||
rpcClient.vpnStatus(),
|
rpcClient.vpnStatus(),
|
||||||
rpcClient.dnsStatus(),
|
rpcClient.dnsStatus(),
|
||||||
])
|
])
|
||||||
if (diagRes.status === 'fulfilled') { networkData.value.torConnected = diagRes.value.tor_connected; networkData.value.wifiCount = diagRes.value.wifi_count !== undefined ? `${diagRes.value.wifi_count} configured` : 'N/A' }
|
if (diagRes.status === 'fulfilled') { networkData.value.torConnected = diagRes.value.tor_connected; networkData.value.wifiCount = diagRes.value.wifi_count !== undefined ? `${diagRes.value.wifi_count} configured` : 'N/A'; networkData.value.wifiSsid = (diagRes.value as { wifi_ssid?: string | null }).wifi_ssid ?? null }
|
||||||
if (fwdRes.status === 'fulfilled') { const c = fwdRes.value.forwards?.length ?? 0; networkData.value.forwardCount = `${c} rule${c !== 1 ? 's' : ''}` }
|
if (fwdRes.status === 'fulfilled') { const c = fwdRes.value.forwards?.length ?? 0; networkData.value.forwardCount = `${c} rule${c !== 1 ? 's' : ''}` }
|
||||||
if (vpnRes.status === 'fulfilled') { networkData.value.vpnConnected = vpnRes.value.connected; networkData.value.vpnProvider = vpnRes.value.provider ?? ''; networkData.value.vpnIp = (vpnRes.value.ip_address ?? '').replace(/\/\d+$/, ''); networkData.value.wgIp = vpnRes.value.wg_ip ?? ''; networkData.value.wgPubkey = (vpnRes.value as Record<string, unknown>).wg_pubkey as string ?? '' }
|
if (vpnRes.status === 'fulfilled') { networkData.value.vpnConnected = vpnRes.value.connected; networkData.value.vpnProvider = vpnRes.value.provider ?? ''; networkData.value.vpnIp = (vpnRes.value.ip_address ?? '').replace(/\/\d+$/, ''); networkData.value.wgIp = vpnRes.value.wg_ip ?? ''; networkData.value.wgPubkey = (vpnRes.value as Record<string, unknown>).wg_pubkey as string ?? '' }
|
||||||
if (dnsRes.status === 'fulfilled') { networkData.value.dnsProvider = dnsRes.value.provider; networkData.value.dnsServers = dnsRes.value.resolv_conf_servers ?? []; networkData.value.dnsDoH = dnsRes.value.doh_enabled }
|
if (dnsRes.status === 'fulfilled') { networkData.value.dnsProvider = dnsRes.value.provider; networkData.value.dnsServers = dnsRes.value.resolv_conf_servers ?? []; networkData.value.dnsDoH = dnsRes.value.doh_enabled }
|
||||||
@ -672,7 +708,7 @@ async function createService(name: string, port: number | null) {
|
|||||||
catch (e) { addServiceError.value = e instanceof Error ? e.message : 'Failed to create service' } finally { addingService.value = false }
|
catch (e) { addServiceError.value = e instanceof Error ? e.message : 'Failed to create service' } finally { addingService.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers() })
|
onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers(); loadFipsSummary() })
|
||||||
|
|
||||||
// Poll VPN status every 15s so IP updates after pairing
|
// Poll VPN status every 15s so IP updates after pairing
|
||||||
const vpnPollInterval = setInterval(async () => {
|
const vpnPollInterval = setInterval(async () => {
|
||||||
|
|||||||
@ -1,160 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
data-controller-container
|
|
||||||
tabindex="0"
|
|
||||||
class="home-card controller-focusable"
|
|
||||||
:class="{ 'home-card-animate': animate }"
|
|
||||||
style="--card-stagger: 5"
|
|
||||||
>
|
|
||||||
<div class="home-card-shell">
|
|
||||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
|
||||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
|
||||||
<div class="home-card-text">
|
|
||||||
<h2 class="text-xl font-semibold text-white mb-1">Network</h2>
|
|
||||||
<p class="text-sm text-white/70">FIPS mesh — preferred over Tor for peer traffic</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2" :title="statusLabel">
|
|
||||||
<span class="w-2 h-2 rounded-full" :class="statusDotColor"></span>
|
|
||||||
<span class="text-sm font-medium" :class="statusTextColor">{{ statusLabel }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-card-stats grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
|
||||||
<div class="p-4 bg-white/5 rounded-lg">
|
|
||||||
<p class="text-xs text-white/60 mb-1">Daemon version</p>
|
|
||||||
<p class="text-sm font-medium text-white break-all">{{ status.version || '—' }}</p>
|
|
||||||
<p v-if="!status.installed" class="text-xs text-white/40 mt-1">Package not installed</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 bg-white/5 rounded-lg">
|
|
||||||
<p class="text-xs text-white/60 mb-1">FIPS npub</p>
|
|
||||||
<p class="text-sm font-mono text-white break-all">{{ npubDisplay }}</p>
|
|
||||||
<p v-if="!status.key_present" class="text-xs text-white/40 mt-1">Unlock your seed to derive the FIPS key</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="updateInfo" class="mb-3 p-3 bg-white/5 rounded-lg border-l-2 border-orange-400">
|
|
||||||
<p class="text-xs text-orange-400 font-medium mb-1">{{ updateInfo.update_available ? 'Update available' : 'Up to date' }}</p>
|
|
||||||
<p class="text-xs text-white/70 break-all">{{ updateInfo.notes }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="statusMessage" class="mb-3 p-3 rounded-lg text-xs" :class="statusIsError ? 'bg-red-400/10 text-red-300' : 'bg-green-400/10 text-green-300'">{{ statusMessage }}</div>
|
|
||||||
|
|
||||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
|
||||||
<button
|
|
||||||
class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
|
||||||
:disabled="checking"
|
|
||||||
@click="checkForUpdate"
|
|
||||||
>{{ checking ? 'Checking…' : 'Check for update' }}</button>
|
|
||||||
<button
|
|
||||||
v-if="status.key_present && !status.service_active"
|
|
||||||
class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
|
||||||
:disabled="installing"
|
|
||||||
@click="installAndActivate"
|
|
||||||
>{{ installing ? 'Installing…' : 'Activate' }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
|
||||||
|
|
||||||
defineProps<{ animate: boolean }>()
|
|
||||||
|
|
||||||
interface FipsStatus {
|
|
||||||
installed: boolean
|
|
||||||
version: string | null
|
|
||||||
service_state: string
|
|
||||||
service_active: boolean
|
|
||||||
key_present: boolean
|
|
||||||
npub: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateCheck {
|
|
||||||
current: string | null
|
|
||||||
latest_commit: string
|
|
||||||
update_available: boolean
|
|
||||||
notes: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = ref<FipsStatus>({
|
|
||||||
installed: false,
|
|
||||||
version: null,
|
|
||||||
service_state: 'unknown',
|
|
||||||
service_active: false,
|
|
||||||
key_present: false,
|
|
||||||
npub: null,
|
|
||||||
})
|
|
||||||
const updateInfo = ref<UpdateCheck | null>(null)
|
|
||||||
const checking = ref(false)
|
|
||||||
const installing = ref(false)
|
|
||||||
const statusMessage = ref('')
|
|
||||||
const statusIsError = ref(false)
|
|
||||||
|
|
||||||
const statusLabel = computed(() => {
|
|
||||||
if (!status.value.installed) return 'not installed'
|
|
||||||
if (!status.value.key_present) return 'awaiting seed'
|
|
||||||
if (status.value.service_active) return 'active'
|
|
||||||
return status.value.service_state
|
|
||||||
})
|
|
||||||
|
|
||||||
const statusDotColor = computed(() => {
|
|
||||||
if (status.value.service_active) return 'bg-green-400'
|
|
||||||
if (!status.value.installed || !status.value.key_present) return 'bg-white/30'
|
|
||||||
return 'bg-orange-400'
|
|
||||||
})
|
|
||||||
|
|
||||||
const statusTextColor = computed(() => {
|
|
||||||
if (status.value.service_active) return 'text-green-400'
|
|
||||||
if (!status.value.installed || !status.value.key_present) return 'text-white/50'
|
|
||||||
return 'text-orange-400'
|
|
||||||
})
|
|
||||||
|
|
||||||
const npubDisplay = computed(() => {
|
|
||||||
const n = status.value.npub
|
|
||||||
if (!n) return '—'
|
|
||||||
return n.length > 20 ? `${n.slice(0, 12)}…${n.slice(-6)}` : n
|
|
||||||
})
|
|
||||||
|
|
||||||
function flash(msg: string, isError = false) {
|
|
||||||
statusMessage.value = msg
|
|
||||||
statusIsError.value = isError
|
|
||||||
setTimeout(() => { statusMessage.value = '' }, 6000)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStatus() {
|
|
||||||
try {
|
|
||||||
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.status' })
|
|
||||||
} catch (e) {
|
|
||||||
if (import.meta.env.DEV) console.warn('fips.status failed', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkForUpdate() {
|
|
||||||
checking.value = true
|
|
||||||
try {
|
|
||||||
updateInfo.value = await rpcClient.call<UpdateCheck>({ method: 'fips.check-update' })
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
|
||||||
flash(`Update check failed: ${msg}`, true)
|
|
||||||
} finally {
|
|
||||||
checking.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installAndActivate() {
|
|
||||||
installing.value = true
|
|
||||||
try {
|
|
||||||
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
|
|
||||||
flash('FIPS installed and activated')
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
|
||||||
flash(`Install failed: ${msg}`, true)
|
|
||||||
} finally {
|
|
||||||
installing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadStatus)
|
|
||||||
</script>
|
|
||||||
152
neode-ui/src/views/server/FipsNetworkCard.vue
Normal file
152
neode-ui/src/views/server/FipsNetworkCard.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col transition-all hover:-translate-y-1">
|
||||||
|
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||||
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-2">
|
||||||
|
<h2 class="text-xl font-semibold text-white">FIPS Mesh</h2>
|
||||||
|
<div class="flex items-center gap-2" :title="statusLabel">
|
||||||
|
<span class="w-2 h-2 rounded-full" :class="statusDotColor"></span>
|
||||||
|
<span class="text-sm font-medium" :class="statusTextColor">{{ statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-white/70 text-sm mb-4">Fast Nostr-keyed mesh routing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3 flex-1 min-h-0">
|
||||||
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
|
<p class="text-xs text-white/60 mb-1">Daemon version</p>
|
||||||
|
<p class="text-sm font-medium text-white break-all">{{ status.version || '—' }}</p>
|
||||||
|
<p v-if="!status.installed" class="text-xs text-white/40 mt-1">Package not installed</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-white/5 rounded-lg">
|
||||||
|
<div class="flex items-center justify-between gap-2 mb-1">
|
||||||
|
<p class="text-xs text-white/60">FIPS npub</p>
|
||||||
|
<button
|
||||||
|
v-if="status.npub"
|
||||||
|
type="button"
|
||||||
|
class="text-xs text-white/60 hover:text-white transition-colors flex items-center gap-1"
|
||||||
|
:title="copied ? 'Copied!' : 'Copy full npub to clipboard'"
|
||||||
|
@click="copyNpub"
|
||||||
|
>
|
||||||
|
<svg v-if="!copied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
||||||
|
<svg v-else class="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
|
||||||
|
<span :class="{ 'text-green-400': copied }">{{ copied ? 'Copied' : 'Copy' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-mono text-white break-all select-all">{{ npubDisplay }}</p>
|
||||||
|
<p v-if="!status.key_present && status.npub" class="text-xs text-white/40 mt-1">Upstream key (not seed-derived)</p>
|
||||||
|
<p v-else-if="!status.key_present" class="text-xs text-white/40 mt-1">Unlock seed to derive archipelago-managed key</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="statusMessage" class="mb-3 p-3 rounded-lg text-xs" :class="statusIsError ? 'bg-red-400/10 text-red-300' : 'bg-green-400/10 text-green-300'">{{ statusMessage }}</div>
|
||||||
|
|
||||||
|
<div v-if="status.key_present && !status.service_active" class="flex gap-2 mt-auto pt-3 shrink-0">
|
||||||
|
<button class="flex-1 min-h-[44px] px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" :disabled="installing" @click="installAndActivate">{{ installing ? 'Installing…' : 'Activate' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
import { safeClipboardWrite } from '@/views/web5/utils'
|
||||||
|
|
||||||
|
interface FipsStatus {
|
||||||
|
installed: boolean
|
||||||
|
version: string | null
|
||||||
|
service_state: string
|
||||||
|
upstream_service_state: string
|
||||||
|
service_active: boolean
|
||||||
|
key_present: boolean
|
||||||
|
npub: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = ref<FipsStatus>({
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
service_state: 'unknown',
|
||||||
|
upstream_service_state: 'unknown',
|
||||||
|
service_active: false,
|
||||||
|
key_present: false,
|
||||||
|
npub: null,
|
||||||
|
})
|
||||||
|
const installing = ref(false)
|
||||||
|
const statusMessage = ref('')
|
||||||
|
const statusIsError = ref(false)
|
||||||
|
const copied = ref(false)
|
||||||
|
|
||||||
|
async function copyNpub() {
|
||||||
|
if (!status.value.npub) return
|
||||||
|
try {
|
||||||
|
await safeClipboardWrite(status.value.npub)
|
||||||
|
copied.value = true
|
||||||
|
setTimeout(() => { copied.value = false }, 2000)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
flash(`Copy failed: ${msg}`, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusLabel = computed(() => {
|
||||||
|
if (!status.value.installed) return 'not installed'
|
||||||
|
// Active takes precedence: the daemon may be running from its own upstream
|
||||||
|
// key on a legacy/dev node that doesn't have a seed-derived archipelago key.
|
||||||
|
if (status.value.service_active) return 'active'
|
||||||
|
if (!status.value.key_present) return 'awaiting seed'
|
||||||
|
return status.value.service_state
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusDotColor = computed(() => {
|
||||||
|
if (status.value.service_active) return 'bg-green-400'
|
||||||
|
if (!status.value.installed || !status.value.key_present) return 'bg-white/30'
|
||||||
|
return 'bg-orange-400'
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusTextColor = computed(() => {
|
||||||
|
if (status.value.service_active) return 'text-green-400'
|
||||||
|
if (!status.value.installed || !status.value.key_present) return 'text-white/50'
|
||||||
|
return 'text-orange-400'
|
||||||
|
})
|
||||||
|
|
||||||
|
const npubDisplay = computed(() => {
|
||||||
|
const n = status.value.npub
|
||||||
|
if (!n) return '—'
|
||||||
|
return n.length > 20 ? `${n.slice(0, 12)}…${n.slice(-6)}` : n
|
||||||
|
})
|
||||||
|
|
||||||
|
function flash(msg: string, isError = false) {
|
||||||
|
statusMessage.value = msg
|
||||||
|
statusIsError.value = isError
|
||||||
|
setTimeout(() => { statusMessage.value = '' }, 6000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStatus() {
|
||||||
|
try {
|
||||||
|
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.status' })
|
||||||
|
} catch (e) {
|
||||||
|
if (import.meta.env.DEV) console.warn('fips.status failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installAndActivate() {
|
||||||
|
installing.value = true
|
||||||
|
try {
|
||||||
|
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
|
||||||
|
flash('FIPS installed and activated')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
flash(`Install failed: ${msg}`, true)
|
||||||
|
} finally {
|
||||||
|
installing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadStatus)
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user