getpatter 0.6.0 → 0.6.1
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/dist/barge-in-strategies-X6ARMGIQ.mjs +12 -0
- package/dist/chunk-D4424JZR.mjs +71 -0
- package/dist/{chunk-X3364LSI.mjs → chunk-RV7APPYE.mjs} +36 -2
- package/dist/{chunk-JUQ5WQTQ.mjs → chunk-TEW3NAZJ.mjs} +3244 -1674
- package/dist/cli.js +277 -24
- package/dist/dashboard/ui.html +13 -13
- package/dist/index.d.mts +1525 -364
- package/dist/index.d.ts +1525 -364
- package/dist/index.js +3921 -986
- package/dist/index.mjs +1310 -70
- package/dist/{silero-vad-YLCXT5GQ.mjs → silero-vad-NSEXI4XS.mjs} +1 -1
- package/dist/{test-mode-Y7YG5LFZ.mjs → test-mode-WEKKNBLD.mjs} +1 -1
- package/package.json +1 -1
- package/src/dashboard/ui.html +13 -13
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,19 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
50
50
|
maxCalls;
|
|
51
51
|
calls = [];
|
|
52
52
|
activeCalls = /* @__PURE__ */ new Map();
|
|
53
|
+
/**
|
|
54
|
+
* User-driven soft delete: call_ids the operator removed from the
|
|
55
|
+
* dashboard view. The on-disk artefacts written by ``CallLogger``
|
|
56
|
+
* (``metadata.json``, ``transcript.jsonl``) are intentionally NOT
|
|
57
|
+
* touched — they serve as the durable backup. All read paths
|
|
58
|
+
* (``getCalls`` / ``getCall`` / ``getAggregates`` / ``getCallsInRange``
|
|
59
|
+
* / ``hydrate``) filter against this set so the call is invisible
|
|
60
|
+
* to the UI and excluded from rolling metrics. Populated from
|
|
61
|
+
* ``<logRoot>/.deleted_call_ids.json`` on hydrate so deletions
|
|
62
|
+
* survive a process restart. Parity with Python.
|
|
63
|
+
*/
|
|
64
|
+
deletedCallIds = /* @__PURE__ */ new Set();
|
|
65
|
+
deletedIdsPath = null;
|
|
53
66
|
/**
|
|
54
67
|
* Accepts either a numeric ``maxCalls`` (legacy positional — matches the
|
|
55
68
|
* original TS API) or an options object ``{ maxCalls }`` to align with the
|
|
@@ -142,6 +155,8 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
142
155
|
ended_at: Date.now() / 1e3,
|
|
143
156
|
status,
|
|
144
157
|
metrics: null,
|
|
158
|
+
...active.turns && active.turns.length > 0 ? { turns: active.turns } : {},
|
|
159
|
+
...active.transcript && active.transcript.length > 0 ? { transcript: active.transcript } : {},
|
|
145
160
|
...extra
|
|
146
161
|
};
|
|
147
162
|
this.activeCalls.delete(callId);
|
|
@@ -170,6 +185,21 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
170
185
|
if (active) {
|
|
171
186
|
if (!active.turns) active.turns = [];
|
|
172
187
|
active.turns.push(turn);
|
|
188
|
+
if (!active.transcript) active.transcript = [];
|
|
189
|
+
const turnRecord = turn;
|
|
190
|
+
const userText = typeof turnRecord.user_text === "string" ? turnRecord.user_text : "";
|
|
191
|
+
const agentText = typeof turnRecord.agent_text === "string" ? turnRecord.agent_text : "";
|
|
192
|
+
const ts = typeof turnRecord.timestamp === "number" ? turnRecord.timestamp : Date.now() / 1e3;
|
|
193
|
+
if (userText.length > 0) {
|
|
194
|
+
active.transcript.push({ role: "user", text: userText, timestamp: ts });
|
|
195
|
+
}
|
|
196
|
+
if (agentText.length > 0 && agentText !== "[interrupted]") {
|
|
197
|
+
active.transcript.push({
|
|
198
|
+
role: "assistant",
|
|
199
|
+
text: agentText,
|
|
200
|
+
timestamp: ts
|
|
201
|
+
});
|
|
202
|
+
}
|
|
173
203
|
}
|
|
174
204
|
this.publish("turn_complete", { call_id: callId, turn });
|
|
175
205
|
}
|
|
@@ -179,40 +209,140 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
179
209
|
if (!callId) return;
|
|
180
210
|
const active = this.activeCalls.get(callId);
|
|
181
211
|
this.activeCalls.delete(callId);
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
let existingIdx = -1;
|
|
213
|
+
if (active === void 0) {
|
|
214
|
+
for (let i = this.calls.length - 1; i >= 0; i--) {
|
|
215
|
+
if (this.calls[i].call_id === callId) {
|
|
216
|
+
existingIdx = i;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const existing = existingIdx >= 0 ? this.calls[existingIdx] : void 0;
|
|
222
|
+
const priorStatus = active?.status ?? existing?.status;
|
|
223
|
+
const resolvedStatus = priorStatus && priorStatus !== "in-progress" ? priorStatus : "completed";
|
|
224
|
+
const dataTranscript = data.transcript;
|
|
225
|
+
const resolvedTranscript = dataTranscript && dataTranscript.length > 0 ? dataTranscript : active?.transcript && active.transcript.length > 0 ? active.transcript : existing?.transcript && existing.transcript.length > 0 ? existing.transcript : [];
|
|
226
|
+
const resolvedTurns = active?.turns && active.turns.length > 0 ? active.turns : existing?.turns && existing.turns.length > 0 ? existing.turns : void 0;
|
|
184
227
|
const entry = {
|
|
185
228
|
call_id: callId,
|
|
186
|
-
caller: data.caller || active?.caller || "",
|
|
187
|
-
callee: data.callee || active?.callee || "",
|
|
188
|
-
direction: active?.direction || data.direction || "inbound",
|
|
189
|
-
started_at: active?.started_at || 0,
|
|
229
|
+
caller: data.caller || active?.caller || existing?.caller || "",
|
|
230
|
+
callee: data.callee || active?.callee || existing?.callee || "",
|
|
231
|
+
direction: active?.direction || existing?.direction || data.direction || "inbound",
|
|
232
|
+
started_at: active?.started_at || existing?.started_at || 0,
|
|
190
233
|
ended_at: Date.now() / 1e3,
|
|
191
|
-
transcript:
|
|
234
|
+
transcript: resolvedTranscript,
|
|
235
|
+
...resolvedTurns ? { turns: resolvedTurns } : {},
|
|
192
236
|
status: resolvedStatus,
|
|
193
|
-
metrics: metrics ?? null
|
|
237
|
+
metrics: metrics ?? existing?.metrics ?? null
|
|
194
238
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
239
|
+
if (existingIdx >= 0) {
|
|
240
|
+
this.calls[existingIdx] = entry;
|
|
241
|
+
} else {
|
|
242
|
+
this.calls.push(entry);
|
|
243
|
+
if (this.calls.length > this.maxCalls) {
|
|
244
|
+
this.calls = this.calls.slice(-this.maxCalls);
|
|
245
|
+
}
|
|
198
246
|
}
|
|
199
247
|
this.publish("call_end", {
|
|
200
248
|
call_id: callId,
|
|
201
249
|
metrics: entry.metrics ?? null
|
|
202
250
|
});
|
|
203
251
|
}
|
|
204
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Return a window of completed calls in newest-first order.
|
|
254
|
+
*
|
|
255
|
+
* Soft-deleted call_ids (see ``deleteCalls``) are filtered out so the
|
|
256
|
+
* dashboard never re-shows a row the user removed. The on-disk
|
|
257
|
+
* artefacts are intentionally preserved as a backup.
|
|
258
|
+
*/
|
|
205
259
|
getCalls(limit = 50, offset = 0) {
|
|
206
|
-
const
|
|
260
|
+
const visible = this.calls.filter((c) => !this.deletedCallIds.has(c.call_id));
|
|
261
|
+
const ordered = visible.reverse();
|
|
207
262
|
return ordered.slice(offset, offset + limit);
|
|
208
263
|
}
|
|
209
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Look up a completed call by id (newest match wins).
|
|
266
|
+
*
|
|
267
|
+
* Soft-deleted call_ids resolve to ``null`` so the SPA's detail pane
|
|
268
|
+
* cannot render a row the user removed.
|
|
269
|
+
*/
|
|
210
270
|
getCall(callId) {
|
|
271
|
+
if (this.deletedCallIds.has(callId)) return null;
|
|
211
272
|
for (let i = this.calls.length - 1; i >= 0; i--) {
|
|
212
273
|
if (this.calls[i].call_id === callId) return this.calls[i];
|
|
213
274
|
}
|
|
214
275
|
return null;
|
|
215
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Soft-delete one or more calls from the dashboard view.
|
|
279
|
+
*
|
|
280
|
+
* Adds each ``call_id`` to an in-memory set. Subsequent reads via
|
|
281
|
+
* ``getCalls`` / ``getCall`` / ``getAggregates`` / ``getCallsInRange``
|
|
282
|
+
* exclude the deleted ids, so rolling metrics (avg latency, total
|
|
283
|
+
* spend) are recomputed without them. The on-disk
|
|
284
|
+
* ``metadata.json`` / ``transcript.jsonl`` files written by
|
|
285
|
+
* ``CallLogger`` are NOT touched — they serve as a durable backup
|
|
286
|
+
* the operator can audit outside the dashboard.
|
|
287
|
+
*
|
|
288
|
+
* Active calls are never deletable. A call_id that is currently
|
|
289
|
+
* in ``activeCalls`` is silently skipped so a mid-call delete
|
|
290
|
+
* from the UI cannot orphan the live transcript pane.
|
|
291
|
+
*
|
|
292
|
+
* Persisted to ``<logRoot>/.deleted_call_ids.json`` (best-effort)
|
|
293
|
+
* when ``hydrate()`` has been called with a log root. Parity with
|
|
294
|
+
* Python ``delete_calls``.
|
|
295
|
+
*
|
|
296
|
+
* @returns The list of call_ids actually accepted as deleted.
|
|
297
|
+
*/
|
|
298
|
+
deleteCalls(callIds) {
|
|
299
|
+
const ids = /* @__PURE__ */ new Set();
|
|
300
|
+
for (const cid of callIds || []) {
|
|
301
|
+
if (typeof cid === "string" && cid && !this.activeCalls.has(cid)) {
|
|
302
|
+
ids.add(cid);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (ids.size === 0) return [];
|
|
306
|
+
const accepted = [];
|
|
307
|
+
for (const cid of ids) {
|
|
308
|
+
if (!this.deletedCallIds.has(cid)) {
|
|
309
|
+
this.deletedCallIds.add(cid);
|
|
310
|
+
accepted.push(cid);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (accepted.length === 0) return [];
|
|
314
|
+
accepted.sort();
|
|
315
|
+
this.persistDeletedIds();
|
|
316
|
+
this.publish("calls_deleted", { call_ids: accepted });
|
|
317
|
+
return accepted;
|
|
318
|
+
}
|
|
319
|
+
/** Whether ``callId`` was soft-deleted from the dashboard. */
|
|
320
|
+
isDeleted(callId) {
|
|
321
|
+
return this.deletedCallIds.has(callId);
|
|
322
|
+
}
|
|
323
|
+
/** Snapshot of soft-deleted call_ids (sorted). */
|
|
324
|
+
getDeletedCallIds() {
|
|
325
|
+
return Array.from(this.deletedCallIds).sort();
|
|
326
|
+
}
|
|
327
|
+
/** Atomically persist the deleted-ids set to disk. Best-effort. */
|
|
328
|
+
persistDeletedIds() {
|
|
329
|
+
if (this.deletedIdsPath === null) return;
|
|
330
|
+
try {
|
|
331
|
+
const dir = path.dirname(this.deletedIdsPath);
|
|
332
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
333
|
+
const tmp = this.deletedIdsPath + ".tmp";
|
|
334
|
+
const payload = {
|
|
335
|
+
version: 1,
|
|
336
|
+
deleted_call_ids: Array.from(this.deletedCallIds).sort()
|
|
337
|
+
};
|
|
338
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
339
|
+
fs.renameSync(tmp, this.deletedIdsPath);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
getLogger().debug(
|
|
342
|
+
`MetricsStore.persistDeletedIds: ${String(err)}`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
216
346
|
/** Look up an active call by id (returns undefined if not active or unknown). */
|
|
217
347
|
getActive(callId) {
|
|
218
348
|
return this.activeCalls.get(callId);
|
|
@@ -221,9 +351,17 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
221
351
|
getActiveCalls() {
|
|
222
352
|
return Array.from(this.activeCalls.values());
|
|
223
353
|
}
|
|
224
|
-
/**
|
|
354
|
+
/**
|
|
355
|
+
* Compute summary statistics across the buffered call history.
|
|
356
|
+
*
|
|
357
|
+
* Soft-deleted calls are excluded so rolling metrics (avg latency,
|
|
358
|
+
* total spend) match exactly what the operator sees in the call list.
|
|
359
|
+
*/
|
|
225
360
|
getAggregates() {
|
|
226
|
-
const
|
|
361
|
+
const visible = this.calls.filter(
|
|
362
|
+
(c) => !this.deletedCallIds.has(c.call_id)
|
|
363
|
+
);
|
|
364
|
+
const totalCalls = visible.length;
|
|
227
365
|
if (totalCalls === 0) {
|
|
228
366
|
return {
|
|
229
367
|
total_calls: 0,
|
|
@@ -242,7 +380,7 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
242
380
|
let costTts = 0;
|
|
243
381
|
let costLlm = 0;
|
|
244
382
|
let costTel = 0;
|
|
245
|
-
for (const call of
|
|
383
|
+
for (const call of visible) {
|
|
246
384
|
const m = call.metrics;
|
|
247
385
|
if (!m) continue;
|
|
248
386
|
const cost = m.cost || {};
|
|
@@ -253,7 +391,7 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
253
391
|
costTel += cost.telephony || 0;
|
|
254
392
|
totalDuration += m.duration_seconds || 0;
|
|
255
393
|
const avgLat = m.latency_avg || {};
|
|
256
|
-
const tMs = avgLat.total_ms || 0;
|
|
394
|
+
const tMs = avgLat.agent_response_ms || avgLat.total_ms || 0;
|
|
257
395
|
if (tMs > 0) {
|
|
258
396
|
totalLatency += tMs;
|
|
259
397
|
latencyCount++;
|
|
@@ -273,18 +411,26 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
273
411
|
active_calls: this.activeCalls.size
|
|
274
412
|
};
|
|
275
413
|
}
|
|
276
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Return calls whose `started_at` falls within `[fromTs, toTs]` (Unix
|
|
416
|
+
* seconds). Soft-deleted calls are filtered out.
|
|
417
|
+
*/
|
|
277
418
|
getCallsInRange(fromTs = 0, toTs = 0) {
|
|
278
419
|
return this.calls.filter((call) => {
|
|
420
|
+
if (this.deletedCallIds.has(call.call_id)) return false;
|
|
279
421
|
const started = call.started_at || 0;
|
|
280
422
|
if (fromTs && started < fromTs) return false;
|
|
281
423
|
if (toTs && started > toTs) return false;
|
|
282
424
|
return true;
|
|
283
425
|
});
|
|
284
426
|
}
|
|
285
|
-
/** Number of completed calls currently in the ring buffer. */
|
|
427
|
+
/** Number of completed (non-deleted) calls currently in the ring buffer. */
|
|
286
428
|
get callCount() {
|
|
287
|
-
|
|
429
|
+
let n = 0;
|
|
430
|
+
for (const c of this.calls) {
|
|
431
|
+
if (!this.deletedCallIds.has(c.call_id)) n++;
|
|
432
|
+
}
|
|
433
|
+
return n;
|
|
288
434
|
}
|
|
289
435
|
/**
|
|
290
436
|
* Rebuild the in-memory call list from `metadata.json` files written by
|
|
@@ -298,6 +444,24 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
298
444
|
*/
|
|
299
445
|
hydrate(logRoot) {
|
|
300
446
|
if (!logRoot) return 0;
|
|
447
|
+
const deletedIdsPath = path.join(logRoot, ".deleted_call_ids.json");
|
|
448
|
+
this.deletedIdsPath = deletedIdsPath;
|
|
449
|
+
if (fs.existsSync(deletedIdsPath)) {
|
|
450
|
+
try {
|
|
451
|
+
const raw = fs.readFileSync(deletedIdsPath, "utf8");
|
|
452
|
+
const payload = JSON.parse(raw);
|
|
453
|
+
const arr = Array.isArray(payload.deleted_call_ids) ? payload.deleted_call_ids : [];
|
|
454
|
+
for (const cid of arr) {
|
|
455
|
+
if (typeof cid === "string" && cid.length > 0) {
|
|
456
|
+
this.deletedCallIds.add(cid);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch (err) {
|
|
460
|
+
getLogger().debug(
|
|
461
|
+
`MetricsStore.hydrate: skipping ${deletedIdsPath}: ${String(err)}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
301
465
|
const callsRoot = path.join(logRoot, "calls");
|
|
302
466
|
if (!fs.existsSync(callsRoot)) return 0;
|
|
303
467
|
const collected = [];
|
|
@@ -332,6 +496,12 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
332
496
|
);
|
|
333
497
|
continue;
|
|
334
498
|
}
|
|
499
|
+
if (!record.transcript || record.transcript.length === 0) {
|
|
500
|
+
const fromJsonl = loadTranscriptJsonl(
|
|
501
|
+
path.join(childPath, "transcript.jsonl")
|
|
502
|
+
);
|
|
503
|
+
if (fromJsonl.length > 0) record.transcript = fromJsonl;
|
|
504
|
+
}
|
|
335
505
|
collected.push(record);
|
|
336
506
|
seen.add(callId);
|
|
337
507
|
} catch (err) {
|
|
@@ -353,12 +523,45 @@ var MetricsStore = class extends import_events.EventEmitter {
|
|
|
353
523
|
return collected.length;
|
|
354
524
|
}
|
|
355
525
|
};
|
|
526
|
+
function metricsFromTopLevel(meta) {
|
|
527
|
+
const cost = meta.cost && typeof meta.cost === "object" ? meta.cost : null;
|
|
528
|
+
const latency = meta.latency && typeof meta.latency === "object" ? meta.latency : null;
|
|
529
|
+
const durationMs = meta.duration_ms;
|
|
530
|
+
const telephony = meta.telephony_provider;
|
|
531
|
+
if (cost === null && latency === null && durationMs == null && !telephony) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const out = {};
|
|
535
|
+
if (cost !== null) out.cost = cost;
|
|
536
|
+
if (latency !== null) {
|
|
537
|
+
const fullAvg = latency.avg && typeof latency.avg === "object" ? latency.avg : null;
|
|
538
|
+
const fullP50 = latency.p50 && typeof latency.p50 === "object" ? latency.p50 : null;
|
|
539
|
+
const fullP95 = latency.p95 && typeof latency.p95 === "object" ? latency.p95 : null;
|
|
540
|
+
const fullP99 = latency.p99 && typeof latency.p99 === "object" ? latency.p99 : null;
|
|
541
|
+
if (fullAvg) out.latency_avg = fullAvg;
|
|
542
|
+
if (fullP50) out.latency_p50 = fullP50;
|
|
543
|
+
if (fullP95) out.latency_p95 = fullP95;
|
|
544
|
+
if (fullP99) out.latency_p99 = fullP99;
|
|
545
|
+
if (!fullAvg && !fullP50 && !fullP95) {
|
|
546
|
+
const totalMs = typeof latency.p95_ms === "number" && latency.p95_ms || typeof latency.p50_ms === "number" && latency.p50_ms || 0;
|
|
547
|
+
out.latency_avg = { total_ms: totalMs };
|
|
548
|
+
}
|
|
549
|
+
out.latency = latency;
|
|
550
|
+
}
|
|
551
|
+
if (typeof durationMs === "number" && durationMs > 0) {
|
|
552
|
+
out.duration_seconds = durationMs / 1e3;
|
|
553
|
+
}
|
|
554
|
+
if (typeof telephony === "string" && telephony) {
|
|
555
|
+
out.telephony_provider = telephony;
|
|
556
|
+
}
|
|
557
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
558
|
+
}
|
|
356
559
|
function metadataToCallRecord(callId, meta) {
|
|
357
560
|
const startedAt = parseTimestamp(meta.started_at);
|
|
358
561
|
if (startedAt === null) return null;
|
|
359
562
|
const endedAt = parseTimestamp(meta.ended_at);
|
|
360
563
|
const status = meta.status || "completed";
|
|
361
|
-
const metrics = meta.metrics && typeof meta.metrics === "object" ? meta.metrics :
|
|
564
|
+
const metrics = meta.metrics && typeof meta.metrics === "object" ? meta.metrics : metricsFromTopLevel(meta);
|
|
362
565
|
const transcript = Array.isArray(meta.transcript) ? meta.transcript : [];
|
|
363
566
|
return {
|
|
364
567
|
call_id: callId,
|
|
@@ -372,6 +575,36 @@ function metadataToCallRecord(callId, meta) {
|
|
|
372
575
|
transcript
|
|
373
576
|
};
|
|
374
577
|
}
|
|
578
|
+
function loadTranscriptJsonl(filePath) {
|
|
579
|
+
try {
|
|
580
|
+
if (!fs.existsSync(filePath)) return [];
|
|
581
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
582
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
583
|
+
const out = [];
|
|
584
|
+
for (const line of lines) {
|
|
585
|
+
let row;
|
|
586
|
+
try {
|
|
587
|
+
row = JSON.parse(line);
|
|
588
|
+
} catch {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const tsIso = typeof row.ts === "string" ? Date.parse(row.ts) : NaN;
|
|
592
|
+
const tsNumeric = typeof row.timestamp === "number" ? row.timestamp * 1e3 : NaN;
|
|
593
|
+
const timestamp = Number.isFinite(tsIso) ? tsIso : Number.isFinite(tsNumeric) ? tsNumeric : 0;
|
|
594
|
+
const userText = typeof row.user_text === "string" ? row.user_text : "";
|
|
595
|
+
const agentText = typeof row.agent_text === "string" ? row.agent_text : "";
|
|
596
|
+
if (userText.length > 0) {
|
|
597
|
+
out.push({ role: "user", text: userText, timestamp });
|
|
598
|
+
}
|
|
599
|
+
if (agentText.length > 0 && agentText !== "[interrupted]") {
|
|
600
|
+
out.push({ role: "assistant", text: agentText, timestamp });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
} catch {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
375
608
|
function parseTimestamp(raw) {
|
|
376
609
|
if (typeof raw === "number") {
|
|
377
610
|
return Number.isFinite(raw) ? raw : null;
|
|
@@ -512,7 +745,8 @@ function mountDashboard(app, store, token = "") {
|
|
|
512
745
|
res.json(store.getCalls(limit, offset));
|
|
513
746
|
});
|
|
514
747
|
app.get("/api/dashboard/calls/:callId", auth, (req, res) => {
|
|
515
|
-
const
|
|
748
|
+
const callId = String(req.params.callId);
|
|
749
|
+
const call = store.getCall(callId) ?? store.getActive(callId);
|
|
516
750
|
if (!call) {
|
|
517
751
|
res.status(404).json({ error: "Not found" });
|
|
518
752
|
return;
|
|
@@ -525,6 +759,24 @@ function mountDashboard(app, store, token = "") {
|
|
|
525
759
|
app.get("/api/dashboard/aggregates", auth, (_req, res) => {
|
|
526
760
|
res.json(store.getAggregates());
|
|
527
761
|
});
|
|
762
|
+
app.delete("/api/dashboard/calls/:callId", auth, (req, res) => {
|
|
763
|
+
const callId = String(req.params.callId);
|
|
764
|
+
const accepted = store.deleteCalls([callId]);
|
|
765
|
+
res.json({ deleted: accepted, count: accepted.length });
|
|
766
|
+
});
|
|
767
|
+
app.post("/api/dashboard/calls/delete", auth, (req, res) => {
|
|
768
|
+
const body = req.body ?? {};
|
|
769
|
+
const raw = body.call_ids;
|
|
770
|
+
if (!Array.isArray(raw)) {
|
|
771
|
+
res.status(400).json({ error: "Expected JSON body { 'call_ids': [...] }" });
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const ids = raw.filter(
|
|
775
|
+
(cid) => typeof cid === "string" && cid.length > 0
|
|
776
|
+
);
|
|
777
|
+
const accepted = store.deleteCalls(ids);
|
|
778
|
+
res.json({ deleted: accepted, count: accepted.length });
|
|
779
|
+
});
|
|
528
780
|
app.get("/api/dashboard/events", auth, (req, res) => {
|
|
529
781
|
res.writeHead(200, {
|
|
530
782
|
"Content-Type": "text/event-stream",
|
|
@@ -597,7 +849,8 @@ function mountApi(app, store, token = "") {
|
|
|
597
849
|
res.json({ data: active, count: active.length });
|
|
598
850
|
});
|
|
599
851
|
app.get("/api/v1/calls/:callId", auth, (req, res) => {
|
|
600
|
-
const
|
|
852
|
+
const callId = String(req.params.callId);
|
|
853
|
+
const call = store.getCall(callId) ?? store.getActive(callId);
|
|
601
854
|
if (!call) {
|
|
602
855
|
res.status(404).json({ error: "Call not found" });
|
|
603
856
|
return;
|