feat: enrich presence with roles

This commit is contained in:
Peter Steinberger
2026-01-20 12:16:36 +00:00
parent 7720106624
commit 409a16060b
8 changed files with 81 additions and 0 deletions

View File

@@ -205,6 +205,9 @@ public struct PresenceEntry: Codable, Sendable {
public let tags: [String]?
public let text: String?
public let ts: Int
public let deviceid: String?
public let roles: [String]?
public let scopes: [String]?
public let instanceid: String?
public init(
@@ -220,6 +223,9 @@ public struct PresenceEntry: Codable, Sendable {
tags: [String]?,
text: String?,
ts: Int,
deviceid: String?,
roles: [String]?,
scopes: [String]?,
instanceid: String?
) {
self.host = host
@@ -234,6 +240,9 @@ public struct PresenceEntry: Codable, Sendable {
self.tags = tags
self.text = text
self.ts = ts
self.deviceid = deviceid
self.roles = roles
self.scopes = scopes
self.instanceid = instanceid
}
private enum CodingKeys: String, CodingKey {
@@ -249,6 +258,9 @@ public struct PresenceEntry: Codable, Sendable {
case tags
case text
case ts
case deviceid = "deviceId"
case roles
case scopes
case instanceid = "instanceId"
}
}

View File

@@ -205,6 +205,9 @@ public struct PresenceEntry: Codable, Sendable {
public let tags: [String]?
public let text: String?
public let ts: Int
public let deviceid: String?
public let roles: [String]?
public let scopes: [String]?
public let instanceid: String?
public init(
@@ -220,6 +223,9 @@ public struct PresenceEntry: Codable, Sendable {
tags: [String]?,
text: String?,
ts: Int,
deviceid: String?,
roles: [String]?,
scopes: [String]?,
instanceid: String?
) {
self.host = host
@@ -234,6 +240,9 @@ public struct PresenceEntry: Codable, Sendable {
self.tags = tags
self.text = text
self.ts = ts
self.deviceid = deviceid
self.roles = roles
self.scopes = scopes
self.instanceid = instanceid
}
private enum CodingKeys: String, CodingKey {
@@ -249,6 +258,9 @@ public struct PresenceEntry: Codable, Sendable {
case tags
case text
case ts
case deviceid = "deviceId"
case roles
case scopes
case instanceid = "instanceId"
}
}

View File

@@ -15,6 +15,9 @@ export const PresenceEntrySchema = Type.Object(
tags: Type.Optional(Type.Array(NonEmptyString)),
text: Type.Optional(Type.String()),
ts: Type.Integer({ minimum: 0 }),
deviceId: Type.Optional(NonEmptyString),
roles: Type.Optional(Type.Array(NonEmptyString)),
scopes: Type.Optional(Type.Array(NonEmptyString)),
instanceId: Type.Optional(NonEmptyString),
},
{ additionalProperties: false },

View File

@@ -37,6 +37,7 @@ export const systemHandlers: GatewayRequestHandlers = {
return;
}
const sessionKey = resolveMainSessionKeyFromConfig();
const deviceId = typeof params.deviceId === "string" ? params.deviceId : undefined;
const instanceId = typeof params.instanceId === "string" ? params.instanceId : undefined;
const host = typeof params.host === "string" ? params.host : undefined;
const ip = typeof params.ip === "string" ? params.ip : undefined;
@@ -51,12 +52,21 @@ export const systemHandlers: GatewayRequestHandlers = {
? params.lastInputSeconds
: undefined;
const reason = typeof params.reason === "string" ? params.reason : undefined;
const roles =
Array.isArray(params.roles) && params.roles.every((t) => typeof t === "string")
? (params.roles as string[])
: undefined;
const scopes =
Array.isArray(params.scopes) && params.scopes.every((t) => typeof t === "string")
? (params.scopes as string[])
: undefined;
const tags =
Array.isArray(params.tags) && params.tags.every((t) => typeof t === "string")
? (params.tags as string[])
: undefined;
const presenceUpdate = updateSystemPresence({
text,
deviceId,
instanceId,
host,
ip,
@@ -67,6 +77,8 @@ export const systemHandlers: GatewayRequestHandlers = {
modelIdentifier,
lastInputSeconds,
reason,
roles,
scopes,
tags,
});
const isNodePresenceLine = text.startsWith("Node:");

View File

@@ -619,6 +619,9 @@ export function attachGatewayWsMessageHandler(params: {
deviceFamily: connectParams.client.deviceFamily,
modelIdentifier: connectParams.client.modelIdentifier,
mode: connectParams.client.mode,
deviceId: connectParams.device?.id,
roles: [role],
scopes,
instanceId: connectParams.device?.id ?? instanceId,
reason: "connect",
});

View File

@@ -11,6 +11,9 @@ export type SystemPresence = {
lastInputSeconds?: number;
mode?: string;
reason?: string;
deviceId?: string;
roles?: string[];
scopes?: string[];
instanceId?: string;
text: string;
ts: number;
@@ -153,6 +156,7 @@ function parsePresence(text: string): SystemPresence {
type SystemPresencePayload = {
text: string;
deviceId?: string;
instanceId?: string;
host?: string;
ip?: string;
@@ -163,13 +167,28 @@ type SystemPresencePayload = {
lastInputSeconds?: number;
mode?: string;
reason?: string;
roles?: string[];
scopes?: string[];
tags?: string[];
};
function mergeStringList(...values: Array<string[] | undefined>): string[] | undefined {
const out = new Set<string>();
for (const list of values) {
if (!Array.isArray(list)) continue;
for (const item of list) {
const trimmed = String(item).trim();
if (trimmed) out.add(trimmed);
}
}
return out.size > 0 ? [...out] : undefined;
}
export function updateSystemPresence(payload: SystemPresencePayload): SystemPresenceUpdate {
ensureSelfPresence();
const parsed = parsePresence(payload.text);
const key =
normalizePresenceKey(payload.deviceId) ||
normalizePresenceKey(payload.instanceId) ||
normalizePresenceKey(parsed.instanceId) ||
normalizePresenceKey(parsed.host) ||
@@ -191,6 +210,9 @@ export function updateSystemPresence(payload: SystemPresencePayload): SystemPres
lastInputSeconds:
payload.lastInputSeconds ?? parsed.lastInputSeconds ?? existing.lastInputSeconds,
reason: payload.reason ?? parsed.reason ?? existing.reason,
deviceId: payload.deviceId ?? existing.deviceId,
roles: mergeStringList(existing.roles, payload.roles),
scopes: mergeStringList(existing.scopes, payload.scopes),
instanceId: payload.instanceId ?? parsed.instanceId ?? existing.instanceId,
text: payload.text || parsed.text || existing.text,
ts: Date.now(),
@@ -221,9 +243,13 @@ export function upsertPresence(key: string, presence: Partial<SystemPresence>) {
ensureSelfPresence();
const normalizedKey = normalizePresenceKey(key) ?? os.hostname().toLowerCase();
const existing = entries.get(normalizedKey) ?? ({} as SystemPresence);
const roles = mergeStringList(existing.roles, presence.roles);
const scopes = mergeStringList(existing.scopes, presence.scopes);
const merged: SystemPresence = {
...existing,
...presence,
roles,
scopes,
ts: Date.now(),
text:
presence.text ||

View File

@@ -246,6 +246,7 @@ export type ConfigSchemaResponse = {
};
export type PresenceEntry = {
deviceId?: string | null;
instanceId?: string | null;
host?: string | null;
ip?: string | null;
@@ -256,6 +257,8 @@ export type PresenceEntry = {
mode?: string | null;
lastInputSeconds?: number | null;
reason?: string | null;
roles?: string[] | null;
scopes?: string[] | null;
text?: string | null;
ts?: number | null;
};

View File

@@ -48,6 +48,14 @@ function renderEntry(entry: PresenceEntry) {
? `${entry.lastInputSeconds}s ago`
: "n/a";
const mode = entry.mode ?? "unknown";
const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : [];
const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : [];
const scopesLabel =
scopes.length > 0
? scopes.length > 3
? `${scopes.length} scopes`
: `scopes: ${scopes.join(", ")}`
: null;
return html`
<div class="list-item">
<div class="list-main">
@@ -55,6 +63,8 @@ function renderEntry(entry: PresenceEntry) {
<div class="list-sub">${formatPresenceSummary(entry)}</div>
<div class="chip-row">
<span class="chip">${mode}</span>
${roles.map((role) => html`<span class="chip">${role}</span>`)}
${scopesLabel ? html`<span class="chip">${scopesLabel}</span>` : nothing}
${entry.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
${entry.deviceFamily
? html`<span class="chip">${entry.deviceFamily}</span>`