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

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;
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,6 +30397,11 @@ 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 = {}) => {
@@ -30346,12 +30414,20 @@ var renderVoiceIncidentTimelineHTML = (report, options = {}) => {
30346
30414
  ${event.detail ? `<p>${escapeHtml43(event.detail)}</p>` : ""}
30347
30415
  <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
30416
  </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>`;
30417
+ const actions = report.actions.map((action) => {
30418
+ const label = escapeHtml43(action.label);
30419
+ const detail = action.detail ? `<p>${escapeHtml43(action.detail)}</p>` : "";
30420
+ const href = action.href ? `<a href="${escapeHtml43(action.href)}">Open target</a>` : "";
30421
+ const control = action.method === "POST" ? `<button type="button" data-voice-incident-action="${escapeHtml43(action.id)}" ${action.disabled ? "disabled" : ""}>${label}</button>` : href;
30422
+ return `<article class="action"><span>${escapeHtml43(action.method ?? "GET")}</span><h2>${label}</h2>${detail}<div>${control}${href && action.method === "POST" ? href : ""}</div></article>`;
30423
+ }).join("");
30424
+ 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>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("/api/voice/incident-timeline/actions/"+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
30425
  };
30351
30426
  var createVoiceIncidentTimelineRoutes = (options) => {
30352
30427
  const path = options.path ?? "/api/voice/incident-timeline";
30353
30428
  const htmlPath = options.htmlPath === undefined ? "/voice/incident-timeline" : options.htmlPath;
30354
30429
  const markdownPath = options.markdownPath === undefined ? "/voice/incident-timeline.md" : options.markdownPath;
30430
+ const actionPath = options.actionPath === undefined ? "/api/voice/incident-timeline/actions" : options.actionPath;
30355
30431
  const routes = new Elysia46({
30356
30432
  name: options.name ?? "absolutejs-voice-incident-timeline"
30357
30433
  }).get(path, async () => {
@@ -30389,6 +30465,65 @@ var createVoiceIncidentTimelineRoutes = (options) => {
30389
30465
  });
30390
30466
  });
30391
30467
  }
30468
+ if (actionPath !== false) {
30469
+ routes.get(actionPath, async () => {
30470
+ const report = await buildVoiceIncidentTimelineReport(options);
30471
+ return new Response(JSON.stringify({
30472
+ actions: report.actions,
30473
+ generatedAt: report.generatedAt,
30474
+ status: report.status
30475
+ }), {
30476
+ headers: {
30477
+ "Content-Type": "application/json; charset=utf-8",
30478
+ ...options.headers
30479
+ }
30480
+ });
30481
+ }).post(`${actionPath}/:actionId`, async ({ params, request }) => {
30482
+ const actionId = params.actionId;
30483
+ const report = await buildVoiceIncidentTimelineReport(options);
30484
+ const action = report.actions.find((item) => item.id === actionId);
30485
+ const handler = options.actionHandlers?.[actionId];
30486
+ if (!action) {
30487
+ return new Response(JSON.stringify({
30488
+ actionId,
30489
+ ok: false,
30490
+ status: "not_found"
30491
+ }), {
30492
+ headers: {
30493
+ "Content-Type": "application/json; charset=utf-8",
30494
+ ...options.headers
30495
+ },
30496
+ status: 404
30497
+ });
30498
+ }
30499
+ if (action.disabled || action.method !== "POST" || !handler) {
30500
+ return new Response(JSON.stringify({
30501
+ actionId,
30502
+ ok: false,
30503
+ status: action.disabled ? "disabled" : "not_executable"
30504
+ }), {
30505
+ headers: {
30506
+ "Content-Type": "application/json; charset=utf-8",
30507
+ ...options.headers
30508
+ },
30509
+ status: 409
30510
+ });
30511
+ }
30512
+ const result = await handler({
30513
+ action,
30514
+ actionId,
30515
+ report,
30516
+ request
30517
+ });
30518
+ return new Response(JSON.stringify(result), {
30519
+ headers: {
30520
+ "Content-Type": "application/json; charset=utf-8",
30521
+ ...options.headers
30522
+ },
30523
+ status: result.ok ? 200 : 500
30524
+ });
30525
+ });
30526
+ }
30392
30527
  return routes;
30393
30528
  };
30394
30529
  // 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.433",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",