feat: pin TCP/QUIC listen ports + visible error banner

Two related improvements requested while testing the private fork:

1. Custom TCP/QUIC listen ports (SidecarConfig.tcpListenPort,
   .quicListenPort). With ephemeral ports, the daemon's peer-listen
   port changes at every restart, which makes port-forwarding on a
   home router useless after the first daemon stop. Pinning these
   keeps the same port across restarts so other private-network
   nodes can dial in reliably.

   Backend uses the configured port if Some, falls back to
   pick_port_skip otherwise. Frontend exposes two inputs in
   Settings → Daemon configuration with a help line explaining
   when to fill them.

2. Daemon-failure banner in App.vue. The previous behaviour was
   silent: a click on \"Start daemon\" that hit a backend error
   only flipped the sidebar dot to red, with no message visible.
   Now an inline banner at the top of the main content area shows
   the error, plus a \"Go to Settings\" shortcut when the message
   mentions a network config issue.
This commit is contained in:
syoul
2026-04-27 02:29:31 +02:00
parent 8b83fc10d5
commit a930c035c0
7 changed files with 183 additions and 2 deletions

View File

@@ -33,6 +33,12 @@ pub struct SidecarConfig {
/// UTF-8 network identifier (2..=64 bytes). Public; not a secret.
/// All nodes joining the same private overlay must agree on this.
pub network_name: Option<String>,
/// Pin the TCP listen port for inbound peer connections. Required
/// when the user port-forwards a fixed port on their router so
/// other nodes can reliably dial in. `None` ⇒ ephemeral port.
pub tcp_listen_port: Option<u16>,
/// Same as above for QUIC (UDP). `None` ⇒ ephemeral port.
pub quic_listen_port: Option<u16>,
}
/// Holds the running mycelium child process plus a small in-memory log
@@ -149,8 +155,14 @@ impl SidecarHandle {
// is already up. Always picking ephemeral ports avoids that at the
// cost of inbound peers needing the actual port number.
let api_port = pick_port()?;
let tcp_port = pick_port_skip(&[api_port])?;
let quic_port = pick_port_skip(&[api_port, tcp_port])?;
let tcp_port = match config.tcp_listen_port {
Some(p) if p != 0 => p,
_ => pick_port_skip(&[api_port])?,
};
let quic_port = match config.quic_listen_port {
Some(p) if p != 0 => p,
_ => pick_port_skip(&[api_port, tcp_port])?,
};
// mycelium also opens an internal JSON-RPC / metrics endpoint on
// 127.0.0.1:8990 by default; if 8990 is already taken (e.g. by an
// orphan from a previous run we couldn't SIGKILL because it ran as