@absolutejs/voice 0.0.22-beta.152 → 0.0.22-beta.153

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 CHANGED
@@ -653,6 +653,8 @@ export function DeliveryWorkers() {
653
653
  }
654
654
  ```
655
655
 
656
+ The widget includes operator actions by default: `Tick workers` drains pending/failed deliveries, and `Requeue dead letters` moves reviewed dead-lettered audit/trace deliveries back into the live queues. Pass `includeActions={false}` when you only want a read-only status card.
657
+
656
658
  ```ts
657
659
  import { VoiceDeliveryRuntime } from '@absolutejs/voice/vue';
658
660
  import { createVoiceDeliveryRuntime } from '@absolutejs/voice/svelte';
@@ -197,6 +197,12 @@ let _VoiceOpsStatusService = VoiceOpsStatusService;
197
197
  import { computed as computed2, Injectable as Injectable2, signal as signal2 } from "@angular/core";
198
198
 
199
199
  // src/client/deliveryRuntime.ts
200
+ var getDefaultActionPath = (path, action, options) => {
201
+ if (action === "tick") {
202
+ return options.tickPath ?? `${path.replace(/\/$/, "")}/tick`;
203
+ }
204
+ return options.requeueDeadLettersPath ?? `${path.replace(/\/$/, "")}/requeue-dead-letters`;
205
+ };
200
206
  var fetchVoiceDeliveryRuntime = async (path = "/api/voice-delivery-runtime", options = {}) => {
201
207
  const fetchImpl = options.fetch ?? globalThis.fetch;
202
208
  const response = await fetchImpl(path);
@@ -205,11 +211,29 @@ var fetchVoiceDeliveryRuntime = async (path = "/api/voice-delivery-runtime", opt
205
211
  }
206
212
  return await response.json();
207
213
  };
214
+ var runVoiceDeliveryRuntimeAction = async (action, path = "/api/voice-delivery-runtime", options = {}) => {
215
+ const fetchImpl = options.fetch ?? globalThis.fetch;
216
+ const response = await fetchImpl(getDefaultActionPath(path, action, options), {
217
+ method: "POST"
218
+ });
219
+ if (!response.ok) {
220
+ throw new Error(`Voice delivery runtime ${action} failed: HTTP ${response.status}`);
221
+ }
222
+ const body = await response.json();
223
+ return {
224
+ action,
225
+ result: body.result,
226
+ summary: body.summary,
227
+ updatedAt: Date.now()
228
+ };
229
+ };
208
230
  var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", options = {}) => {
209
231
  const listeners = new Set;
210
232
  let closed = false;
211
233
  let timer;
212
234
  let snapshot = {
235
+ actionError: null,
236
+ actionStatus: "idle",
213
237
  error: null,
214
238
  isLoading: false
215
239
  };
@@ -231,6 +255,7 @@ var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", opt
231
255
  try {
232
256
  const report = await fetchVoiceDeliveryRuntime(path, options);
233
257
  snapshot = {
258
+ ...snapshot,
234
259
  error: null,
235
260
  isLoading: false,
236
261
  report,
@@ -248,6 +273,37 @@ var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", opt
248
273
  throw error;
249
274
  }
250
275
  };
276
+ const runAction = async (action) => {
277
+ if (closed) {
278
+ return snapshot.lastAction;
279
+ }
280
+ snapshot = {
281
+ ...snapshot,
282
+ actionError: null,
283
+ actionStatus: "running"
284
+ };
285
+ emit();
286
+ try {
287
+ const result = await runVoiceDeliveryRuntimeAction(action, path, options);
288
+ snapshot = {
289
+ ...snapshot,
290
+ actionError: null,
291
+ actionStatus: "completed",
292
+ lastAction: result
293
+ };
294
+ emit();
295
+ await refresh();
296
+ return result;
297
+ } catch (error) {
298
+ snapshot = {
299
+ ...snapshot,
300
+ actionError: error instanceof Error ? error.message : String(error),
301
+ actionStatus: "failed"
302
+ };
303
+ emit();
304
+ throw error;
305
+ }
306
+ };
251
307
  const close = () => {
252
308
  closed = true;
253
309
  if (timer) {
@@ -265,7 +321,9 @@ var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", opt
265
321
  close,
266
322
  getServerSnapshot: () => snapshot,
267
323
  getSnapshot: () => snapshot,
324
+ requeueDeadLetters: () => runAction("requeue-dead-letters"),
268
325
  refresh,
326
+ tick: () => runAction("tick"),
269
327
  subscribe: (listener) => {
270
328
  listeners.add(listener);
271
329
  return () => {
@@ -284,12 +342,16 @@ var _init = __decoratorStart(undefined);
284
342
  class VoiceDeliveryRuntimeService {
285
343
  connect(path = "/api/voice-delivery-runtime", options = {}) {
286
344
  const store = createVoiceDeliveryRuntimeStore(path, options);
345
+ const actionErrorSignal = signal2(null);
346
+ const actionStatusSignal = signal2("idle");
287
347
  const errorSignal = signal2(null);
288
348
  const isLoadingSignal = signal2(false);
289
349
  const reportSignal = signal2(undefined);
290
350
  const updatedAtSignal = signal2(undefined);
291
351
  const sync = () => {
292
352
  const snapshot = store.getSnapshot();
353
+ actionErrorSignal.set(snapshot.actionError);
354
+ actionStatusSignal.set(snapshot.actionStatus);
293
355
  errorSignal.set(snapshot.error);
294
356
  isLoadingSignal.set(snapshot.isLoading);
295
357
  reportSignal.set(snapshot.report);
@@ -306,9 +368,13 @@ class VoiceDeliveryRuntimeService {
306
368
  store.close();
307
369
  },
308
370
  error: computed2(() => errorSignal()),
371
+ actionError: computed2(() => actionErrorSignal()),
372
+ actionStatus: computed2(() => actionStatusSignal()),
309
373
  isLoading: computed2(() => isLoadingSignal()),
374
+ requeueDeadLetters: store.requeueDeadLetters,
310
375
  refresh: store.refresh,
311
376
  report: computed2(() => reportSignal()),
377
+ tick: store.tick,
312
378
  updatedAt: computed2(() => updatedAtSignal())
313
379
  };
314
380
  }
@@ -2817,6 +2883,8 @@ var createVoiceDeliveryRuntimeViewModel = (snapshot, options = {}) => {
2817
2883
  return {
2818
2884
  description: options.description ?? DEFAULT_DESCRIPTION,
2819
2885
  error: snapshot.error,
2886
+ actionError: snapshot.actionError,
2887
+ actionStatus: snapshot.actionStatus,
2820
2888
  isLoading: snapshot.isLoading,
2821
2889
  isRunning: Boolean(report?.isRunning),
2822
2890
  label: snapshot.error ? "Unavailable" : report ? report.isRunning ? "Running" : "Stopped" : "Checking",
@@ -2833,6 +2901,11 @@ var renderVoiceDeliveryRuntimeHTML = (snapshot, options = {}) => {
2833
2901
  <strong>${escapeHtml(surface.detail)}</strong>
2834
2902
  <small>${String(surface.failed)} failed &middot; ${String(surface.deadLettered)} dead-lettered</small>
2835
2903
  </li>`).join("");
2904
+ const actions = options.includeActions === false ? "" : `<div class="absolute-voice-delivery-runtime__actions">
2905
+ <button type="button" data-absolute-voice-delivery-runtime-action="tick">${model.actionStatus === "running" ? "Working..." : "Tick workers"}</button>
2906
+ <button type="button" data-absolute-voice-delivery-runtime-action="requeue-dead-letters"${model.surfaces.some((surface) => surface.deadLettered > 0) ? "" : " disabled"}>Requeue dead letters</button>
2907
+ </div>`;
2908
+ const actionError = model.actionError ? `<p class="absolute-voice-delivery-runtime__error">${escapeHtml(model.actionError)}</p>` : "";
2836
2909
  return `<section class="absolute-voice-delivery-runtime absolute-voice-delivery-runtime--${escapeHtml(model.status)}">
2837
2910
  <header class="absolute-voice-delivery-runtime__header">
2838
2911
  <span class="absolute-voice-delivery-runtime__eyebrow">${escapeHtml(model.title)}</span>
@@ -2840,20 +2913,38 @@ var renderVoiceDeliveryRuntimeHTML = (snapshot, options = {}) => {
2840
2913
  </header>
2841
2914
  <p class="absolute-voice-delivery-runtime__description">${escapeHtml(model.description)}</p>
2842
2915
  <ul class="absolute-voice-delivery-runtime__surfaces">${surfaces}</ul>
2916
+ ${actions}
2917
+ ${actionError}
2843
2918
  ${model.error ? `<p class="absolute-voice-delivery-runtime__error">${escapeHtml(model.error)}</p>` : ""}
2844
2919
  </section>`;
2845
2920
  };
2846
- var getVoiceDeliveryRuntimeCSS = () => `.absolute-voice-delivery-runtime{border:1px solid #c9d8cf;border-radius:20px;background:#f6fff9;color:#0d1b12;padding:18px;box-shadow:0 18px 40px rgba(19,55,35,.12);font-family:inherit}.absolute-voice-delivery-runtime--warn,.absolute-voice-delivery-runtime--error{border-color:#f2b56b;background:#fff9ed}.absolute-voice-delivery-runtime__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-delivery-runtime__eyebrow{color:#4e6b59;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-delivery-runtime__label{font-size:28px;line-height:1}.absolute-voice-delivery-runtime__description{color:#33483b;margin:12px 0 0}.absolute-voice-delivery-runtime__surfaces{display:grid;gap:8px;list-style:none;margin:16px 0 0;padding:0}.absolute-voice-delivery-runtime__surface{background:#fff;border:1px solid #d9eadf;border-radius:14px;display:grid;gap:4px;padding:10px 12px}.absolute-voice-delivery-runtime__surface--warn{border-color:#f2b56b}.absolute-voice-delivery-runtime__surface--disabled{opacity:.72}.absolute-voice-delivery-runtime__surface span,.absolute-voice-delivery-runtime__surface small{color:#587063}.absolute-voice-delivery-runtime__error{color:#9f1239;font-weight:700}`;
2921
+ var getVoiceDeliveryRuntimeCSS = () => `.absolute-voice-delivery-runtime{border:1px solid #c9d8cf;border-radius:20px;background:#f6fff9;color:#0d1b12;padding:18px;box-shadow:0 18px 40px rgba(19,55,35,.12);font-family:inherit}.absolute-voice-delivery-runtime--warn,.absolute-voice-delivery-runtime--error{border-color:#f2b56b;background:#fff9ed}.absolute-voice-delivery-runtime__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-delivery-runtime__eyebrow{color:#4e6b59;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-delivery-runtime__label{font-size:28px;line-height:1}.absolute-voice-delivery-runtime__description{color:#33483b;margin:12px 0 0}.absolute-voice-delivery-runtime__surfaces{display:grid;gap:8px;list-style:none;margin:16px 0 0;padding:0}.absolute-voice-delivery-runtime__surface{background:#fff;border:1px solid #d9eadf;border-radius:14px;display:grid;gap:4px;padding:10px 12px}.absolute-voice-delivery-runtime__surface--warn{border-color:#f2b56b}.absolute-voice-delivery-runtime__surface--disabled{opacity:.72}.absolute-voice-delivery-runtime__surface span,.absolute-voice-delivery-runtime__surface small{color:#587063}.absolute-voice-delivery-runtime__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}.absolute-voice-delivery-runtime__actions button{background:#134e2d;border:0;border-radius:999px;color:#f6fff9;cursor:pointer;font:inherit;font-weight:800;padding:8px 12px}.absolute-voice-delivery-runtime__actions button:disabled{cursor:not-allowed;opacity:.48}.absolute-voice-delivery-runtime__error{color:#9f1239;font-weight:700}`;
2847
2922
  var mountVoiceDeliveryRuntime = (element, path = "/api/voice-delivery-runtime", options = {}) => {
2848
2923
  const store = createVoiceDeliveryRuntimeStore(path, options);
2849
2924
  const render = () => {
2850
2925
  element.innerHTML = renderVoiceDeliveryRuntimeHTML(store.getSnapshot(), options);
2851
2926
  };
2852
2927
  const unsubscribe = store.subscribe(render);
2928
+ const handleClick = (event) => {
2929
+ const target = event.target;
2930
+ if (!(target instanceof Element)) {
2931
+ return;
2932
+ }
2933
+ const action = target.closest("[data-absolute-voice-delivery-runtime-action]");
2934
+ const actionName = action?.getAttribute("data-absolute-voice-delivery-runtime-action");
2935
+ if (actionName === "tick") {
2936
+ store.tick().catch(() => {});
2937
+ }
2938
+ if (actionName === "requeue-dead-letters") {
2939
+ store.requeueDeadLetters().catch(() => {});
2940
+ }
2941
+ };
2942
+ element.addEventListener?.("click", handleClick);
2853
2943
  render();
2854
2944
  store.refresh().catch(() => {});
2855
2945
  return {
2856
2946
  close: () => {
2947
+ element.removeEventListener?.("click", handleClick);
2857
2948
  unsubscribe();
2858
2949
  store.close();
2859
2950
  },
@@ -2932,6 +3023,29 @@ var _dec = [
2932
3023
  </li>
2933
3024
  }
2934
3025
  </ul>
3026
+ <div class="absolute-voice-delivery-runtime__actions">
3027
+ <button
3028
+ type="button"
3029
+ [disabled]="model().actionStatus === 'running'"
3030
+ (click)="tick()"
3031
+ >
3032
+ {{ model().actionStatus === "running" ? "Working..." : "Tick workers" }}
3033
+ </button>
3034
+ <button
3035
+ type="button"
3036
+ [disabled]="
3037
+ model().actionStatus === 'running' || deadLettered() === 0
3038
+ "
3039
+ (click)="requeueDeadLetters()"
3040
+ >
3041
+ Requeue dead letters
3042
+ </button>
3043
+ </div>
3044
+ @if (model().actionError) {
3045
+ <p class="absolute-voice-delivery-runtime__error">
3046
+ {{ model().actionError }}
3047
+ </p>
3048
+ }
2935
3049
  @if (model().error) {
2936
3050
  <p class="absolute-voice-delivery-runtime__error">
2937
3051
  {{ model().error }}
@@ -2969,6 +3083,8 @@ class VoiceDeliveryRuntimeComponent {
2969
3083
  cleanup = () => {};
2970
3084
  store;
2971
3085
  model = signal13(createVoiceDeliveryRuntimeViewModel({
3086
+ actionError: null,
3087
+ actionStatus: "idle",
2972
3088
  error: null,
2973
3089
  isLoading: true
2974
3090
  }));
@@ -2988,6 +3104,15 @@ class VoiceDeliveryRuntimeComponent {
2988
3104
  this.cleanup();
2989
3105
  this.store?.close();
2990
3106
  }
3107
+ deadLettered() {
3108
+ return this.model().surfaces.reduce((total, surface) => total + surface.deadLettered, 0);
3109
+ }
3110
+ tick() {
3111
+ this.store?.tick().catch(() => {});
3112
+ }
3113
+ requeueDeadLetters() {
3114
+ this.store?.requeueDeadLetters().catch(() => {});
3115
+ }
2991
3116
  options() {
2992
3117
  return {
2993
3118
  description: this.description,
@@ -10,5 +10,8 @@ export declare class VoiceDeliveryRuntimeComponent implements OnDestroy, OnInit
10
10
  model: import("@angular/core").WritableSignal<VoiceDeliveryRuntimeViewModel>;
11
11
  ngOnInit(): void;
12
12
  ngOnDestroy(): void;
13
+ deadLettered(): number;
14
+ tick(): void;
15
+ requeueDeadLetters(): void;
13
16
  private options;
14
17
  }
@@ -4,9 +4,13 @@ export declare class VoiceDeliveryRuntimeService {
4
4
  connect(path?: string, options?: VoiceDeliveryRuntimeClientOptions): {
5
5
  close: () => void;
6
6
  error: import("@angular/core").Signal<string | null>;
7
+ actionError: import("@angular/core").Signal<string | null>;
8
+ actionStatus: import("@angular/core").Signal<"completed" | "failed" | "idle" | "running">;
7
9
  isLoading: import("@angular/core").Signal<boolean>;
10
+ requeueDeadLetters: () => Promise<import("../client").VoiceDeliveryRuntimeActionResult | undefined>;
8
11
  refresh: () => Promise<VoiceDeliveryRuntimeReport | undefined>;
9
12
  report: import("@angular/core").Signal<VoiceDeliveryRuntimeReport | undefined>;
13
+ tick: () => Promise<import("../client").VoiceDeliveryRuntimeActionResult | undefined>;
10
14
  updatedAt: import("@angular/core").Signal<number | undefined>;
11
15
  };
12
16
  }
@@ -2,18 +2,33 @@ import type { VoiceDeliveryRuntimeReport } from '../deliveryRuntime';
2
2
  export type VoiceDeliveryRuntimeClientOptions = {
3
3
  fetch?: typeof fetch;
4
4
  intervalMs?: number;
5
+ requeueDeadLettersPath?: string;
6
+ tickPath?: string;
5
7
  };
6
8
  export type VoiceDeliveryRuntimeSnapshot = {
9
+ actionError: string | null;
10
+ actionStatus: 'idle' | 'running' | 'completed' | 'failed';
7
11
  error: string | null;
8
12
  isLoading: boolean;
13
+ lastAction?: VoiceDeliveryRuntimeActionResult;
9
14
  report?: VoiceDeliveryRuntimeReport;
10
15
  updatedAt?: number;
11
16
  };
17
+ export type VoiceDeliveryRuntimeAction = 'tick' | 'requeue-dead-letters';
18
+ export type VoiceDeliveryRuntimeActionResult = {
19
+ action: VoiceDeliveryRuntimeAction;
20
+ result?: unknown;
21
+ summary?: VoiceDeliveryRuntimeReport['summary'];
22
+ updatedAt: number;
23
+ };
12
24
  export declare const fetchVoiceDeliveryRuntime: (path?: string, options?: Pick<VoiceDeliveryRuntimeClientOptions, "fetch">) => Promise<VoiceDeliveryRuntimeReport>;
25
+ export declare const runVoiceDeliveryRuntimeAction: (action: VoiceDeliveryRuntimeAction, path?: string, options?: VoiceDeliveryRuntimeClientOptions) => Promise<VoiceDeliveryRuntimeActionResult>;
13
26
  export declare const createVoiceDeliveryRuntimeStore: (path?: string, options?: VoiceDeliveryRuntimeClientOptions) => {
14
27
  close: () => void;
15
28
  getServerSnapshot: () => VoiceDeliveryRuntimeSnapshot;
16
29
  getSnapshot: () => VoiceDeliveryRuntimeSnapshot;
30
+ requeueDeadLetters: () => Promise<VoiceDeliveryRuntimeActionResult | undefined>;
17
31
  refresh: () => Promise<VoiceDeliveryRuntimeReport | undefined>;
32
+ tick: () => Promise<VoiceDeliveryRuntimeActionResult | undefined>;
18
33
  subscribe: (listener: () => void) => () => void;
19
34
  };
@@ -12,6 +12,8 @@ export type VoiceDeliveryRuntimeSurfaceView = {
12
12
  export type VoiceDeliveryRuntimeViewModel = {
13
13
  description: string;
14
14
  error: string | null;
15
+ actionError: string | null;
16
+ actionStatus: VoiceDeliveryRuntimeSnapshot['actionStatus'];
15
17
  isLoading: boolean;
16
18
  isRunning: boolean;
17
19
  label: string;
@@ -22,6 +24,7 @@ export type VoiceDeliveryRuntimeViewModel = {
22
24
  };
23
25
  export type VoiceDeliveryRuntimeWidgetOptions = VoiceDeliveryRuntimeClientOptions & {
24
26
  description?: string;
27
+ includeActions?: boolean;
25
28
  title?: string;
26
29
  };
27
30
  export declare const createVoiceDeliveryRuntimeViewModel: (snapshot: VoiceDeliveryRuntimeSnapshot, options?: VoiceDeliveryRuntimeWidgetOptions) => VoiceDeliveryRuntimeViewModel;
@@ -8,7 +8,7 @@ export { createMicrophoneCapture } from './microphone';
8
8
  export { createVoiceBargeInMonitor } from './bargeInMonitor';
9
9
  export { createVoiceLiveTurnLatencyMonitor } from './liveTurnLatency';
10
10
  export { createVoiceOpsStatusStore, fetchVoiceOpsStatus } from './opsStatus';
11
- export { createVoiceDeliveryRuntimeStore, fetchVoiceDeliveryRuntime } from './deliveryRuntime';
11
+ export { createVoiceDeliveryRuntimeStore, fetchVoiceDeliveryRuntime, runVoiceDeliveryRuntimeAction } from './deliveryRuntime';
12
12
  export { createVoiceOpsStatusViewModel, defineVoiceOpsStatusElement, getVoiceOpsStatusCSS, getVoiceOpsStatusLabel, mountVoiceOpsStatus, renderVoiceOpsStatusHTML } from './opsStatusWidget';
13
13
  export { createVoiceDeliveryRuntimeViewModel, defineVoiceDeliveryRuntimeElement, getVoiceDeliveryRuntimeCSS, mountVoiceDeliveryRuntime, renderVoiceDeliveryRuntimeHTML } from './deliveryRuntimeWidget';
14
14
  export { createVoiceRoutingStatusStore, fetchVoiceRoutingStatus } from './routingStatus';
@@ -28,7 +28,7 @@ export { createVoiceTurnLatencyViewModel, defineVoiceTurnLatencyElement, mountVo
28
28
  export { createVoiceTraceTimelineViewModel, defineVoiceTraceTimelineElement, getVoiceTraceTimelineCSS, mountVoiceTraceTimeline, renderVoiceTraceTimelineWidgetHTML } from './traceTimelineWidget';
29
29
  export { createVoiceWorkflowStatusStore, fetchVoiceWorkflowStatus } from './workflowStatus';
30
30
  export type { VoiceOpsStatusClientOptions, VoiceOpsStatusSnapshot } from './opsStatus';
31
- export type { VoiceDeliveryRuntimeClientOptions, VoiceDeliveryRuntimeSnapshot } from './deliveryRuntime';
31
+ export type { VoiceDeliveryRuntimeClientOptions, VoiceDeliveryRuntimeAction, VoiceDeliveryRuntimeActionResult, VoiceDeliveryRuntimeSnapshot } from './deliveryRuntime';
32
32
  export type { VoiceBargeInMonitorOptions } from './bargeInMonitor';
33
33
  export type { VoiceLiveTurnLatencyEvent, VoiceLiveTurnLatencyMonitorOptions, VoiceLiveTurnLatencySnapshot, VoiceLiveTurnLatencyStatus } from './liveTurnLatency';
34
34
  export type { VoiceOpsStatusSurfaceView, VoiceOpsStatusViewModel, VoiceOpsStatusWidgetOptions } from './opsStatusWidget';
@@ -2063,6 +2063,12 @@ var createVoiceOpsStatusStore = (path = "/api/voice/ops-status", options = {}) =
2063
2063
  };
2064
2064
  };
2065
2065
  // src/client/deliveryRuntime.ts
2066
+ var getDefaultActionPath = (path, action, options) => {
2067
+ if (action === "tick") {
2068
+ return options.tickPath ?? `${path.replace(/\/$/, "")}/tick`;
2069
+ }
2070
+ return options.requeueDeadLettersPath ?? `${path.replace(/\/$/, "")}/requeue-dead-letters`;
2071
+ };
2066
2072
  var fetchVoiceDeliveryRuntime = async (path = "/api/voice-delivery-runtime", options = {}) => {
2067
2073
  const fetchImpl = options.fetch ?? globalThis.fetch;
2068
2074
  const response = await fetchImpl(path);
@@ -2071,11 +2077,29 @@ var fetchVoiceDeliveryRuntime = async (path = "/api/voice-delivery-runtime", opt
2071
2077
  }
2072
2078
  return await response.json();
2073
2079
  };
2080
+ var runVoiceDeliveryRuntimeAction = async (action, path = "/api/voice-delivery-runtime", options = {}) => {
2081
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2082
+ const response = await fetchImpl(getDefaultActionPath(path, action, options), {
2083
+ method: "POST"
2084
+ });
2085
+ if (!response.ok) {
2086
+ throw new Error(`Voice delivery runtime ${action} failed: HTTP ${response.status}`);
2087
+ }
2088
+ const body = await response.json();
2089
+ return {
2090
+ action,
2091
+ result: body.result,
2092
+ summary: body.summary,
2093
+ updatedAt: Date.now()
2094
+ };
2095
+ };
2074
2096
  var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", options = {}) => {
2075
2097
  const listeners = new Set;
2076
2098
  let closed = false;
2077
2099
  let timer;
2078
2100
  let snapshot = {
2101
+ actionError: null,
2102
+ actionStatus: "idle",
2079
2103
  error: null,
2080
2104
  isLoading: false
2081
2105
  };
@@ -2097,6 +2121,7 @@ var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", opt
2097
2121
  try {
2098
2122
  const report = await fetchVoiceDeliveryRuntime(path, options);
2099
2123
  snapshot = {
2124
+ ...snapshot,
2100
2125
  error: null,
2101
2126
  isLoading: false,
2102
2127
  report,
@@ -2114,6 +2139,37 @@ var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", opt
2114
2139
  throw error;
2115
2140
  }
2116
2141
  };
2142
+ const runAction = async (action) => {
2143
+ if (closed) {
2144
+ return snapshot.lastAction;
2145
+ }
2146
+ snapshot = {
2147
+ ...snapshot,
2148
+ actionError: null,
2149
+ actionStatus: "running"
2150
+ };
2151
+ emit();
2152
+ try {
2153
+ const result = await runVoiceDeliveryRuntimeAction(action, path, options);
2154
+ snapshot = {
2155
+ ...snapshot,
2156
+ actionError: null,
2157
+ actionStatus: "completed",
2158
+ lastAction: result
2159
+ };
2160
+ emit();
2161
+ await refresh();
2162
+ return result;
2163
+ } catch (error) {
2164
+ snapshot = {
2165
+ ...snapshot,
2166
+ actionError: error instanceof Error ? error.message : String(error),
2167
+ actionStatus: "failed"
2168
+ };
2169
+ emit();
2170
+ throw error;
2171
+ }
2172
+ };
2117
2173
  const close = () => {
2118
2174
  closed = true;
2119
2175
  if (timer) {
@@ -2131,7 +2187,9 @@ var createVoiceDeliveryRuntimeStore = (path = "/api/voice-delivery-runtime", opt
2131
2187
  close,
2132
2188
  getServerSnapshot: () => snapshot,
2133
2189
  getSnapshot: () => snapshot,
2190
+ requeueDeadLetters: () => runAction("requeue-dead-letters"),
2134
2191
  refresh,
2192
+ tick: () => runAction("tick"),
2135
2193
  subscribe: (listener) => {
2136
2194
  listeners.add(listener);
2137
2195
  return () => {
@@ -2303,6 +2361,8 @@ var createVoiceDeliveryRuntimeViewModel = (snapshot, options = {}) => {
2303
2361
  return {
2304
2362
  description: options.description ?? DEFAULT_DESCRIPTION2,
2305
2363
  error: snapshot.error,
2364
+ actionError: snapshot.actionError,
2365
+ actionStatus: snapshot.actionStatus,
2306
2366
  isLoading: snapshot.isLoading,
2307
2367
  isRunning: Boolean(report?.isRunning),
2308
2368
  label: snapshot.error ? "Unavailable" : report ? report.isRunning ? "Running" : "Stopped" : "Checking",
@@ -2319,6 +2379,11 @@ var renderVoiceDeliveryRuntimeHTML = (snapshot, options = {}) => {
2319
2379
  <strong>${escapeHtml2(surface.detail)}</strong>
2320
2380
  <small>${String(surface.failed)} failed &middot; ${String(surface.deadLettered)} dead-lettered</small>
2321
2381
  </li>`).join("");
2382
+ const actions = options.includeActions === false ? "" : `<div class="absolute-voice-delivery-runtime__actions">
2383
+ <button type="button" data-absolute-voice-delivery-runtime-action="tick">${model.actionStatus === "running" ? "Working..." : "Tick workers"}</button>
2384
+ <button type="button" data-absolute-voice-delivery-runtime-action="requeue-dead-letters"${model.surfaces.some((surface) => surface.deadLettered > 0) ? "" : " disabled"}>Requeue dead letters</button>
2385
+ </div>`;
2386
+ const actionError = model.actionError ? `<p class="absolute-voice-delivery-runtime__error">${escapeHtml2(model.actionError)}</p>` : "";
2322
2387
  return `<section class="absolute-voice-delivery-runtime absolute-voice-delivery-runtime--${escapeHtml2(model.status)}">
2323
2388
  <header class="absolute-voice-delivery-runtime__header">
2324
2389
  <span class="absolute-voice-delivery-runtime__eyebrow">${escapeHtml2(model.title)}</span>
@@ -2326,20 +2391,38 @@ var renderVoiceDeliveryRuntimeHTML = (snapshot, options = {}) => {
2326
2391
  </header>
2327
2392
  <p class="absolute-voice-delivery-runtime__description">${escapeHtml2(model.description)}</p>
2328
2393
  <ul class="absolute-voice-delivery-runtime__surfaces">${surfaces}</ul>
2394
+ ${actions}
2395
+ ${actionError}
2329
2396
  ${model.error ? `<p class="absolute-voice-delivery-runtime__error">${escapeHtml2(model.error)}</p>` : ""}
2330
2397
  </section>`;
2331
2398
  };
2332
- var getVoiceDeliveryRuntimeCSS = () => `.absolute-voice-delivery-runtime{border:1px solid #c9d8cf;border-radius:20px;background:#f6fff9;color:#0d1b12;padding:18px;box-shadow:0 18px 40px rgba(19,55,35,.12);font-family:inherit}.absolute-voice-delivery-runtime--warn,.absolute-voice-delivery-runtime--error{border-color:#f2b56b;background:#fff9ed}.absolute-voice-delivery-runtime__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-delivery-runtime__eyebrow{color:#4e6b59;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-delivery-runtime__label{font-size:28px;line-height:1}.absolute-voice-delivery-runtime__description{color:#33483b;margin:12px 0 0}.absolute-voice-delivery-runtime__surfaces{display:grid;gap:8px;list-style:none;margin:16px 0 0;padding:0}.absolute-voice-delivery-runtime__surface{background:#fff;border:1px solid #d9eadf;border-radius:14px;display:grid;gap:4px;padding:10px 12px}.absolute-voice-delivery-runtime__surface--warn{border-color:#f2b56b}.absolute-voice-delivery-runtime__surface--disabled{opacity:.72}.absolute-voice-delivery-runtime__surface span,.absolute-voice-delivery-runtime__surface small{color:#587063}.absolute-voice-delivery-runtime__error{color:#9f1239;font-weight:700}`;
2399
+ var getVoiceDeliveryRuntimeCSS = () => `.absolute-voice-delivery-runtime{border:1px solid #c9d8cf;border-radius:20px;background:#f6fff9;color:#0d1b12;padding:18px;box-shadow:0 18px 40px rgba(19,55,35,.12);font-family:inherit}.absolute-voice-delivery-runtime--warn,.absolute-voice-delivery-runtime--error{border-color:#f2b56b;background:#fff9ed}.absolute-voice-delivery-runtime__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-delivery-runtime__eyebrow{color:#4e6b59;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-delivery-runtime__label{font-size:28px;line-height:1}.absolute-voice-delivery-runtime__description{color:#33483b;margin:12px 0 0}.absolute-voice-delivery-runtime__surfaces{display:grid;gap:8px;list-style:none;margin:16px 0 0;padding:0}.absolute-voice-delivery-runtime__surface{background:#fff;border:1px solid #d9eadf;border-radius:14px;display:grid;gap:4px;padding:10px 12px}.absolute-voice-delivery-runtime__surface--warn{border-color:#f2b56b}.absolute-voice-delivery-runtime__surface--disabled{opacity:.72}.absolute-voice-delivery-runtime__surface span,.absolute-voice-delivery-runtime__surface small{color:#587063}.absolute-voice-delivery-runtime__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}.absolute-voice-delivery-runtime__actions button{background:#134e2d;border:0;border-radius:999px;color:#f6fff9;cursor:pointer;font:inherit;font-weight:800;padding:8px 12px}.absolute-voice-delivery-runtime__actions button:disabled{cursor:not-allowed;opacity:.48}.absolute-voice-delivery-runtime__error{color:#9f1239;font-weight:700}`;
2333
2400
  var mountVoiceDeliveryRuntime = (element, path = "/api/voice-delivery-runtime", options = {}) => {
2334
2401
  const store = createVoiceDeliveryRuntimeStore(path, options);
2335
2402
  const render = () => {
2336
2403
  element.innerHTML = renderVoiceDeliveryRuntimeHTML(store.getSnapshot(), options);
2337
2404
  };
2338
2405
  const unsubscribe = store.subscribe(render);
2406
+ const handleClick = (event) => {
2407
+ const target = event.target;
2408
+ if (!(target instanceof Element)) {
2409
+ return;
2410
+ }
2411
+ const action = target.closest("[data-absolute-voice-delivery-runtime-action]");
2412
+ const actionName = action?.getAttribute("data-absolute-voice-delivery-runtime-action");
2413
+ if (actionName === "tick") {
2414
+ store.tick().catch(() => {});
2415
+ }
2416
+ if (actionName === "requeue-dead-letters") {
2417
+ store.requeueDeadLetters().catch(() => {});
2418
+ }
2419
+ };
2420
+ element.addEventListener?.("click", handleClick);
2339
2421
  render();
2340
2422
  store.refresh().catch(() => {});
2341
2423
  return {
2342
2424
  close: () => {
2425
+ element.removeEventListener?.("click", handleClick);
2343
2426
  unsubscribe();
2344
2427
  store.close();
2345
2428
  },
@@ -3852,6 +3935,7 @@ var createVoiceWorkflowStatusStore = (path = "/evals/scenarios/json", options =
3852
3935
  };
3853
3936
  export {
3854
3937
  runVoiceTurnLatencyProof,
3938
+ runVoiceDeliveryRuntimeAction,
3855
3939
  runVoiceCampaignDialerProofAction,
3856
3940
  renderVoiceTurnQualityHTML,
3857
3941
  renderVoiceTurnLatencyHTML,
@@ -21,6 +21,11 @@ export type VoiceDeliveryRuntimeTickResult = {
21
21
  audit?: VoiceAuditSinkDeliveryWorkerResult;
22
22
  trace?: VoiceTraceSinkDeliveryWorkerResult;
23
23
  };
24
+ export type VoiceDeliveryRuntimeRequeueDeadLettersResult = {
25
+ audit: number;
26
+ trace: number;
27
+ total: number;
28
+ };
24
29
  export type VoiceDeliveryRuntimeSummary = {
25
30
  audit?: VoiceAuditSinkDeliveryQueueSummary;
26
31
  trace?: VoiceTraceSinkDeliveryQueueSummary;
@@ -28,6 +33,7 @@ export type VoiceDeliveryRuntimeSummary = {
28
33
  export type VoiceDeliveryRuntime = {
29
34
  audit?: ReturnType<typeof createVoiceAuditSinkDeliveryWorker>;
30
35
  isRunning: () => boolean;
36
+ requeueDeadLetters: () => Promise<VoiceDeliveryRuntimeRequeueDeadLettersResult>;
31
37
  start: () => void;
32
38
  stop: () => void;
33
39
  summarize: () => Promise<VoiceDeliveryRuntimeSummary>;
@@ -44,8 +50,13 @@ export type VoiceDeliveryRuntimeRoutesOptions = {
44
50
  htmlPath?: false | string;
45
51
  name?: string;
46
52
  path?: string;
47
- render?: (report: VoiceDeliveryRuntimeReport) => string | Promise<string>;
53
+ render?: (report: VoiceDeliveryRuntimeReport, options: {
54
+ requeueDeadLettersPath?: false | string;
55
+ tickPath?: false | string;
56
+ title?: string;
57
+ }) => string | Promise<string>;
48
58
  runtime: VoiceDeliveryRuntime;
59
+ requeueDeadLettersPath?: false | string;
49
60
  tickPath?: false | string;
50
61
  title?: string;
51
62
  };
@@ -101,6 +112,7 @@ export declare const createVoiceDeliveryRuntimePresetConfig: (options: VoiceDeli
101
112
  export declare const createVoiceDeliveryRuntime: <TAuditDelivery extends VoiceAuditSinkDeliveryRecord = VoiceAuditSinkDeliveryRecord, TTraceDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(config: VoiceDeliveryRuntimeConfig<TAuditDelivery, TTraceDelivery>) => VoiceDeliveryRuntime;
102
113
  export declare const buildVoiceDeliveryRuntimeReport: (runtime: VoiceDeliveryRuntime) => Promise<VoiceDeliveryRuntimeReport>;
103
114
  export declare const renderVoiceDeliveryRuntimeHTML: (report: VoiceDeliveryRuntimeReport, options?: {
115
+ requeueDeadLettersPath?: false | string;
104
116
  tickPath?: false | string;
105
117
  title?: string;
106
118
  }) => string;
package/dist/index.js CHANGED
@@ -11579,6 +11579,15 @@ var resolvePresetLeases = (leases) => ("claim" in leases) ? {
11579
11579
  audit: leases,
11580
11580
  trace: leases
11581
11581
  } : leases;
11582
+ var requeueDelivery = (delivery) => ({
11583
+ ...delivery,
11584
+ deliveredAt: undefined,
11585
+ deliveryAttempts: 0,
11586
+ deliveryError: undefined,
11587
+ deliveryStatus: "pending",
11588
+ sinkDeliveries: undefined,
11589
+ updatedAt: Date.now()
11590
+ });
11582
11591
  var createDeliveryRuntimeFileSink = (input) => ({
11583
11592
  deliver: async ({ events }) => {
11584
11593
  const firstEvent = events[0];
@@ -11706,6 +11715,29 @@ var createVoiceDeliveryRuntime = (config) => {
11706
11715
  return {
11707
11716
  audit,
11708
11717
  isRunning: () => Boolean(auditLoop?.isRunning() || traceLoop?.isRunning()),
11718
+ requeueDeadLetters: async () => {
11719
+ let audit2 = 0;
11720
+ let trace2 = 0;
11721
+ if (config.audit?.deadLetters) {
11722
+ for (const delivery of await config.audit.deadLetters.list()) {
11723
+ await config.audit.deliveries.set(delivery.id, requeueDelivery(delivery));
11724
+ await config.audit.deadLetters.remove(delivery.id);
11725
+ audit2 += 1;
11726
+ }
11727
+ }
11728
+ if (config.trace?.deadLetters) {
11729
+ for (const delivery of await config.trace.deadLetters.list()) {
11730
+ await config.trace.deliveries.set(delivery.id, requeueDelivery(delivery));
11731
+ await config.trace.deadLetters.remove(delivery.id);
11732
+ trace2 += 1;
11733
+ }
11734
+ }
11735
+ return {
11736
+ audit: audit2,
11737
+ trace: trace2,
11738
+ total: audit2 + trace2
11739
+ };
11740
+ },
11709
11741
  start: () => {
11710
11742
  if (config.audit?.autoStart) {
11711
11743
  auditLoop?.start();
@@ -11753,15 +11785,18 @@ var buildVoiceDeliveryRuntimeReport = async (runtime) => ({
11753
11785
  var renderVoiceDeliveryRuntimeHTML = (report, options = {}) => {
11754
11786
  const title = options.title ?? "AbsoluteJS Voice Delivery Runtime";
11755
11787
  const tickForm = options.tickPath === false ? "" : `<form method="post" action="${escapeHtml15(options.tickPath ?? "/api/voice-delivery-runtime/tick")}"><button type="submit">Tick delivery workers</button></form>`;
11756
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml15(title)}</title><style>body{background:#0f1411;color:#f7f2df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}a{color:#86efac;text-decoration:none}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(14,165,233,.13));border:1px solid #263a30;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#86efac;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.8rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #64748b;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.status.running{border-color:rgba(34,197,94,.7);color:#bbf7d0}.muted{color:#b9c3b4}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin:18px 0}article,.card{background:#151d18;border:1px solid #263a30;border-radius:22px;padding:18px}article span{color:#b9c3b4;display:block;font-weight:800}article strong{display:block;font-size:2.3rem;margin-top:8px}button{background:#86efac;border:0;border-radius:999px;color:#07120b;cursor:pointer;font-weight:900;margin-top:12px;padding:10px 14px}pre{background:#09100c;border:1px solid #263a30;border-radius:18px;color:#dcfce7;overflow:auto;padding:16px}</style></head><body><main><p><a href="/delivery-sinks">Delivery sinks</a></p><section class="hero"><p class="eyebrow">Worker control plane</p><h1>${escapeHtml15(title)}</h1><p class="muted">Inspect queue summaries and manually tick audit and trace delivery workers from one runtime primitive.</p><p class="status ${report.isRunning ? "running" : ""}">${report.isRunning ? "Running" : "Stopped"}</p><p class="muted">Checked ${escapeHtml15(new Date(report.checkedAt).toLocaleString())}</p>${tickForm}</section><section class="grid">${renderSummaryCard("Audit", report.summary.audit)}${renderSummaryCard("Trace", report.summary.trace)}</section><section class="card"><h2>Runtime shape</h2><pre>const runtime = createVoiceDeliveryRuntime({ audit, trace })
11788
+ const requeueForm = options.requeueDeadLettersPath === false ? "" : `<form method="post" action="${escapeHtml15(options.requeueDeadLettersPath ?? "/api/voice-delivery-runtime/requeue-dead-letters")}"><button type="submit">Requeue dead letters</button></form>`;
11789
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml15(title)}</title><style>body{background:#0f1411;color:#f7f2df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}a{color:#86efac;text-decoration:none}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(14,165,233,.13));border:1px solid #263a30;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#86efac;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.8rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #64748b;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.status.running{border-color:rgba(34,197,94,.7);color:#bbf7d0}.muted{color:#b9c3b4}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin:18px 0}article,.card{background:#151d18;border:1px solid #263a30;border-radius:22px;padding:18px}article span{color:#b9c3b4;display:block;font-weight:800}article strong{display:block;font-size:2.3rem;margin-top:8px}.actions{display:flex;flex-wrap:wrap;gap:10px}button{background:#86efac;border:0;border-radius:999px;color:#07120b;cursor:pointer;font-weight:900;margin-top:12px;padding:10px 14px}pre{background:#09100c;border:1px solid #263a30;border-radius:18px;color:#dcfce7;overflow:auto;padding:16px}</style></head><body><main><p><a href="/delivery-sinks">Delivery sinks</a></p><section class="hero"><p class="eyebrow">Worker control plane</p><h1>${escapeHtml15(title)}</h1><p class="muted">Inspect queue summaries, manually tick failed/pending audit and trace deliveries, and requeue dead letters after operator review.</p><p class="status ${report.isRunning ? "running" : ""}">${report.isRunning ? "Running" : "Stopped"}</p><p class="muted">Checked ${escapeHtml15(new Date(report.checkedAt).toLocaleString())}</p><div class="actions">${tickForm}${requeueForm}</div></section><section class="grid">${renderSummaryCard("Audit", report.summary.audit)}${renderSummaryCard("Trace", report.summary.trace)}</section><section class="card"><h2>Runtime shape</h2><pre>const runtime = createVoiceDeliveryRuntime({ audit, trace })
11757
11790
 
11758
11791
  await runtime.tick()
11792
+ await runtime.requeueDeadLetters()
11759
11793
  await runtime.summarize()</pre></section></main></body></html>`;
11760
11794
  };
11761
11795
  var createVoiceDeliveryRuntimeRoutes = (options) => {
11762
11796
  const path = options.path ?? "/api/voice-delivery-runtime";
11763
11797
  const htmlPath = options.htmlPath === undefined ? "/delivery-runtime" : options.htmlPath;
11764
11798
  const tickPath = options.tickPath === undefined ? "/api/voice-delivery-runtime/tick" : options.tickPath;
11799
+ const requeueDeadLettersPath = options.requeueDeadLettersPath === undefined ? "/api/voice-delivery-runtime/requeue-dead-letters" : options.requeueDeadLettersPath;
11765
11800
  const routes = new Elysia12({
11766
11801
  name: options.name ?? "absolutejs-voice-delivery-runtime"
11767
11802
  }).get(path, () => buildVoiceDeliveryRuntimeReport(options.runtime));
@@ -11772,11 +11807,19 @@ var createVoiceDeliveryRuntimeRoutes = (options) => {
11772
11807
  summary: await options.runtime.summarize()
11773
11808
  }));
11774
11809
  }
11810
+ if (requeueDeadLettersPath !== false) {
11811
+ routes.post(requeueDeadLettersPath, async () => ({
11812
+ requeuedAt: Date.now(),
11813
+ result: await options.runtime.requeueDeadLetters(),
11814
+ summary: await options.runtime.summarize()
11815
+ }));
11816
+ }
11775
11817
  if (htmlPath !== false) {
11776
11818
  routes.get(htmlPath, async () => {
11777
11819
  const report = await buildVoiceDeliveryRuntimeReport(options.runtime);
11778
11820
  const body = await (options.render ?? renderVoiceDeliveryRuntimeHTML)(report, {
11779
11821
  tickPath,
11822
+ requeueDeadLettersPath,
11780
11823
  title: options.title
11781
11824
  });
11782
11825
  return new Response(body, {