fix(ai): fix mention status colors and dedup for docker/VM/LXC agents (#1252)

Three fixes for remaining mention autocomplete issues:

- Status dots now correctly show green/red/yellow for online/offline/
  degraded statuses (previously only handled running/stopped/paused)
- Docker hosts merge with their host agent via agentId cross-reference
- VMs and LXC containers merge with host agents running inside them
  via linkedVmId/linkedContainerId backend ID aliases
This commit is contained in:
rcourtman 2026-02-20 22:53:28 +00:00
parent 06bd8cf2b6
commit 50e476c942
3 changed files with 139 additions and 10 deletions

View file

@ -110,12 +110,20 @@ export function MentionAutocomplete(props: MentionAutocompleteProps) {
// Get status color
const getStatusColor = (status?: string) => {
switch (status) {
switch (status?.toLowerCase()) {
case 'running':
case 'online':
case 'healthy':
case 'up':
return 'bg-green-500';
case 'stopped':
case 'offline':
case 'error':
case 'failed':
return 'bg-red-500';
case 'paused':
case 'degraded':
case 'warning':
return 'bg-yellow-500';
default:
return 'bg-gray-400';

View file

@ -113,6 +113,109 @@ describe('mentionResources', () => {
expect(nodeOrHost[0].name).toBe('pve01');
});
it('deduplicates docker host and host agent via agentId (#1252)', () => {
// Docker host has agentId matching the host agent's id
const state = {
nodes: [],
vms: [],
containers: [],
dockerHosts: [
{
id: 'docker-host-abc',
agentId: 'agent-xyz',
hostname: 'docker01',
displayName: 'docker01',
status: 'online',
lastSeen: Date.now(),
containers: [],
},
],
hosts: [
{
id: 'agent-xyz',
hostname: 'docker01',
displayName: 'docker01',
status: 'online',
lastSeen: Date.now(),
memory: { total: 16, used: 8, free: 8, usage: 50 },
},
],
} as unknown as State;
const resources = buildMentionResources(state);
const hostEntries = resources.filter((r) => r.type === 'host');
expect(hostEntries).toHaveLength(1);
expect(hostEntries[0].name).toBe('docker01');
});
it('deduplicates VM and host agent running inside it via linkedVmId (#1252)', () => {
const state = {
nodes: [],
vms: [
{
id: 'pve01-100',
vmid: 100,
name: 'my-vm',
node: 'pve01',
instance: 'pve-instance-1',
status: 'running',
},
],
containers: [],
dockerHosts: [],
hosts: [
{
id: 'agent-in-vm',
hostname: 'my-vm',
displayName: 'my-vm',
status: 'online',
lastSeen: Date.now(),
linkedVmId: 'pve01-100',
memory: { total: 8, used: 4, free: 4, usage: 50 },
},
],
} as unknown as State;
const resources = buildMentionResources(state);
const vmOrHost = resources.filter((r) => r.type === 'vm' || r.type === 'host');
expect(vmOrHost).toHaveLength(1);
expect(vmOrHost[0].name).toBe('my-vm');
});
it('deduplicates LXC container and host agent running inside it via linkedContainerId (#1252)', () => {
const state = {
nodes: [],
vms: [],
containers: [
{
id: 'pve01-200',
vmid: 200,
name: 'my-ct',
node: 'pve01',
instance: 'pve-instance-1',
status: 'running',
},
],
dockerHosts: [],
hosts: [
{
id: 'agent-in-ct',
hostname: 'my-ct',
displayName: 'my-ct',
status: 'online',
lastSeen: Date.now(),
linkedContainerId: 'pve01-200',
memory: { total: 4, used: 2, free: 2, usage: 50 },
},
],
} as unknown as State;
const resources = buildMentionResources(state);
const ctOrHost = resources.filter((r) => r.type === 'container' || r.type === 'host');
expect(ctOrHost).toHaveLength(1);
expect(ctOrHost[0].name).toBe('my-ct');
});
it('deduplicates cluster node mentions from multiple instances and keeps the healthiest status', () => {
const state = {
nodes: [

View file

@ -138,7 +138,11 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc
status: vm.status,
node: vm.node,
},
[`vm-id:${vm.node}:${vm.vmid}`],
[
`vm-id:${vm.node}:${vm.vmid}`,
// Register backend VM ID so host agents with linkedVmId can merge (#1252)
`vm-backend-id:${vm.id}`,
],
);
}
@ -153,12 +157,25 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc
status: container.status,
node: container.node,
},
[`lxc-id:${container.node}:${container.vmid}`],
[
`lxc-id:${container.node}:${container.vmid}`,
// Register backend container ID so host agents with linkedContainerId can merge (#1252)
`lxc-backend-id:${container.id}`,
],
);
}
for (const host of state.dockerHosts || []) {
const hostName = host.displayName || host.hostname || host.id;
const dockerAliases = [
`docker-host-id:${host.id}`,
`host-name:${hostName}`,
`host-hostname:${host.hostname}`,
];
// Link docker host to its host agent via agentId so they merge (#1252)
if (host.agentId) {
dockerAliases.push(`agent-host-id:${host.agentId}`);
}
upsertMentionResource(
byKey,
aliasToKey,
@ -168,11 +185,7 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc
type: 'host',
status: host.status || 'online',
},
[
`docker-host-id:${host.id}`,
`host-name:${hostName}`,
`host-hostname:${host.hostname}`,
],
dockerAliases,
);
for (const container of host.containers || []) {
@ -223,11 +236,16 @@ export function buildMentionResources(state: MentionStateSubset): MentionResourc
`host-name:${hostName}`,
`host-hostname:${host.hostname}`,
];
// If this host agent is linked to a PVE node, add the node's backend ID alias so they merge (#1252).
// linkedNodeId is the node's backend ID (format: "instance-nodeName").
// If this host agent is linked to a PVE entity, add its backend ID alias so they merge (#1252).
if (host.linkedNodeId) {
aliases.push(`node-backend-id:${host.linkedNodeId}`);
}
if (host.linkedVmId) {
aliases.push(`vm-backend-id:${host.linkedVmId}`);
}
if (host.linkedContainerId) {
aliases.push(`lxc-backend-id:${host.linkedContainerId}`);
}
upsertMentionResource(
byKey,
aliasToKey,