feat(electrobun): multi-machine relay + OTEL telemetry

Multi-machine relay:
- relay-client.ts: WebSocket client for agor-relay with token auth,
  exponential backoff (1s-30s), TCP probe, heartbeat (15s ping)
- machines-store.svelte.ts: remote machine state tracking
- RemoteMachinesSettings.svelte: machine list, add/connect/disconnect UI
- 7 RPC types (remote.connect/disconnect/list/send/status + events)

Telemetry:
- telemetry.ts: OTEL spans + OTLP/HTTP export to Tempo,
  controlled by AGOR_OTLP_ENDPOINT env var
- telemetry-bridge.ts: tel.info/warn/error frontend convenience API
- telemetry.log RPC for frontend→Bun tracing
This commit is contained in:
Hibryda 2026-03-22 01:46:03 +01:00
parent ec30c69c3e
commit 88206205fe
11 changed files with 1458 additions and 15 deletions

View file

@ -10,9 +10,15 @@ import { SidecarManager } from "./sidecar-manager.ts";
import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts";
import { randomUUID } from "crypto";
import { SearchDb } from "./search-db.ts";
import { checkForUpdates, getLastCheckTimestamp } from "./updater.ts";
import { RelayClient } from "./relay-client.ts";
import { initTelemetry, telemetry } from "./telemetry.ts";
import { homedir } from "os";
import { join } from "path";
/** Current app version — sourced from electrobun.config.ts at build time. */
const APP_VERSION = "0.0.1";
const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
@ -21,8 +27,12 @@ const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
const ptyClient = new PtyClient();
const sidecarManager = new SidecarManager();
const searchDb = new SearchDb();
const relayClient = new RelayClient();
const PLUGINS_DIR = join(homedir(), ".config", "agor", "plugins");
// Initialize telemetry (console-only unless AGOR_OTLP_ENDPOINT is set)
initTelemetry();
async function connectToDaemon(retries = 5, delayMs = 500): Promise<boolean> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
@ -874,6 +884,103 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
return { ok: false, content: "", error };
}
},
// ── Updater handlers ──────────────────────────────────────────────────
"updater.check": async () => {
try {
const result = await checkForUpdates(APP_VERSION);
return { ...result, error: undefined };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[updater.check]", err);
return {
available: false,
version: "",
downloadUrl: "",
releaseNotes: "",
checkedAt: Date.now(),
error,
};
}
},
"updater.getVersion": () => {
return {
version: APP_VERSION,
lastCheck: getLastCheckTimestamp(),
};
},
// ── Remote machine (relay) handlers ──────────────────────────────────
"remote.connect": async ({ url, token, label }) => {
try {
const machineId = await relayClient.connect(url, token, label);
return { ok: true, machineId };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[remote.connect]", err);
return { ok: false, error };
}
},
"remote.disconnect": ({ machineId }) => {
try {
relayClient.disconnect(machineId);
return { ok: true };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[remote.disconnect]", err);
return { ok: false, error };
}
},
"remote.list": () => {
try {
return { machines: relayClient.listMachines() };
} catch (err) {
console.error("[remote.list]", err);
return { machines: [] };
}
},
"remote.send": ({ machineId, command, payload }) => {
try {
relayClient.sendCommand(machineId, command, payload);
return { ok: true };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[remote.send]", err);
return { ok: false, error };
}
},
"remote.status": ({ machineId }) => {
try {
const info = relayClient.getStatus(machineId);
if (!info) {
return { status: "disconnected" as const, latencyMs: null, error: "Machine not found" };
}
return { status: info.status, latencyMs: info.latencyMs };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[remote.status]", err);
return { status: "error" as const, latencyMs: null, error };
}
},
// ── Telemetry handler ────────────────────────────────────────────────
"telemetry.log": ({ level, message, attributes }) => {
try {
telemetry.log(level, `[frontend] ${message}`, attributes ?? {});
return { ok: true };
} catch (err) {
console.error("[telemetry.log]", err);
return { ok: false };
}
},
},
messages: {},
@ -900,6 +1007,29 @@ ptyClient.on("session_closed", (msg) => {
}
});
// ── Forward relay events to WebView ─────────────────────────────────────────
relayClient.onEvent((machineId, event) => {
try {
rpc.send["remote.event"]({
machineId,
eventType: event.type,
sessionId: event.sessionId,
payload: event.payload,
});
} catch (err) {
console.error("[remote.event] forward error:", err);
}
});
relayClient.onStatus((machineId, status, error) => {
try {
rpc.send["remote.statusChange"]({ machineId, status, error });
} catch (err) {
console.error("[remote.statusChange] forward error:", err);
}
});
// ── App window ───────────────────────────────────────────────────────────────
async function getMainViewUrl(): Promise<string> {