P1: sidecar lifecycle and HTTP bridge
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
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { api, type DaemonStatus, type NodeInfo } from "@/lib/api";
|
||||
import { Events, on } from "@/lib/events";
|
||||
|
||||
export type Phase = "idle" | "starting" | "ready" | "error";
|
||||
|
||||
export const useNodeStore = defineStore("node", () => {
|
||||
const phase = ref<Phase>("idle");
|
||||
const status = ref<DaemonStatus | null>(null);
|
||||
const info = ref<NodeInfo | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
let exitedUnlisten: (() => void) | null = null;
|
||||
let readyUnlisten: (() => void) | null = null;
|
||||
|
||||
async function bootstrap() {
|
||||
if (!exitedUnlisten) {
|
||||
exitedUnlisten = await on<number>(Events.SidecarExited, (e) => {
|
||||
error.value = `daemon exited (code ${e.payload})`;
|
||||
phase.value = "error";
|
||||
status.value = { running: false, apiUrl: null, keyPath: null, configPath: null };
|
||||
info.value = null;
|
||||
});
|
||||
}
|
||||
if (!readyUnlisten) {
|
||||
readyUnlisten = await on<string>(Events.SidecarReady, async () => {
|
||||
await refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const cur = await api.daemonStatus();
|
||||
status.value = cur;
|
||||
if (cur.running) {
|
||||
phase.value = "ready";
|
||||
try {
|
||||
info.value = await api.nodeInfo();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
phase.value = "starting";
|
||||
error.value = null;
|
||||
try {
|
||||
const s = await api.startDaemon();
|
||||
status.value = s;
|
||||
info.value = await api.nodeInfo();
|
||||
phase.value = "ready";
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
phase.value = "error";
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function stop() {
|
||||
try {
|
||||
const s = await api.stopDaemon();
|
||||
status.value = s;
|
||||
info.value = null;
|
||||
phase.value = "idle";
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
status.value = await api.daemonStatus();
|
||||
if (status.value.running) {
|
||||
info.value = await api.nodeInfo();
|
||||
phase.value = "ready";
|
||||
} else {
|
||||
info.value = null;
|
||||
phase.value = "idle";
|
||||
}
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
exitedUnlisten?.();
|
||||
readyUnlisten?.();
|
||||
exitedUnlisten = null;
|
||||
readyUnlisten = null;
|
||||
}
|
||||
|
||||
return {
|
||||
phase,
|
||||
status,
|
||||
info,
|
||||
error,
|
||||
bootstrap,
|
||||
start,
|
||||
stop,
|
||||
refresh,
|
||||
dispose,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user