@absolutejs/voice 0.0.22-beta.432 → 0.0.22-beta.434

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.
@@ -10,6 +10,30 @@ export type VoiceIncidentTimelineAction = {
10
10
  label: string;
11
11
  method?: 'GET' | 'POST';
12
12
  };
13
+ export type VoiceIncidentRecoveryAction = {
14
+ detail?: string;
15
+ disabled?: boolean;
16
+ eventId?: string;
17
+ href?: string;
18
+ id: string;
19
+ label: string;
20
+ method?: 'GET' | 'POST';
21
+ sessionId?: string;
22
+ };
23
+ export type VoiceIncidentRecoveryActionResult = {
24
+ actionId: string;
25
+ detail?: string;
26
+ href?: string;
27
+ ok: boolean;
28
+ status?: string;
29
+ };
30
+ export type VoiceIncidentRecoveryActionHandlerInput = {
31
+ action: VoiceIncidentRecoveryAction;
32
+ actionId: string;
33
+ report: VoiceIncidentTimelineReport;
34
+ request: Request;
35
+ };
36
+ export type VoiceIncidentRecoveryActionHandler = (input: VoiceIncidentRecoveryActionHandlerInput) => Promise<VoiceIncidentRecoveryActionResult> | VoiceIncidentRecoveryActionResult;
13
37
  export type VoiceIncidentTimelineEvent = {
14
38
  action?: VoiceIncidentTimelineAction;
15
39
  at: number;
@@ -24,6 +48,7 @@ export type VoiceIncidentTimelineEvent = {
24
48
  value?: number | string;
25
49
  };
26
50
  export type VoiceIncidentTimelineReport = {
51
+ actions: VoiceIncidentRecoveryAction[];
27
52
  events: VoiceIncidentTimelineEvent[];
28
53
  generatedAt: number;
29
54
  links: VoiceIncidentTimelineLinks;
@@ -57,9 +82,15 @@ export type VoiceIncidentTimelineOptions = {
57
82
  operationalStatus?: VoiceIncidentTimelineValue<VoiceOperationalStatusReport>;
58
83
  operationsRecords?: VoiceIncidentTimelineValue<readonly VoiceOperationsRecord[]>;
59
84
  opsRecovery?: VoiceIncidentTimelineValue<VoiceOpsRecoveryReport>;
85
+ recoveryActions?: readonly VoiceIncidentRecoveryAction[] | ((input: {
86
+ events: readonly VoiceIncidentTimelineEvent[];
87
+ report: Omit<VoiceIncidentTimelineReport, 'actions'>;
88
+ }) => Promise<readonly VoiceIncidentRecoveryAction[]> | readonly VoiceIncidentRecoveryAction[]);
60
89
  windowMs?: number;
61
90
  };
62
91
  export type VoiceIncidentTimelineRoutesOptions = VoiceIncidentTimelineOptions & {
92
+ actionHandlers?: Record<string, VoiceIncidentRecoveryActionHandler>;
93
+ actionPath?: false | string;
63
94
  headers?: HeadersInit;
64
95
  htmlPath?: false | string;
65
96
  markdownPath?: false | string;
@@ -73,6 +104,7 @@ export declare const renderVoiceIncidentTimelineMarkdown: (report: VoiceIncident
73
104
  title?: string;
74
105
  }) => string;
75
106
  export declare const renderVoiceIncidentTimelineHTML: (report: VoiceIncidentTimelineReport, options?: {
107
+ actionPath?: string;
76
108
  title?: string;
77
109
  }) => string;
78
110
  export declare const createVoiceIncidentTimelineRoutes: (options: VoiceIncidentTimelineRoutesOptions) => Elysia<"", {
package/dist/index.d.ts CHANGED
@@ -60,7 +60,7 @@ export type { VoiceDeliverySinkDescriptor, VoiceDeliverySinkDescriptorInput, Voi
60
60
  export type { VoiceOpsActionAuditRecord, VoiceOpsActionAuditRoutesOptions, VoiceOpsActionHistoryEntry, VoiceOpsActionHistoryReport } from './opsActionAuditRoutes';
61
61
  export type { VoiceDeliveryRuntime, VoiceDeliveryRuntimeAuditConfig, VoiceDeliveryRuntimeConfig, VoiceDeliveryRuntimeFilePresetOptions, VoiceDeliveryRuntimePresetLeaseConfig, VoiceDeliveryRuntimePresetMode, VoiceDeliveryRuntimePresetOptions, VoiceDeliveryRuntimeReport, VoiceDeliveryRuntimeRoutesOptions, VoiceDeliveryRuntimeS3PresetOptions, VoiceDeliveryRuntimeSummary, VoiceDeliveryRuntimeTickResult, VoiceDeliveryRuntimeTraceConfig, VoiceDeliveryRuntimeWebhookPresetOptions } from './deliveryRuntime';
62
62
  export type { VoiceOperationalStatus, VoiceOperationalStatusCheck, VoiceOperationalStatusOptions, VoiceOperationalStatusReport, VoiceOperationalStatusRoutesOptions, VoiceOperationalStatusValue } from './operationalStatus';
63
- export type { VoiceIncidentTimelineAction, VoiceIncidentTimelineEvent, VoiceIncidentTimelineLinks, VoiceIncidentTimelineOptions, VoiceIncidentTimelineReport, VoiceIncidentTimelineRoutesOptions, VoiceIncidentTimelineSeverity, VoiceIncidentTimelineStatus, VoiceIncidentTimelineValue } from './incidentTimeline';
63
+ export type { VoiceIncidentRecoveryAction, VoiceIncidentRecoveryActionHandler, VoiceIncidentRecoveryActionHandlerInput, VoiceIncidentRecoveryActionResult, VoiceIncidentTimelineAction, VoiceIncidentTimelineEvent, VoiceIncidentTimelineLinks, VoiceIncidentTimelineOptions, VoiceIncidentTimelineReport, VoiceIncidentTimelineRoutesOptions, VoiceIncidentTimelineSeverity, VoiceIncidentTimelineStatus, VoiceIncidentTimelineValue } from './incidentTimeline';
64
64
  export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceFileScenarioFixtureStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, renderVoiceScenarioEvalHTML, renderVoiceScenarioFixtureEvalHTML, runVoiceScenarioEvals, runVoiceScenarioFixtureEvals, runVoiceSessionEvals } from './evalRoutes';
65
65
  export { assertVoiceSimulationSuiteEvidence, createVoiceSimulationSuiteRoutes, evaluateVoiceSimulationSuiteEvidence, renderVoiceSimulationSuiteHTML, runVoiceSimulationSuite } from './simulationSuite';
66
66
  export { createVoiceWorkflowContract, createVoiceWorkflowContractHandler, createVoiceWorkflowContractPreset, createVoiceWorkflowScenario, recordVoiceWorkflowContractTrace, validateVoiceWorkflowRouteResult } from './workflowContract';
package/dist/index.js CHANGED
@@ -30127,6 +30127,61 @@ var statusToSeverity = (status) => status === "fail" || status === "failed" ? "c
30127
30127
  var failureReplayStatusToSeverity = (status) => status === "failed" ? "critical" : status === "healthy" ? "info" : "warn";
30128
30128
  var withinWindow = (event, now, windowMs) => !windowMs || event.at >= now - windowMs;
30129
30129
  var eventStatus2 = (event) => event.severity === "critical" ? "fail" : event.severity === "warn" ? "warn" : "pass";
30130
+ var defaultIncidentRecoveryActions = (events, links) => {
30131
+ const actions = [];
30132
+ const add = (action) => {
30133
+ const key = `${action.id}:${action.sessionId ?? ""}:${action.href ?? ""}`;
30134
+ if (actions.some((existing) => `${existing.id}:${existing.sessionId ?? ""}:${existing.href ?? ""}` === key)) {
30135
+ return;
30136
+ }
30137
+ actions.push(action);
30138
+ };
30139
+ for (const event of events) {
30140
+ if (event.category === "delivery") {
30141
+ add({
30142
+ detail: "Ask the app to tick delivery workers or retry failed delivery queue work.",
30143
+ eventId: event.id,
30144
+ href: links.deliveryRuntime,
30145
+ id: "delivery.retry",
30146
+ label: "Retry delivery work",
30147
+ method: "POST",
30148
+ sessionId: event.sessionId
30149
+ });
30150
+ }
30151
+ if (event.category === "readiness" || event.category === "operational-status") {
30152
+ add({
30153
+ detail: "Refresh production readiness and proof freshness before declaring the incident resolved.",
30154
+ eventId: event.id,
30155
+ href: links.productionReadiness ?? links.operationalStatus,
30156
+ id: "readiness.refresh",
30157
+ label: "Refresh readiness proof",
30158
+ method: "POST",
30159
+ sessionId: event.sessionId
30160
+ });
30161
+ }
30162
+ if (event.sessionId) {
30163
+ add({
30164
+ detail: "Generate or open a support/debug artifact for the affected call.",
30165
+ eventId: event.id,
30166
+ href: linkForSession(links.supportBundle, event.sessionId) ?? linkForSession(links.callDebugger, event.sessionId),
30167
+ id: "support.bundle",
30168
+ label: "Generate support bundle",
30169
+ method: "POST",
30170
+ sessionId: event.sessionId
30171
+ });
30172
+ }
30173
+ }
30174
+ if (events.some((event) => event.severity !== "info")) {
30175
+ add({
30176
+ detail: "Rerun the app proof pack to confirm the current release evidence is fresh.",
30177
+ href: links.proofPack,
30178
+ id: "proof.rerun",
30179
+ label: "Rerun proof pack",
30180
+ method: "POST"
30181
+ });
30182
+ }
30183
+ return actions;
30184
+ };
30130
30185
  var worstStatus3 = (statuses) => statuses.includes("fail") ? "fail" : statuses.includes("warn") ? "warn" : "pass";
30131
30186
  var pushOperationalStatusEvents = (events, report, links) => {
30132
30187
  if (!report) {
@@ -30304,7 +30359,7 @@ var buildVoiceIncidentTimelineReport = async (options) => {
30304
30359
  total: filtered.length,
30305
30360
  warn: filtered.filter((event) => event.severity === "warn").length
30306
30361
  };
30307
- return {
30362
+ const baseReport = {
30308
30363
  events: filtered,
30309
30364
  generatedAt: now,
30310
30365
  links,
@@ -30312,6 +30367,14 @@ var buildVoiceIncidentTimelineReport = async (options) => {
30312
30367
  summary,
30313
30368
  windowMs: options.windowMs
30314
30369
  };
30370
+ const configuredActions = typeof options.recoveryActions === "function" ? await options.recoveryActions({
30371
+ events: filtered,
30372
+ report: baseReport
30373
+ }) : options.recoveryActions;
30374
+ return {
30375
+ ...baseReport,
30376
+ actions: configuredActions === undefined ? defaultIncidentRecoveryActions(filtered, links) : [...configuredActions]
30377
+ };
30315
30378
  };
30316
30379
  var renderVoiceIncidentTimelineMarkdown = (report, options = {}) => {
30317
30380
  const title = options.title ?? "AbsoluteJS Voice Incident Timeline";
@@ -30334,10 +30397,16 @@ Summary: ${report.summary.critical} critical, ${report.summary.warn} warn, ${rep
30334
30397
  ## Events
30335
30398
 
30336
30399
  ${rows || "- No incident timeline events."}
30400
+
30401
+ ## Recovery Actions
30402
+
30403
+ ${report.actions.map((action) => `- ${action.method ?? "GET"} ${action.id}: ${action.label}${action.href ? ` (${action.href})` : ""}${action.detail ? ` - ${action.detail}` : ""}`).join(`
30404
+ `) || "- No recovery actions."}
30337
30405
  `;
30338
30406
  };
30339
30407
  var renderVoiceIncidentTimelineHTML = (report, options = {}) => {
30340
30408
  const title = options.title ?? "AbsoluteJS Voice Incident Timeline";
30409
+ const actionPath = options.actionPath ?? "/api/voice/incident-timeline/actions";
30341
30410
  const events = report.events.map((event) => `<article class="${escapeHtml43(event.severity)}">
30342
30411
  <span>${escapeHtml43(event.severity.toUpperCase())} / ${escapeHtml43(event.category)}</span>
30343
30412
  <h2>${escapeHtml43(event.label)}</h2>
@@ -30346,12 +30415,20 @@ var renderVoiceIncidentTimelineHTML = (report, options = {}) => {
30346
30415
  ${event.detail ? `<p>${escapeHtml43(event.detail)}</p>` : ""}
30347
30416
  <div>${event.href ? `<a href="${escapeHtml43(event.href)}">Open source</a>` : ""}${event.action?.href ? `<a href="${escapeHtml43(event.action.href)}">${escapeHtml43(event.action.label)}</a>` : ""}</div>
30348
30417
  </article>`).join("");
30349
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml43(title)}</title><style>body{background:#11110d;color:#faf4df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1100px;padding:32px}.hero{background:linear-gradient(135deg,rgba(248,113,113,.2),rgba(245,158,11,.13),rgba(34,197,94,.12));border:1px solid #39301d;border-radius:30px;margin-bottom:18px;padding:28px}.eyebrow{color:#fcd34d;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #575030;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.status.pass{border-color:rgba(34,197,94,.65)}.status.warn{border-color:rgba(245,158,11,.75)}.status.fail{border-color:rgba(239,68,68,.85)}.grid{display:grid;gap:14px}.summary{display:flex;flex-wrap:wrap;gap:10px}.summary span{background:#181711;border:1px solid #39301d;border-radius:999px;padding:8px 12px}article{background:#181711;border:1px solid #39301d;border-radius:22px;padding:18px}article.critical{border-color:rgba(239,68,68,.85)}article.warn{border-color:rgba(245,158,11,.75)}article.info{border-color:rgba(34,197,94,.55)}article span{color:#fcd34d;font-size:.78rem;font-weight:900;letter-spacing:.08em}article h2{margin:.35rem 0}.muted,article p{color:#cfc5a8}article strong{display:block;font-size:1.3rem;margin:.5rem 0}a{color:#fde68a;margin-right:12px}</style></head><body><main><section class="hero"><p class="eyebrow">Operational triage</p><h1>${escapeHtml43(title)}</h1><p class="status ${escapeHtml43(report.status)}">Overall: ${escapeHtml43(report.status.toUpperCase())}</p><p class="muted">Generated ${escapeHtml43(new Date(report.generatedAt).toLocaleString())}</p><div class="summary"><span>${String(report.summary.critical)} critical</span><span>${String(report.summary.warn)} warn</span><span>${String(report.summary.info)} info</span><span>${String(report.summary.total)} total</span></div></section><section class="grid">${events || '<article class="info"><span>INFO</span><h2>No incident events</h2><p>No non-pass operational events were found in this window.</p></article>'}</section></main></body></html>`;
30418
+ const actions = report.actions.map((action) => {
30419
+ const label = escapeHtml43(action.label);
30420
+ const detail = action.detail ? `<p>${escapeHtml43(action.detail)}</p>` : "";
30421
+ const href = action.href ? `<a href="${escapeHtml43(action.href)}">Open target</a>` : "";
30422
+ const control = action.method === "POST" ? `<button type="button" data-voice-incident-action="${escapeHtml43(action.id)}" ${action.disabled ? "disabled" : ""}>${label}</button>` : href;
30423
+ return `<article class="action"><span>${escapeHtml43(action.method ?? "GET")}</span><h2>${label}</h2>${detail}<div>${control}${href && action.method === "POST" ? href : ""}</div></article>`;
30424
+ }).join("");
30425
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml43(title)}</title><style>body{background:#11110d;color:#faf4df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1100px;padding:32px}.hero{background:linear-gradient(135deg,rgba(248,113,113,.2),rgba(245,158,11,.13),rgba(34,197,94,.12));border:1px solid #39301d;border-radius:30px;margin-bottom:18px;padding:28px}.eyebrow{color:#fcd34d;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #575030;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.status.pass{border-color:rgba(34,197,94,.65)}.status.warn{border-color:rgba(245,158,11,.75)}.status.fail{border-color:rgba(239,68,68,.85)}.grid{display:grid;gap:14px}.actions{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:0 0 18px}.summary{display:flex;flex-wrap:wrap;gap:10px}.summary span{background:#181711;border:1px solid #39301d;border-radius:999px;padding:8px 12px}article{background:#181711;border:1px solid #39301d;border-radius:22px;padding:18px}article.critical{border-color:rgba(239,68,68,.85)}article.warn{border-color:rgba(245,158,11,.75)}article.info{border-color:rgba(34,197,94,.55)}article.action{border-color:#5b4a22}article span{color:#fcd34d;font-size:.78rem;font-weight:900;letter-spacing:.08em}article h2{margin:.35rem 0}.muted,article p{color:#cfc5a8}article strong{display:block;font-size:1.3rem;margin:.5rem 0}a{color:#fde68a;margin-right:12px}button{background:#fcd34d;border:0;border-radius:999px;color:#171307;cursor:pointer;font-weight:900;padding:10px 14px}button:disabled{cursor:not-allowed;opacity:.55}</style></head><body><main><section class="hero"><p class="eyebrow">Operational triage</p><h1>${escapeHtml43(title)}</h1><p class="status ${escapeHtml43(report.status)}">Overall: ${escapeHtml43(report.status.toUpperCase())}</p><p class="muted">Generated ${escapeHtml43(new Date(report.generatedAt).toLocaleString())}</p><div class="summary"><span>${String(report.summary.critical)} critical</span><span>${String(report.summary.warn)} warn</span><span>${String(report.summary.info)} info</span><span>${String(report.summary.total)} total</span></div></section><h2>Recovery actions</h2><section class="actions">${actions || '<article class="action"><span>NONE</span><h2>No recovery actions</h2><p>No executable actions are available for this report.</p></article>'}</section><h2>Timeline</h2><section class="grid">${events || '<article class="info"><span>INFO</span><h2>No incident events</h2><p>No non-pass operational events were found in this window.</p></article>'}</section></main><script>const voiceIncidentActionPath=${JSON.stringify(actionPath)};document.querySelectorAll("[data-voice-incident-action]").forEach((button)=>{button.addEventListener("click",async()=>{const id=button.getAttribute("data-voice-incident-action");if(!id)return;button.disabled=true;const original=button.textContent;button.textContent="Running...";try{const response=await fetch(voiceIncidentActionPath+"/"+encodeURIComponent(id),{method:"POST"});button.textContent=response.ok?"Done":"Failed";if(response.ok)setTimeout(()=>location.reload(),700)}catch{button.textContent="Failed"}finally{setTimeout(()=>{button.disabled=false;button.textContent=original},1600)}})});</script></body></html>`;
30350
30426
  };
30351
30427
  var createVoiceIncidentTimelineRoutes = (options) => {
30352
30428
  const path = options.path ?? "/api/voice/incident-timeline";
30353
30429
  const htmlPath = options.htmlPath === undefined ? "/voice/incident-timeline" : options.htmlPath;
30354
30430
  const markdownPath = options.markdownPath === undefined ? "/voice/incident-timeline.md" : options.markdownPath;
30431
+ const actionPath = options.actionPath === undefined ? "/api/voice/incident-timeline/actions" : options.actionPath;
30355
30432
  const routes = new Elysia46({
30356
30433
  name: options.name ?? "absolutejs-voice-incident-timeline"
30357
30434
  }).get(path, async () => {
@@ -30367,7 +30444,10 @@ var createVoiceIncidentTimelineRoutes = (options) => {
30367
30444
  if (htmlPath !== false) {
30368
30445
  routes.get(htmlPath, async () => {
30369
30446
  const report = await buildVoiceIncidentTimelineReport(options);
30370
- const body = await (options.render ?? ((input) => renderVoiceIncidentTimelineHTML(input, { title: options.title })))(report);
30447
+ const body = await (options.render ?? ((input) => renderVoiceIncidentTimelineHTML(input, {
30448
+ actionPath: actionPath === false ? undefined : actionPath,
30449
+ title: options.title
30450
+ })))(report);
30371
30451
  return new Response(body, {
30372
30452
  headers: {
30373
30453
  "Content-Type": "text/html; charset=utf-8",
@@ -30389,6 +30469,65 @@ var createVoiceIncidentTimelineRoutes = (options) => {
30389
30469
  });
30390
30470
  });
30391
30471
  }
30472
+ if (actionPath !== false) {
30473
+ routes.get(actionPath, async () => {
30474
+ const report = await buildVoiceIncidentTimelineReport(options);
30475
+ return new Response(JSON.stringify({
30476
+ actions: report.actions,
30477
+ generatedAt: report.generatedAt,
30478
+ status: report.status
30479
+ }), {
30480
+ headers: {
30481
+ "Content-Type": "application/json; charset=utf-8",
30482
+ ...options.headers
30483
+ }
30484
+ });
30485
+ }).post(`${actionPath}/:actionId`, async ({ params, request }) => {
30486
+ const actionId = params.actionId;
30487
+ const report = await buildVoiceIncidentTimelineReport(options);
30488
+ const action = report.actions.find((item) => item.id === actionId);
30489
+ const handler = options.actionHandlers?.[actionId];
30490
+ if (!action) {
30491
+ return new Response(JSON.stringify({
30492
+ actionId,
30493
+ ok: false,
30494
+ status: "not_found"
30495
+ }), {
30496
+ headers: {
30497
+ "Content-Type": "application/json; charset=utf-8",
30498
+ ...options.headers
30499
+ },
30500
+ status: 404
30501
+ });
30502
+ }
30503
+ if (action.disabled || action.method !== "POST" || !handler) {
30504
+ return new Response(JSON.stringify({
30505
+ actionId,
30506
+ ok: false,
30507
+ status: action.disabled ? "disabled" : "not_executable"
30508
+ }), {
30509
+ headers: {
30510
+ "Content-Type": "application/json; charset=utf-8",
30511
+ ...options.headers
30512
+ },
30513
+ status: 409
30514
+ });
30515
+ }
30516
+ const result = await handler({
30517
+ action,
30518
+ actionId,
30519
+ report,
30520
+ request
30521
+ });
30522
+ return new Response(JSON.stringify(result), {
30523
+ headers: {
30524
+ "Content-Type": "application/json; charset=utf-8",
30525
+ ...options.headers
30526
+ },
30527
+ status: result.ok ? 200 : 500
30528
+ });
30529
+ });
30530
+ }
30392
30531
  return routes;
30393
30532
  };
30394
30533
  // src/dataControl.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.432",
3
+ "version": "0.0.22-beta.434",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",