@absolutejs/voice 0.0.22-beta.111 → 0.0.22-beta.113
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/campaign.d.ts +114 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +209 -1
- package/package.json +1 -1
package/dist/campaign.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
|
+
import type { VoiceRedisTaskLeaseCoordinator } from './queue';
|
|
2
3
|
export type VoiceCampaignStatus = 'canceled' | 'completed' | 'draft' | 'paused' | 'running';
|
|
3
4
|
export type VoiceCampaignRecipientStatus = 'canceled' | 'completed' | 'failed' | 'pending' | 'queued';
|
|
4
5
|
export type VoiceCampaignAttemptStatus = 'canceled' | 'failed' | 'queued' | 'running' | 'succeeded';
|
|
@@ -153,17 +154,116 @@ export type VoiceCampaignRuntime = {
|
|
|
153
154
|
export type VoiceCampaignRoutesOptions = VoiceCampaignRuntimeOptions & {
|
|
154
155
|
headers?: HeadersInit;
|
|
155
156
|
htmlPath?: false | string;
|
|
157
|
+
observability?: VoiceCampaignObservabilityOptions;
|
|
156
158
|
name?: string;
|
|
157
159
|
path?: string;
|
|
158
160
|
title?: string;
|
|
159
161
|
};
|
|
162
|
+
export type VoiceCampaignWorkerOptions = {
|
|
163
|
+
campaignIds?: string[];
|
|
164
|
+
leaseMs?: number;
|
|
165
|
+
leases: VoiceRedisTaskLeaseCoordinator;
|
|
166
|
+
maxCampaigns?: number;
|
|
167
|
+
runtime?: VoiceCampaignRuntime;
|
|
168
|
+
statuses?: VoiceCampaignStatus[];
|
|
169
|
+
store?: VoiceCampaignStore;
|
|
170
|
+
workerId: string;
|
|
171
|
+
};
|
|
172
|
+
export type VoiceCampaignWorkerResult = {
|
|
173
|
+
attempted: number;
|
|
174
|
+
campaigns: number;
|
|
175
|
+
errors: Array<{
|
|
176
|
+
campaignId: string;
|
|
177
|
+
error: string;
|
|
178
|
+
}>;
|
|
179
|
+
skipped: number;
|
|
180
|
+
started: VoiceCampaignAttempt[];
|
|
181
|
+
};
|
|
182
|
+
export type VoiceCampaignWorker = {
|
|
183
|
+
drain: () => Promise<VoiceCampaignWorkerResult>;
|
|
184
|
+
};
|
|
185
|
+
export type VoiceCampaignWorkerLoopOptions = {
|
|
186
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
187
|
+
pollIntervalMs?: number;
|
|
188
|
+
worker: VoiceCampaignWorker;
|
|
189
|
+
};
|
|
190
|
+
export type VoiceCampaignWorkerLoop = {
|
|
191
|
+
isRunning: () => boolean;
|
|
192
|
+
start: () => void;
|
|
193
|
+
stop: () => void;
|
|
194
|
+
tick: () => Promise<VoiceCampaignWorkerResult>;
|
|
195
|
+
};
|
|
196
|
+
export type VoiceCampaignObservabilityOptions = {
|
|
197
|
+
leases?: VoiceRedisTaskLeaseCoordinator;
|
|
198
|
+
now?: number;
|
|
199
|
+
rateWindowMs?: number;
|
|
200
|
+
stuckAfterMs?: number;
|
|
201
|
+
};
|
|
202
|
+
export type VoiceCampaignObservabilityReport = {
|
|
203
|
+
attemptRate: {
|
|
204
|
+
failed: number;
|
|
205
|
+
started: number;
|
|
206
|
+
succeeded: number;
|
|
207
|
+
windowMs: number;
|
|
208
|
+
};
|
|
209
|
+
campaigns: Array<{
|
|
210
|
+
activeAttempts: number;
|
|
211
|
+
campaignId: string;
|
|
212
|
+
lease?: {
|
|
213
|
+
expiresAt: number;
|
|
214
|
+
workerId: string;
|
|
215
|
+
};
|
|
216
|
+
name: string;
|
|
217
|
+
queueDepth: number;
|
|
218
|
+
status: VoiceCampaignStatus;
|
|
219
|
+
stuckAttempts: number;
|
|
220
|
+
stuckRecipients: number;
|
|
221
|
+
updatedAt: number;
|
|
222
|
+
}>;
|
|
223
|
+
failureReasons: Array<{
|
|
224
|
+
count: number;
|
|
225
|
+
reason: string;
|
|
226
|
+
}>;
|
|
227
|
+
generatedAt: number;
|
|
228
|
+
leases: {
|
|
229
|
+
active: number;
|
|
230
|
+
known: boolean;
|
|
231
|
+
};
|
|
232
|
+
queue: {
|
|
233
|
+
activeAttempts: number;
|
|
234
|
+
queuedRecipients: number;
|
|
235
|
+
runningCampaigns: number;
|
|
236
|
+
};
|
|
237
|
+
stuck: {
|
|
238
|
+
attempts: Array<{
|
|
239
|
+
attemptId: string;
|
|
240
|
+
campaignId: string;
|
|
241
|
+
recipientId: string;
|
|
242
|
+
status: VoiceCampaignAttemptStatus;
|
|
243
|
+
updatedAt: number;
|
|
244
|
+
}>;
|
|
245
|
+
recipients: Array<{
|
|
246
|
+
campaignId: string;
|
|
247
|
+
recipientId: string;
|
|
248
|
+
status: VoiceCampaignRecipientStatus;
|
|
249
|
+
updatedAt: number;
|
|
250
|
+
}>;
|
|
251
|
+
};
|
|
252
|
+
summary: VoiceCampaignSummary;
|
|
253
|
+
};
|
|
160
254
|
export declare const createVoiceMemoryCampaignStore: () => VoiceCampaignStore;
|
|
161
255
|
export declare const summarizeVoiceCampaigns: (records: VoiceCampaignRecord[]) => VoiceCampaignSummary;
|
|
256
|
+
export declare const buildVoiceCampaignObservabilityReport: (records: VoiceCampaignRecord[], options?: VoiceCampaignObservabilityOptions) => Promise<VoiceCampaignObservabilityReport>;
|
|
162
257
|
export declare const createVoiceCampaign: (options: VoiceCampaignRuntimeOptions) => VoiceCampaignRuntime;
|
|
258
|
+
export declare const createVoiceCampaignWorker: (options: VoiceCampaignWorkerOptions) => VoiceCampaignWorker;
|
|
259
|
+
export declare const createVoiceCampaignWorkerLoop: (options: VoiceCampaignWorkerLoopOptions) => VoiceCampaignWorkerLoop;
|
|
163
260
|
export declare const runVoiceCampaignProof: (options?: VoiceCampaignProofOptions) => Promise<VoiceCampaignProofReport>;
|
|
164
261
|
export declare const renderVoiceCampaignsHTML: (records: VoiceCampaignRecord[], options?: {
|
|
165
262
|
title?: string;
|
|
166
263
|
}) => string;
|
|
264
|
+
export declare const renderVoiceCampaignObservabilityHTML: (report: VoiceCampaignObservabilityReport, options?: {
|
|
265
|
+
title?: string;
|
|
266
|
+
}) => string;
|
|
167
267
|
export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOptions) => Elysia<"", {
|
|
168
268
|
decorator: {};
|
|
169
269
|
store: {};
|
|
@@ -194,6 +294,20 @@ export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOpt
|
|
|
194
294
|
};
|
|
195
295
|
};
|
|
196
296
|
};
|
|
297
|
+
} & {
|
|
298
|
+
[x: string]: {
|
|
299
|
+
observability: {
|
|
300
|
+
get: {
|
|
301
|
+
body: unknown;
|
|
302
|
+
params: {};
|
|
303
|
+
query: unknown;
|
|
304
|
+
headers: unknown;
|
|
305
|
+
response: {
|
|
306
|
+
200: VoiceCampaignObservabilityReport;
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
};
|
|
197
311
|
} & {
|
|
198
312
|
[x: string]: {
|
|
199
313
|
post: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { voice } from './plugin';
|
|
2
2
|
export { createVoiceAppKit, createVoiceAppKitRoutes, summarizeVoiceAppKitStatus } from './appKit';
|
|
3
|
-
export { createVoiceCampaign, createVoiceCampaignRoutes, createVoiceMemoryCampaignStore, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
|
|
3
|
+
export { buildVoiceCampaignObservabilityReport, createVoiceCampaign, createVoiceCampaignRoutes, createVoiceCampaignWorker, createVoiceCampaignWorkerLoop, createVoiceMemoryCampaignStore, renderVoiceCampaignObservabilityHTML, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
|
|
4
4
|
export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
|
|
5
5
|
export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
|
|
6
6
|
export { createVoiceBargeInRoutes, renderVoiceBargeInHTML, summarizeVoiceBargeIn } from './bargeInRoutes';
|
package/dist/index.js
CHANGED
|
@@ -10628,6 +10628,112 @@ var summarizeVoiceCampaigns = (records) => {
|
|
|
10628
10628
|
}
|
|
10629
10629
|
return summary;
|
|
10630
10630
|
};
|
|
10631
|
+
var buildVoiceCampaignObservabilityReport = async (records, options = {}) => {
|
|
10632
|
+
const now = options.now ?? Date.now();
|
|
10633
|
+
const stuckAfterMs = Math.max(1, options.stuckAfterMs ?? 15 * 60000);
|
|
10634
|
+
const rateWindowMs = Math.max(1, options.rateWindowMs ?? 60 * 60000);
|
|
10635
|
+
const rateWindowStart = now - rateWindowMs;
|
|
10636
|
+
const failureReasons = new Map;
|
|
10637
|
+
const report = {
|
|
10638
|
+
attemptRate: {
|
|
10639
|
+
failed: 0,
|
|
10640
|
+
started: 0,
|
|
10641
|
+
succeeded: 0,
|
|
10642
|
+
windowMs: rateWindowMs
|
|
10643
|
+
},
|
|
10644
|
+
campaigns: [],
|
|
10645
|
+
failureReasons: [],
|
|
10646
|
+
generatedAt: now,
|
|
10647
|
+
leases: {
|
|
10648
|
+
active: 0,
|
|
10649
|
+
known: Boolean(options.leases)
|
|
10650
|
+
},
|
|
10651
|
+
queue: {
|
|
10652
|
+
activeAttempts: 0,
|
|
10653
|
+
queuedRecipients: 0,
|
|
10654
|
+
runningCampaigns: 0
|
|
10655
|
+
},
|
|
10656
|
+
stuck: {
|
|
10657
|
+
attempts: [],
|
|
10658
|
+
recipients: []
|
|
10659
|
+
},
|
|
10660
|
+
summary: summarizeVoiceCampaigns(records)
|
|
10661
|
+
};
|
|
10662
|
+
for (const record of records) {
|
|
10663
|
+
const campaignId = record.campaign.id;
|
|
10664
|
+
const queuedRecipients = record.recipients.filter((recipient) => recipient.status === "queued");
|
|
10665
|
+
const activeAttempts = record.attempts.filter((attempt) => attempt.status === "queued" || attempt.status === "running");
|
|
10666
|
+
const campaignReport = {
|
|
10667
|
+
activeAttempts: activeAttempts.length,
|
|
10668
|
+
campaignId,
|
|
10669
|
+
name: record.campaign.name,
|
|
10670
|
+
queueDepth: queuedRecipients.length,
|
|
10671
|
+
status: record.campaign.status,
|
|
10672
|
+
stuckAttempts: 0,
|
|
10673
|
+
stuckRecipients: 0,
|
|
10674
|
+
updatedAt: record.campaign.updatedAt
|
|
10675
|
+
};
|
|
10676
|
+
if (record.campaign.status === "running") {
|
|
10677
|
+
report.queue.runningCampaigns += 1;
|
|
10678
|
+
}
|
|
10679
|
+
report.queue.queuedRecipients += queuedRecipients.length;
|
|
10680
|
+
report.queue.activeAttempts += activeAttempts.length;
|
|
10681
|
+
for (const recipient of record.recipients) {
|
|
10682
|
+
if ((recipient.status === "pending" || recipient.status === "queued") && now - recipient.updatedAt >= stuckAfterMs) {
|
|
10683
|
+
campaignReport.stuckRecipients += 1;
|
|
10684
|
+
report.stuck.recipients.push({
|
|
10685
|
+
campaignId,
|
|
10686
|
+
recipientId: recipient.id,
|
|
10687
|
+
status: recipient.status,
|
|
10688
|
+
updatedAt: recipient.updatedAt
|
|
10689
|
+
});
|
|
10690
|
+
}
|
|
10691
|
+
if (recipient.error) {
|
|
10692
|
+
failureReasons.set(recipient.error, (failureReasons.get(recipient.error) ?? 0) + 1);
|
|
10693
|
+
}
|
|
10694
|
+
}
|
|
10695
|
+
for (const attempt of record.attempts) {
|
|
10696
|
+
if ((attempt.startedAt ?? attempt.createdAt) >= rateWindowStart) {
|
|
10697
|
+
report.attemptRate.started += 1;
|
|
10698
|
+
}
|
|
10699
|
+
if (attempt.status === "failed" && attempt.updatedAt >= rateWindowStart) {
|
|
10700
|
+
report.attemptRate.failed += 1;
|
|
10701
|
+
}
|
|
10702
|
+
if (attempt.status === "succeeded" && attempt.updatedAt >= rateWindowStart) {
|
|
10703
|
+
report.attemptRate.succeeded += 1;
|
|
10704
|
+
}
|
|
10705
|
+
if (attempt.error) {
|
|
10706
|
+
failureReasons.set(attempt.error, (failureReasons.get(attempt.error) ?? 0) + 1);
|
|
10707
|
+
}
|
|
10708
|
+
if ((attempt.status === "queued" || attempt.status === "running") && now - attempt.updatedAt >= stuckAfterMs) {
|
|
10709
|
+
campaignReport.stuckAttempts += 1;
|
|
10710
|
+
report.stuck.attempts.push({
|
|
10711
|
+
attemptId: attempt.id,
|
|
10712
|
+
campaignId,
|
|
10713
|
+
recipientId: attempt.recipientId,
|
|
10714
|
+
status: attempt.status,
|
|
10715
|
+
updatedAt: attempt.updatedAt
|
|
10716
|
+
});
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10719
|
+
if (options.leases) {
|
|
10720
|
+
const lease = await options.leases.get(getCampaignLeaseTaskId(campaignId));
|
|
10721
|
+
if (lease) {
|
|
10722
|
+
report.leases.active += 1;
|
|
10723
|
+
campaignReport.lease = {
|
|
10724
|
+
expiresAt: lease.expiresAt,
|
|
10725
|
+
workerId: lease.workerId
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
report.campaigns.push(campaignReport);
|
|
10730
|
+
}
|
|
10731
|
+
report.failureReasons = [...failureReasons.entries()].map(([reason, count]) => ({ count, reason })).sort((left, right) => right.count === left.count ? left.reason.localeCompare(right.reason) : right.count - left.count);
|
|
10732
|
+
report.campaigns.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
10733
|
+
report.stuck.attempts.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
10734
|
+
report.stuck.recipients.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
10735
|
+
return report;
|
|
10736
|
+
};
|
|
10631
10737
|
var createVoiceCampaign = (options) => {
|
|
10632
10738
|
const { store } = options;
|
|
10633
10739
|
return {
|
|
@@ -10794,6 +10900,90 @@ var createVoiceCampaign = (options) => {
|
|
|
10794
10900
|
}
|
|
10795
10901
|
};
|
|
10796
10902
|
};
|
|
10903
|
+
var getCampaignLeaseTaskId = (campaignId) => `voice-campaign:${campaignId}`;
|
|
10904
|
+
var createVoiceCampaignWorker = (options) => {
|
|
10905
|
+
const leaseMs = Math.max(1, options.leaseMs ?? 30000);
|
|
10906
|
+
const runtime = options.runtime ?? createVoiceCampaign({
|
|
10907
|
+
store: options.store ?? createVoiceMemoryCampaignStore()
|
|
10908
|
+
});
|
|
10909
|
+
const statuses = options.statuses ?? ["running"];
|
|
10910
|
+
const campaignIds = new Set(options.campaignIds);
|
|
10911
|
+
const maxCampaigns = Math.max(1, options.maxCampaigns ?? Number.MAX_SAFE_INTEGER);
|
|
10912
|
+
return {
|
|
10913
|
+
drain: async () => {
|
|
10914
|
+
const result = {
|
|
10915
|
+
attempted: 0,
|
|
10916
|
+
campaigns: 0,
|
|
10917
|
+
errors: [],
|
|
10918
|
+
skipped: 0,
|
|
10919
|
+
started: []
|
|
10920
|
+
};
|
|
10921
|
+
const campaigns = [...await runtime.list()].filter((record) => (campaignIds.size === 0 || campaignIds.has(record.campaign.id)) && statuses.includes(record.campaign.status)).sort((left, right) => left.campaign.createdAt - right.campaign.createdAt).slice(0, maxCampaigns);
|
|
10922
|
+
for (const record of campaigns) {
|
|
10923
|
+
const campaignId = record.campaign.id;
|
|
10924
|
+
const taskId = getCampaignLeaseTaskId(campaignId);
|
|
10925
|
+
const claimed = await options.leases.claim({
|
|
10926
|
+
leaseMs,
|
|
10927
|
+
taskId,
|
|
10928
|
+
workerId: options.workerId
|
|
10929
|
+
});
|
|
10930
|
+
if (!claimed) {
|
|
10931
|
+
result.skipped += 1;
|
|
10932
|
+
continue;
|
|
10933
|
+
}
|
|
10934
|
+
try {
|
|
10935
|
+
const tick = await runtime.tick(campaignId);
|
|
10936
|
+
result.campaigns += 1;
|
|
10937
|
+
result.attempted += tick.attempted;
|
|
10938
|
+
result.started.push(...tick.started);
|
|
10939
|
+
result.errors.push(...tick.errors.map((error) => ({
|
|
10940
|
+
campaignId,
|
|
10941
|
+
error: error.error
|
|
10942
|
+
})));
|
|
10943
|
+
} catch (error) {
|
|
10944
|
+
result.errors.push({
|
|
10945
|
+
campaignId,
|
|
10946
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10947
|
+
});
|
|
10948
|
+
} finally {
|
|
10949
|
+
await options.leases.release({
|
|
10950
|
+
taskId,
|
|
10951
|
+
workerId: options.workerId
|
|
10952
|
+
});
|
|
10953
|
+
}
|
|
10954
|
+
}
|
|
10955
|
+
return result;
|
|
10956
|
+
}
|
|
10957
|
+
};
|
|
10958
|
+
};
|
|
10959
|
+
var createVoiceCampaignWorkerLoop = (options) => {
|
|
10960
|
+
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
|
|
10961
|
+
let timer;
|
|
10962
|
+
let running = false;
|
|
10963
|
+
const tick = async () => options.worker.drain();
|
|
10964
|
+
return {
|
|
10965
|
+
isRunning: () => running,
|
|
10966
|
+
start: () => {
|
|
10967
|
+
if (timer) {
|
|
10968
|
+
return;
|
|
10969
|
+
}
|
|
10970
|
+
running = true;
|
|
10971
|
+
timer = setInterval(() => {
|
|
10972
|
+
tick().catch((error) => {
|
|
10973
|
+
options.onError?.(error);
|
|
10974
|
+
});
|
|
10975
|
+
}, pollIntervalMs);
|
|
10976
|
+
},
|
|
10977
|
+
stop: () => {
|
|
10978
|
+
if (timer) {
|
|
10979
|
+
clearInterval(timer);
|
|
10980
|
+
timer = undefined;
|
|
10981
|
+
}
|
|
10982
|
+
running = false;
|
|
10983
|
+
},
|
|
10984
|
+
tick
|
|
10985
|
+
};
|
|
10986
|
+
};
|
|
10797
10987
|
var defaultProofRecipients = () => [
|
|
10798
10988
|
{
|
|
10799
10989
|
id: "campaign-proof-recipient-1",
|
|
@@ -10869,6 +11059,12 @@ var renderVoiceCampaignsHTML = (records, options = {}) => {
|
|
|
10869
11059
|
const summary = summarizeVoiceCampaigns(records);
|
|
10870
11060
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#111827;color:#f9fafb;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}.hero{background:linear-gradient(135deg,rgba(251,146,60,.18),rgba(45,212,191,.12));border:1px solid #334155;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#fdba74;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.grid article,table{background:#172033;border:1px solid #334155;border-radius:18px}.grid article{padding:16px}.grid span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:12px;text-align:left}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted outbound</p><h1>${escapeHtml18(title)}</h1><p>Campaign orchestration, recipients, attempts, retries, and outcomes without a hosted dialer dashboard.</p><section class="grid"><article><span>Campaigns</span><strong>${String(summary.campaigns.total)}</strong></article><article><span>Recipients</span><strong>${String(summary.recipients.total)}</strong></article><article><span>Attempts</span><strong>${String(summary.attempts.total)}</strong></article><article><span>Running</span><strong>${String(summary.campaigns.running)}</strong></article></section></section><table><thead><tr><th>Name</th><th>Status</th><th>Recipients</th><th>Attempts</th><th>Updated</th></tr></thead><tbody>${rows || '<tr><td colspan="5">No campaigns yet.</td></tr>'}</tbody></table></main></body></html>`;
|
|
10871
11061
|
};
|
|
11062
|
+
var renderVoiceCampaignObservabilityHTML = (report, options = {}) => {
|
|
11063
|
+
const title = options.title ?? "Voice Campaign Observability";
|
|
11064
|
+
const campaignRows = report.campaigns.map((campaign) => `<tr><td>${escapeHtml18(campaign.name)}</td><td>${escapeHtml18(campaign.status)}</td><td>${String(campaign.queueDepth)}</td><td>${String(campaign.activeAttempts)}</td><td>${String(campaign.stuckRecipients + campaign.stuckAttempts)}</td><td>${campaign.lease ? escapeHtml18(campaign.lease.workerId) : "none"}</td></tr>`).join("");
|
|
11065
|
+
const failureRows = report.failureReasons.map((failure) => `<tr><td>${escapeHtml18(failure.reason)}</td><td>${String(failure.count)}</td></tr>`).join("");
|
|
11066
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#0b1220;color:#e5edf7;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.2),rgba(251,146,60,.14));border:1px solid #334155;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.6rem);line-height:.95;margin:.2rem 0 1rem}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin:18px 0}.card,table{background:#111c2f;border:1px solid #334155;border-radius:18px}.card{padding:16px}.card span{color:#9fb0c5}.card strong{display:block;font-size:2rem;margin:.25rem 0}table{border-collapse:collapse;margin-top:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:12px;text-align:left}.warn{color:#fde68a}.bad{color:#fecaca}</style></head><body><main><section class="hero"><p class="eyebrow">Campaign ops</p><h1>${escapeHtml18(title)}</h1><p>Queue depth, active leases, attempt rates, failure reasons, and stuck work for self-hosted outbound voice.</p><section class="grid"><article class="card"><span>Queued recipients</span><strong>${String(report.queue.queuedRecipients)}</strong></article><article class="card"><span>Active attempts</span><strong>${String(report.queue.activeAttempts)}</strong></article><article class="card"><span>Running campaigns</span><strong>${String(report.queue.runningCampaigns)}</strong></article><article class="card"><span>Active leases</span><strong>${report.leases.known ? String(report.leases.active) : "n/a"}</strong></article><article class="card"><span>Attempts/window</span><strong>${String(report.attemptRate.started)}</strong></article><article class="card"><span>Stuck work</span><strong class="${report.stuck.attempts.length + report.stuck.recipients.length > 0 ? "bad" : ""}">${String(report.stuck.attempts.length + report.stuck.recipients.length)}</strong></article></section></section><h2>Campaigns</h2><table><thead><tr><th>Name</th><th>Status</th><th>Queued</th><th>Active</th><th>Stuck</th><th>Lease</th></tr></thead><tbody>${campaignRows || '<tr><td colspan="6">No campaigns yet.</td></tr>'}</tbody></table><h2>Failure Reasons</h2><table><thead><tr><th>Reason</th><th>Count</th></tr></thead><tbody>${failureRows || '<tr><td colspan="2">No failures recorded.</td></tr>'}</tbody></table></main></body></html>`;
|
|
11067
|
+
};
|
|
10872
11068
|
var readJsonBody = async (request) => {
|
|
10873
11069
|
const text = await request.text();
|
|
10874
11070
|
return text.trim() ? JSON.parse(text) : {};
|
|
@@ -10880,7 +11076,7 @@ var createVoiceCampaignRoutes = (options) => {
|
|
|
10880
11076
|
const app = new Elysia17({ name: options.name ?? "absolutejs-voice-campaigns" }).get(path, async () => ({
|
|
10881
11077
|
campaigns: await runtime.list(),
|
|
10882
11078
|
summary: await runtime.summarize()
|
|
10883
|
-
})).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
|
|
11079
|
+
})).get(`${path}/observability`, async () => buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability)).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
|
|
10884
11080
|
await runtime.remove(params.campaignId);
|
|
10885
11081
|
return { ok: true };
|
|
10886
11082
|
}).post(`${path}/:campaignId/recipients`, async ({ params, request }) => {
|
|
@@ -10899,6 +11095,14 @@ var createVoiceCampaignRoutes = (options) => {
|
|
|
10899
11095
|
...options.headers
|
|
10900
11096
|
}
|
|
10901
11097
|
});
|
|
11098
|
+
}).get(`${htmlPath}/observability`, async () => {
|
|
11099
|
+
const report = await buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability);
|
|
11100
|
+
return new Response(renderVoiceCampaignObservabilityHTML(report, options), {
|
|
11101
|
+
headers: {
|
|
11102
|
+
"content-type": "text/html; charset=utf-8",
|
|
11103
|
+
...options.headers
|
|
11104
|
+
}
|
|
11105
|
+
});
|
|
10902
11106
|
});
|
|
10903
11107
|
}
|
|
10904
11108
|
return app;
|
|
@@ -18949,6 +19153,7 @@ export {
|
|
|
18949
19153
|
renderVoiceEvalHTML,
|
|
18950
19154
|
renderVoiceEvalBaselineHTML,
|
|
18951
19155
|
renderVoiceCampaignsHTML,
|
|
19156
|
+
renderVoiceCampaignObservabilityHTML,
|
|
18952
19157
|
renderVoiceCallReviewMarkdown,
|
|
18953
19158
|
renderVoiceCallReviewHTML,
|
|
18954
19159
|
renderVoiceBargeInHTML,
|
|
@@ -19123,6 +19328,8 @@ export {
|
|
|
19123
19328
|
createVoiceExperiment,
|
|
19124
19329
|
createVoiceEvalRoutes,
|
|
19125
19330
|
createVoiceDiagnosticsRoutes,
|
|
19331
|
+
createVoiceCampaignWorkerLoop,
|
|
19332
|
+
createVoiceCampaignWorker,
|
|
19126
19333
|
createVoiceCampaignRoutes,
|
|
19127
19334
|
createVoiceCampaign,
|
|
19128
19335
|
createVoiceCallReviewRecorder,
|
|
@@ -19176,6 +19383,7 @@ export {
|
|
|
19176
19383
|
buildVoiceOpsTaskFromReview,
|
|
19177
19384
|
buildVoiceOpsConsoleReport,
|
|
19178
19385
|
buildVoiceDiagnosticsMarkdown,
|
|
19386
|
+
buildVoiceCampaignObservabilityReport,
|
|
19179
19387
|
assignVoiceOpsTask,
|
|
19180
19388
|
applyVoiceTelephonyOutcome,
|
|
19181
19389
|
applyVoiceOpsTaskPolicy,
|