agent-relay-server 0.4.15 → 0.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -3
- package/package.json +2 -2
- package/public/dashboard.js +100 -7
- package/public/index.html +26 -1
- package/src/cli.ts +384 -0
- package/src/config.ts +1 -0
- package/src/db.ts +597 -28
- package/src/index.ts +13 -3
- package/src/routes.ts +330 -13
- package/src/security.ts +31 -1
- package/src/sse.ts +8 -2
- package/src/types.ts +65 -0
package/README.md
CHANGED
|
@@ -82,18 +82,42 @@ Open a second session and say:
|
|
|
82
82
|
|
|
83
83
|
## Configuration
|
|
84
84
|
|
|
85
|
-
Both integrations share the same
|
|
85
|
+
Both integrations share the same provider env vars:
|
|
86
86
|
|
|
87
87
|
| Env var | Default | Purpose |
|
|
88
88
|
|---------|---------|---------|
|
|
89
89
|
| `AGENT_RELAY_URL` | `http://localhost:4850` | Relay server URL |
|
|
90
90
|
| `AGENT_RELAY_TOKEN` | unset | Auth token (required for remote relays) |
|
|
91
91
|
| `AGENT_RELAY_CAPS` | `chat` | Comma-separated agent capabilities |
|
|
92
|
+
| `AGENT_RELAY_TAGS` | unset | Extra comma-separated tags added to provider defaults |
|
|
93
|
+
| `AGENT_RELAY_LABEL` | unset | Human-friendly agent label set at registration |
|
|
94
|
+
| `AGENT_RELAY_CHANNELS` | all | Comma-separated channel subscriptions; unchannelled direct work still arrives |
|
|
95
|
+
| `AGENT_RELAY_PROFILE` | unset | Named profile loaded from the profiles file |
|
|
96
|
+
| `AGENT_RELAY_PROFILES_FILE` | `~/.config/agent-relay/profiles.json` | JSON profile file |
|
|
92
97
|
| `AGENT_RELAY_APPROVAL` | `open` | Approval mode: `open`, `guarded`, `read-only` |
|
|
93
98
|
|
|
99
|
+
Profiles preconfigure labels, tags, capabilities, channels, approval mode, and meta fields:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"backend-tester": {
|
|
104
|
+
"label": "backend tester",
|
|
105
|
+
"tags": ["backend", "api", "test"],
|
|
106
|
+
"capabilities": ["chat", "review", "test", "backend"],
|
|
107
|
+
"channels": ["backend", "qa"],
|
|
108
|
+
"approval": "guarded",
|
|
109
|
+
"meta": { "role": "backend-tester" }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Launch with `AGENT_RELAY_PROFILE=backend-tester codex` or
|
|
115
|
+
`AGENT_RELAY_PROFILE=backend-tester claude`. Explicit env vars override the
|
|
116
|
+
profile where they overlap.
|
|
117
|
+
|
|
94
118
|
Codex has additional tuning vars documented in [codex/README.md](codex/README.md).
|
|
95
119
|
|
|
96
|
-
Agent IDs are deterministic: `{hostname}-{
|
|
120
|
+
Agent IDs are deterministic: `{hostname}-{project}-{session-hash}`.
|
|
97
121
|
|
|
98
122
|
## Message Targeting
|
|
99
123
|
|
|
@@ -111,7 +135,7 @@ The plugin or sidecar registers the agent and injects messaging context:
|
|
|
111
135
|
|
|
112
136
|
```
|
|
113
137
|
Agent Relay active. Your agent ID: macmini2-cli-myproject-a1b2c3
|
|
114
|
-
Relay URL: http://localhost:4850 | Server: 0.4.
|
|
138
|
+
Relay URL: http://localhost:4850 | Server: 0.4.16 | Plugin: 0.4.16
|
|
115
139
|
Approval mode: open
|
|
116
140
|
```
|
|
117
141
|
|
|
@@ -167,6 +191,14 @@ claimable message. Posting `"status": "resolved"` marks the active task `done`.
|
|
|
167
191
|
When an integration has `callbackUrl`, Agent Relay posts task lifecycle events
|
|
168
192
|
(creation, claim, status changes) back to the caller.
|
|
169
193
|
|
|
194
|
+
Integration tokens can also be used as scoped API tokens when the full admin
|
|
195
|
+
`AGENT_RELAY_TOKEN` would be too broad. Supported scopes include `stats:read`,
|
|
196
|
+
`health:read`, `events:read`, `agents:read`, `agents:write`, `messages:read`,
|
|
197
|
+
`messages:write`, `tasks:read`, `tasks:write`, `pairs:read`, `pairs:write`,
|
|
198
|
+
`system:write`, and `*`.
|
|
199
|
+
The event ingress endpoint also accepts the legacy `tasks:create` and
|
|
200
|
+
`events:create` scopes.
|
|
201
|
+
|
|
170
202
|
Task lifecycle API:
|
|
171
203
|
|
|
172
204
|
| Method | Path | Purpose |
|
|
@@ -281,6 +313,46 @@ curl -H "X-Agent-Relay-Token: $AGENT_RELAY_TOKEN" http://localhost:4850/api/stat
|
|
|
281
313
|
| `DELETE` | `/messages/:id` | Delete message |
|
|
282
314
|
| `GET` | `/messages/cursor` | Latest message ID (for poller bootstrap) |
|
|
283
315
|
|
|
316
|
+
### Pair Sessions
|
|
317
|
+
|
|
318
|
+
Pair sessions are exclusive two-agent live chats backed by normal relay
|
|
319
|
+
messages. They are useful when you want two agent-sessions to collaborate
|
|
320
|
+
in real time without turning the relay into a group chat.
|
|
321
|
+
The sessions can be claude to codex, codex to codex or claude to claude,
|
|
322
|
+
since agent-relay is provider-independent.
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
agent-relay /pair codex "Debug flaky auth tests"
|
|
326
|
+
agent-relay pair codex --objective "Debug flaky auth tests"
|
|
327
|
+
agent-relay pair accept PAIR_ID --agent "$AGENT_RELAY_ID"
|
|
328
|
+
agent-relay pair send PAIR_ID --from "$AGENT_RELAY_ID" --body "What do you see?"
|
|
329
|
+
agent-relay /disconnect
|
|
330
|
+
agent-relay /status
|
|
331
|
+
agent-relay /label backend-fixer
|
|
332
|
+
agent-relay /tags backend tests urgent
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Inside provider sessions, the Codex skill and Claude plugin context instruct
|
|
336
|
+
agents to treat slash-style user messages like `/pair codex`, `/status`,
|
|
337
|
+
`/label backend-fixer`, and `/tags backend tests` as relay commands and run the
|
|
338
|
+
CLI. The CLI tries to auto-detect the current agent id from provider state; pass
|
|
339
|
+
`--from` or `--agent` if you want to be explicit.
|
|
340
|
+
|
|
341
|
+
If the target is already in a pending or active pair, the relay returns
|
|
342
|
+
`409 Busy`. If a target like `codex` matches multiple available agents, the
|
|
343
|
+
relay asks for a more specific target such as `id:...`, `label:...`, `tag:...`,
|
|
344
|
+
`cap:...` or `machine:...`.
|
|
345
|
+
|
|
346
|
+
| Method | Path | Purpose |
|
|
347
|
+
|--------|------|---------|
|
|
348
|
+
| `POST` | `/pairs` | Create a pair invite |
|
|
349
|
+
| `GET` | `/pairs` | List pairs (`?agent=`, `?status=`) |
|
|
350
|
+
| `GET` | `/pairs/:id` | Get one pair |
|
|
351
|
+
| `POST` | `/pairs/:id/accept` | Accept a pending pair |
|
|
352
|
+
| `POST` | `/pairs/:id/reject` | Reject a pending pair |
|
|
353
|
+
| `POST` | `/pairs/:id/messages` | Send a pair message |
|
|
354
|
+
| `POST` | `/pairs/:id/hangup` | End a pending or active pair |
|
|
355
|
+
|
|
284
356
|
### Tasks
|
|
285
357
|
|
|
286
358
|
| Method | Path | Purpose |
|
|
@@ -358,3 +430,10 @@ claude --plugin-dir ./claude # test plugin locally
|
|
|
358
430
|
## License
|
|
359
431
|
|
|
360
432
|
AGPL-3.0-or-later. See [LICENSE](LICENSE).
|
|
433
|
+
|
|
434
|
+
## Related
|
|
435
|
+
|
|
436
|
+
- **[callmux](https://github.com/edimuj/callmux)** - MCP multiplexer: parallel execution, batching, caching, pipelining, and shared infrastructure for any AI agent
|
|
437
|
+
- **[tokenlean](https://github.com/edimuj/tokenlean)** - Make agents less wasteful: Token-efficient CLI tool-toolbox for AI agents
|
|
438
|
+
- **[claude-mneme](https://github.com/edimuj/claude-mneme) / [codex-mneme](https://github.com/edimuj/codex-mneme)** - Lightweight persistent agent memory: so every session picks up where the last one left off
|
|
439
|
+
- **[agent-awareness](https://github.com/edimuj/agent-awareness)** - Real-time situational awareness for your agents
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "bun run src/index.ts",
|
|
21
21
|
"dev": "bun --watch run src/index.ts",
|
|
22
|
-
"test": "bun test
|
|
22
|
+
"test": "bun test",
|
|
23
23
|
"typecheck": "tsc --noEmit"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
package/public/dashboard.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const DEFAULT_COMPOSE = { from: "", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
4
4
|
const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
5
5
|
const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
|
|
6
|
+
const LIVE_REFRESH_MS = 5_000;
|
|
6
7
|
|
|
7
8
|
function loadPref(key, fallback) {
|
|
8
9
|
try {
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
tasks: [],
|
|
33
34
|
taskEvents: [],
|
|
34
35
|
stats: {},
|
|
36
|
+
health: null,
|
|
37
|
+
now: Date.now(),
|
|
35
38
|
authToken: loadPref("authToken", ""),
|
|
36
39
|
|
|
37
40
|
selectedAgent: "",
|
|
@@ -57,12 +60,19 @@
|
|
|
57
60
|
|
|
58
61
|
chartInstances: {},
|
|
59
62
|
_es: null,
|
|
60
|
-
|
|
63
|
+
_clockTimer: null,
|
|
64
|
+
_refreshTimer: null,
|
|
65
|
+
_refreshInFlight: false,
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
function watchPersistedPrefs(vm) {
|
|
65
70
|
vm.$watch("showOffline", (value) => vm.save("showOffline", value));
|
|
71
|
+
vm.$watch("autoRefresh", (value) => {
|
|
72
|
+
vm.save("autoRefresh", value);
|
|
73
|
+
if (value) vm.startAutoRefresh();
|
|
74
|
+
else vm.stopAutoRefresh();
|
|
75
|
+
});
|
|
66
76
|
vm.$watch("agentSort", (value) => vm.save("agentSort", value));
|
|
67
77
|
vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
|
|
68
78
|
vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
|
|
@@ -95,12 +105,31 @@
|
|
|
95
105
|
else vm.tasks.unshift(task);
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
function syncAgentStats(vm) {
|
|
109
|
+
vm.stats.agents = vm.agents.length;
|
|
110
|
+
vm.stats.online = vm.agents.filter((agent) => agent.status !== "offline").length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function syncMessageStats(vm, msg) {
|
|
114
|
+
vm.stats.messages = (vm.stats.messages ?? 0) + 1;
|
|
115
|
+
const createdAt = new Date(msg.createdAt || Date.now()).getTime();
|
|
116
|
+
if (Number.isFinite(createdAt) && Date.now() - createdAt <= 86_400_000) {
|
|
117
|
+
vm.stats.messagesLast24h = (vm.stats.messagesLast24h ?? 0) + 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function refreshChartsIfVisible(vm) {
|
|
122
|
+
if (vm.view !== "analytics") return;
|
|
123
|
+
vm.$nextTick(() => vm.renderCharts());
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
function createLifecycleMethods() {
|
|
99
127
|
return {
|
|
100
128
|
async init() {
|
|
101
129
|
await this.refresh();
|
|
102
130
|
this.connectSSE();
|
|
103
|
-
this.
|
|
131
|
+
this.startClock();
|
|
132
|
+
this.startAutoRefresh();
|
|
104
133
|
watchPersistedPrefs(this);
|
|
105
134
|
},
|
|
106
135
|
|
|
@@ -113,6 +142,25 @@
|
|
|
113
142
|
if (view === "messages") this.fetchMessages();
|
|
114
143
|
if (view === "tasks") this.fetchTasks();
|
|
115
144
|
},
|
|
145
|
+
|
|
146
|
+
startClock() {
|
|
147
|
+
if (this._clockTimer) return;
|
|
148
|
+
this._clockTimer = setInterval(() => {
|
|
149
|
+
this.now = Date.now();
|
|
150
|
+
}, 1_000);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
startAutoRefresh() {
|
|
154
|
+
this.stopAutoRefresh();
|
|
155
|
+
if (!this.autoRefresh) return;
|
|
156
|
+
this._refreshTimer = setInterval(() => this.refreshLiveData(), LIVE_REFRESH_MS);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
stopAutoRefresh() {
|
|
160
|
+
if (!this._refreshTimer) return;
|
|
161
|
+
clearInterval(this._refreshTimer);
|
|
162
|
+
this._refreshTimer = null;
|
|
163
|
+
},
|
|
116
164
|
};
|
|
117
165
|
}
|
|
118
166
|
|
|
@@ -139,6 +187,7 @@
|
|
|
139
187
|
es.addEventListener("agent.status", (event) => handleAgentStatus(this, parseEventData(event)));
|
|
140
188
|
es.addEventListener("agent.removed", (event) => handleAgentRemoved(this, parseEventData(event)));
|
|
141
189
|
es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
|
|
190
|
+
es.addEventListener("message.claim_released", (event) => handleMessageClaimReleased(this, parseEventData(event)));
|
|
142
191
|
es.addEventListener("message.deleted", (event) => handleMessageDeleted(this, parseEventData(event)));
|
|
143
192
|
registerTaskEvents(this, es);
|
|
144
193
|
}
|
|
@@ -156,22 +205,37 @@
|
|
|
156
205
|
|
|
157
206
|
vm.messages.push(msg);
|
|
158
207
|
if (vm.messages.length > 200) vm.messages.shift();
|
|
159
|
-
|
|
208
|
+
syncMessageStats(vm, msg);
|
|
209
|
+
refreshChartsIfVisible(vm);
|
|
160
210
|
}
|
|
161
211
|
|
|
162
212
|
function handleAgentStatus(vm, agent) {
|
|
163
213
|
upsertById(vm.agents, agent);
|
|
164
214
|
vm.agentsById[agent.id] = agent;
|
|
215
|
+
syncAgentStats(vm);
|
|
216
|
+
refreshChartsIfVisible(vm);
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
function handleAgentRemoved(vm, data) {
|
|
168
220
|
vm.agents = vm.agents.filter((agent) => agent.id !== data.id);
|
|
169
221
|
delete vm.agentsById[data.id];
|
|
222
|
+
syncAgentStats(vm);
|
|
223
|
+
refreshChartsIfVisible(vm);
|
|
170
224
|
}
|
|
171
225
|
|
|
172
226
|
function handleMessageClaimed(vm, data) {
|
|
173
227
|
const msg = vm.messages.find((item) => item.id === data.messageId);
|
|
174
|
-
if (msg)
|
|
228
|
+
if (!msg) return;
|
|
229
|
+
msg.claimedBy = data.claimedBy;
|
|
230
|
+
msg.claimExpiresAt = data.claimExpiresAt;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleMessageClaimReleased(vm, data) {
|
|
234
|
+
const msg = vm.messages.find((item) => item.id === data.messageId);
|
|
235
|
+
if (!msg) return;
|
|
236
|
+
delete msg.claimedBy;
|
|
237
|
+
delete msg.claimedAt;
|
|
238
|
+
delete msg.claimExpiresAt;
|
|
175
239
|
}
|
|
176
240
|
|
|
177
241
|
function handleMessageDeleted(vm, data) {
|
|
@@ -213,7 +277,18 @@
|
|
|
213
277
|
},
|
|
214
278
|
|
|
215
279
|
async refresh() {
|
|
216
|
-
await Promise.all([this.fetchStats(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
|
|
280
|
+
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async refreshLiveData() {
|
|
284
|
+
if (this._refreshInFlight || this.authNeeded) return;
|
|
285
|
+
this._refreshInFlight = true;
|
|
286
|
+
try {
|
|
287
|
+
await this.refresh();
|
|
288
|
+
refreshChartsIfVisible(this);
|
|
289
|
+
} finally {
|
|
290
|
+
this._refreshInFlight = false;
|
|
291
|
+
}
|
|
217
292
|
},
|
|
218
293
|
|
|
219
294
|
async fetchStats() {
|
|
@@ -222,6 +297,12 @@
|
|
|
222
297
|
} catch {}
|
|
223
298
|
},
|
|
224
299
|
|
|
300
|
+
async fetchHealth() {
|
|
301
|
+
try {
|
|
302
|
+
this.health = await this.api("GET", "/health");
|
|
303
|
+
} catch {}
|
|
304
|
+
},
|
|
305
|
+
|
|
225
306
|
async fetchAgents() {
|
|
226
307
|
try {
|
|
227
308
|
this.agents = await this.api("GET", "/agents");
|
|
@@ -260,6 +341,7 @@
|
|
|
260
341
|
uniqueLabels: { get: getUniqueLabels },
|
|
261
342
|
uniqueCaps: { get: getUniqueCaps },
|
|
262
343
|
uniqueTags: { get: getUniqueTags },
|
|
344
|
+
healthIssues: { get: getHealthIssues },
|
|
263
345
|
};
|
|
264
346
|
}
|
|
265
347
|
|
|
@@ -336,6 +418,10 @@
|
|
|
336
418
|
return [...new Set(this.agents.flatMap((agent) => agent.tags || []))];
|
|
337
419
|
}
|
|
338
420
|
|
|
421
|
+
function getHealthIssues() {
|
|
422
|
+
return (this.health?.checks || []).filter((check) => check.status !== "ok");
|
|
423
|
+
}
|
|
424
|
+
|
|
339
425
|
function compareAgents(vm, a, b) {
|
|
340
426
|
switch (vm.agentSort) {
|
|
341
427
|
case "name":
|
|
@@ -359,6 +445,7 @@
|
|
|
359
445
|
agentStatusTitle,
|
|
360
446
|
timeAgo,
|
|
361
447
|
fmtTime,
|
|
448
|
+
healthAlertClass,
|
|
362
449
|
};
|
|
363
450
|
}
|
|
364
451
|
|
|
@@ -392,7 +479,7 @@
|
|
|
392
479
|
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
393
480
|
if (!Number.isFinite(lastSeenMs)) return "Trying to reconnect...";
|
|
394
481
|
|
|
395
|
-
const ageSec = Math.max(0, (Date.now() - lastSeenMs) / 1000);
|
|
482
|
+
const ageSec = Math.max(0, ((this.now || Date.now()) - lastSeenMs) / 1000);
|
|
396
483
|
return ageSec <= 45 ? "Starting up..." : "Trying to reconnect...";
|
|
397
484
|
}
|
|
398
485
|
|
|
@@ -401,7 +488,7 @@
|
|
|
401
488
|
const ts = new Date(iso).getTime();
|
|
402
489
|
if (!Number.isFinite(ts)) return "";
|
|
403
490
|
|
|
404
|
-
const diff = Math.max(0, (Date.now() - ts) / 1000);
|
|
491
|
+
const diff = Math.max(0, ((this.now || Date.now()) - ts) / 1000);
|
|
405
492
|
if (diff < 60) return Math.floor(diff) + "s ago";
|
|
406
493
|
if (diff < 3600) return Math.floor(diff / 60) + "m ago";
|
|
407
494
|
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
|
@@ -413,6 +500,12 @@
|
|
|
413
500
|
return new Date(iso).toLocaleString();
|
|
414
501
|
}
|
|
415
502
|
|
|
503
|
+
function healthAlertClass(status) {
|
|
504
|
+
if (status === "error") return "alert-danger";
|
|
505
|
+
if (status === "degraded") return "alert-warning";
|
|
506
|
+
return "alert-success";
|
|
507
|
+
}
|
|
508
|
+
|
|
416
509
|
function createMessageActions() {
|
|
417
510
|
return {
|
|
418
511
|
openCompose,
|
package/public/index.html
CHANGED
|
@@ -109,6 +109,12 @@
|
|
|
109
109
|
<input type="checkbox" class="form-check-input" x-model="showOffline">
|
|
110
110
|
<span class="form-check-label small">Show offline</span>
|
|
111
111
|
</label>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="d-flex align-items-center gap-2 mb-2">
|
|
114
|
+
<label class="form-check form-switch mb-0">
|
|
115
|
+
<input type="checkbox" class="form-check-input" x-model="autoRefresh">
|
|
116
|
+
<span class="form-check-label small">Auto refresh</span>
|
|
117
|
+
</label>
|
|
112
118
|
</div>
|
|
113
119
|
<div class="text-muted small" x-show="stats.version" x-text="'v' + stats.version"></div>
|
|
114
120
|
</div>
|
|
@@ -201,6 +207,25 @@
|
|
|
201
207
|
</div>
|
|
202
208
|
</div>
|
|
203
209
|
|
|
210
|
+
<template x-if="health">
|
|
211
|
+
<div class="alert d-flex align-items-start gap-3 mb-4" :class="healthAlertClass(health.status)">
|
|
212
|
+
<i class="ti ti-heartbeat mt-1"></i>
|
|
213
|
+
<div class="flex-grow-1">
|
|
214
|
+
<div class="fw-bold" x-text="'Relay health: ' + health.status"></div>
|
|
215
|
+
<template x-if="healthIssues.length === 0">
|
|
216
|
+
<div class="small">All checks passing</div>
|
|
217
|
+
</template>
|
|
218
|
+
<template x-if="healthIssues.length > 0">
|
|
219
|
+
<div class="small">
|
|
220
|
+
<template x-for="check in healthIssues" :key="check.name">
|
|
221
|
+
<span class="me-3" x-text="check.detail || check.name"></span>
|
|
222
|
+
</template>
|
|
223
|
+
</div>
|
|
224
|
+
</template>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</template>
|
|
228
|
+
|
|
204
229
|
<!-- Two-column: Agents + Recent messages -->
|
|
205
230
|
<div class="row g-3">
|
|
206
231
|
<div class="col-lg-5">
|
|
@@ -389,7 +414,7 @@
|
|
|
389
414
|
<div class="ms-auto d-flex gap-2 align-items-center flex-wrap">
|
|
390
415
|
<select class="form-select form-select-sm" style="width: auto; min-width: 160px" x-model="selectedAgent" @change="fetchMessages()">
|
|
391
416
|
<option value="">All agents</option>
|
|
392
|
-
<template x-for="a in
|
|
417
|
+
<template x-for="a in composeAgents" :key="a.id">
|
|
393
418
|
<option :value="a.id" x-text="displayName(a) + ' [' + a.id.slice(-6) + ']'"></option>
|
|
394
419
|
</template>
|
|
395
420
|
</select>
|