feat: enrich presence with roles
This commit is contained in:
@@ -205,6 +205,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
public let tags: [String]?
|
public let tags: [String]?
|
||||||
public let text: String?
|
public let text: String?
|
||||||
public let ts: Int
|
public let ts: Int
|
||||||
|
public let deviceid: String?
|
||||||
|
public let roles: [String]?
|
||||||
|
public let scopes: [String]?
|
||||||
public let instanceid: String?
|
public let instanceid: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -220,6 +223,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
tags: [String]?,
|
tags: [String]?,
|
||||||
text: String?,
|
text: String?,
|
||||||
ts: Int,
|
ts: Int,
|
||||||
|
deviceid: String?,
|
||||||
|
roles: [String]?,
|
||||||
|
scopes: [String]?,
|
||||||
instanceid: String?
|
instanceid: String?
|
||||||
) {
|
) {
|
||||||
self.host = host
|
self.host = host
|
||||||
@@ -234,6 +240,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.text = text
|
self.text = text
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
|
self.deviceid = deviceid
|
||||||
|
self.roles = roles
|
||||||
|
self.scopes = scopes
|
||||||
self.instanceid = instanceid
|
self.instanceid = instanceid
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
@@ -249,6 +258,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
case tags
|
case tags
|
||||||
case text
|
case text
|
||||||
case ts
|
case ts
|
||||||
|
case deviceid = "deviceId"
|
||||||
|
case roles
|
||||||
|
case scopes
|
||||||
case instanceid = "instanceId"
|
case instanceid = "instanceId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
public let tags: [String]?
|
public let tags: [String]?
|
||||||
public let text: String?
|
public let text: String?
|
||||||
public let ts: Int
|
public let ts: Int
|
||||||
|
public let deviceid: String?
|
||||||
|
public let roles: [String]?
|
||||||
|
public let scopes: [String]?
|
||||||
public let instanceid: String?
|
public let instanceid: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -220,6 +223,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
tags: [String]?,
|
tags: [String]?,
|
||||||
text: String?,
|
text: String?,
|
||||||
ts: Int,
|
ts: Int,
|
||||||
|
deviceid: String?,
|
||||||
|
roles: [String]?,
|
||||||
|
scopes: [String]?,
|
||||||
instanceid: String?
|
instanceid: String?
|
||||||
) {
|
) {
|
||||||
self.host = host
|
self.host = host
|
||||||
@@ -234,6 +240,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.text = text
|
self.text = text
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
|
self.deviceid = deviceid
|
||||||
|
self.roles = roles
|
||||||
|
self.scopes = scopes
|
||||||
self.instanceid = instanceid
|
self.instanceid = instanceid
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
@@ -249,6 +258,9 @@ public struct PresenceEntry: Codable, Sendable {
|
|||||||
case tags
|
case tags
|
||||||
case text
|
case text
|
||||||
case ts
|
case ts
|
||||||
|
case deviceid = "deviceId"
|
||||||
|
case roles
|
||||||
|
case scopes
|
||||||
case instanceid = "instanceId"
|
case instanceid = "instanceId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export const PresenceEntrySchema = Type.Object(
|
|||||||
tags: Type.Optional(Type.Array(NonEmptyString)),
|
tags: Type.Optional(Type.Array(NonEmptyString)),
|
||||||
text: Type.Optional(Type.String()),
|
text: Type.Optional(Type.String()),
|
||||||
ts: Type.Integer({ minimum: 0 }),
|
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),
|
instanceId: Type.Optional(NonEmptyString),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const systemHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sessionKey = resolveMainSessionKeyFromConfig();
|
const sessionKey = resolveMainSessionKeyFromConfig();
|
||||||
|
const deviceId = typeof params.deviceId === "string" ? params.deviceId : undefined;
|
||||||
const instanceId = typeof params.instanceId === "string" ? params.instanceId : undefined;
|
const instanceId = typeof params.instanceId === "string" ? params.instanceId : undefined;
|
||||||
const host = typeof params.host === "string" ? params.host : undefined;
|
const host = typeof params.host === "string" ? params.host : undefined;
|
||||||
const ip = typeof params.ip === "string" ? params.ip : undefined;
|
const ip = typeof params.ip === "string" ? params.ip : undefined;
|
||||||
@@ -51,12 +52,21 @@ export const systemHandlers: GatewayRequestHandlers = {
|
|||||||
? params.lastInputSeconds
|
? params.lastInputSeconds
|
||||||
: undefined;
|
: undefined;
|
||||||
const reason = typeof params.reason === "string" ? params.reason : 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 =
|
const tags =
|
||||||
Array.isArray(params.tags) && params.tags.every((t) => typeof t === "string")
|
Array.isArray(params.tags) && params.tags.every((t) => typeof t === "string")
|
||||||
? (params.tags as string[])
|
? (params.tags as string[])
|
||||||
: undefined;
|
: undefined;
|
||||||
const presenceUpdate = updateSystemPresence({
|
const presenceUpdate = updateSystemPresence({
|
||||||
text,
|
text,
|
||||||
|
deviceId,
|
||||||
instanceId,
|
instanceId,
|
||||||
host,
|
host,
|
||||||
ip,
|
ip,
|
||||||
@@ -67,6 +77,8 @@ export const systemHandlers: GatewayRequestHandlers = {
|
|||||||
modelIdentifier,
|
modelIdentifier,
|
||||||
lastInputSeconds,
|
lastInputSeconds,
|
||||||
reason,
|
reason,
|
||||||
|
roles,
|
||||||
|
scopes,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
const isNodePresenceLine = text.startsWith("Node:");
|
const isNodePresenceLine = text.startsWith("Node:");
|
||||||
|
|||||||
@@ -619,6 +619,9 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
deviceFamily: connectParams.client.deviceFamily,
|
deviceFamily: connectParams.client.deviceFamily,
|
||||||
modelIdentifier: connectParams.client.modelIdentifier,
|
modelIdentifier: connectParams.client.modelIdentifier,
|
||||||
mode: connectParams.client.mode,
|
mode: connectParams.client.mode,
|
||||||
|
deviceId: connectParams.device?.id,
|
||||||
|
roles: [role],
|
||||||
|
scopes,
|
||||||
instanceId: connectParams.device?.id ?? instanceId,
|
instanceId: connectParams.device?.id ?? instanceId,
|
||||||
reason: "connect",
|
reason: "connect",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export type SystemPresence = {
|
|||||||
lastInputSeconds?: number;
|
lastInputSeconds?: number;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
deviceId?: string;
|
||||||
|
roles?: string[];
|
||||||
|
scopes?: string[];
|
||||||
instanceId?: string;
|
instanceId?: string;
|
||||||
text: string;
|
text: string;
|
||||||
ts: number;
|
ts: number;
|
||||||
@@ -153,6 +156,7 @@ function parsePresence(text: string): SystemPresence {
|
|||||||
|
|
||||||
type SystemPresencePayload = {
|
type SystemPresencePayload = {
|
||||||
text: string;
|
text: string;
|
||||||
|
deviceId?: string;
|
||||||
instanceId?: string;
|
instanceId?: string;
|
||||||
host?: string;
|
host?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
@@ -163,13 +167,28 @@ type SystemPresencePayload = {
|
|||||||
lastInputSeconds?: number;
|
lastInputSeconds?: number;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
roles?: string[];
|
||||||
|
scopes?: string[];
|
||||||
tags?: 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 {
|
export function updateSystemPresence(payload: SystemPresencePayload): SystemPresenceUpdate {
|
||||||
ensureSelfPresence();
|
ensureSelfPresence();
|
||||||
const parsed = parsePresence(payload.text);
|
const parsed = parsePresence(payload.text);
|
||||||
const key =
|
const key =
|
||||||
|
normalizePresenceKey(payload.deviceId) ||
|
||||||
normalizePresenceKey(payload.instanceId) ||
|
normalizePresenceKey(payload.instanceId) ||
|
||||||
normalizePresenceKey(parsed.instanceId) ||
|
normalizePresenceKey(parsed.instanceId) ||
|
||||||
normalizePresenceKey(parsed.host) ||
|
normalizePresenceKey(parsed.host) ||
|
||||||
@@ -191,6 +210,9 @@ export function updateSystemPresence(payload: SystemPresencePayload): SystemPres
|
|||||||
lastInputSeconds:
|
lastInputSeconds:
|
||||||
payload.lastInputSeconds ?? parsed.lastInputSeconds ?? existing.lastInputSeconds,
|
payload.lastInputSeconds ?? parsed.lastInputSeconds ?? existing.lastInputSeconds,
|
||||||
reason: payload.reason ?? parsed.reason ?? existing.reason,
|
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,
|
instanceId: payload.instanceId ?? parsed.instanceId ?? existing.instanceId,
|
||||||
text: payload.text || parsed.text || existing.text,
|
text: payload.text || parsed.text || existing.text,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
@@ -221,9 +243,13 @@ export function upsertPresence(key: string, presence: Partial<SystemPresence>) {
|
|||||||
ensureSelfPresence();
|
ensureSelfPresence();
|
||||||
const normalizedKey = normalizePresenceKey(key) ?? os.hostname().toLowerCase();
|
const normalizedKey = normalizePresenceKey(key) ?? os.hostname().toLowerCase();
|
||||||
const existing = entries.get(normalizedKey) ?? ({} as SystemPresence);
|
const existing = entries.get(normalizedKey) ?? ({} as SystemPresence);
|
||||||
|
const roles = mergeStringList(existing.roles, presence.roles);
|
||||||
|
const scopes = mergeStringList(existing.scopes, presence.scopes);
|
||||||
const merged: SystemPresence = {
|
const merged: SystemPresence = {
|
||||||
...existing,
|
...existing,
|
||||||
...presence,
|
...presence,
|
||||||
|
roles,
|
||||||
|
scopes,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
text:
|
text:
|
||||||
presence.text ||
|
presence.text ||
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export type ConfigSchemaResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PresenceEntry = {
|
export type PresenceEntry = {
|
||||||
|
deviceId?: string | null;
|
||||||
instanceId?: string | null;
|
instanceId?: string | null;
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
ip?: string | null;
|
ip?: string | null;
|
||||||
@@ -256,6 +257,8 @@ export type PresenceEntry = {
|
|||||||
mode?: string | null;
|
mode?: string | null;
|
||||||
lastInputSeconds?: number | null;
|
lastInputSeconds?: number | null;
|
||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
|
roles?: string[] | null;
|
||||||
|
scopes?: string[] | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
ts?: number | null;
|
ts?: number | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ function renderEntry(entry: PresenceEntry) {
|
|||||||
? `${entry.lastInputSeconds}s ago`
|
? `${entry.lastInputSeconds}s ago`
|
||||||
: "n/a";
|
: "n/a";
|
||||||
const mode = entry.mode ?? "unknown";
|
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`
|
return html`
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
<div class="list-main">
|
<div class="list-main">
|
||||||
@@ -55,6 +63,8 @@ function renderEntry(entry: PresenceEntry) {
|
|||||||
<div class="list-sub">${formatPresenceSummary(entry)}</div>
|
<div class="list-sub">${formatPresenceSummary(entry)}</div>
|
||||||
<div class="chip-row">
|
<div class="chip-row">
|
||||||
<span class="chip">${mode}</span>
|
<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.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
|
||||||
${entry.deviceFamily
|
${entry.deviceFamily
|
||||||
? html`<span class="chip">${entry.deviceFamily}</span>`
|
? html`<span class="chip">${entry.deviceFamily}</span>`
|
||||||
|
|||||||
Reference in New Issue
Block a user