Direct mycelium runs and our pkexec spawns are both healthy
(sidecar logs show acquired routes streaming for 20+s). Yet our
reqwest poller can't reach 127.0.0.1:port after the first
successful request. Smoking gun: failure happens ~10s after the
first reply — exactly when an idle keep-alive connection would
have been reaped.
Disable pooling (pool_max_idle_per_host(0)) so every call opens a
fresh TCP connection. Loopback overhead is negligible (~50us per
request) and we're immune to server-side idle closes. Also pin
connect_timeout to 3s so a wedged half-open doesn't block for
the full 10s request timeout.
The overlay covered the whole window in error phase too, blocking
access to the sidebar and Settings page where the user needs to
read the sidecar logs to diagnose the failure.
Now in error phase the sidebar status dot turns red, the start
button is back in the sidebar, and the Settings page is reachable
to inspect the in-app log buffer.
Even with --api-addr set, mycelium also opens an internal JSON-RPC
endpoint on 127.0.0.1:8990 by default (visible in --debug as
\"Starting JSON-RPC server listen_addr=127.0.0.1:8990\"). When a
previous run left an orphan mycelium running as root — which we
can't SIGKILL from a user-level Child::kill_on_drop — the new
instance fails to bind 8990 and exits ~10s after startup, with no
sidecar://exited event because the kill never landed.
Pin --metrics-api-address to a fresh ephemeral port alongside the
other three. Known limitation: stopping the daemon still doesn't
reliably kill the root child; the user has to `sudo pkill mycelium`
between runs. Will be addressed by an elevated-shutdown hook.
mycelium defaults --tcp-listen-port and --quic-listen-port to 9651
when not provided. If anything else holds 9651 (a previous test
instance, a Docker container from the level-1 procedure, another
mycelium running on the host), the daemon comes up far enough to
serve the loopback API for a few seconds before tearing itself
down on the listen failure.
Pick three distinct ephemeral ports (api, tcp, quic) per spawn.
Trade-off: inbound peers need the actual port number, which we
already log; the user can pin via a future SidecarConfig field.
`tracing_subscriber::fmt().try_init()` installs a global log
implementation via the tracing-log shim. tauri-plugin-log then
calls `log::set_logger()` which panics with "attempted to set a
logger after the logging system was already initialized".
The plugin was never wired in JS code (no @tauri-apps/plugin-log
import) — backend logging via tracing is sufficient. Drop:
- tauri-plugin-log dep + plugin registration
- log:default capability permission
- @tauri-apps/plugin-log JS dep
Verified: cargo check is clean, app starts past the panic.
Backend
- regenerate_identity command stops the daemon, deletes
priv_key.bin, leaves the user to restart for a fresh identity;
falls back to the canonical XDG path when sidecar.key_path()
isn't populated yet
- tauri.conf.json ships the polkit policy via deb.files mapping;
src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy
declares the spawn action with auth_admin_keep so the dialog
appears once per session
Frontend
- config store persists SidecarConfig (peers, tunName, noTun)
through tauri-plugin-store; App.vue reads it and forwards to
start_daemon, replacing the hard-coded defaults
- Settings view: daemon-config form, identity panel with the
destructive regenerate button, sidecar log viewer, About
- README rewritten end-to-end: HTTP-loopback architecture, polkit
install path, build commands, verification matrix, and a
honest "known limitations" section
Backend
- api/messages.rs covers send/pop/reply/status with an externally
tagged MessageDestination enum that matches the daemon's
{ip|pk: ...} body shape; pop_message uses an inflated request
timeout to outlast the long-poll window
- api/topics.rs implements default action, topic CRUD, sources
whitelist, and forward-socket get/set/remove. POST /topics ships
the raw base64 string as the body (not JSON); path segments are
percent-encoded inline (topics contain '/' and '+')
- api/pubkey.rs resolves an overlay IPv6 to a hex public key
- poller spawns a third long-poll loop on /messages?peek=false
that fans every inbound message into a 200-deep ring buffer and
emits messages://incoming for the UI
Frontend
- messages store: live inbox via the event, persisted outbox via
tauri-plugin-store keyed under outbox.json
- ComposeMessage form: ip/pk toggle, optional UTF-8 topic and
payload that get base64-encoded with a TextEncoder-based helper
- MessageList renders printable payloads decoded; binary payloads
fall back to a "(N bytes binary)" hint
- Topics view: split layout with whitelist on the left, per-topic
sources/forward editor on the right; default-action toggle is
surfaced at the top
Backend
- api/routes.rs models the Babel-style route shape; metric uses an
untagged enum to round-trip both numeric hop counts and the
literal "infinite" string the daemon emits for poisoned routes
- routes_snapshot() runs the three GETs concurrently with try_join
so the snapshot is internally consistent
- poller spawns a second 5s loop emitting routes://updated; both
loops are owned by the Poller and aborted together on stop_daemon
Frontend
- routes store mirrors the snapshot shape; tabbed view (radix-vue)
with selected, fallback and queried lists
- RouteTable component shared by selected/fallback; metric column
is colour-coded (0 green, low neutral, high yellow, infinite red)
- Queried subnets show a live `expires in 12s` countdown driven by
a 1Hz tick ref instead of mutating the store
Backend
- api/peers.rs: list/add/remove + aggregate() that derives totals,
per-state counts, and tx/rx sums in one pass over the peer list
- poller.rs spawns a 3s tokio loop that emits peers://updated and
stats://updated; cancelled via abort() on stop_daemon
- DELETE peer URL-encodes the endpoint (the path includes ://) with
a small inline percent-encoder to avoid a url crate dep
- Tauri commands: peers_list, peer_add (with empty-string guard),
peer_remove, peers_stats
Frontend
- peers store subscribes to the two events and refreshes after
add/remove for immediate UI feedback
- Peers view renders endpoint, type, color-coded state badge, and
formatBytes-formatted rx/tx; the four stat cards re-use a
reusable Stat component
- AddPeerDialog uses radix-vue's Dialog primitive with regex
validation for tcp:// and quic:// schemes
Backend
- sidecar.rs supervises the bundled `mycelium` binary launched via
pkexec; locates it in resource_dir or CARGO_MANIFEST_DIR/binaries
matching $TAURI_ENV_TARGET_TRIPLE
- ephemeral port via portpicker, key + config persisted in
app_data_dir, kill_on_drop with explicit start_kill on stop
- health-check loop calls /api/v1/admin until 2xx (timeout 20s);
emits sidecar://ready and sidecar://exited
- 500-line ring buffer of stdout/stderr surfaced via sidecar_logs
command for the upcoming Settings page
- elevation::is_auth_failure(126|127) maps pkexec cancel to a
dedicated AppError variant
- AppError uses thiserror, Serialize impl renders messages as
plain strings for the JS side
Frontend
- typed `api` wrapper around invoke() in src/lib/api.ts
- node store (Pinia) bootstraps on mount, listens on
sidecar://ready and sidecar://exited
- StartupOverlay covers the whole window for idle/starting/error
phases; sidebar status dot + start/stop button
- Status view renders subnet, pubkey, api endpoint and key path
with one-click clipboard copy