Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b09fa2cd |
@@ -80,11 +80,19 @@ pub struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Detect primary host IP (default-route interface, not `hostname -I` order)
|
/// Detect primary host IP (first non-loopback IPv4)
|
||||||
async fn detect_host_ip() -> Result<String> {
|
async fn detect_host_ip() -> Result<String> {
|
||||||
Ok(crate::host_ip::primary_host_ipv4()
|
let output = tokio::process::Command::new("hostname")
|
||||||
|
.args(["-I"])
|
||||||
|
.output()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|| "127.0.0.1".to_string()))
|
.context("Failed to run hostname -I")?;
|
||||||
|
let s = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let ip = s
|
||||||
|
.split_whitespace()
|
||||||
|
.find(|s| !s.starts_with("127.") && s.contains('.'))
|
||||||
|
.unwrap_or("127.0.0.1");
|
||||||
|
Ok(ip.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load() -> Result<Self> {
|
pub async fn load() -> Result<Self> {
|
||||||
|
|||||||
@@ -696,11 +696,22 @@ async fn netbird_configured_launch_url() -> Option<String> {
|
|||||||
PodmanClient::lan_address_for("netbird")
|
PodmanClient::lan_address_for("netbird")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The node's primary host IP. Mirrors the orchestrator's `detect_host_ip`
|
/// First address from `hostname -I` — the node's primary host IP. Mirrors the
|
||||||
/// so launch URLs match the cert/config the orchestrator renders for
|
/// orchestrator's `detect_host_ip` so launch URLs match the cert/config the
|
||||||
/// `{{HOST_IP}}`.
|
/// orchestrator renders for `{{HOST_IP}}`.
|
||||||
async fn first_host_ip() -> Option<String> {
|
async fn first_host_ip() -> Option<String> {
|
||||||
crate::host_ip::primary_host_ipv4().await
|
let out = tokio::process::Command::new("hostname")
|
||||||
|
.arg("-I")
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reachable_lan_address(app_id: &str, candidate: Option<String>) -> Option<String> {
|
async fn reachable_lan_address(app_id: &str, candidate: Option<String>) -> Option<String> {
|
||||||
|
|||||||
@@ -3087,7 +3087,16 @@ impl ProdContainerOrchestrator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn detect_host_ip() -> Option<String> {
|
async fn detect_host_ip() -> Option<String> {
|
||||||
crate::host_ip::primary_host_ipv4().await
|
let output = tokio::process::Command::new("hostname")
|
||||||
|
.arg("-I")
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
stdout.split_whitespace().next().map(|s| s.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn detect_host_mdns() -> String {
|
async fn detect_host_mdns() -> String {
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
//! Primary host LAN IPv4 detection.
|
|
||||||
//!
|
|
||||||
//! `hostname -I` lists addresses in interface-creation order, so once a VPN
|
|
||||||
//! or bridge interface exists (NetBird's WireGuard tunnel, br-tollgate, …)
|
|
||||||
//! its address can sort ahead of the real NIC — a fresh-ISO node handed out
|
|
||||||
//! `https://10.44.0.1:8087` as NetBird's launch URL instead of the LAN IP.
|
|
||||||
//! The main routing table's default route names the physical uplink even when
|
|
||||||
//! a VPN is active (NetBird/Tailscale steer traffic via policy-routing rules
|
|
||||||
//! in separate tables, not by replacing the main-table default), so that is
|
|
||||||
//! the authoritative source, with `hostname -I` kept only as the last resort
|
|
||||||
//! for hosts with no default route at all.
|
|
||||||
|
|
||||||
/// The node's primary LAN IPv4, as a string.
|
|
||||||
///
|
|
||||||
/// Resolution order:
|
|
||||||
/// 1. `src`/`dev` of the main-table default route (`ip -4 route show default`)
|
|
||||||
/// 2. source address of a connected UDP socket (never transmits)
|
|
||||||
/// 3. first non-loopback IPv4 from `hostname -I` (legacy behaviour)
|
|
||||||
pub(crate) async fn primary_host_ipv4() -> Option<String> {
|
|
||||||
if let Some(ip) = default_route_ip().await {
|
|
||||||
return Some(ip);
|
|
||||||
}
|
|
||||||
if let Some(ip) = udp_route_ip() {
|
|
||||||
return Some(ip);
|
|
||||||
}
|
|
||||||
hostname_i_ip().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn default_route_ip() -> Option<String> {
|
|
||||||
let out = tokio::process::Command::new("ip")
|
|
||||||
.args(["-4", "route", "show", "default"])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let route = String::from_utf8_lossy(&out.stdout);
|
|
||||||
if let Some(ip) = parse_route_src(&route) {
|
|
||||||
return Some(ip);
|
|
||||||
}
|
|
||||||
// No `src` hint on the route — resolve the device's global address.
|
|
||||||
let dev = parse_route_dev(&route)?;
|
|
||||||
let out = tokio::process::Command::new("ip")
|
|
||||||
.args(["-4", "-o", "addr", "show", "dev", &dev, "scope", "global"])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
parse_addr_inet(&String::from_utf8_lossy(&out.stdout))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_route_src(route: &str) -> Option<String> {
|
|
||||||
field_after(route.lines().next()?, "src")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_route_dev(route: &str) -> Option<String> {
|
|
||||||
field_after(route.lines().next()?, "dev")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_after(line: &str, key: &str) -> Option<String> {
|
|
||||||
let mut words = line.split_whitespace();
|
|
||||||
while let Some(w) = words.next() {
|
|
||||||
if w == key {
|
|
||||||
return words.next().map(ToOwned::to_owned);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_addr_inet(out: &str) -> Option<String> {
|
|
||||||
let cidr = field_after(out.lines().next()?, "inet")?;
|
|
||||||
Some(cidr.split('/').next().unwrap_or(&cidr).to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A connected UDP socket's local address is the source IP the kernel would
|
|
||||||
/// use to reach the peer; nothing is sent. Can still land on a tunnel IP when
|
|
||||||
/// a VPN policy-routes all traffic, hence only a fallback.
|
|
||||||
fn udp_route_ip() -> Option<String> {
|
|
||||||
let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?;
|
|
||||||
sock.connect("8.8.8.8:80").ok()?;
|
|
||||||
match sock.local_addr().ok()?.ip() {
|
|
||||||
std::net::IpAddr::V4(v4) if !v4.is_loopback() && !v4.is_unspecified() => {
|
|
||||||
Some(v4.to_string())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn hostname_i_ip() -> Option<String> {
|
|
||||||
let out = tokio::process::Command::new("hostname")
|
|
||||||
.arg("-I")
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
String::from_utf8_lossy(&out.stdout)
|
|
||||||
.split_whitespace()
|
|
||||||
.find(|s| !s.starts_with("127.") && s.contains('.'))
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn route_src_wins() {
|
|
||||||
let route = "default via 192.168.1.254 dev wlp3s0 proto dhcp src 192.168.1.116 metric 600";
|
|
||||||
assert_eq!(parse_route_src(route).as_deref(), Some("192.168.1.116"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn route_dev_without_src() {
|
|
||||||
let route = "default via 192.168.1.1 dev enp0s31f6 proto static";
|
|
||||||
assert_eq!(parse_route_src(route), None);
|
|
||||||
assert_eq!(parse_route_dev(route).as_deref(), Some("enp0s31f6"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn addr_inet_strips_prefix() {
|
|
||||||
let out = "3: wlp3s0 inet 192.168.1.65/24 brd 192.168.1.255 scope global dynamic noprefixroute wlp3s0\\ valid_lft 85328sec preferred_lft 85328sec";
|
|
||||||
assert_eq!(parse_addr_inet(out).as_deref(), Some("192.168.1.65"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_route_table() {
|
|
||||||
assert_eq!(parse_route_src(""), None);
|
|
||||||
assert_eq!(parse_route_dev(""), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,6 @@ mod electrs_status;
|
|||||||
mod federation;
|
mod federation;
|
||||||
mod fips;
|
mod fips;
|
||||||
mod health_monitor;
|
mod health_monitor;
|
||||||
mod host_ip;
|
|
||||||
mod identity;
|
mod identity;
|
||||||
mod identity_manager;
|
mod identity_manager;
|
||||||
mod marketplace;
|
mod marketplace;
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<title id="pageTitle">Bitcoin Node - Archipelago</title>
|
<title id="pageTitle">Bitcoin Node - Archipelago</title>
|
||||||
<!-- Relative: the shell is served at / in the container but under
|
<link rel="stylesheet" href="/tailwind.css">
|
||||||
/app/bitcoin-ui/ in the public demo — absolute paths 404 there. -->
|
|
||||||
<link rel="stylesheet" href="tailwind.css">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -341,7 +339,7 @@
|
|||||||
<div class="logo-gradient-border">
|
<div class="logo-gradient-border">
|
||||||
<img
|
<img
|
||||||
id="implLogo"
|
id="implLogo"
|
||||||
src="assets/img/app-icons/bitcoin-knots.webp"
|
src="/assets/img/app-icons/bitcoin-knots.webp"
|
||||||
alt="Bitcoin Node"
|
alt="Bitcoin Node"
|
||||||
class="w-16 h-16"
|
class="w-16 h-16"
|
||||||
style="object-fit: contain;"
|
style="object-fit: contain;"
|
||||||
@@ -986,8 +984,8 @@
|
|||||||
? 'Enhanced Bitcoin node implementation'
|
? 'Enhanced Bitcoin node implementation'
|
||||||
: 'Reference Bitcoin node implementation';
|
: 'Reference Bitcoin node implementation';
|
||||||
const icon = isKnots
|
const icon = isKnots
|
||||||
? 'assets/img/app-icons/bitcoin-knots.webp'
|
? '/assets/img/app-icons/bitcoin-knots.webp'
|
||||||
: 'assets/img/app-icons/bitcoin-core.svg';
|
: '/assets/img/app-icons/bitcoin-core.svg';
|
||||||
const pageTitle = document.getElementById('pageTitle');
|
const pageTitle = document.getElementById('pageTitle');
|
||||||
const implName = document.getElementById('implName');
|
const implName = document.getElementById('implName');
|
||||||
const implTagline = document.getElementById('implTagline');
|
const implTagline = document.getElementById('implTagline');
|
||||||
|
|||||||
@@ -386,7 +386,7 @@
|
|||||||
<section class="glass-card">
|
<section class="glass-card">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo-gradient-border">
|
<div class="logo-gradient-border">
|
||||||
<img src="assets/img/app-icons/fedimint.jpg" alt="Fedimint Guardian">
|
<img src="/assets/img/app-icons/fedimint.jpg" alt="Fedimint Guardian">
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>Fedimint Guardian</h1>
|
<h1>Fedimint Guardian</h1>
|
||||||
|
|||||||
@@ -91,10 +91,7 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Proxy FileBrowser API to mock backend (demo mode)
|
# Proxy FileBrowser API to mock backend (demo mode)
|
||||||
# ^~ on every /app/ prefix: the .css/.js/.img cache regex below must
|
location /app/filebrowser/ {
|
||||||
# never swallow app-shell assets (they live on the backend, not in the
|
|
||||||
# web root — without ^~ nginx prefers the regex and 404s them).
|
|
||||||
location ^~ /app/filebrowser/ {
|
|
||||||
client_max_body_size 10G;
|
client_max_body_size 10G;
|
||||||
proxy_pass http://neode-backend:5959;
|
proxy_pass http://neode-backend:5959;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -106,7 +103,7 @@ http {
|
|||||||
# IndeeHub: reverse-proxy the real site same-origin, strip framing headers,
|
# IndeeHub: reverse-proxy the real site same-origin, strip framing headers,
|
||||||
# and rewrite its absolute asset paths (/assets, /, src, href) to the
|
# and rewrite its absolute asset paths (/assets, /, src, href) to the
|
||||||
# /app/indeedhub/ prefix so the SPA loads inside the iframe.
|
# /app/indeedhub/ prefix so the SPA loads inside the iframe.
|
||||||
location ^~ /app/indeedhub/ {
|
location /app/indeedhub/ {
|
||||||
proxy_pass https://indee.tx1138.com/;
|
proxy_pass https://indee.tx1138.com/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host indee.tx1138.com;
|
proxy_set_header Host indee.tx1138.com;
|
||||||
@@ -132,7 +129,7 @@ http {
|
|||||||
# Proxy every other app UI (/app/<id>/) to the mock backend, which serves
|
# Proxy every other app UI (/app/<id>/) to the mock backend, which serves
|
||||||
# the per-app mock UIs (bitcoin-ui, electrumx, lnd, fedimint) and the
|
# the per-app mock UIs (bitcoin-ui, electrumx, lnd, fedimint) and the
|
||||||
# generic "Not available in the demo" notice for the rest.
|
# generic "Not available in the demo" notice for the rest.
|
||||||
location ^~ /app/ {
|
location /app/ {
|
||||||
proxy_pass http://neode-backend:5959;
|
proxy_pass http://neode-backend:5959;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -1249,8 +1249,7 @@ const DOCKER_UI = path.join(__dirname, '..', 'docker')
|
|||||||
for (const [prefixes, dir] of [
|
for (const [prefixes, dir] of [
|
||||||
[['/app/bitcoin-core', '/app/bitcoin-knots', '/app/bitcoin-ui'], 'bitcoin-ui'],
|
[['/app/bitcoin-core', '/app/bitcoin-knots', '/app/bitcoin-ui'], 'bitcoin-ui'],
|
||||||
[['/app/electrumx', '/app/electrs', '/app/archy-electrs-ui'], 'electrs-ui'],
|
[['/app/electrumx', '/app/electrs', '/app/archy-electrs-ui'], 'electrs-ui'],
|
||||||
// lnd deliberately NOT here: the real lnd-ui shell reads poorly in the demo
|
[['/app/lnd', '/app/lnd-ui', '/app/archy-lnd-ui', '/app/thunderhub'], 'lnd-ui'],
|
||||||
// iframe, so /app/lnd/ gets a DEMO_APP_PAGES placeholder dashboard instead.
|
|
||||||
[['/app/fedimint', '/app/fedimintd'], 'fedimint-ui'],
|
[['/app/fedimint', '/app/fedimintd'], 'fedimint-ui'],
|
||||||
]) {
|
]) {
|
||||||
for (const p of prefixes) app.use(p, express.static(path.join(DOCKER_UI, dir)))
|
for (const p of prefixes) app.use(p, express.static(path.join(DOCKER_UI, dir)))
|
||||||
@@ -2419,34 +2418,20 @@ app.post('/rpc/v1', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'network.dns-status': {
|
case 'network.dns-status': {
|
||||||
const dns = mockState.dns || { provider: 'system', servers: ['1.1.1.1', '9.9.9.9'], doh_enabled: false }
|
|
||||||
return res.json({
|
return res.json({
|
||||||
result: {
|
result: {
|
||||||
provider: dns.provider,
|
provider: 'system',
|
||||||
servers: dns.servers,
|
servers: ['1.1.1.1', '9.9.9.9'],
|
||||||
doh_enabled: dns.doh_enabled,
|
doh_enabled: false,
|
||||||
doh_url: null,
|
doh_url: null,
|
||||||
resolv_conf_servers: dns.servers,
|
resolv_conf_servers: ['1.1.1.1', '9.9.9.9'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'network.configure-dns': {
|
case 'network.configure-dns': {
|
||||||
const dnsProviders = {
|
console.log(`[Network] DNS configured: ${params?.provider}`)
|
||||||
system: ['192.168.4.1'],
|
return res.json({ result: { success: true } })
|
||||||
cloudflare: ['1.1.1.1', '1.0.0.1'],
|
|
||||||
google: ['8.8.8.8', '8.8.4.4'],
|
|
||||||
quad9: ['9.9.9.9', '149.112.112.112'],
|
|
||||||
mullvad: ['194.242.2.2'],
|
|
||||||
}
|
|
||||||
const provider = params?.provider || 'system'
|
|
||||||
const servers = provider === 'custom'
|
|
||||||
? (Array.isArray(params?.servers) ? params.servers : [])
|
|
||||||
: (dnsProviders[provider] || dnsProviders.system)
|
|
||||||
const doh_enabled = ['cloudflare', 'google', 'quad9', 'mullvad'].includes(provider)
|
|
||||||
mockState.dns = { provider, servers, doh_enabled }
|
|
||||||
console.log(`[Network] DNS configured: ${provider} → ${servers.join(', ')}`)
|
|
||||||
return res.json({ result: { provider, servers, doh_enabled } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'network.accept-request': {
|
case 'network.accept-request': {
|
||||||
@@ -4756,34 +4741,6 @@ app.get('/app/thunderhub/api/forwards', (req, res) => res.json(MOCK_LND_DATA.for
|
|||||||
// something plausible in the in-app iframe. Registered before the generic
|
// something plausible in the in-app iframe. Registered before the generic
|
||||||
// /app/:id notice handler so these win.
|
// /app/:id notice handler so these win.
|
||||||
const DEMO_APP_PAGES = {
|
const DEMO_APP_PAGES = {
|
||||||
// Placeholder LND dashboard — the real lnd-ui shell reads poorly inside the
|
|
||||||
// demo iframe. Numbers stay consistent with the /proxy/lnd/v1/* mocks.
|
|
||||||
lnd: () => demoAppShell('Lightning Network Daemon', 'archipelago-lnd · v0.18.3-beta · signet', '/assets/img/app-icons/lnd.png', `
|
|
||||||
<div class="grid">
|
|
||||||
<div class="card"><div class="k">Status</div><div class="v"><span class="badge">Running · synced</span></div></div>
|
|
||||||
<div class="card"><div class="k">On-chain</div><div class="v">2,450,000 sats</div></div>
|
|
||||||
<div class="card"><div class="k">Lightning (local)</div><div class="v">8,250,000 sats</div></div>
|
|
||||||
<div class="card"><div class="k">Inbound capacity</div><div class="v">11,750,000 sats</div></div>
|
|
||||||
</div>
|
|
||||||
<div class="grid">
|
|
||||||
<div class="card"><div class="k">Channels</div><div class="v">4 active</div></div>
|
|
||||||
<div class="card"><div class="k">Peers</div><div class="v">11</div></div>
|
|
||||||
<div class="card"><div class="k">Block height</div><div class="v">902,418</div></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="k" style="margin-bottom:6px">Channels</div>
|
|
||||||
<table>
|
|
||||||
<tr><th>Peer</th><th>Capacity</th><th>Local / Remote</th><th style="width:30%">Balance</th></tr>
|
|
||||||
<tr><td>ACINQ</td><td>5,000,000</td><td>2,450,000 / 2,550,000</td><td><div class="bar"><i style="width:49%"></i></div></td></tr>
|
|
||||||
<tr><td>Voltage</td><td>10,000,000</td><td>4,500,000 / 5,500,000</td><td><div class="bar"><i style="width:45%"></i></div></td></tr>
|
|
||||||
<tr><td>Kraken</td><td>3,000,000</td><td>1,800,000 / 1,200,000</td><td><div class="bar"><i style="width:60%"></i></div></td></tr>
|
|
||||||
<tr><td>Wallet of Satoshi</td><td>2,000,000</td><td>1,200,000 / 800,000</td><td><div class="bar"><i style="width:60%"></i></div></td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="margin-top:14px">
|
|
||||||
<div class="k" style="margin-bottom:6px">Node URI</div>
|
|
||||||
<div class="v mono">02c9f0a1…e47b@lnd7f3a2c9d1b4e8f6.onion:9735</div>
|
|
||||||
</div>`),
|
|
||||||
'btcpay-server': () => demoAppShell('BTCPay Server', 'Self-hosted payment processor · signet', '/assets/img/app-icons/btcpay-server.png', `
|
'btcpay-server': () => demoAppShell('BTCPay Server', 'Self-hosted payment processor · signet', '/assets/img/app-icons/btcpay-server.png', `
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card"><div class="k">Store</div><div class="v">Archipelago Shop</div></div>
|
<div class="card"><div class="k">Store</div><div class="v">Archipelago Shop</div></div>
|
||||||
|
|||||||
@@ -630,11 +630,7 @@ async function applyDnsConfig(customServers: string) {
|
|||||||
const params: { provider: DnsProviderValue; servers?: string[] } = { provider }
|
const params: { provider: DnsProviderValue; servers?: string[] } = { provider }
|
||||||
if (provider === 'custom') { params.servers = customServers.split(',').map(s => s.trim()).filter(s => s.length > 0) }
|
if (provider === 'custom') { params.servers = customServers.split(',').map(s => s.trim()).filter(s => s.length > 0) }
|
||||||
const res = await rpcClient.configureDns(params)
|
const res = await rpcClient.configureDns(params)
|
||||||
// Never trust the response shape: an undefined `servers` used to reach the
|
networkData.value.dnsProvider = res.provider; networkData.value.dnsServers = res.servers; networkData.value.dnsDoH = res.doh_enabled
|
||||||
// dnsDisplayLabel computed and crash the whole page render on `.length`.
|
|
||||||
networkData.value.dnsProvider = res?.provider ?? provider
|
|
||||||
networkData.value.dnsServers = Array.isArray(res?.servers) ? res.servers : (params.servers ?? [])
|
|
||||||
networkData.value.dnsDoH = !!res?.doh_enabled
|
|
||||||
showDnsModal.value = false
|
showDnsModal.value = false
|
||||||
} catch (e) { dnsError.value = e instanceof Error ? e.message : 'DNS configuration failed.' } finally { dnsApplying.value = false }
|
} catch (e) { dnsError.value = e instanceof Error ? e.message : 'DNS configuration failed.' } finally { dnsApplying.value = false }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,6 @@
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<!-- WiFi Scan Modal -->
|
<!-- WiFi Scan Modal -->
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="showWifiModal" class="fixed inset-0 bg-black/60 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="$emit('closeWifi')">
|
<div v-if="showWifiModal" class="fixed inset-0 bg-black/60 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="$emit('closeWifi')">
|
||||||
<div class="glass-card p-6 w-full max-w-md">
|
<div class="glass-card p-6 w-full max-w-md">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@@ -162,10 +161,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<!-- DNS Configuration Modal -->
|
<!-- DNS Configuration Modal -->
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="showDnsModal" class="fixed inset-0 bg-black/60 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="$emit('closeDns')">
|
<div v-if="showDnsModal" class="fixed inset-0 bg-black/60 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="$emit('closeDns')">
|
||||||
<div class="glass-card p-6 w-full max-w-md">
|
<div class="glass-card p-6 w-full max-w-md">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@@ -226,7 +223,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Reference in New Issue
Block a user