# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Common commands Frontend (run from repo root): - `pnpm install` — install JS deps (pnpm, not npm/yarn). - `pnpm typecheck` — `vue-tsc --noEmit`. The CI gate. - `pnpm build` — typecheck + `vite build` into `dist/`. Tauri runs this as `beforeBuildCommand`. - `pnpm dev` — Vite alone on port 1420. Mostly only useful through `tauri dev`. Tauri / desktop: - `bash scripts/fetch-mycelium.sh [VERSION]` — downloads the `mycelium-private` release tarball from `threefoldtech/mycelium` and installs it as `src-tauri/binaries/mycelium-private-` (the suffix Tauri's `externalBin` requires). Default version pinned in the script (currently `v0.6.1`). **Must be run before the first `tauri dev` / `tauri build` on a fresh checkout.** - `pnpm tauri dev` — full dev cycle (Vite + cargo + window). - `pnpm tauri build --bundles deb` — produces `src-tauri/target/release/bundle/deb/Mycellium UI Private_*.deb`. Backend (run from `src-tauri/`): - `cargo fmt --all -- --check` - `cargo clippy --all-targets --locked -- -D warnings` - `cargo test --locked` - `cargo test --locked ` — run a single test by name substring. CI (`.github/workflows/ci.yml`) gates on: `pnpm typecheck`, `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test`. There is no JS test runner configured. ## Fork relationship This is the **private-network** fork of [`syoul/Mycell-UI`](https://git.open.us.org/syoul/Mycell-UI) (public variant). Tracked as `origin/upstream`; bugfixes are cherry-picked. The two apps are designed to coexist on the same machine — distinct identifier, binary, polkit action, `.deb` package. The only divergence files (per `README.md`): - `src-tauri/src/sidecar.rs` — adds `--network-name` + `--network-key-file` args, refuses to start without them. - `src-tauri/src/commands.rs` — adds `network_key_*` command family. - `src-tauri/packaging/` — `mycellium-bootstrap-private` cleanup wrapper, polkit policy with new action ID. - `src-tauri/tauri.conf.json` — `productName`, `identifier`, `externalBin`. - `src/views/Settings.vue` — "Private network" section. - `src/stores/config.ts` — empty default peer list (no Threefold-operated seed exists for private). Everything else should match upstream byte-for-byte; cherry-picks usually apply cleanly. ## Architecture Two-layer Tauri 2 desktop app. Rust supervises a `mycelium-private` daemon child process and exposes it to the Vue frontend via Tauri commands + events. ### Backend (`src-tauri/src/`) - `lib.rs::run()` — Tauri builder. Registers store/shell/dialog plugins, manages `AppState`, declares every `commands::*` handler. - `state.rs` — `AppState { sidecar: Arc, poller: Arc }`, single instance via `app.manage()`. - `sidecar.rs` — **the core lifecycle**. Spawns `mycelium-private` as a child via `pkexec`, runs a 20s health-check loop against the daemon's HTTP API, captures stdout/stderr into a 500-line ring buffer, watches for child exit and emits `sidecar://exited`. Pre-flight refuses to start without a non-empty `network_name` (2..=64 UTF-8 bytes) AND an existing `network_key.bin` — surfaces clear errors before pkexec fires. - `elevation.rs` — thin `pkexec` wrapper. `is_auth_failure(code)` distinguishes pkexec dialog cancel/deny (`126`/`127`) from daemon crashes. - `poller.rs` — three independent tokio loops on a started sidecar: peers (3s), routes (5s), inbox (2s short-poll). Each emits its own event; the inbox loop is **a short-poll workaround** for an upstream bug (mycelium 0.6.1's HTTP server serialises requests behind one worker, so a 30s long-poll starves every other endpoint). - `commands.rs` — every `#[tauri::command]`. The frontend does not talk to mycelium HTTP directly; commands either delegate to `MyceliumClient` (`require_client(state)`) or manage local state (network key files, sidecar logs). - `api/` — typed `MyceliumClient` over `reqwest`. **`pool_max_idle_per_host(0)` is intentional** — mycelium drops idle keep-alives around 10s, and reusing a stale pooled connection surfaces as a generic send error after `start_daemon` already returned. Each submodule (`admin`, `peers`, `routes`, `messages`, `topics`, `pubkey`) wraps one API surface. - `error.rs` — `AppError` is `Serialize`-as-string so `invoke()` rejects with the bare message. Frontend matches on substring or shows it raw. ### Sidecar startup details - Four ephemeral ports are picked per spawn (`api`, `tcp-listen`, `quic-listen`, `metrics`). Pinning `--metrics-api-address` to a fresh port matters: mycelium 0.6.1 hardcodes the JSON-RPC/metrics endpoint to `127.0.0.1:8990` by default, so an orphan from a previous run kills the new instance a few seconds in. - In a `.deb` install, the elevation target is `/usr/bin/mycellium-bootstrap-private`, not the daemon directly — the wrapper `pkill`s any orphan `mycelium` / `mycelium-private` and `ip link del`s leftover TUN devices before `exec`ing the daemon. Because both the wrapper and daemon run under one pkexec call, polkit's `auth_admin_keep` shows only **one** dialog per session. In `tauri dev` (where the wrapper isn't installed) the bare binary is used and orphan cleanup must be handled manually. - The sidecar binary is located by probing, in order: dir of `current_exe()` → `app.path().resource_dir()` → `CARGO_MANIFEST_DIR/binaries/` → `$PATH`. Both the suffixed name (`mycelium-private-`) and the bare name are tried, since `tauri build` strips the suffix when bundling into `/usr/bin/`. - The daemon's HTTP API only exposes the overlay subnet, so `sidecar.rs` parses the full IPv6 host portion out of the daemon's `Node overlay IP: ...` log line. The scanner strips ANSI SGR escapes (no `regex` crate dependency) and stores the result in `SidecarHandle::overlay_ip` — the Status page surfaces both subnet and full IP. ### Frontend (`src/`) - `main.ts` mounts Vue 3 + Pinia + Vue Router (`createWebHashHistory`). - `App.vue` is the only persistent shell — sidebar nav + start/stop daemon button + `StartupOverlay` for the `phase === "starting"` state. The amber accent on "Mycellium **Private**" distinguishes the fork visually. - `lib/api.ts` — **typed `invoke()` wrappers, one per Rust command.** All TS types here mirror Rust structs (`#[serde(rename_all = "camelCase")]` on the Rust side, camelCase fields on the TS side). When you add or change a `#[tauri::command]`, update this file in lockstep — it is the only source of truth for the frontend's view of the backend surface. - `lib/events.ts` — typed `listen()` over the six emitted events (`sidecar://ready`, `sidecar://exited`, `peers://updated`, `stats://updated`, `routes://updated`, `messages://incoming`). The `Events` constant must stay in sync with `app.emit()` calls in `sidecar.rs` and `poller.rs`. - `stores/` — Pinia setup-style stores. `node.ts` owns the `Phase` machine (`idle | starting | ready | error`) and bridges sidecar events to UI state. `config.ts` persists `SidecarConfig` via `@tauri-apps/plugin-store` (`config.json`); private fork's `DEFAULT_CONFIG.peers` is `[]` (no seed). `peers/routes/messages/topics` are screen-specific stores. - `views/` — one component per route (Status, Peers, Routes, Messages, Topics, Settings). `Settings.vue` is the only place that touches the network-key commands. - `components/` — leaf widgets only; no business logic. ### Path alias `@/*` resolves to `src/*` in both `tsconfig.json` and `vite.config.ts`. Use `@/lib/api`, `@/stores/node`, etc. ## Cross-cutting conventions - **Adding a new daemon-backed command** requires four edits in lockstep: `src-tauri/src/api/.rs` (HTTP wrapper) → `src-tauri/src/commands.rs` (`#[tauri::command]`) → `lib.rs::run()` `invoke_handler!` macro → `src/lib/api.ts` (TS wrapper + types). Forgetting the `invoke_handler!` registration is the most common silent failure — the command compiles but `invoke()` rejects with "command not found". - **Serde casing**: every Rust type crossing into JS uses `#[serde(rename_all = "camelCase")]`; TS types use camelCase. Don't add snake_case TS types or you'll get runtime undefined fields. - **No `unwrap()` in command paths.** Errors flow through `AppError` so the frontend gets a clean string. `panic!` will tear down the Tauri runtime. - **One private overlay at a time.** No multi-network support. The network key lives at `$XDG_DATA_HOME/tech.threefold.mycellium-ui-private/network_key.bin` (mode `0600`, raw 32 bytes). It is not encrypted at rest — host disk encryption is the only layer. - **`*.bin` is gitignored.** Never commit the dev network key or daemon identity. `docs-syoul/` (personal notes) is also gitignored — don't add files there expecting CI to see them. - **Linux-only.** No macOS/Windows code paths. `pkexec` and the polkit action ID are wired into the architecture; do not add `cfg(target_os = "...")` branches without thinking about the elevation model.