mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 03:36:59 +00:00
fix(mesh): route peer→central traffic over the existing forward WS (#1094)
* fix(mesh): route peer→central traffic over the existing forward WS The reverse mesh callback path (`/api/mesh/proxy-tunnel-from-peer`) needed SENCHO_PRIMARY_URL on central plus a publicly reachable origin from the peer's perspective. In a typical homelab where central sits behind NAT, peer→central dispatch silently failed at the dialer's short-circuit and the headline "call any service on any node by hostname" worked one way only. The forward WS at `/api/mesh/proxy-tunnel` is already bidirectional end to end. Make the bridge a persistent control-plane primitive: dial every mesh-enabled proxy peer at startup, reconcile every 60 s, never idle-close. Peer→central traffic multiplexes over the same WS via `tcp_open_reverse`. Removed: - `meshProxyTunnelFromPeer.ts` WS handler and dispatch - `MeshCentralRegistry`, `PeerToCentralMeshSessionDialer` - `mesh_handshake` first-frame state machine in `meshProxyTunnel.ts` - `maybeSendBootstrap`, `buildHandshakeFrame` in the dialer - `mesh_proxy_callback_bootstrap` capability and `maybeWarnUnsetPrimaryUrl` - `mesh_centrals` table (drop migration; greenfield, no users) - `PilotTunnelManager.replaceOrRegisterProxyBridge` (dead after handler removal) - twelve associated unit/integration tests plus the peer-recovery branch in `MeshService.openCrossNode` Added: - `MeshService.proactiveBridgeFanout` selects every mesh-enabled proxy peer (no longer gated on `mesh_stacks` rows) - `startBridgeReconcileLoop` runs the fanout every 60 s (override via `SENCHO_MESH_RECONCILE_INTERVAL_MS`) - `MeshProxyTunnelDialer` default idle TTL is now `0` and exposes `isDialing(nodeId)` for the status surface - `MeshNodeStatus.reverseCallbackStatus` discriminator (`connected | connecting | unavailable | not_applicable`) surfaced via `/api/mesh/status` and rendered as a pill in the Routing tab - `openCrossNode` error message distinguishes "no proxy target" from "waiting for central to dial the reverse bridge" - New tests: `mesh-service-proxy-tunnel-reconcile`, `mesh-status-reverse-callback`, `mesh-proxy-tunnel-dialer-no-idle-close` SENCHO_PRIMARY_URL is no longer required for any mesh function. * fix(mesh): rewrite proxy-tunnel reconcile test contents The previous commit renamed the file but the rewritten test bodies stayed unstaged on top of the rename. This commit lands the actual rewrite: the fanout assertion now requires every mesh-enabled proxy peer to be dialed, not just those with `mesh_stacks` rows, and adds a reconcile-tick repeated-call test.
This commit is contained in:
@@ -21,6 +21,7 @@ function makeNode(over: Partial<MeshNodeStatus> & Pick<MeshNodeStatus, 'nodeId'
|
||||
pilotConnected: over.pilotConnected ?? false,
|
||||
reachableMode: over.reachableMode ?? 'unreachable',
|
||||
reachableReason: over.reachableReason ?? null,
|
||||
reverseCallbackStatus: over.reverseCallbackStatus ?? 'not_applicable',
|
||||
optedInStacks: over.optedInStacks ?? [],
|
||||
activeStreamCount: over.activeStreamCount ?? 0,
|
||||
};
|
||||
|
||||
@@ -72,6 +72,22 @@ export function RoutingNodeCard({
|
||||
unreachable
|
||||
</span>
|
||||
)}
|
||||
{status.reverseCallbackStatus === 'connecting' && (
|
||||
<span
|
||||
title="Central is dialing the reverse bridge to this peer."
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-card-border bg-card text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle"
|
||||
>
|
||||
reconnecting
|
||||
</span>
|
||||
)}
|
||||
{status.reverseCallbackStatus === 'unavailable' && (
|
||||
<span
|
||||
title="Peer→central tunnel is between dials. Central will redial on its next reconcile tick."
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-amber-500/40 bg-amber-500/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-amber-600 dark:text-amber-400"
|
||||
>
|
||||
reverse unavailable
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<TogglePill
|
||||
|
||||
@@ -22,6 +22,7 @@ function makeNode(over: Partial<MeshNodeStatus> & Pick<MeshNodeStatus, 'nodeId'
|
||||
pilotConnected: over.pilotConnected ?? false,
|
||||
reachableMode: over.reachableMode ?? 'unreachable',
|
||||
reachableReason: over.reachableReason ?? null,
|
||||
reverseCallbackStatus: over.reverseCallbackStatus ?? 'not_applicable',
|
||||
optedInStacks: over.optedInStacks ?? [],
|
||||
activeStreamCount: over.activeStreamCount ?? 0,
|
||||
};
|
||||
|
||||
@@ -28,6 +28,17 @@ export interface MeshAlias {
|
||||
|
||||
export type MeshReachableMode = 'local' | 'pilot' | 'proxy' | 'unreachable';
|
||||
|
||||
/**
|
||||
* State of the peer→central reverse path for a proxy-mode peer. Central
|
||||
* maintains a persistent forward WS to every mesh-enabled proxy peer;
|
||||
* peer→central traffic multiplexes over that same bridge. The four values:
|
||||
* - `connected`: bridge open, reverse multiplex available.
|
||||
* - `connecting`: dial in flight (transient).
|
||||
* - `unavailable`: bridge not open, no dial in flight; central will redial on its next reconcile tick.
|
||||
* - `not_applicable`: local node, pilot-mode peer, or mesh disabled.
|
||||
*/
|
||||
export type MeshReverseCallbackStatus = 'connected' | 'connecting' | 'unavailable' | 'not_applicable';
|
||||
|
||||
export interface MeshNodeStatus {
|
||||
nodeId: number;
|
||||
nodeName: string;
|
||||
@@ -40,6 +51,8 @@ export interface MeshNodeStatus {
|
||||
reachableMode: MeshReachableMode;
|
||||
/** Operator-facing reason when `reachableMode === 'unreachable'`. Null otherwise. */
|
||||
reachableReason: string | null;
|
||||
/** Peer→central reverse path state. `not_applicable` for non-proxy peers. */
|
||||
reverseCallbackStatus: MeshReverseCallbackStatus;
|
||||
optedInStacks: string[];
|
||||
activeStreamCount: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user