fix(presence): hide cli sessions; use numeric mac build
This commit is contained in:
@@ -70,7 +70,10 @@ final class PresenceReporter {
|
|||||||
private static func appVersionString() -> String {
|
private static func appVersionString() -> String {
|
||||||
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev"
|
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev"
|
||||||
if let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String {
|
if let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String {
|
||||||
return "\(version) (\(build))"
|
let trimmed = build.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !trimmed.isEmpty, trimmed != version {
|
||||||
|
return "\(version) (\(trimmed))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ This is meant to answer: “Which clients are currently connected?”
|
|||||||
|
|
||||||
Implementation: `src/gateway/server.ts` (WS `hello` handling uses `hello.client.instanceId` when provided; otherwise falls back to `connId`).
|
Implementation: `src/gateway/server.ts` (WS `hello` handling uses `hello.client.instanceId` when provided; otherwise falls back to `connId`).
|
||||||
|
|
||||||
|
#### Why one-off CLI commands do not show up
|
||||||
|
|
||||||
|
The CLI connects to the Gateway to execute one-off commands (health/status/send/agent/etc.). These are not “nodes” and would spam the Instances list, so the Gateway does not create presence entries for clients with `client.mode === "cli"`.
|
||||||
|
|
||||||
### 3) `system-event` beacons (client-reported presence)
|
### 3) `system-event` beacons (client-reported presence)
|
||||||
|
|
||||||
Clients can publish richer periodic beacons via the `system-event` method. The mac app uses this to report:
|
Clients can publish richer periodic beacons via the `system-event` method. The mac app uses this to report:
|
||||||
@@ -112,4 +116,3 @@ The store refreshes periodically and also applies `presence` WS events.
|
|||||||
- confirm clients send a stable `instanceId` in `hello`
|
- confirm clients send a stable `instanceId` in `hello`
|
||||||
- confirm beaconing uses the same `instanceId`
|
- confirm beaconing uses the same `instanceId`
|
||||||
- check whether the connection-derived entry is missing `instanceId` (then it will be keyed by `connId` and duplicates are expected on reconnect)
|
- check whether the connection-derived entry is missing `instanceId` (then it will be keyed by `connId` and duplicates are expected on reconnect)
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ BUNDLE_ID="${BUNDLE_ID:-com.steipete.clawdis.debug}"
|
|||||||
PKG_VERSION="$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")"
|
PKG_VERSION="$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")"
|
||||||
BUILD_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
BUILD_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
GIT_COMMIT=$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
GIT_COMMIT=$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
GIT_BUILD_NUMBER=$(cd "$ROOT_DIR" && git rev-list --count HEAD 2>/dev/null || echo "0")
|
||||||
APP_VERSION="${APP_VERSION:-$PKG_VERSION}"
|
APP_VERSION="${APP_VERSION:-$PKG_VERSION}"
|
||||||
APP_BUILD="${APP_BUILD:-$PKG_VERSION}"
|
APP_BUILD="${APP_BUILD:-$GIT_BUILD_NUMBER}"
|
||||||
BUILD_CONFIG="${BUILD_CONFIG:-debug}"
|
BUILD_CONFIG="${BUILD_CONFIG:-debug}"
|
||||||
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
|
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
|
||||||
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml}"
|
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml}"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { type AddressInfo, createServer } from "node:net";
|
import { type AddressInfo, createServer } from "node:net";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -792,6 +793,47 @@ describe("gateway server", () => {
|
|||||||
await server.close();
|
await server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("cli connections are not tracked as instances", async () => {
|
||||||
|
const { server, ws } = await startServerWithClient();
|
||||||
|
const cliId = `cli-${randomUUID()}`;
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "hello",
|
||||||
|
minProtocol: 1,
|
||||||
|
maxProtocol: 1,
|
||||||
|
client: {
|
||||||
|
name: "cli",
|
||||||
|
version: "dev",
|
||||||
|
platform: "test",
|
||||||
|
mode: "cli",
|
||||||
|
instanceId: cliId,
|
||||||
|
},
|
||||||
|
caps: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await onceMessage(ws, (o) => o.type === "hello-ok");
|
||||||
|
|
||||||
|
const presenceP = onceMessage(
|
||||||
|
ws,
|
||||||
|
(o) => o.type === "res" && o.id === "cli-presence",
|
||||||
|
4000,
|
||||||
|
);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "req",
|
||||||
|
id: "cli-presence",
|
||||||
|
method: "system-presence",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const presenceRes = await presenceP;
|
||||||
|
const entries = presenceRes.payload as Array<Record<string, unknown>>;
|
||||||
|
expect(entries.some((e) => e.instanceId === cliId)).toBe(false);
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
test("refuses to start when port already bound", async () => {
|
test("refuses to start when port already bound", async () => {
|
||||||
const { server: blocker, port } = await occupyPort();
|
const { server: blocker, port } = await occupyPort();
|
||||||
await expect(startGatewayServer(port)).rejects.toBeInstanceOf(
|
await expect(startGatewayServer(port)).rejects.toBeInstanceOf(
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type Client = {
|
|||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
hello: Hello;
|
hello: Hello;
|
||||||
connId: string;
|
connId: string;
|
||||||
|
presenceKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const METHODS = [
|
const METHODS = [
|
||||||
@@ -545,10 +546,9 @@ export async function startGatewayServer(
|
|||||||
`webchat disconnected code=${code} reason=${reason?.toString() || "n/a"} conn=${connId}`,
|
`webchat disconnected code=${code} reason=${reason?.toString() || "n/a"} conn=${connId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (client) {
|
if (client?.presenceKey) {
|
||||||
// mark presence as disconnected
|
// mark presence as disconnected
|
||||||
const key = client.hello.client.instanceId || connId;
|
upsertPresence(client.presenceKey, {
|
||||||
upsertPresence(key, {
|
|
||||||
reason: "disconnect",
|
reason: "disconnect",
|
||||||
});
|
});
|
||||||
presenceVersion += 1;
|
presenceVersion += 1;
|
||||||
@@ -639,8 +639,11 @@ export async function startGatewayServer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldTrackPresence = hello.client.mode !== "cli";
|
||||||
// synthesize presence entry for this connection (client fingerprint)
|
// synthesize presence entry for this connection (client fingerprint)
|
||||||
const presenceKey = hello.client.instanceId || connId;
|
const presenceKey = shouldTrackPresence
|
||||||
|
? hello.client.instanceId || connId
|
||||||
|
: undefined;
|
||||||
logWs("in", "hello", {
|
logWs("in", "hello", {
|
||||||
connId,
|
connId,
|
||||||
client: hello.client.name,
|
client: hello.client.name,
|
||||||
@@ -655,15 +658,17 @@ export async function startGatewayServer(
|
|||||||
`webchat connected conn=${connId} remote=${remoteAddr ?? "?"} client=${describeHello(hello)}`,
|
`webchat connected conn=${connId} remote=${remoteAddr ?? "?"} client=${describeHello(hello)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
upsertPresence(presenceKey, {
|
if (presenceKey) {
|
||||||
host: hello.client.name || os.hostname(),
|
upsertPresence(presenceKey, {
|
||||||
ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr,
|
host: hello.client.name || os.hostname(),
|
||||||
version: hello.client.version,
|
ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr,
|
||||||
mode: hello.client.mode,
|
version: hello.client.version,
|
||||||
instanceId: hello.client.instanceId,
|
mode: hello.client.mode,
|
||||||
reason: "connect",
|
instanceId: hello.client.instanceId,
|
||||||
});
|
reason: "connect",
|
||||||
presenceVersion += 1;
|
});
|
||||||
|
presenceVersion += 1;
|
||||||
|
}
|
||||||
const snapshot = buildSnapshot();
|
const snapshot = buildSnapshot();
|
||||||
if (healthCache) {
|
if (healthCache) {
|
||||||
snapshot.health = healthCache;
|
snapshot.health = healthCache;
|
||||||
@@ -692,7 +697,7 @@ export async function startGatewayServer(
|
|||||||
clearTimeout(handshakeTimer);
|
clearTimeout(handshakeTimer);
|
||||||
// Add the client only after the hello response is ready so no tick/presence
|
// Add the client only after the hello response is ready so no tick/presence
|
||||||
// events reach it before the handshake completes.
|
// events reach it before the handshake completes.
|
||||||
client = { socket, hello, connId };
|
client = { socket, hello, connId, presenceKey };
|
||||||
logWs("out", "hello-ok", {
|
logWs("out", "hello-ok", {
|
||||||
connId,
|
connId,
|
||||||
methods: METHODS.length,
|
methods: METHODS.length,
|
||||||
|
|||||||
Reference in New Issue
Block a user