a2acalling 0.6.48 → 0.6.49
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/bin/cli.js +23 -0
- package/docs/plans/2026-02-16-auto-updater.md +1284 -0
- package/docs/plans/2026-02-16-e2e-test-prompt-sequence.md +3085 -0
- package/docs/plans/2026-02-17-claude-code-codex-skills.md +770 -0
- package/docs/prompts/e2e-test-agent.md +368 -0
- package/docs/protocol.md +79 -0
- package/package.json +1 -1
- package/src/dashboard/public/app.js +108 -1
- package/src/dashboard/public/index.html +9 -0
- package/src/dashboard/public/style.css +27 -0
- package/src/lib/config.js +41 -0
- package/src/lib/conversation-driver.js +62 -21
- package/src/lib/openclaw-integration.js +22 -66
- package/src/lib/summary-formatter.js +168 -0
- package/src/lib/summary-prompt.js +203 -0
- package/src/lib/update-checker.js +93 -0
- package/src/lib/update-manager.js +313 -0
- package/src/routes/a2a.js +8 -1
- package/src/routes/dashboard.js +103 -1
- package/src/server.js +115 -26
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# E2E Test Agent Prompt Sequence
|
|
2
|
+
|
|
3
|
+
Run a full end-to-end validation of `a2acalling` in an isolated environment. Execute each step sequentially. If a step fails, log the failure and continue to the next step unless the failure is blocking (steps 1-3 block all subsequent steps).
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Before starting, verify:
|
|
8
|
+
|
|
9
|
+
- **Node.js** >= 18.0.0 (`node --version`)
|
|
10
|
+
- **npm** available (`npm --version`)
|
|
11
|
+
- **Ports** 3100-3199 range available (used by test servers)
|
|
12
|
+
- **Disk** at least 200MB free in system temp directory
|
|
13
|
+
- **Network** localhost connections allowed (no firewall blocking loopback)
|
|
14
|
+
|
|
15
|
+
If any prerequisite is missing, report it as a step-0 failure and abort.
|
|
16
|
+
|
|
17
|
+
## Output Format
|
|
18
|
+
|
|
19
|
+
Report each step as a JSON object:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"step": 1,
|
|
24
|
+
"name": "install",
|
|
25
|
+
"status": "pass",
|
|
26
|
+
"duration_ms": 4230,
|
|
27
|
+
"error": null
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
On completion, return a JSON array of all step results:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"run_id": "e2e_<timestamp>",
|
|
36
|
+
"started_at": "<ISO-8601>",
|
|
37
|
+
"finished_at": "<ISO-8601>",
|
|
38
|
+
"total_steps": 9,
|
|
39
|
+
"passed": 8,
|
|
40
|
+
"failed": 1,
|
|
41
|
+
"results": [ ... ]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 1: Install from npm
|
|
48
|
+
|
|
49
|
+
**What to do:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
WORKDIR=$(mktemp -d -t a2a-e2e-XXXXXX)
|
|
53
|
+
cd "$WORKDIR"
|
|
54
|
+
npm init -y --silent
|
|
55
|
+
npm install a2acalling
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Expected outcome:**
|
|
59
|
+
- Exit code 0 from `npm install`
|
|
60
|
+
- `node_modules/a2acalling/bin/cli.js` exists
|
|
61
|
+
- `node_modules/a2acalling/src/server.js` exists
|
|
62
|
+
|
|
63
|
+
**Failure:**
|
|
64
|
+
- If npm install fails, record the stderr and abort all remaining steps (blocking).
|
|
65
|
+
|
|
66
|
+
**Variables to carry forward:**
|
|
67
|
+
- `WORKDIR` -- root temp directory
|
|
68
|
+
- `CLI` = `$WORKDIR/node_modules/.bin/a2a`
|
|
69
|
+
- `A2A_CONFIG_DIR` = `$WORKDIR/config` (create this directory)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Step 2: Run Quickstart Onboarding
|
|
74
|
+
|
|
75
|
+
**What to do:**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export A2A_CONFIG_DIR="$WORKDIR/config"
|
|
79
|
+
export CI=true
|
|
80
|
+
mkdir -p "$A2A_CONFIG_DIR"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
First, write a minimal config to simulate port detection completing:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cat > "$A2A_CONFIG_DIR/a2a-config.json" << 'CONF'
|
|
87
|
+
{
|
|
88
|
+
"onboarding": { "version": 2, "step": "awaiting_disclosure", "server_port": 3100 },
|
|
89
|
+
"agent": { "hostname": "localhost:3100", "name": "e2e-test-agent" },
|
|
90
|
+
"tiers": {}
|
|
91
|
+
}
|
|
92
|
+
CONF
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then submit the disclosure manifest:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
node "$CLI" onboard --submit '{
|
|
99
|
+
"tiers": {
|
|
100
|
+
"public": {
|
|
101
|
+
"topics": [{"topic": "General", "description": "Open discussion"}],
|
|
102
|
+
"objectives": [],
|
|
103
|
+
"do_not_discuss": []
|
|
104
|
+
},
|
|
105
|
+
"friends": {
|
|
106
|
+
"topics": [{"topic": "Projects", "description": "Current work"}],
|
|
107
|
+
"objectives": [],
|
|
108
|
+
"do_not_discuss": []
|
|
109
|
+
},
|
|
110
|
+
"family": {
|
|
111
|
+
"topics": [{"topic": "Everything", "description": "Full access"}],
|
|
112
|
+
"objectives": [],
|
|
113
|
+
"do_not_discuss": []
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"never_disclose": ["passwords", "api-keys"],
|
|
117
|
+
"personality_notes": "E2E test agent. Direct and minimal responses."
|
|
118
|
+
}'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Expected outcome:**
|
|
122
|
+
- Exit code 0
|
|
123
|
+
- stdout contains "Onboarding complete"
|
|
124
|
+
- `$A2A_CONFIG_DIR/a2a-config.json` has `onboarding.step` set to `"complete"`
|
|
125
|
+
|
|
126
|
+
**Failure:**
|
|
127
|
+
- If onboarding fails, record stdout/stderr and abort (blocking).
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Step 3: Verify Server Health
|
|
132
|
+
|
|
133
|
+
**What to do:**
|
|
134
|
+
|
|
135
|
+
Start the server, then check health endpoints:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
node "$WORKDIR/node_modules/a2acalling/src/server.js" &
|
|
139
|
+
SERVER_PID=$!
|
|
140
|
+
sleep 2
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Check ping:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
curl -s http://localhost:3100/api/a2a/ping
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Check status:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
curl -s http://localhost:3100/api/a2a/status
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Expected outcome:**
|
|
156
|
+
- Ping returns `{"pong": true, "timestamp": "..."}` with HTTP 200
|
|
157
|
+
- Status returns JSON with `"a2a": true` and a `"version"` field
|
|
158
|
+
- Server process is running (PID exists)
|
|
159
|
+
|
|
160
|
+
**Failure:**
|
|
161
|
+
- If server does not start or endpoints do not respond within 10 seconds, abort (blocking).
|
|
162
|
+
|
|
163
|
+
**Variables to carry forward:**
|
|
164
|
+
- `SERVER_PID`
|
|
165
|
+
- `BASE_URL` = `http://localhost:3100`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Step 4: Create Invite Token
|
|
170
|
+
|
|
171
|
+
**What to do:**
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
node "$CLI" create --name "E2E-Caller" --tier public --expires 1h
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Parse the output to extract the token and invite URL.
|
|
178
|
+
|
|
179
|
+
**Expected outcome:**
|
|
180
|
+
- Exit code 0
|
|
181
|
+
- Output contains an invite URL matching `a2a://localhost:3100/fed_...`
|
|
182
|
+
- Output contains a token matching `fed_[A-Za-z0-9_-]+`
|
|
183
|
+
|
|
184
|
+
**Variables to carry forward:**
|
|
185
|
+
- `TOKEN` -- the `fed_...` string
|
|
186
|
+
- `INVITE_URL` -- the full `a2a://...` URL
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Step 5: Test Inbound Call
|
|
191
|
+
|
|
192
|
+
**What to do:**
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
curl -s -X POST "$BASE_URL/api/a2a/invoke" \
|
|
196
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
197
|
+
-H "Content-Type: application/json" \
|
|
198
|
+
-d '{
|
|
199
|
+
"message": "Hello from E2E test",
|
|
200
|
+
"caller": {
|
|
201
|
+
"name": "E2E Test Runner",
|
|
202
|
+
"instance": "localhost",
|
|
203
|
+
"context": "Automated E2E validation"
|
|
204
|
+
}
|
|
205
|
+
}'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Expected outcome:**
|
|
209
|
+
- HTTP 200
|
|
210
|
+
- Response JSON has `"success": true`
|
|
211
|
+
- Response JSON has a `"conversation_id"` starting with `conv_`
|
|
212
|
+
- Response JSON has a non-empty `"response"` field
|
|
213
|
+
- Response JSON has `"can_continue": true`
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Step 6: Test Multi-turn Conversation
|
|
218
|
+
|
|
219
|
+
**What to do:**
|
|
220
|
+
|
|
221
|
+
Use the `conversation_id` from step 5. Make two follow-up calls:
|
|
222
|
+
|
|
223
|
+
Call 1:
|
|
224
|
+
```bash
|
|
225
|
+
curl -s -X POST "$BASE_URL/api/a2a/invoke" \
|
|
226
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
227
|
+
-H "Content-Type: application/json" \
|
|
228
|
+
-d "{
|
|
229
|
+
\"message\": \"Follow-up question 1\",
|
|
230
|
+
\"conversation_id\": \"$CONVERSATION_ID\"
|
|
231
|
+
}"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Call 2:
|
|
235
|
+
```bash
|
|
236
|
+
curl -s -X POST "$BASE_URL/api/a2a/invoke" \
|
|
237
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
238
|
+
-H "Content-Type: application/json" \
|
|
239
|
+
-d "{
|
|
240
|
+
\"message\": \"Follow-up question 2\",
|
|
241
|
+
\"conversation_id\": \"$CONVERSATION_ID\"
|
|
242
|
+
}"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Expected outcome:**
|
|
246
|
+
- Both calls return HTTP 200 with `"success": true`
|
|
247
|
+
- Both return the same `conversation_id` as the original
|
|
248
|
+
- Both have non-empty `"response"` fields
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Step 7: Test Error Cases
|
|
253
|
+
|
|
254
|
+
Run three negative tests:
|
|
255
|
+
|
|
256
|
+
### 7a: No auth header
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/a2a/invoke" \
|
|
260
|
+
-H "Content-Type: application/json" \
|
|
261
|
+
-d '{"message": "no auth"}'
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Expected:** HTTP 401, response body has `"error": "missing_token"`
|
|
265
|
+
|
|
266
|
+
### 7b: Bad token
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
curl -s -X POST "$BASE_URL/api/a2a/invoke" \
|
|
270
|
+
-H "Authorization: Bearer fed_invalid_token_value" \
|
|
271
|
+
-H "Content-Type: application/json" \
|
|
272
|
+
-d '{"message": "bad token"}'
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Expected:** HTTP 401 or 403, response body has `"success": false`
|
|
276
|
+
|
|
277
|
+
### 7c: Missing message body
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
curl -s -X POST "$BASE_URL/api/a2a/invoke" \
|
|
281
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
282
|
+
-H "Content-Type: application/json" \
|
|
283
|
+
-d '{}'
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Expected:** HTTP 400, response body has `"error": "missing_message"`
|
|
287
|
+
|
|
288
|
+
**Step passes only if all three sub-tests pass.** Report sub-test details in the error field if any fail.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Step 8: Test Token Revocation
|
|
293
|
+
|
|
294
|
+
**What to do:**
|
|
295
|
+
|
|
296
|
+
First, find the token ID:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
node "$CLI" list
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Parse the token ID (`tok_...`) from the output, then revoke it:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
node "$CLI" revoke "$TOKEN_ID"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Then attempt a call with the revoked token:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
curl -s -X POST "$BASE_URL/api/a2a/invoke" \
|
|
312
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
313
|
+
-H "Content-Type: application/json" \
|
|
314
|
+
-d '{"message": "should fail"}'
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Expected outcome:**
|
|
318
|
+
- Revoke command exits 0
|
|
319
|
+
- Post-revocation call returns `"success": false`
|
|
320
|
+
- Error field is `"token_revoked"` or similar auth error
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Step 9: Cleanup
|
|
325
|
+
|
|
326
|
+
**What to do:**
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
kill $SERVER_PID 2>/dev/null
|
|
330
|
+
wait $SERVER_PID 2>/dev/null
|
|
331
|
+
|
|
332
|
+
rm -rf "$WORKDIR"
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Verify:
|
|
336
|
+
```bash
|
|
337
|
+
! kill -0 $SERVER_PID 2>/dev/null # process is gone
|
|
338
|
+
[ ! -d "$WORKDIR" ] # directory is gone
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Expected outcome:**
|
|
342
|
+
- Server process is terminated
|
|
343
|
+
- Temp directory is removed
|
|
344
|
+
- No orphaned processes on port 3100
|
|
345
|
+
|
|
346
|
+
**This step always passes unless cleanup throws an unexpected error.**
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Failure Reporting
|
|
351
|
+
|
|
352
|
+
When a step fails, generate a Linear bug report payload:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"title": "E2E: Step <N> (<name>) failed",
|
|
357
|
+
"description": "## Failure\n\n<error message>\n\n## Reproduction\n\nRun `node test/e2e/orchestrate.js --verbose`\n\n## Environment\n\n- Node: <version>\n- npm: <version>\n- OS: <platform>\n- a2acalling: <version>",
|
|
358
|
+
"priority": 2,
|
|
359
|
+
"labels": ["bug", "e2e"],
|
|
360
|
+
"team": "ENG"
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Priority mapping:
|
|
365
|
+
- Steps 1-3 (blocking): priority 1 (Urgent)
|
|
366
|
+
- Steps 4-6 (core flow): priority 2 (High)
|
|
367
|
+
- Steps 7-8 (error handling): priority 3 (Normal)
|
|
368
|
+
- Step 9 (cleanup): priority 4 (Low)
|
package/docs/protocol.md
CHANGED
|
@@ -309,6 +309,85 @@ a2a_call({
|
|
|
309
309
|
}
|
|
310
310
|
```
|
|
311
311
|
|
|
312
|
+
## E2E Testing
|
|
313
|
+
|
|
314
|
+
### Architecture
|
|
315
|
+
|
|
316
|
+
E2E tests run in fully isolated environments. Each test gets its own temp directory, config directory (`A2A_CONFIG_DIR`), and ephemeral port. No shared state between tests; no pollution of the host system.
|
|
317
|
+
|
|
318
|
+
Core components:
|
|
319
|
+
|
|
320
|
+
| Component | File | Purpose |
|
|
321
|
+
|-----------|------|---------|
|
|
322
|
+
| Environment | `test/e2e/env.js` | Creates isolated temp dir, config dir, port finder, cleanup |
|
|
323
|
+
| Two-server harness | `test/e2e/two-server.js` | Spins up two independent A2A servers for cross-agent tests |
|
|
324
|
+
| CLI runner | `test/e2e/cli-runner.js` | Wraps `bin/cli.js` as child process with structured output |
|
|
325
|
+
| Agent prompt | `docs/prompts/e2e-test-agent.md` | 9-step prompt sequence for Claude subagent validation |
|
|
326
|
+
|
|
327
|
+
### Test Categories
|
|
328
|
+
|
|
329
|
+
- **Infrastructure** (`env.test.js`) -- Isolated environments, port allocation, cleanup
|
|
330
|
+
- **CLI integration** (`cli-runner.test.js`) -- Command execution, onboarding, exit codes, timeouts
|
|
331
|
+
- **Cross-agent flow** (`two-server.test.js`) -- Two servers, token isolation, ping/invoke across instances
|
|
332
|
+
- **Error handling** -- Bad tokens, missing auth, revoked tokens, malformed requests
|
|
333
|
+
- **Summary validation** -- Prompt correctness across `server.js`, `openclaw-integration.js`, `claude-subagent.js` paths
|
|
334
|
+
|
|
335
|
+
### Entry Points
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# Run all tests (unit + integration + E2E)
|
|
339
|
+
node test/run.js
|
|
340
|
+
|
|
341
|
+
# Run E2E tests only
|
|
342
|
+
node test/run.js --e2e
|
|
343
|
+
|
|
344
|
+
# Standalone orchestrator with verbose or JSON output
|
|
345
|
+
node test/e2e/orchestrate.js [--verbose] [--json]
|
|
346
|
+
|
|
347
|
+
# Default: excludes E2E for faster feedback
|
|
348
|
+
node test/run.js
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The `--e2e` flag filters to files under `test/e2e/`. The standalone orchestrator runs the full 9-step sequence from `docs/prompts/e2e-test-agent.md` and outputs structured JSON results.
|
|
352
|
+
|
|
353
|
+
### File Structure
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
test/e2e/
|
|
357
|
+
env.js # createE2EEnv() — isolated temp + config + port
|
|
358
|
+
env.test.js # Tests for env isolation and port allocation
|
|
359
|
+
cli-runner.js # CLIRunner class — child-process CLI wrapper
|
|
360
|
+
cli-runner.test.js # Tests for CLI execution, onboarding, timeouts
|
|
361
|
+
two-server.js # TwoServerHarness — two independent A2A instances
|
|
362
|
+
two-server.test.js # Tests for cross-agent token isolation and ping
|
|
363
|
+
orchestrate.js # Standalone E2E orchestrator (planned)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Adding New E2E Tests
|
|
367
|
+
|
|
368
|
+
1. Create `test/e2e/<name>.test.js` exporting `function(test, assert, helpers)`.
|
|
369
|
+
2. Use `createE2EEnv()` for isolation. Always call `env.cleanup()` in a finally block.
|
|
370
|
+
3. Use `TwoServerHarness` if you need two agents. Call `harness.teardown()` after.
|
|
371
|
+
4. Use `CLIRunner` for CLI interactions. It handles `A2A_CONFIG_DIR` automatically.
|
|
372
|
+
5. The test runner discovers all `*.test.js` files recursively -- no registration needed.
|
|
373
|
+
|
|
374
|
+
Example skeleton:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
module.exports = function (test, assert, helpers) {
|
|
378
|
+
const { createE2EEnv } = require('./env');
|
|
379
|
+
|
|
380
|
+
test('my new E2E scenario', async () => {
|
|
381
|
+
const env = createE2EEnv('my-test');
|
|
382
|
+
try {
|
|
383
|
+
// ... test logic using env.configDir, env.findAvailablePort()
|
|
384
|
+
} finally {
|
|
385
|
+
env.cleanup();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
```
|
|
390
|
+
|
|
312
391
|
## Future Protocol Extensions (v1+)
|
|
313
392
|
|
|
314
393
|
- **Capability advertisement**: Agents declare what they can help with
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const state = {
|
|
2
2
|
settings: null,
|
|
3
3
|
dashboardStatus: null,
|
|
4
|
+
autoUpdate: null,
|
|
4
5
|
callbookDevices: [],
|
|
5
6
|
contacts: [],
|
|
6
7
|
selectedContactId: null,
|
|
@@ -63,6 +64,20 @@ function esc(text) {
|
|
|
63
64
|
.replaceAll("'", ''');
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
function formatUpdaterState(stateValue) {
|
|
68
|
+
const state = String(stateValue || '').trim() || 'unknown';
|
|
69
|
+
return state.replaceAll('_', ' ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function updaterPillClass(stateValue) {
|
|
73
|
+
const state = String(stateValue || '').trim();
|
|
74
|
+
if (state === 'failed') return 'err';
|
|
75
|
+
if (state === 'waiting_for_safe_restart' || state === 'checking' || state === 'downloading' || state === 'applying' || state === 'restarting') {
|
|
76
|
+
return 'warn';
|
|
77
|
+
}
|
|
78
|
+
return 'ok';
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
async function copyText(value) {
|
|
67
82
|
const text = String(value || '');
|
|
68
83
|
if (!text) return false;
|
|
@@ -284,7 +299,7 @@ function bindContactsActions() {
|
|
|
284
299
|
if (!urlEl || !serverNameEl) return;
|
|
285
300
|
if (mineEl && !mineEl.checked) return;
|
|
286
301
|
if (serverNameEl.value.trim()) return;
|
|
287
|
-
const match = String(urlEl.value || '').trim().match(/^(?:a2a|oclaw)
|
|
302
|
+
const match = String(urlEl.value || '').trim().match(/^(?:a2a|oclaw):\/\/([^/]+)\//);
|
|
288
303
|
if (match && match[1]) {
|
|
289
304
|
serverNameEl.value = match[1];
|
|
290
305
|
}
|
|
@@ -1035,14 +1050,59 @@ function renderCallbookStatus() {
|
|
|
1035
1050
|
`;
|
|
1036
1051
|
}
|
|
1037
1052
|
|
|
1053
|
+
function renderAutoUpdateStatus() {
|
|
1054
|
+
const el = document.getElementById('auto-update-status');
|
|
1055
|
+
const toggleBtn = document.getElementById('auto-update-toggle');
|
|
1056
|
+
if (!el) return;
|
|
1057
|
+
|
|
1058
|
+
const au = state.autoUpdate;
|
|
1059
|
+
if (!au) {
|
|
1060
|
+
el.textContent = 'Loading…';
|
|
1061
|
+
if (toggleBtn) toggleBtn.disabled = true;
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const stateText = formatUpdaterState(au.state);
|
|
1066
|
+
const pillClass = updaterPillClass(au.state);
|
|
1067
|
+
const enabled = Boolean(au.enabled);
|
|
1068
|
+
const intervalSec = Number.isFinite(au.interval_ms) ? Math.floor(au.interval_ms / 1000) : null;
|
|
1069
|
+
|
|
1070
|
+
el.innerHTML = `
|
|
1071
|
+
<div><strong>Status:</strong> <span class="status-pill ${pillClass}">${esc(stateText)}</span></div>
|
|
1072
|
+
<div><strong>Enabled:</strong> ${enabled ? 'yes' : 'no'}</div>
|
|
1073
|
+
<div><strong>Current version:</strong> <span class="mono">${esc(au.current_version || '-')}</span></div>
|
|
1074
|
+
<div><strong>Latest version:</strong> <span class="mono">${esc(au.latest_version || '-')}</span></div>
|
|
1075
|
+
<div><strong>Target version:</strong> <span class="mono">${esc(au.target_version || '-')}</span></div>
|
|
1076
|
+
<div><strong>Active calls:</strong> ${esc(String(au.active_calls || 0))}</div>
|
|
1077
|
+
<div><strong>Interval:</strong> ${intervalSec === null ? '-' : `${intervalSec}s`}</div>
|
|
1078
|
+
<div><strong>Last checked:</strong> ${esc(fmtDate(au.last_checked_at))}</div>
|
|
1079
|
+
<div><strong>Last success:</strong> ${esc(fmtDate(au.last_success_at))}</div>
|
|
1080
|
+
${au.defer_reason ? `<div><strong>Deferred:</strong> ${esc(au.defer_reason)}</div>` : ''}
|
|
1081
|
+
${au.last_error ? `<div><strong>Error:</strong> <span class="mono">${esc(au.last_error)}</span></div>` : ''}
|
|
1082
|
+
`;
|
|
1083
|
+
|
|
1084
|
+
if (toggleBtn) {
|
|
1085
|
+
toggleBtn.disabled = false;
|
|
1086
|
+
toggleBtn.textContent = enabled ? 'Disable auto-update' : 'Enable auto-update';
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1038
1090
|
async function loadDashboardStatus(refreshIp = false) {
|
|
1039
1091
|
const payload = await request(`/status${refreshIp ? '?refresh_ip=true' : ''}`);
|
|
1040
1092
|
state.dashboardStatus = payload;
|
|
1093
|
+
state.autoUpdate = payload.auto_update || state.autoUpdate;
|
|
1041
1094
|
renderCallbookStatus();
|
|
1095
|
+
renderAutoUpdateStatus();
|
|
1042
1096
|
renderContacts();
|
|
1043
1097
|
renderContactDetail();
|
|
1044
1098
|
}
|
|
1045
1099
|
|
|
1100
|
+
async function loadAutoUpdateStatus() {
|
|
1101
|
+
const payload = await request('/update/status');
|
|
1102
|
+
state.autoUpdate = payload.auto_update || null;
|
|
1103
|
+
renderAutoUpdateStatus();
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1046
1106
|
function renderCallbookDevices() {
|
|
1047
1107
|
const tbody = document.querySelector('#callbook-devices-table tbody');
|
|
1048
1108
|
if (!tbody) return;
|
|
@@ -1154,6 +1214,47 @@ function bindCallbookActions() {
|
|
|
1154
1214
|
});
|
|
1155
1215
|
}
|
|
1156
1216
|
|
|
1217
|
+
function bindAutoUpdateActions() {
|
|
1218
|
+
document.getElementById('auto-update-refresh')?.addEventListener('click', () => {
|
|
1219
|
+
loadAutoUpdateStatus().catch(err => showNotice(err.message));
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
document.getElementById('auto-update-check')?.addEventListener('click', async () => {
|
|
1223
|
+
try {
|
|
1224
|
+
await request('/update/check', { method: 'POST', body: JSON.stringify({}) });
|
|
1225
|
+
await loadAutoUpdateStatus();
|
|
1226
|
+
showNotice('Update check complete');
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
showNotice(err.message);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
document.getElementById('auto-update-now')?.addEventListener('click', async () => {
|
|
1233
|
+
try {
|
|
1234
|
+
await request('/update/now', { method: 'POST', body: JSON.stringify({}) });
|
|
1235
|
+
await loadAutoUpdateStatus();
|
|
1236
|
+
showNotice('Update triggered');
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
showNotice(err.message);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
document.getElementById('auto-update-toggle')?.addEventListener('click', async () => {
|
|
1243
|
+
const au = state.autoUpdate || {};
|
|
1244
|
+
const nextEnabled = !Boolean(au.enabled);
|
|
1245
|
+
try {
|
|
1246
|
+
await request('/update/config', {
|
|
1247
|
+
method: 'PUT',
|
|
1248
|
+
body: JSON.stringify({ enabled: nextEnabled })
|
|
1249
|
+
});
|
|
1250
|
+
await loadAutoUpdateStatus();
|
|
1251
|
+
showNotice(nextEnabled ? 'Auto-update enabled' : 'Auto-update disabled');
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
showNotice(err.message);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1157
1258
|
function renderInvites() {
|
|
1158
1259
|
const tbody = document.querySelector('#invites-table tbody');
|
|
1159
1260
|
tbody.innerHTML = '';
|
|
@@ -1247,6 +1348,7 @@ async function bootstrap() {
|
|
|
1247
1348
|
bindContactsActions();
|
|
1248
1349
|
bindSettingsActions();
|
|
1249
1350
|
bindCallbookActions();
|
|
1351
|
+
bindAutoUpdateActions();
|
|
1250
1352
|
bindInviteActions();
|
|
1251
1353
|
bindRefreshButtons();
|
|
1252
1354
|
|
|
@@ -1254,6 +1356,7 @@ async function bootstrap() {
|
|
|
1254
1356
|
await Promise.all([
|
|
1255
1357
|
loadSettings(),
|
|
1256
1358
|
loadDashboardStatus(),
|
|
1359
|
+
loadAutoUpdateStatus(),
|
|
1257
1360
|
loadCallbookDevices(),
|
|
1258
1361
|
loadContacts(),
|
|
1259
1362
|
loadCalls(),
|
|
@@ -1262,6 +1365,10 @@ async function bootstrap() {
|
|
|
1262
1365
|
loadLogs()
|
|
1263
1366
|
]);
|
|
1264
1367
|
showNotice('Dashboard loaded');
|
|
1368
|
+
|
|
1369
|
+
setInterval(() => {
|
|
1370
|
+
loadAutoUpdateStatus().catch(() => {});
|
|
1371
|
+
}, 10000);
|
|
1265
1372
|
} catch (err) {
|
|
1266
1373
|
showNotice(err.message);
|
|
1267
1374
|
}
|
|
@@ -171,6 +171,15 @@
|
|
|
171
171
|
<h3>Remote Callbook</h3>
|
|
172
172
|
<div id="callbook-status" class="card"></div>
|
|
173
173
|
|
|
174
|
+
<h3>Auto Update</h3>
|
|
175
|
+
<div id="auto-update-status" class="card">Loading…</div>
|
|
176
|
+
<div class="row">
|
|
177
|
+
<button id="auto-update-refresh" type="button">Refresh</button>
|
|
178
|
+
<button id="auto-update-check" type="button">Check now</button>
|
|
179
|
+
<button id="auto-update-now" type="button">Update now</button>
|
|
180
|
+
<button id="auto-update-toggle" type="button">Disable auto-update</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
174
183
|
<form id="callbook-provision-form" class="card">
|
|
175
184
|
<div class="row">
|
|
176
185
|
<button type="submit">Create Install Link (24h)</button>
|
|
@@ -202,6 +202,33 @@ tr[data-selected="1"] td {
|
|
|
202
202
|
display: none;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
.status-pill {
|
|
206
|
+
display: inline-block;
|
|
207
|
+
padding: 0.15rem 0.45rem;
|
|
208
|
+
border-radius: 999px;
|
|
209
|
+
border: 1px solid var(--line);
|
|
210
|
+
font-size: 0.78rem;
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.status-pill.ok {
|
|
215
|
+
background: #ecfdf3;
|
|
216
|
+
border-color: #9bd8b8;
|
|
217
|
+
color: #125934;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.status-pill.warn {
|
|
221
|
+
background: #fff7e8;
|
|
222
|
+
border-color: #f1d08e;
|
|
223
|
+
color: #8a5a00;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.status-pill.err {
|
|
227
|
+
background: #fff0f1;
|
|
228
|
+
border-color: #efb1b6;
|
|
229
|
+
color: #8c1d26;
|
|
230
|
+
}
|
|
231
|
+
|
|
205
232
|
@media (max-width: 720px) {
|
|
206
233
|
nav {
|
|
207
234
|
overflow-x: auto;
|