P5: settings, persistence, polkit packaging, README
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
This commit is contained in:
111
README.md
111
README.md
@@ -2,11 +2,110 @@
|
|||||||
|
|
||||||
Cross-platform desktop GUI for [Mycelium](https://github.com/threefoldtech/mycelium) — Threefold's end-to-end encrypted IPv6 overlay network.
|
Cross-platform desktop GUI for [Mycelium](https://github.com/threefoldtech/mycelium) — Threefold's end-to-end encrypted IPv6 overlay network.
|
||||||
|
|
||||||
Status: scaffolding. See the implementation plan for architecture, IPC protocol, and phasing.
|
The app embeds the official `mycelium` binary as a Tauri sidecar and pilots it through its HTTP API on a loopback ephemeral port. Root privileges (required to create the TUN interface) are obtained via `pkexec`.
|
||||||
|
|
||||||
## Stack (planned)
|
## Status
|
||||||
|
|
||||||
- **App**: Tauri v2 + Vue 3 + TypeScript
|
v1, Linux-only. Implements the full `docs/api.yaml` surface of mycelium v0.6.1: admin, peers (CRUD), routes (selected/fallback/queried), messages (send/receive/reply/status), topics (default + whitelist + sources + forward), pubkey lookup.
|
||||||
- **Daemon**: Rust (`myceliumd`) running as system service, IPC via Unix socket / named pipe
|
|
||||||
- **Mycelium engine**: official `mycelium` binary embedded as Tauri sidecar
|
## Architecture
|
||||||
- **Targets v1**: Linux + Windows
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ WebView (Vue 3 + TS + Tailwind + radix-vue + Pinia) │
|
||||||
|
│ Status / Peers / Routes / Messages / Topics / Settings │
|
||||||
|
└────────────────┬─────────────────────────────────────────────┘
|
||||||
|
│ invoke() / Tauri events
|
||||||
|
┌────────────────┴─────────────────────────────────────────────┐
|
||||||
|
│ Tauri core (Rust, tokio + reqwest) │
|
||||||
|
│ • sidecar.rs — supervises mycelium via pkexec │
|
||||||
|
│ • api/* — typed REST client │
|
||||||
|
│ • poller.rs — emits peers://, stats://, routes://, messages://incoming │
|
||||||
|
└────────────────┬─────────────────────────────────────────────┘
|
||||||
|
│ HTTP loopback :ephemeral
|
||||||
|
┌────────────────┴─────────────────────────────────────────────┐
|
||||||
|
│ mycelium daemon (sidecar binary, runs as root via pkexec) │
|
||||||
|
│ TUN0 ◄─► overlay network │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no Unix socket / named pipe IPC — the daemon's own HTTP API is the integration point.
|
||||||
|
|
||||||
|
## Prerequisites (Debian / Ubuntu)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y \
|
||||||
|
libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1-dev \
|
||||||
|
libsoup-3.0-dev libayatana-appindicator3-dev librsvg2-dev \
|
||||||
|
build-essential curl wget file libssl-dev libgtk-3-dev libxdo-dev \
|
||||||
|
pkg-config policykit-1
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
```
|
||||||
|
|
||||||
|
Then Node 20+ and pnpm 10+.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install JS deps
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 2. Fetch the mycelium sidecar binary for your target triple
|
||||||
|
bash scripts/fetch-mycelium.sh # uses MYCELIUM_VERSION (default v0.6.1)
|
||||||
|
# or: MYCELIUM_VERSION=v0.6.1 bash scripts/fetch-mycelium.sh
|
||||||
|
|
||||||
|
# 3. Run in dev
|
||||||
|
pnpm tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The first start triggers a `pkexec` dialog asking you to authenticate; the polkit policy installed by the `.deb` caches the auth for the user session.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm tauri build # → src-tauri/target/release/bundle/{deb,appimage}/
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.deb` declares `Depends: policykit-1` and ships the polkit policy under `/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy`. The AppImage relies on `pkexec` being present on the host — on systems without polkit, fall back to running with `sudo` after disabling the sidecar's pkexec wrapper.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/ # Vue 3 frontend
|
||||||
|
views/ # one file per nav item
|
||||||
|
components/ # shadcn-style UI primitives + dialogs
|
||||||
|
stores/ # Pinia: node, peers, routes, messages, topics, config
|
||||||
|
lib/ # api wrapper, events, base64 + format helpers
|
||||||
|
src-tauri/
|
||||||
|
src/
|
||||||
|
sidecar.rs # spawn + supervise mycelium
|
||||||
|
elevation.rs # pkexec command builder
|
||||||
|
poller.rs # 3 background loops (peers, routes, inbox long-poll)
|
||||||
|
api/ # REST client modules (admin, peers, routes,
|
||||||
|
# messages, topics, pubkey)
|
||||||
|
commands.rs # #[tauri::command] handlers, 1:1 with REST
|
||||||
|
error.rs # AppError + Serialize-as-string for invoke()
|
||||||
|
binaries/ # gitignored; populated by scripts/fetch-mycelium.sh
|
||||||
|
packaging/polkit/ # XML policy bundled into the .deb
|
||||||
|
scripts/fetch-mycelium.sh
|
||||||
|
.github/workflows/ci.yml # pnpm typecheck + cargo fmt/clippy/test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification matrix
|
||||||
|
|
||||||
|
| Test | How |
|
||||||
|
|------|-----|
|
||||||
|
| Sidecar starts under pkexec | `pnpm tauri dev`, daemon visible in `ps`, splash disappears in <10 s |
|
||||||
|
| Peers connect | Add `tcp://188.40.132.242:9651` from the Peers page; state turns to `alive` within ~10 s |
|
||||||
|
| Routes propagate | `Routes/Selected` becomes non-empty after ~30 s |
|
||||||
|
| Live event stream | Sidebar status dot tracks ready/idle, peers table updates without manual refresh |
|
||||||
|
| Bidirectional messages | Two instances on different VMs, exchange via Compose → Inbox |
|
||||||
|
| Identity regen | Settings → Regenerate; restart daemon; new IP appears on Status |
|
||||||
|
| `.deb` install | Fresh Ubuntu LTS / Debian 12; daemon spawns under polkit on first start |
|
||||||
|
|
||||||
|
## Known limitations (v1)
|
||||||
|
|
||||||
|
- Linux only. Windows is reachable (sidecar via `runas` / Wintun driver) but not implemented.
|
||||||
|
- Auto-start at login isn't wired — the desktop entry installed by the `.deb` is the manual launcher.
|
||||||
|
- The TOML config editor in Settings only exposes `peers`, `tunName`, `noTun`. Other keys (`metricsApiAddress`, etc.) are passed-through if you edit the file directly at `~/.local/share/tech.threefold.mycellium-ui/mycelium.toml` and restart the daemon.
|
||||||
|
- `message_status` is forwarded as opaque JSON; the upstream schema isn't pinned in the spec, so we don't strongly type it.
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE policyconfig PUBLIC
|
||||||
|
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||||
|
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||||
|
<policyconfig>
|
||||||
|
<vendor>Threefold</vendor>
|
||||||
|
<vendor_url>https://threefold.io</vendor_url>
|
||||||
|
|
||||||
|
<action id="tech.threefold.mycellium-ui.spawn">
|
||||||
|
<description>Run the Mycelium overlay daemon</description>
|
||||||
|
<description xml:lang="fr">Lancer le démon de l'overlay Mycelium</description>
|
||||||
|
<message>Authentication is required to start the Mycelium overlay daemon.</message>
|
||||||
|
<message xml:lang="fr">Une authentification est requise pour démarrer le démon Mycelium.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin</allow_any>
|
||||||
|
<allow_inactive>auth_admin</allow_inactive>
|
||||||
|
<!-- Cache the authentication for the user's session so the polkit
|
||||||
|
dialog only appears once per login (5-minute window). To allow
|
||||||
|
passwordless start for trusted desktops, change to "yes" — be
|
||||||
|
aware this lets any process on the machine spawn the daemon. -->
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
</policyconfig>
|
||||||
@@ -247,3 +247,52 @@ pub async fn topic_forward_remove(state: State<'_, AppState>, topic_b64: String)
|
|||||||
pub async fn lookup_pubkey(state: State<'_, AppState>, ip: String) -> AppResult<PubkeyLookup> {
|
pub async fn lookup_pubkey(state: State<'_, AppState>, ip: String) -> AppResult<PubkeyLookup> {
|
||||||
require_client(&state)?.lookup_pubkey(&ip).await
|
require_client(&state)?.lookup_pubkey(&ip).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Identity ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Stops the daemon (if running), removes the saved private key file, and
|
||||||
|
/// returns the daemon to the idle state. The caller restarts the daemon
|
||||||
|
/// to provoke regeneration.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn regenerate_identity(state: State<'_, AppState>) -> AppResult<()> {
|
||||||
|
state.poller.stop();
|
||||||
|
let key_path = state.sidecar.key_path();
|
||||||
|
state.sidecar.stop().await;
|
||||||
|
|
||||||
|
if let Some(path) = key_path {
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(&path).map_err(AppError::from)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sidecar never started; resolve the canonical path via app_data_dir.
|
||||||
|
if let Some(p) = default_key_path() {
|
||||||
|
if p.exists() {
|
||||||
|
std::fs::remove_file(&p).map_err(AppError::from)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_key_path() -> Option<std::path::PathBuf> {
|
||||||
|
// Best-effort: fall back to the same XDG location the sidecar uses.
|
||||||
|
dirs_like_app_data().ok().map(|d| d.join("priv_key.bin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dirs_like_app_data() -> std::io::Result<std::path::PathBuf> {
|
||||||
|
// We can't reach the AppHandle here, so we mirror Tauri's path:
|
||||||
|
// $XDG_DATA_HOME/<identifier>/ or $HOME/.local/share/<identifier>/.
|
||||||
|
let identifier = "tech.threefold.mycellium-ui";
|
||||||
|
if let Ok(d) = std::env::var("XDG_DATA_HOME") {
|
||||||
|
return Ok(std::path::PathBuf::from(d).join(identifier));
|
||||||
|
}
|
||||||
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
|
return Ok(std::path::PathBuf::from(home)
|
||||||
|
.join(".local/share")
|
||||||
|
.join(identifier));
|
||||||
|
}
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"no app data dir",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ pub fn run() {
|
|||||||
commands::topic_forward_set,
|
commands::topic_forward_set,
|
||||||
commands::topic_forward_remove,
|
commands::topic_forward_remove,
|
||||||
commands::lookup_pubkey,
|
commands::lookup_pubkey,
|
||||||
|
commands::regenerate_identity,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -40,7 +40,10 @@
|
|||||||
"externalBin": ["binaries/mycelium"],
|
"externalBin": ["binaries/mycelium"],
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": ["policykit-1"]
|
"depends": ["policykit-1"],
|
||||||
|
"files": {
|
||||||
|
"/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy": "packaging/polkit/tech.threefold.mycellium-ui.policy"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import {
|
|||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import StartupOverlay from "@/components/StartupOverlay.vue";
|
import StartupOverlay from "@/components/StartupOverlay.vue";
|
||||||
import { useNodeStore } from "@/stores/node";
|
import { useNodeStore } from "@/stores/node";
|
||||||
|
import { useConfigStore } from "@/stores/config";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const node = useNodeStore();
|
const node = useNodeStore();
|
||||||
|
const config = useConfigStore();
|
||||||
const { phase, info, error } = storeToRefs(node);
|
const { phase, info, error } = storeToRefs(node);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -30,8 +32,9 @@ const navItems = [
|
|||||||
|
|
||||||
const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium");
|
const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
node.bootstrap();
|
await config.load();
|
||||||
|
await node.bootstrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -40,7 +43,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
async function handleStart() {
|
async function handleStart() {
|
||||||
try {
|
try {
|
||||||
await node.start();
|
await node.start(config.config);
|
||||||
} catch {
|
} catch {
|
||||||
// error already in store
|
// error already in store
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export const api = {
|
|||||||
cmd<void>("topic_forward_remove", { topicB64 }),
|
cmd<void>("topic_forward_remove", { topicB64 }),
|
||||||
|
|
||||||
lookupPubkey: (ip: string) => cmd<PubkeyLookup>("lookup_pubkey", { ip }),
|
lookupPubkey: (ip: string) => cmd<PubkeyLookup>("lookup_pubkey", { ip }),
|
||||||
|
regenerateIdentity: () => cmd<void>("regenerate_identity"),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Format the canonical peer endpoint string the API expects. */
|
/** Format the canonical peer endpoint string the API expects. */
|
||||||
|
|||||||
57
src/stores/config.ts
Normal file
57
src/stores/config.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { Store as TauriStore } from "@tauri-apps/plugin-store";
|
||||||
|
import type { SidecarConfig } from "@/lib/api";
|
||||||
|
|
||||||
|
const STORE_FILE = "config.json";
|
||||||
|
const KEY = "sidecar";
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: SidecarConfig = {
|
||||||
|
peers: [
|
||||||
|
"tcp://188.40.132.242:9651",
|
||||||
|
"quic://[2a01:4f8:212:fa6::2]:9651",
|
||||||
|
],
|
||||||
|
tunName: null,
|
||||||
|
noTun: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfigStore = defineStore("config", () => {
|
||||||
|
const config = ref<SidecarConfig>({ ...DEFAULT_CONFIG });
|
||||||
|
const loaded = ref(false);
|
||||||
|
let store: TauriStore | null = null;
|
||||||
|
|
||||||
|
async function ensureStore() {
|
||||||
|
if (!store) store = await TauriStore.load(STORE_FILE);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const s = await ensureStore();
|
||||||
|
const saved = await s.get<SidecarConfig>(KEY);
|
||||||
|
if (saved) {
|
||||||
|
config.value = {
|
||||||
|
peers: Array.isArray(saved.peers) ? saved.peers : DEFAULT_CONFIG.peers,
|
||||||
|
tunName: saved.tunName ?? null,
|
||||||
|
noTun: !!saved.noTun,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
loaded.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(next: SidecarConfig) {
|
||||||
|
config.value = {
|
||||||
|
peers: next.peers.map((p) => p.trim()).filter(Boolean),
|
||||||
|
tunName: next.tunName?.trim() ? next.tunName.trim() : null,
|
||||||
|
noTun: !!next.noTun,
|
||||||
|
};
|
||||||
|
const s = await ensureStore();
|
||||||
|
await s.set(KEY, config.value);
|
||||||
|
await s.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
return save({ ...DEFAULT_CONFIG });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { config, loaded, load, save, reset };
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { api, type DaemonStatus, type NodeInfo } from "@/lib/api";
|
import { api, type DaemonStatus, type NodeInfo, type SidecarConfig } from "@/lib/api";
|
||||||
import { Events, on } from "@/lib/events";
|
import { Events, on } from "@/lib/events";
|
||||||
|
|
||||||
export type Phase = "idle" | "starting" | "ready" | "error";
|
export type Phase = "idle" | "starting" | "ready" | "error";
|
||||||
@@ -41,11 +41,11 @@ export const useNodeStore = defineStore("node", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
async function start(config?: SidecarConfig) {
|
||||||
phase.value = "starting";
|
phase.value = "starting";
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const s = await api.startDaemon();
|
const s = await api.startDaemon(config);
|
||||||
status.value = s;
|
status.value = s;
|
||||||
info.value = await api.nodeInfo();
|
info.value = await api.nodeInfo();
|
||||||
phase.value = "ready";
|
phase.value = "ready";
|
||||||
|
|||||||
@@ -1,5 +1,272 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import {
|
||||||
|
Save,
|
||||||
|
RotateCcw,
|
||||||
|
RefreshCw,
|
||||||
|
KeyRound,
|
||||||
|
AlertTriangle,
|
||||||
|
TerminalSquare,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import { useConfigStore } from "@/stores/config";
|
||||||
|
import { useNodeStore } from "@/stores/node";
|
||||||
|
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const node = useNodeStore();
|
||||||
|
const { config } = storeToRefs(configStore);
|
||||||
|
const { phase, status } = storeToRefs(node);
|
||||||
|
|
||||||
|
// Local working copy for the form. We mirror the canonical config and
|
||||||
|
// synchronise on every write to the store.
|
||||||
|
const draft = reactive({
|
||||||
|
peers: "",
|
||||||
|
tunName: "",
|
||||||
|
noTun: false,
|
||||||
|
});
|
||||||
|
const dirty = ref(false);
|
||||||
|
|
||||||
|
function loadDraft() {
|
||||||
|
draft.peers = config.value.peers.join("\n");
|
||||||
|
draft.tunName = config.value.tunName ?? "";
|
||||||
|
draft.noTun = config.value.noTun;
|
||||||
|
dirty.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(config, loadDraft, { immediate: true });
|
||||||
|
|
||||||
|
watch(
|
||||||
|
draft,
|
||||||
|
() => {
|
||||||
|
dirty.value = true;
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
await configStore.save({
|
||||||
|
peers: draft.peers
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
tunName: draft.tunName.trim() || null,
|
||||||
|
noTun: draft.noTun,
|
||||||
|
});
|
||||||
|
dirty.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
if (!confirm("Restore default daemon configuration?")) return;
|
||||||
|
await configStore.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Identity ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const regenBusy = ref(false);
|
||||||
|
const regenError = ref<string | null>(null);
|
||||||
|
|
||||||
|
async function regenerate() {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Regenerate identity? This deletes the current private key. Your overlay IPv6 and public key will change. Anyone with peers configured by your old IP/key will have to update them.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
regenBusy.value = true;
|
||||||
|
regenError.value = null;
|
||||||
|
try {
|
||||||
|
await api.regenerateIdentity();
|
||||||
|
} catch (e) {
|
||||||
|
regenError.value = String(e);
|
||||||
|
} finally {
|
||||||
|
regenBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Logs ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const logs = ref<string[]>([]);
|
||||||
|
const logsLoading = ref(false);
|
||||||
|
|
||||||
|
async function refreshLogs() {
|
||||||
|
logsLoading.value = true;
|
||||||
|
try {
|
||||||
|
logs.value = await api.sidecarLogs();
|
||||||
|
} finally {
|
||||||
|
logsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReady = computed(() => phase.value === "ready");
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (isReady.value) await refreshLogs();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p class="text-sm text-muted-foreground">Settings view — wired in P5.</p>
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<!-- Daemon configuration ───────────────────────────────────────────── -->
|
||||||
|
<section class="rounded-lg border border-border bg-card">
|
||||||
|
<header class="border-b border-border px-4 py-3">
|
||||||
|
<h2 class="text-sm font-medium">Daemon configuration</h2>
|
||||||
|
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||||
|
Applied next time the daemon starts. Currently running daemon
|
||||||
|
isn't reconfigured live.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="space-y-4 px-4 py-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Static peers (one per line)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="draft.peers"
|
||||||
|
rows="6"
|
||||||
|
spellcheck="false"
|
||||||
|
class="mt-1 w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="tcp://188.40.132.242:9651"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
id="no-tun"
|
||||||
|
v-model="draft.noTun"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<label for="no-tun" class="text-sm">
|
||||||
|
Disable TUN interface (<code class="font-mono text-xs">--no-tun</code>)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
TUN interface name (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="draft.tunName"
|
||||||
|
type="text"
|
||||||
|
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-1.5 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="mycelium0"
|
||||||
|
:disabled="draft.noTun"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-secondary"
|
||||||
|
@click="reset"
|
||||||
|
>
|
||||||
|
<RotateCcw class="h-3 w-3" />
|
||||||
|
Defaults
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
|
||||||
|
:disabled="!dirty"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
<Save class="h-3 w-3" />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Identity ───────────────────────────────────────────────────────── -->
|
||||||
|
<section class="rounded-lg border border-border bg-card">
|
||||||
|
<header class="border-b border-border px-4 py-3">
|
||||||
|
<h2 class="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<KeyRound class="h-4 w-4" /> Identity
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<div class="space-y-3 px-4 py-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Key file
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 break-all font-mono text-xs">
|
||||||
|
{{ status?.keyPath ?? "(unknown — start the daemon at least once)" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-xs">
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<AlertTriangle class="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>
|
||||||
|
Regenerating discards your current private key, which means a
|
||||||
|
new overlay IPv6 and public key. This is irreversible.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md border border-destructive px-3 py-1 text-destructive hover:bg-destructive hover:text-destructive-foreground disabled:opacity-50"
|
||||||
|
:disabled="regenBusy"
|
||||||
|
@click="regenerate"
|
||||||
|
>
|
||||||
|
<RotateCcw class="h-3 w-3" />
|
||||||
|
Regenerate identity
|
||||||
|
</button>
|
||||||
|
<p v-if="regenError" class="text-destructive">{{ regenError }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Logs ──────────────────────────────────────────────────────────── -->
|
||||||
|
<section class="rounded-lg border border-border bg-card lg:col-span-2">
|
||||||
|
<header class="flex items-center justify-between border-b border-border px-4 py-3">
|
||||||
|
<h2 class="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<TerminalSquare class="h-4 w-4" /> Sidecar logs
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1 text-xs hover:bg-secondary disabled:opacity-50"
|
||||||
|
:disabled="logsLoading"
|
||||||
|
@click="refreshLogs"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
class="h-3 w-3"
|
||||||
|
:class="logsLoading ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="max-h-72 overflow-y-auto bg-background px-4 py-2 font-mono text-[11px]"
|
||||||
|
>
|
||||||
|
<p v-if="!logs.length" class="py-4 text-center text-muted-foreground">
|
||||||
|
No log entries — start the daemon to begin capturing.
|
||||||
|
</p>
|
||||||
|
<pre v-else class="whitespace-pre-wrap break-all">{{ logs.join("\n") }}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- About ────────────────────────────────────────────────────────── -->
|
||||||
|
<section class="rounded-lg border border-border bg-card lg:col-span-2">
|
||||||
|
<header class="border-b border-border px-4 py-3">
|
||||||
|
<h2 class="text-sm font-medium">About</h2>
|
||||||
|
</header>
|
||||||
|
<dl class="grid grid-cols-1 gap-4 px-4 py-4 text-sm sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
App
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 font-mono text-xs">mycellium-ui 0.1.0</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Mycelium daemon
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 font-mono text-xs">v0.6.1 (bundled)</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
API endpoint
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 font-mono text-xs">
|
||||||
|
{{ status?.apiUrl ?? "—" }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user