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

@ -0,0 +1,139 @@
<script lang="ts">
/**
* Full-screen splash overlay shown on app startup.
* Auto-dismisses when the `ready` prop becomes true.
* Fade-out transition: 300ms opacity.
*/
interface Props {
/** Set to true when app initialization is complete. */
ready: boolean;
}
let { ready }: Props = $props();
let visible = $state(true);
let fading = $state(false);
// When ready flips to true, start fade-out then hide
$effect(() => {
if (ready && visible && !fading) {
fading = true;
setTimeout(() => {
visible = false;
}, 300);
}
});
</script>
<div
class="splash"
style:display={visible ? 'flex' : 'none'}
class:fading
role="status"
aria-label="Loading application"
>
<div class="splash-content">
<div class="logo-text" aria-hidden="true">AGOR</div>
<div class="version">v0.0.1</div>
<div class="loading-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<div class="loading-label">Loading...</div>
</div>
</div>
<style>
.splash {
position: fixed;
inset: 0;
z-index: 10000;
background: var(--ctp-base);
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 300ms ease-out;
}
.splash.fading {
opacity: 0;
pointer-events: none;
}
.splash-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.logo-text {
font-family: 'Inter', system-ui, sans-serif;
font-weight: 900;
font-size: 4rem;
letter-spacing: 0.3em;
background: linear-gradient(
135deg,
var(--ctp-mauve),
var(--ctp-blue),
var(--ctp-sapphire),
var(--ctp-teal)
);
background-size: 300% 300%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient-shift 3s ease infinite;
user-select: none;
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.version {
font-size: 0.875rem;
color: var(--ctp-overlay0);
font-family: var(--term-font-family, monospace);
letter-spacing: 0.05em;
}
.loading-indicator {
display: flex;
gap: 0.375rem;
margin-top: 0.5rem;
}
.dot {
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
background: var(--ctp-overlay1);
animation: pulse-dot 1.2s ease-in-out infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse-dot {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
.loading-label {
font-size: 0.75rem;
color: var(--ctp-subtext0);
font-family: var(--ui-font-family, system-ui, sans-serif);
animation: pulse-text 2s ease-in-out infinite;
}
@keyframes pulse-text {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
</style>