@absolutejs/voice 0.0.22-beta.112 → 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.
@@ -154,6 +154,7 @@ export type VoiceCampaignRuntime = {
154
154
  export type VoiceCampaignRoutesOptions = VoiceCampaignRuntimeOptions & {
155
155
  headers?: HeadersInit;
156
156
  htmlPath?: false | string;
157
+ observability?: VoiceCampaignObservabilityOptions;
157
158
  name?: string;
158
159
  path?: string;
159
160
  title?: string;
@@ -192,8 +193,67 @@ export type VoiceCampaignWorkerLoop = {
192
193
  stop: () => void;
193
194
  tick: () => Promise<VoiceCampaignWorkerResult>;
194
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
+ };
195
254
  export declare const createVoiceMemoryCampaignStore: () => VoiceCampaignStore;
196
255
  export declare const summarizeVoiceCampaigns: (records: VoiceCampaignRecord[]) => VoiceCampaignSummary;
256
+ export declare const buildVoiceCampaignObservabilityReport: (records: VoiceCampaignRecord[], options?: VoiceCampaignObservabilityOptions) => Promise<VoiceCampaignObservabilityReport>;
197
257
  export declare const createVoiceCampaign: (options: VoiceCampaignRuntimeOptions) => VoiceCampaignRuntime;
198
258
  export declare const createVoiceCampaignWorker: (options: VoiceCampaignWorkerOptions) => VoiceCampaignWorker;
199
259
  export declare const createVoiceCampaignWorkerLoop: (options: VoiceCampaignWorkerLoopOptions) => VoiceCampaignWorkerLoop;
@@ -201,6 +261,9 @@ export declare const runVoiceCampaignProof: (options?: VoiceCampaignProofOptions
201
261
  export declare const renderVoiceCampaignsHTML: (records: VoiceCampaignRecord[], options?: {
202
262
  title?: string;
203
263
  }) => string;
264
+ export declare const renderVoiceCampaignObservabilityHTML: (report: VoiceCampaignObservabilityReport, options?: {
265
+ title?: string;
266
+ }) => string;
204
267
  export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOptions) => Elysia<"", {
205
268
  decorator: {};
206
269
  store: {};
@@ -231,6 +294,20 @@ export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOpt
231
294
  };
232
295
  };
233
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
+ };
234
311
  } & {
235
312
  [x: string]: {
236
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, createVoiceCampaignWorker, createVoiceCampaignWorkerLoop, 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 {
@@ -10953,6 +11059,12 @@ var renderVoiceCampaignsHTML = (records, options = {}) => {
10953
11059
  const summary = summarizeVoiceCampaigns(records);
10954
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>`;
10955
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
+ };
10956
11068
  var readJsonBody = async (request) => {
10957
11069
  const text = await request.text();
10958
11070
  return text.trim() ? JSON.parse(text) : {};
@@ -10964,7 +11076,7 @@ var createVoiceCampaignRoutes = (options) => {
10964
11076
  const app = new Elysia17({ name: options.name ?? "absolutejs-voice-campaigns" }).get(path, async () => ({
10965
11077
  campaigns: await runtime.list(),
10966
11078
  summary: await runtime.summarize()
10967
- })).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 }) => {
10968
11080
  await runtime.remove(params.campaignId);
10969
11081
  return { ok: true };
10970
11082
  }).post(`${path}/:campaignId/recipients`, async ({ params, request }) => {
@@ -10983,6 +11095,14 @@ var createVoiceCampaignRoutes = (options) => {
10983
11095
  ...options.headers
10984
11096
  }
10985
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
+ });
10986
11106
  });
10987
11107
  }
10988
11108
  return app;
@@ -19033,6 +19153,7 @@ export {
19033
19153
  renderVoiceEvalHTML,
19034
19154
  renderVoiceEvalBaselineHTML,
19035
19155
  renderVoiceCampaignsHTML,
19156
+ renderVoiceCampaignObservabilityHTML,
19036
19157
  renderVoiceCallReviewMarkdown,
19037
19158
  renderVoiceCallReviewHTML,
19038
19159
  renderVoiceBargeInHTML,
@@ -19262,6 +19383,7 @@ export {
19262
19383
  buildVoiceOpsTaskFromReview,
19263
19384
  buildVoiceOpsConsoleReport,
19264
19385
  buildVoiceDiagnosticsMarkdown,
19386
+ buildVoiceCampaignObservabilityReport,
19265
19387
  assignVoiceOpsTask,
19266
19388
  applyVoiceTelephonyOutcome,
19267
19389
  applyVoiceOpsTaskPolicy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.112",
3
+ "version": "0.0.22-beta.113",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",