@absolutejs/voice 0.0.22-beta.214 → 0.0.22-beta.215
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 +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +432 -84
- package/dist/productionReadiness.d.ts +12 -0
- package/dist/providerSlo.d.ts +110 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3621,6 +3621,43 @@ if (!report.pass) {
|
|
|
3621
3621
|
|
|
3622
3622
|
Pass provider routing contract reports into production readiness through `providerRoutingContracts`. Readiness fails when a fallback contract fails, so model-routing regressions become deploy blockers instead of dashboard-only surprises.
|
|
3623
3623
|
|
|
3624
|
+
Use `createVoiceProviderSloRoutes(...)` when provider speed needs to be release evidence instead of a dashboard claim. The report reads the same provider routing trace events and checks LLM, STT, and TTS latency, p95 latency, timeout rate, fallback rate, and unresolved provider error rate.
|
|
3625
|
+
|
|
3626
|
+
```ts
|
|
3627
|
+
import {
|
|
3628
|
+
createVoiceProviderSloRoutes,
|
|
3629
|
+
createVoiceProductionReadinessRoutes
|
|
3630
|
+
} from '@absolutejs/voice';
|
|
3631
|
+
|
|
3632
|
+
const providerSlo = {
|
|
3633
|
+
requiredKinds: ['llm', 'stt', 'tts'],
|
|
3634
|
+
thresholds: {
|
|
3635
|
+
llm: { maxAverageElapsedMs: 2500, maxP95ElapsedMs: 4500 },
|
|
3636
|
+
stt: { maxAverageElapsedMs: 800, maxP95ElapsedMs: 1500 },
|
|
3637
|
+
tts: { maxAverageElapsedMs: 1200, maxP95ElapsedMs: 2200 }
|
|
3638
|
+
}
|
|
3639
|
+
} as const;
|
|
3640
|
+
|
|
3641
|
+
app
|
|
3642
|
+
.use(
|
|
3643
|
+
createVoiceProviderSloRoutes({
|
|
3644
|
+
store: runtime.traces,
|
|
3645
|
+
...providerSlo
|
|
3646
|
+
})
|
|
3647
|
+
)
|
|
3648
|
+
.use(
|
|
3649
|
+
createVoiceProductionReadinessRoutes({
|
|
3650
|
+
store: runtime.traces,
|
|
3651
|
+
providerSlo,
|
|
3652
|
+
links: {
|
|
3653
|
+
providerSlo: '/voice/provider-slos'
|
|
3654
|
+
}
|
|
3655
|
+
})
|
|
3656
|
+
);
|
|
3657
|
+
```
|
|
3658
|
+
|
|
3659
|
+
The provider SLO routes expose JSON at `/api/voice/provider-slos`, HTML at `/voice/provider-slos`, and Markdown at `/voice/provider-slos.md`. Readiness adds a `Provider SLO gates` check when `providerSlo` is configured; failing latency, timeout, fallback, or unresolved-error budgets close the deploy gate.
|
|
3660
|
+
|
|
3624
3661
|
Use `createVoiceProviderContractMatrixPreset(...)` when you want readiness proof for the whole provider stack without hand-writing every LLM, STT, and TTS contract row. The preset stays primitive: you still own provider lists, selected providers, latency budgets, env, capabilities, and route mounting.
|
|
3625
3662
|
|
|
3626
3663
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export { createOpenAIVoiceTTS } from './openaiTTS';
|
|
|
46
46
|
export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
|
|
47
47
|
export { createVoiceProviderCapabilityHTMLHandler, createVoiceProviderCapabilityJSONHandler, createVoiceProviderCapabilityRoutes, renderVoiceProviderCapabilityHTML, summarizeVoiceProviderCapabilities } from './providerCapabilities';
|
|
48
48
|
export { assertVoiceProviderRoutingContract, runVoiceProviderRoutingContract } from './providerRoutingContract';
|
|
49
|
+
export { buildVoiceProviderSloReport, createVoiceProviderSloRoutes, renderVoiceProviderSloHTML, renderVoiceProviderSloMarkdown } from './providerSlo';
|
|
49
50
|
export { createVoicePhoneAgentProductionSmokeHTMLHandler, createVoicePhoneAgentProductionSmokeJSONHandler, createVoicePhoneAgentProductionSmokeRoutes, renderVoicePhoneAgentProductionSmokeHTML, runVoicePhoneAgentProductionSmokeContract } from './phoneAgentProductionSmoke';
|
|
50
51
|
export { buildVoiceProductionReadinessGate, buildVoiceProductionReadinessReport, createVoiceProductionReadinessRoutes, renderVoiceProductionReadinessHTML, summarizeVoiceProductionReadinessGate } from './productionReadiness';
|
|
51
52
|
export { createVoiceReadinessProfile, recommendVoiceReadinessProfile } from './readinessProfiles';
|
|
@@ -101,6 +102,7 @@ export type { OpenAIRealtimeAdapterOptions, OpenAIRealtimeModel, OpenAIRealtimeN
|
|
|
101
102
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
102
103
|
export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
|
|
103
104
|
export type { VoiceProviderRoutingContractDefinition, VoiceProviderRoutingContractIssue, VoiceProviderRoutingContractReport, VoiceProviderRoutingContractRunOptions, VoiceProviderRoutingExpectation, VoiceProviderRoutingStatus } from './providerRoutingContract';
|
|
105
|
+
export type { VoiceProviderSloIssue, VoiceProviderSloKindReport, VoiceProviderSloMetric, VoiceProviderSloReport, VoiceProviderSloReportOptions, VoiceProviderSloRoutesOptions, VoiceProviderSloSessionReport, VoiceProviderSloStatus, VoiceProviderSloThresholdConfig, VoiceProviderSloThresholds } from './providerSlo';
|
|
104
106
|
export type { VoiceTurnLatencyHTMLHandlerOptions, VoiceTurnLatencyItem, VoiceTurnLatencyOptions, VoiceTurnLatencyReport, VoiceTurnLatencyRoutesOptions, VoiceTurnLatencyStage, VoiceTurnLatencyStatus } from './turnLatency';
|
|
105
107
|
export type { VoiceLiveLatencyOptions, VoiceLiveLatencyReport, VoiceLiveLatencyRoutesOptions, VoiceLiveLatencySample, VoiceLiveLatencyStatus } from './liveLatency';
|
|
106
108
|
export type { VoiceLatencySLOBudget, VoiceLatencySLOGateError, VoiceLatencySLOGateOptions, VoiceLatencySLOGateReport, VoiceLatencySLOMeasurement, VoiceLatencySLOStage, VoiceLatencySLOStageSummary, VoiceLatencySLOStatus } from './latencySlo';
|
package/dist/index.js
CHANGED
|
@@ -21888,12 +21888,308 @@ var assertVoiceProviderRoutingContract = async (options) => {
|
|
|
21888
21888
|
}
|
|
21889
21889
|
return report;
|
|
21890
21890
|
};
|
|
21891
|
+
// src/providerSlo.ts
|
|
21892
|
+
import { Elysia as Elysia35 } from "elysia";
|
|
21893
|
+
var defaultThresholds = {
|
|
21894
|
+
llm: {
|
|
21895
|
+
maxAverageElapsedMs: 2500,
|
|
21896
|
+
maxErrorRate: 0.02,
|
|
21897
|
+
maxFallbackRate: 0.25,
|
|
21898
|
+
maxP95ElapsedMs: 4500,
|
|
21899
|
+
maxTimeoutRate: 0.02,
|
|
21900
|
+
minSamples: 1
|
|
21901
|
+
},
|
|
21902
|
+
stt: {
|
|
21903
|
+
maxAverageElapsedMs: 800,
|
|
21904
|
+
maxErrorRate: 0.02,
|
|
21905
|
+
maxFallbackRate: 0.25,
|
|
21906
|
+
maxP95ElapsedMs: 1500,
|
|
21907
|
+
maxTimeoutRate: 0.02,
|
|
21908
|
+
minSamples: 1
|
|
21909
|
+
},
|
|
21910
|
+
tts: {
|
|
21911
|
+
maxAverageElapsedMs: 1200,
|
|
21912
|
+
maxErrorRate: 0.02,
|
|
21913
|
+
maxFallbackRate: 0.25,
|
|
21914
|
+
maxP95ElapsedMs: 2200,
|
|
21915
|
+
maxTimeoutRate: 0.02,
|
|
21916
|
+
minSamples: 1
|
|
21917
|
+
}
|
|
21918
|
+
};
|
|
21919
|
+
var providerKinds = ["llm", "stt", "tts"];
|
|
21920
|
+
var escapeHtml36 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
21921
|
+
var roundMetric3 = (value) => Math.round(value * 1e4) / 1e4;
|
|
21922
|
+
var rate3 = (count, total) => count / Math.max(1, total);
|
|
21923
|
+
var percentile3 = (values, rank) => {
|
|
21924
|
+
if (values.length === 0) {
|
|
21925
|
+
return 0;
|
|
21926
|
+
}
|
|
21927
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
21928
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(rank / 100 * sorted.length) - 1));
|
|
21929
|
+
return sorted[index] ?? 0;
|
|
21930
|
+
};
|
|
21931
|
+
var mergeThresholds = (thresholds) => Object.fromEntries(providerKinds.map((kind) => [
|
|
21932
|
+
kind,
|
|
21933
|
+
{
|
|
21934
|
+
...defaultThresholds[kind],
|
|
21935
|
+
...thresholds?.[kind] ?? {}
|
|
21936
|
+
}
|
|
21937
|
+
]));
|
|
21938
|
+
var createMetric2 = (input) => ({
|
|
21939
|
+
...input,
|
|
21940
|
+
actual: roundMetric3(input.actual),
|
|
21941
|
+
pass: input.actual <= input.threshold
|
|
21942
|
+
});
|
|
21943
|
+
var isRoutingEvent2 = (event) => Boolean(event && typeof event === "object" && "kind" in event && "sessionId" in event && !("payload" in event));
|
|
21944
|
+
var normalizeEvents2 = (events) => events.every(isRoutingEvent2) ? [...events] : listVoiceRoutingEvents(events);
|
|
21945
|
+
var issueFromMetric = (kind, code, metric) => metric.pass ? undefined : {
|
|
21946
|
+
code,
|
|
21947
|
+
detail: `${metric.label} ${formatMetricValue2(metric)} exceeds ${formatMetricThreshold(metric)}.`,
|
|
21948
|
+
kind,
|
|
21949
|
+
label: metric.label,
|
|
21950
|
+
status: "fail",
|
|
21951
|
+
value: formatMetricValue2(metric)
|
|
21952
|
+
};
|
|
21953
|
+
var summarizeKind = (kind, events, thresholds, required) => {
|
|
21954
|
+
const kindEvents = events.filter((event) => event.kind === kind);
|
|
21955
|
+
const latencies = kindEvents.map((event) => event.elapsedMs).filter((value) => typeof value === "number");
|
|
21956
|
+
const errors = kindEvents.filter((event) => event.status === "error");
|
|
21957
|
+
const unresolvedErrors = errors.filter((event) => !kindEvents.some((candidate) => candidate.sessionId === event.sessionId && candidate.at > event.at && (candidate.status === "fallback" || candidate.status === "success")));
|
|
21958
|
+
const fallbacks = kindEvents.filter((event) => event.status === "fallback");
|
|
21959
|
+
const timeouts = kindEvents.filter((event) => event.timedOut);
|
|
21960
|
+
const averageElapsedMs = latencies.length > 0 ? latencies.reduce((sum, value) => sum + value, 0) / latencies.length : 0;
|
|
21961
|
+
const metrics = {
|
|
21962
|
+
averageElapsedMs: createMetric2({
|
|
21963
|
+
actual: averageElapsedMs,
|
|
21964
|
+
label: "Average latency",
|
|
21965
|
+
threshold: thresholds.maxAverageElapsedMs,
|
|
21966
|
+
unit: "ms"
|
|
21967
|
+
}),
|
|
21968
|
+
errorRate: createMetric2({
|
|
21969
|
+
actual: rate3(unresolvedErrors.length, kindEvents.length),
|
|
21970
|
+
label: "Unresolved provider error rate",
|
|
21971
|
+
threshold: thresholds.maxErrorRate,
|
|
21972
|
+
unit: "rate"
|
|
21973
|
+
}),
|
|
21974
|
+
fallbackRate: createMetric2({
|
|
21975
|
+
actual: rate3(fallbacks.length, kindEvents.length),
|
|
21976
|
+
label: "Fallback rate",
|
|
21977
|
+
threshold: thresholds.maxFallbackRate,
|
|
21978
|
+
unit: "rate"
|
|
21979
|
+
}),
|
|
21980
|
+
p95ElapsedMs: createMetric2({
|
|
21981
|
+
actual: percentile3(latencies, 95),
|
|
21982
|
+
label: "P95 latency",
|
|
21983
|
+
threshold: thresholds.maxP95ElapsedMs,
|
|
21984
|
+
unit: "ms"
|
|
21985
|
+
}),
|
|
21986
|
+
timeoutRate: createMetric2({
|
|
21987
|
+
actual: rate3(timeouts.length, kindEvents.length),
|
|
21988
|
+
label: "Timeout rate",
|
|
21989
|
+
threshold: thresholds.maxTimeoutRate,
|
|
21990
|
+
unit: "rate"
|
|
21991
|
+
})
|
|
21992
|
+
};
|
|
21993
|
+
const issues = [
|
|
21994
|
+
(required || kindEvents.length > 0) && latencies.length < thresholds.minSamples ? {
|
|
21995
|
+
code: "provider_slo.insufficient_latency_samples",
|
|
21996
|
+
detail: `${kind.toUpperCase()} needs ${thresholds.minSamples} latency sample(s), saw ${latencies.length}.`,
|
|
21997
|
+
kind,
|
|
21998
|
+
label: "Provider latency samples",
|
|
21999
|
+
status: required ? "fail" : "warn",
|
|
22000
|
+
value: latencies.length
|
|
22001
|
+
} : undefined,
|
|
22002
|
+
issueFromMetric(kind, "provider_slo.average_latency", metrics.averageElapsedMs),
|
|
22003
|
+
issueFromMetric(kind, "provider_slo.p95_latency", metrics.p95ElapsedMs),
|
|
22004
|
+
issueFromMetric(kind, "provider_slo.error_rate", metrics.errorRate),
|
|
22005
|
+
issueFromMetric(kind, "provider_slo.timeout_rate", metrics.timeoutRate),
|
|
22006
|
+
issueFromMetric(kind, "provider_slo.fallback_rate", metrics.fallbackRate)
|
|
22007
|
+
].filter((issue) => issue !== undefined);
|
|
22008
|
+
const providers = new Set;
|
|
22009
|
+
for (const event of kindEvents) {
|
|
22010
|
+
if (event.provider) {
|
|
22011
|
+
providers.add(event.provider);
|
|
22012
|
+
}
|
|
22013
|
+
if (event.selectedProvider) {
|
|
22014
|
+
providers.add(event.selectedProvider);
|
|
22015
|
+
}
|
|
22016
|
+
if (event.fallbackProvider) {
|
|
22017
|
+
providers.add(event.fallbackProvider);
|
|
22018
|
+
}
|
|
22019
|
+
}
|
|
22020
|
+
return {
|
|
22021
|
+
events: kindEvents.length,
|
|
22022
|
+
eventsWithLatency: latencies.length,
|
|
22023
|
+
fallbacks: fallbacks.length,
|
|
22024
|
+
issues,
|
|
22025
|
+
kind,
|
|
22026
|
+
metrics,
|
|
22027
|
+
providers: [...providers].sort(),
|
|
22028
|
+
status: issues.some((issue) => issue.status === "fail") ? "fail" : issues.some((issue) => issue.status === "warn") ? "warn" : "pass",
|
|
22029
|
+
thresholds,
|
|
22030
|
+
timeouts: timeouts.length,
|
|
22031
|
+
unresolvedErrors: unresolvedErrors.length
|
|
22032
|
+
};
|
|
22033
|
+
};
|
|
22034
|
+
var summarizeSessions = (events) => {
|
|
22035
|
+
const sessions = new Map;
|
|
22036
|
+
for (const event of events) {
|
|
22037
|
+
const session = sessions.get(event.sessionId) ?? {
|
|
22038
|
+
errorCount: 0,
|
|
22039
|
+
fallbackCount: 0,
|
|
22040
|
+
kinds: [],
|
|
22041
|
+
lastEventAt: event.at,
|
|
22042
|
+
sessionId: event.sessionId,
|
|
22043
|
+
status: "pass",
|
|
22044
|
+
timeoutCount: 0
|
|
22045
|
+
};
|
|
22046
|
+
session.lastEventAt = Math.max(session.lastEventAt, event.at);
|
|
22047
|
+
if (!session.kinds.includes(event.kind)) {
|
|
22048
|
+
session.kinds.push(event.kind);
|
|
22049
|
+
}
|
|
22050
|
+
if (typeof event.elapsedMs === "number") {
|
|
22051
|
+
session.maxElapsedMs = session.maxElapsedMs === undefined ? event.elapsedMs : Math.max(session.maxElapsedMs, event.elapsedMs);
|
|
22052
|
+
}
|
|
22053
|
+
if (event.status === "error") {
|
|
22054
|
+
session.errorCount += 1;
|
|
22055
|
+
}
|
|
22056
|
+
if (event.status === "fallback") {
|
|
22057
|
+
session.fallbackCount += 1;
|
|
22058
|
+
}
|
|
22059
|
+
if (event.timedOut) {
|
|
22060
|
+
session.timeoutCount += 1;
|
|
22061
|
+
}
|
|
22062
|
+
session.status = session.errorCount > 0 || session.timeoutCount > 0 ? "fail" : session.fallbackCount > 0 ? "warn" : "pass";
|
|
22063
|
+
sessions.set(event.sessionId, session);
|
|
22064
|
+
}
|
|
22065
|
+
return [...sessions.values()].sort((left, right) => right.lastEventAt - left.lastEventAt);
|
|
22066
|
+
};
|
|
22067
|
+
var buildVoiceProviderSloReport = async (options = {}) => {
|
|
22068
|
+
const rawEvents = options.events ?? await options.store?.list() ?? [];
|
|
22069
|
+
const events = normalizeEvents2(rawEvents);
|
|
22070
|
+
const thresholds = mergeThresholds(options.thresholds);
|
|
22071
|
+
const observedKinds = new Set(events.map((event) => event.kind));
|
|
22072
|
+
const requiredKinds = new Set(options.requiredKinds ?? [...observedKinds]);
|
|
22073
|
+
const kindReports = Object.fromEntries(providerKinds.map((kind) => [
|
|
22074
|
+
kind,
|
|
22075
|
+
summarizeKind(kind, events, thresholds[kind], requiredKinds.has(kind))
|
|
22076
|
+
]));
|
|
22077
|
+
const issues = Object.values(kindReports).flatMap((report) => report.issues);
|
|
22078
|
+
const eventsWithLatency = events.filter((event) => typeof event.elapsedMs === "number").length;
|
|
22079
|
+
if (events.length === 0) {
|
|
22080
|
+
issues.push({
|
|
22081
|
+
code: "provider_slo.no_routing_events",
|
|
22082
|
+
detail: "No provider routing events are recorded yet. Run a live turn, smoke, or provider simulation before certifying latency.",
|
|
22083
|
+
label: "Provider routing evidence",
|
|
22084
|
+
status: "warn",
|
|
22085
|
+
value: 0
|
|
22086
|
+
});
|
|
22087
|
+
}
|
|
22088
|
+
return {
|
|
22089
|
+
checkedAt: Date.now(),
|
|
22090
|
+
events: events.length,
|
|
22091
|
+
eventsWithLatency,
|
|
22092
|
+
issues,
|
|
22093
|
+
kinds: kindReports,
|
|
22094
|
+
sessions: summarizeSessions(events),
|
|
22095
|
+
status: issues.some((issue) => issue.status === "fail") ? "fail" : issues.some((issue) => issue.status === "warn") ? "warn" : "pass",
|
|
22096
|
+
thresholds
|
|
22097
|
+
};
|
|
22098
|
+
};
|
|
22099
|
+
var formatMetricValue2 = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
22100
|
+
var formatMetricThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
22101
|
+
var getMetric = (report, key) => report.metrics[key];
|
|
22102
|
+
var renderVoiceProviderSloMarkdown = (report) => {
|
|
22103
|
+
const rows = providerKinds.map((kind) => {
|
|
22104
|
+
const kindReport = report.kinds[kind];
|
|
22105
|
+
return `| ${kind.toUpperCase()} | ${kindReport.status} | ${kindReport.events} | ${kindReport.eventsWithLatency} | ${formatMetricValue2(getMetric(kindReport, "averageElapsedMs"))} | ${formatMetricValue2(getMetric(kindReport, "p95ElapsedMs"))} | ${formatMetricValue2(getMetric(kindReport, "errorRate"))} | ${formatMetricValue2(getMetric(kindReport, "timeoutRate"))} | ${formatMetricValue2(getMetric(kindReport, "fallbackRate"))} |`;
|
|
22106
|
+
}).join(`
|
|
22107
|
+
`);
|
|
22108
|
+
const issues = report.issues.map((issue) => `- ${issue.status}: ${issue.kind ? `${issue.kind.toUpperCase()} ` : ""}${issue.label}${issue.detail ? ` - ${issue.detail}` : ""}`).join(`
|
|
22109
|
+
`) || "No provider SLO issues.";
|
|
22110
|
+
return `# Voice Provider SLO Report
|
|
22111
|
+
|
|
22112
|
+
Generated: ${new Date(report.checkedAt).toISOString()}
|
|
22113
|
+
|
|
22114
|
+
Overall: **${report.status}**
|
|
22115
|
+
|
|
22116
|
+
Events: ${report.events}
|
|
22117
|
+
|
|
22118
|
+
Events with latency: ${report.eventsWithLatency}
|
|
22119
|
+
|
|
22120
|
+
| Kind | Status | Events | Latency Samples | Avg | P95 | Error Rate | Timeout Rate | Fallback Rate |
|
|
22121
|
+
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
22122
|
+
${rows}
|
|
22123
|
+
|
|
22124
|
+
## Issues
|
|
22125
|
+
|
|
22126
|
+
${issues}
|
|
22127
|
+
`;
|
|
22128
|
+
};
|
|
22129
|
+
var renderVoiceProviderSloHTML = (report, options = {}) => {
|
|
22130
|
+
const title = options.title ?? "AbsoluteJS Voice Provider SLOs";
|
|
22131
|
+
const kindCards = providerKinds.map((kind) => {
|
|
22132
|
+
const kindReport = report.kinds[kind];
|
|
22133
|
+
const metrics = Object.values(kindReport.metrics).map((metric) => `<div><dt>${escapeHtml36(metric.label)}</dt><dd>${escapeHtml36(formatMetricValue2(metric))}</dd><small>budget ${escapeHtml36(formatMetricThreshold(metric))}</small></div>`).join("");
|
|
22134
|
+
const providers = kindReport.providers.length ? kindReport.providers.join(", ") : "none recorded";
|
|
22135
|
+
return `<article class="${escapeHtml36(kindReport.status)}"><h2>${kind.toUpperCase()} <span>${escapeHtml36(kindReport.status)}</span></h2><p>${kindReport.events} routing event(s), ${kindReport.eventsWithLatency} latency sample(s), providers: ${escapeHtml36(providers)}.</p><dl>${metrics}</dl></article>`;
|
|
22136
|
+
}).join("");
|
|
22137
|
+
const issues = report.issues.length > 0 ? `<ul>${report.issues.map((issue) => `<li class="${escapeHtml36(issue.status)}"><strong>${escapeHtml36(issue.kind ? `${issue.kind.toUpperCase()} ${issue.label}` : issue.label)}</strong><span>${escapeHtml36(issue.detail ?? "")}</span></li>`).join("")}</ul>` : "<p>No provider SLO issues.</p>";
|
|
22138
|
+
const snippet = `createVoiceProviderSloRoutes({
|
|
22139
|
+
store: runtimeStorage.traces,
|
|
22140
|
+
requiredKinds: ['llm', 'stt', 'tts'],
|
|
22141
|
+
thresholds: {
|
|
22142
|
+
llm: { maxAverageElapsedMs: 2500, maxP95ElapsedMs: 4500 },
|
|
22143
|
+
stt: { maxAverageElapsedMs: 800, maxP95ElapsedMs: 1500 },
|
|
22144
|
+
tts: { maxAverageElapsedMs: 1200, maxP95ElapsedMs: 2200 }
|
|
22145
|
+
}
|
|
22146
|
+
})`;
|
|
22147
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml36(title)}</title><style>body{background:#101318;color:#f8f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,article,.primitive{background:#171b22;border:1px solid #2c3340;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(14,165,233,.2),rgba(245,158,11,.12))}.eyebrow{color:#7dd3fc;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,4.9rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}.status,article h2 span{border:1px solid #475569;border-radius:999px;display:inline-flex;font-size:.85rem;padding:6px 10px}.pass{border-color:rgba(34,197,94,.65)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}dl{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}dt{color:#cbd5e1;font-size:.78rem;text-transform:uppercase}dd{font-size:1.7rem;font-weight:900;margin:0}small{color:#a8b3c2}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#101318;border:1px solid #2c3340;border-radius:16px;padding:12px}li span{color:#cbd5e1;display:block;margin-top:4px}.primitive{background:#11161d}.primitive code{color:#bae6fd}.primitive pre{background:#070b10;border:1px solid #243041;border-radius:16px;color:#e0f2fe;overflow:auto;padding:16px}</style></head><body><main><section class="hero"><p class="eyebrow">Provider latency and fallback proof</p><h1>${escapeHtml36(title)}</h1><p class="status ${escapeHtml36(report.status)}">${escapeHtml36(report.status)}</p><p>${report.events} provider routing event(s), ${report.eventsWithLatency} latency sample(s).</p></section><section class="grid">${kindCards}</section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceProviderSloRoutes(...)</code> turns provider speed into release evidence</h2><p>Pair this report with production readiness so LLM/STT/TTS latency, timeout, fallback, and unresolved error regressions block deploys.</p><pre><code>${escapeHtml36(snippet)}</code></pre></section><section><h2>Issues</h2>${issues}</section></main></body></html>`;
|
|
22148
|
+
};
|
|
22149
|
+
var createVoiceProviderSloRoutes = (options) => {
|
|
22150
|
+
const path = options.path ?? "/api/voice/provider-slos";
|
|
22151
|
+
const htmlPath = options.htmlPath ?? "/voice/provider-slos";
|
|
22152
|
+
const markdownPath = options.markdownPath ?? "/voice/provider-slos.md";
|
|
22153
|
+
const headers = {
|
|
22154
|
+
"cache-control": "no-store",
|
|
22155
|
+
...options.headers ?? {}
|
|
22156
|
+
};
|
|
22157
|
+
const buildReport = () => buildVoiceProviderSloReport(options);
|
|
22158
|
+
const app = new Elysia35({ name: options.name ?? "absolute-voice-provider-slos" });
|
|
22159
|
+
app.get(path, async () => Response.json(await buildReport(), { headers }));
|
|
22160
|
+
if (markdownPath !== false) {
|
|
22161
|
+
app.get(markdownPath, async () => {
|
|
22162
|
+
const report = await buildReport();
|
|
22163
|
+
return new Response(renderVoiceProviderSloMarkdown(report), {
|
|
22164
|
+
headers: {
|
|
22165
|
+
...headers,
|
|
22166
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
22167
|
+
}
|
|
22168
|
+
});
|
|
22169
|
+
});
|
|
22170
|
+
}
|
|
22171
|
+
if (htmlPath !== false) {
|
|
22172
|
+
app.get(htmlPath, async () => {
|
|
22173
|
+
const report = await buildReport();
|
|
22174
|
+
const html = options.render ? await options.render(report) : renderVoiceProviderSloHTML(report, {
|
|
22175
|
+
title: options.title
|
|
22176
|
+
});
|
|
22177
|
+
return new Response(html, {
|
|
22178
|
+
headers: {
|
|
22179
|
+
...headers,
|
|
22180
|
+
"content-type": "text/html; charset=utf-8"
|
|
22181
|
+
}
|
|
22182
|
+
});
|
|
22183
|
+
});
|
|
22184
|
+
}
|
|
22185
|
+
return app;
|
|
22186
|
+
};
|
|
21891
22187
|
// src/productionReadiness.ts
|
|
21892
|
-
import { Elysia as
|
|
22188
|
+
import { Elysia as Elysia37 } from "elysia";
|
|
21893
22189
|
|
|
21894
22190
|
// src/opsRecovery.ts
|
|
21895
|
-
import { Elysia as
|
|
21896
|
-
var
|
|
22191
|
+
import { Elysia as Elysia36 } from "elysia";
|
|
22192
|
+
var escapeHtml37 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
21897
22193
|
var getString14 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
21898
22194
|
var hrefForSession = (value, sessionId) => {
|
|
21899
22195
|
if (typeof value === "function") {
|
|
@@ -22107,19 +22403,19 @@ ${failedSessions || "None."}
|
|
|
22107
22403
|
${report.latency ? renderVoiceLatencySLOMarkdown(report.latency, { title: "Latency SLO" }) : "Latency SLO disabled."}
|
|
22108
22404
|
`;
|
|
22109
22405
|
};
|
|
22110
|
-
var renderDeliverySummary = (label, summary) => summary ? `<article><span>${
|
|
22406
|
+
var renderDeliverySummary = (label, summary) => summary ? `<article><span>${escapeHtml37(label)}</span><strong>${String(summary.failed + summary.deadLettered)} failed</strong><small>${String(summary.pending)} pending \xB7 ${String(summary.retryEligible)} retry eligible \xB7 ${String(summary.total)} total</small></article>` : `<article><span>${escapeHtml37(label)}</span><strong>not configured</strong></article>`;
|
|
22111
22407
|
var renderVoiceOpsRecoveryHTML = (report, options = {}) => {
|
|
22112
22408
|
const title = options.title ?? "Voice Ops Recovery";
|
|
22113
|
-
const issues = report.issues.map((issue) => `<tr><td>${
|
|
22114
|
-
const providers = report.providers.providers.map((provider) => `<tr><td>${
|
|
22115
|
-
const failedSessions = report.failedSessions.map((session) => `<li>${session.operationsRecordHref ? `<a href="${
|
|
22116
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
22409
|
+
const issues = report.issues.map((issue) => `<tr><td>${escapeHtml37(issue.severity)}</td><td><code>${escapeHtml37(issue.code)}</code></td><td>${issue.href ? `<a href="${escapeHtml37(issue.href)}">${escapeHtml37(issue.label)}</a>` : escapeHtml37(issue.label)}</td><td>${escapeHtml37(String(issue.value ?? ""))}</td><td>${escapeHtml37(issue.detail ?? "")}</td></tr>`).join("");
|
|
22410
|
+
const providers = report.providers.providers.map((provider) => `<tr><td>${escapeHtml37(provider.provider)}</td><td>${escapeHtml37(provider.status)}</td><td>${String(provider.runCount)}</td><td>${String(provider.errorCount)}</td><td>${String(provider.fallbackCount)}</td><td>${escapeHtml37(provider.lastError ?? "")}</td></tr>`).join("");
|
|
22411
|
+
const failedSessions = report.failedSessions.map((session) => `<li>${session.operationsRecordHref ? `<a href="${escapeHtml37(session.operationsRecordHref)}">${escapeHtml37(session.sessionId)}</a>` : escapeHtml37(session.sessionId)}${session.provider ? ` via ${escapeHtml37(session.provider)}` : ""}${session.error ? `: ${escapeHtml37(session.error)}` : ""}</li>`).join("");
|
|
22412
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml37(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#f8fafc;color:#172033;margin:2rem;line-height:1.45}main{max-width:1180px;margin:auto}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.75rem;margin:1rem 0}article{background:white;border:1px solid #dbe3ef;border-radius:14px;padding:1rem;box-shadow:0 10px 28px rgba(15,23,42,.05)}article span{display:block;color:#64748b;font-size:.85rem}article strong{display:block;font-size:1.5rem;margin:.2rem 0}article small{color:#64748b}table{border-collapse:collapse;width:100%;background:white;border:1px solid #dbe3ef;border-radius:14px;overflow:hidden}th,td{border-bottom:1px solid #e2e8f0;padding:.7rem;text-align:left;vertical-align:top}code{font-size:.85em}.status{display:inline-flex;border-radius:999px;padding:.35rem .7rem;background:${report.status === "fail" ? "#fee2e2" : report.status === "warn" ? "#fef3c7" : "#dcfce7"};color:${report.status === "fail" ? "#991b1b" : report.status === "warn" ? "#92400e" : "#166534"};font-weight:700}</style></head><body><main><h1>${escapeHtml37(title)}</h1><p><span class="status">${escapeHtml37(report.status)}</span> Checked ${escapeHtml37(new Date(report.checkedAt).toLocaleString())}</p><section class="grid"><article><span>Recovered fallbacks</span><strong>${String(report.providers.recoveredFallbacks)}</strong></article><article><span>Unresolved providers</span><strong>${String(report.providers.unresolvedFailures)}</strong></article><article><span>Operator interventions</span><strong>${String(report.interventions.total)}</strong></article><article><span>Latency status</span><strong>${escapeHtml37(report.latency?.status ?? "disabled")}</strong></article>${renderDeliverySummary("Audit delivery", report.auditDeliveries)}${renderDeliverySummary("Trace delivery", report.traceDeliveries)}${renderDeliverySummary("Handoff delivery", report.handoffDeliveries)}</section><h2>Issues</h2><table><thead><tr><th>Severity</th><th>Code</th><th>Label</th><th>Value</th><th>Detail</th></tr></thead><tbody>${issues || '<tr><td colspan="5">No recovery issues.</td></tr>'}</tbody></table><h2>Providers</h2><table><thead><tr><th>Provider</th><th>Status</th><th>Runs</th><th>Errors</th><th>Fallbacks</th><th>Last error</th></tr></thead><tbody>${providers || '<tr><td colspan="6">No provider activity.</td></tr>'}</tbody></table><h2>Failed Sessions</h2><ul>${failedSessions || "<li>None.</li>"}</ul></main></body></html>`;
|
|
22117
22413
|
};
|
|
22118
22414
|
var createVoiceOpsRecoveryRoutes = (options = {}) => {
|
|
22119
22415
|
const path = options.path ?? "/api/voice/ops-recovery";
|
|
22120
22416
|
const htmlPath = options.htmlPath === undefined ? "/ops-recovery" : options.htmlPath;
|
|
22121
22417
|
const markdownPath = options.markdownPath === undefined ? `${path}.md` : options.markdownPath;
|
|
22122
|
-
const routes = new
|
|
22418
|
+
const routes = new Elysia36({
|
|
22123
22419
|
name: options.name ?? "absolutejs-voice-ops-recovery"
|
|
22124
22420
|
}).get(path, async () => buildVoiceOpsRecoveryReport(options));
|
|
22125
22421
|
if (htmlPath) {
|
|
@@ -22149,7 +22445,7 @@ var createVoiceOpsRecoveryRoutes = (options = {}) => {
|
|
|
22149
22445
|
};
|
|
22150
22446
|
|
|
22151
22447
|
// src/productionReadiness.ts
|
|
22152
|
-
var
|
|
22448
|
+
var escapeHtml38 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
22153
22449
|
var rollupStatus3 = (checks) => checks.some((check) => check.status === "fail") ? "fail" : checks.some((check) => check.status === "warn") ? "warn" : "pass";
|
|
22154
22450
|
var readinessGateCodes = {
|
|
22155
22451
|
"Agent squad contracts": "voice.readiness.agent_squad_contracts",
|
|
@@ -22169,6 +22465,7 @@ var readinessGateCodes = {
|
|
|
22169
22465
|
"Provider fallback recovery": "voice.readiness.provider_fallback_recovery",
|
|
22170
22466
|
"Provider health": "voice.readiness.provider_health",
|
|
22171
22467
|
"Provider routing contracts": "voice.readiness.provider_routing_contracts",
|
|
22468
|
+
"Provider SLO gates": "voice.readiness.provider_slo_gates",
|
|
22172
22469
|
"Provider stack capabilities": "voice.readiness.provider_stack_capabilities",
|
|
22173
22470
|
"Quality gates": "voice.readiness.quality_gates",
|
|
22174
22471
|
"Reconnect recovery contracts": "voice.readiness.reconnect_contracts",
|
|
@@ -22241,6 +22538,18 @@ var resolveProviderRoutingContracts = async (options, input) => {
|
|
|
22241
22538
|
}
|
|
22242
22539
|
return typeof options.providerRoutingContracts === "function" ? await options.providerRoutingContracts(input) : options.providerRoutingContracts;
|
|
22243
22540
|
};
|
|
22541
|
+
var isVoiceProviderSloReport = (value) => typeof value.status === "string" && typeof value.checkedAt === "number" && typeof value.events === "number";
|
|
22542
|
+
var resolveProviderSlo = async (options, input) => {
|
|
22543
|
+
if (options.providerSlo === false || options.providerSlo === undefined) {
|
|
22544
|
+
return;
|
|
22545
|
+
}
|
|
22546
|
+
const value = typeof options.providerSlo === "function" ? await options.providerSlo(input) : options.providerSlo;
|
|
22547
|
+
return isVoiceProviderSloReport(value) ? value : buildVoiceProviderSloReport({
|
|
22548
|
+
...value,
|
|
22549
|
+
events: value.events,
|
|
22550
|
+
store: value.store ?? options.store
|
|
22551
|
+
});
|
|
22552
|
+
};
|
|
22244
22553
|
var resolveProviderStack = async (options, input) => {
|
|
22245
22554
|
if (options.providerStack === false || options.providerStack === undefined) {
|
|
22246
22555
|
return;
|
|
@@ -22567,6 +22876,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22567
22876
|
carriers,
|
|
22568
22877
|
agentSquadContracts,
|
|
22569
22878
|
providerRoutingContracts,
|
|
22879
|
+
providerSlo,
|
|
22570
22880
|
providerStack,
|
|
22571
22881
|
providerContractMatrix,
|
|
22572
22882
|
phoneAgentSmokes,
|
|
@@ -22601,6 +22911,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22601
22911
|
resolveCarriers(options, { query, request }),
|
|
22602
22912
|
resolveAgentSquadContracts(options, { query, request }),
|
|
22603
22913
|
resolveProviderRoutingContracts(options, { query, request }),
|
|
22914
|
+
resolveProviderSlo(options, { query, request }),
|
|
22604
22915
|
resolveProviderStack(options, { query, request }),
|
|
22605
22916
|
resolveProviderContractMatrix(options, { query, request }),
|
|
22606
22917
|
resolvePhoneAgentSmokes(options, { query, request }),
|
|
@@ -22796,6 +23107,12 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22796
23107
|
status: providerRoutingContracts.some((report) => !report.pass) ? "fail" : providerRoutingContracts.length === 0 ? "warn" : "pass",
|
|
22797
23108
|
total: providerRoutingContracts.length
|
|
22798
23109
|
} : undefined;
|
|
23110
|
+
const providerSloSummary = providerSlo ? {
|
|
23111
|
+
events: providerSlo.events,
|
|
23112
|
+
eventsWithLatency: providerSlo.eventsWithLatency,
|
|
23113
|
+
issues: providerSlo.issues.length,
|
|
23114
|
+
status: providerSlo.status
|
|
23115
|
+
} : undefined;
|
|
22799
23116
|
const phoneAgentSmokeSummary = phoneAgentSmokes ? {
|
|
22800
23117
|
failed: phoneAgentSmokes.filter((report) => !report.pass).length,
|
|
22801
23118
|
passed: phoneAgentSmokes.filter((report) => report.pass).length,
|
|
@@ -22854,6 +23171,31 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22854
23171
|
]
|
|
22855
23172
|
});
|
|
22856
23173
|
}
|
|
23174
|
+
if (providerSloSummary && providerSlo) {
|
|
23175
|
+
const firstIssue = providerSlo.issues[0];
|
|
23176
|
+
checks.push({
|
|
23177
|
+
detail: providerSloSummary.status === "pass" ? `${providerSloSummary.eventsWithLatency} provider latency sample(s) are inside LLM/STT/TTS SLO budgets.` : firstIssue?.detail ?? `${providerSloSummary.issues} provider SLO issue(s) need review.`,
|
|
23178
|
+
href: firstIssue?.sessionId ? voiceOperationsRecordHref(options.links?.operationsRecords ?? "/voice-operations", firstIssue.sessionId) : options.links?.providerSlo ?? options.links?.resilience ?? "/voice/provider-slos",
|
|
23179
|
+
label: "Provider SLO gates",
|
|
23180
|
+
proofSource: proofSource("providerSlo", "providerSlos"),
|
|
23181
|
+
status: providerSloSummary.status,
|
|
23182
|
+
value: `${providerSloSummary.eventsWithLatency}/${providerSloSummary.events}`,
|
|
23183
|
+
actions: providerSloSummary.status === "pass" ? [] : [
|
|
23184
|
+
...firstIssue?.sessionId ? [
|
|
23185
|
+
{
|
|
23186
|
+
description: "Open the exact call/session operations record for the first provider SLO issue.",
|
|
23187
|
+
href: voiceOperationsRecordHref(options.links?.operationsRecords ?? "/voice-operations", firstIssue.sessionId),
|
|
23188
|
+
label: "Open impacted operations record"
|
|
23189
|
+
}
|
|
23190
|
+
] : [],
|
|
23191
|
+
{
|
|
23192
|
+
description: "Open provider SLO proof and inspect latency, timeout, fallback, and unresolved error budgets.",
|
|
23193
|
+
href: options.links?.providerSlo ?? options.links?.resilience ?? "/voice/provider-slos",
|
|
23194
|
+
label: "Open provider SLO report"
|
|
23195
|
+
}
|
|
23196
|
+
]
|
|
23197
|
+
});
|
|
23198
|
+
}
|
|
22857
23199
|
if (providerStack) {
|
|
22858
23200
|
const missingLanes = providerStack.gaps.filter((gap) => gap.status !== "pass");
|
|
22859
23201
|
checks.push({
|
|
@@ -23081,6 +23423,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
23081
23423
|
phoneAgentSmoke: "/sessions",
|
|
23082
23424
|
providerContracts: "/provider-contracts",
|
|
23083
23425
|
providerRoutingContracts: "/resilience",
|
|
23426
|
+
providerSlo: "/voice/provider-slos",
|
|
23084
23427
|
quality: "/quality",
|
|
23085
23428
|
reconnectContracts: "/sessions",
|
|
23086
23429
|
resilience: "/resilience",
|
|
@@ -23121,6 +23464,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
23121
23464
|
providerRecovery,
|
|
23122
23465
|
phoneAgentSmokes: phoneAgentSmokeSummary,
|
|
23123
23466
|
providerRoutingContracts: providerRoutingContractSummary,
|
|
23467
|
+
providerSlo: providerSloSummary,
|
|
23124
23468
|
reconnectContracts: reconnectContractSummary,
|
|
23125
23469
|
quality: {
|
|
23126
23470
|
status: quality.status
|
|
@@ -23140,22 +23484,22 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
23140
23484
|
var buildVoiceProductionReadinessGate = async (options, input = {}) => summarizeVoiceProductionReadinessGate(await buildVoiceProductionReadinessReport(options, input), options.gate || undefined);
|
|
23141
23485
|
var renderVoiceProductionReadinessHTML = (report, options = {}) => {
|
|
23142
23486
|
const title = options.title ?? "AbsoluteJS Voice Production Readiness";
|
|
23143
|
-
const profile = report.profile ? `<section class="profile"><p class="eyebrow">Readiness profile</p><h2>${
|
|
23487
|
+
const profile = report.profile ? `<section class="profile"><p class="eyebrow">Readiness profile</p><h2>${escapeHtml38(report.profile.name)}</h2><p>${escapeHtml38(report.profile.description)}</p><p>${escapeHtml38(report.profile.purpose)}</p><div class="profile-surfaces">${report.profile.surfaces.map((surface) => `<article class="${surface.configured ? "pass" : "warn"}"><span>${surface.configured ? "CONFIGURED" : "EXPECTED"}</span><strong>${surface.href ? `<a href="${escapeHtml38(surface.href)}">${escapeHtml38(surface.label)}</a>` : escapeHtml38(surface.label)}</strong></article>`).join("")}</div></section>` : "";
|
|
23144
23488
|
const checks = report.checks.map((check, index) => {
|
|
23145
|
-
const actions = (check.actions ?? []).map((action) => action.method === "POST" ? `<button type="button" data-readiness-action="${index}" data-action-url="${
|
|
23146
|
-
return `<article class="check ${
|
|
23489
|
+
const actions = (check.actions ?? []).map((action) => action.method === "POST" ? `<button type="button" data-readiness-action="${index}" data-action-url="${escapeHtml38(action.href)}">${escapeHtml38(action.label)}</button>` : `<a href="${escapeHtml38(action.href)}">${escapeHtml38(action.label)}</a>`).join("");
|
|
23490
|
+
return `<article class="check ${escapeHtml38(check.status)}">
|
|
23147
23491
|
<div>
|
|
23148
|
-
<span>${
|
|
23149
|
-
<h2>${
|
|
23150
|
-
${check.detail ? `<p>${
|
|
23151
|
-
${check.proofSource ? `<p class="proof-source">Proof source: ${check.proofSource.href ? `<a href="${
|
|
23492
|
+
<span>${escapeHtml38(check.status.toUpperCase())}</span>
|
|
23493
|
+
<h2>${escapeHtml38(check.label)}</h2>
|
|
23494
|
+
${check.detail ? `<p>${escapeHtml38(check.detail)}</p>` : ""}
|
|
23495
|
+
${check.proofSource ? `<p class="proof-source">Proof source: ${check.proofSource.href ? `<a href="${escapeHtml38(check.proofSource.href)}">${escapeHtml38(check.proofSource.sourceLabel)}</a>` : escapeHtml38(check.proofSource.sourceLabel)}${check.proofSource.detail ? ` \xB7 ${escapeHtml38(check.proofSource.detail)}` : ""}</p>` : ""}
|
|
23152
23496
|
${actions ? `<p class="actions">${actions}</p>` : ""}
|
|
23153
23497
|
</div>
|
|
23154
|
-
<strong>${
|
|
23155
|
-
${check.href ? `<a href="${
|
|
23498
|
+
<strong>${escapeHtml38(String(check.value ?? check.status))}</strong>
|
|
23499
|
+
${check.href ? `<a href="${escapeHtml38(check.href)}">Open surface</a>` : ""}
|
|
23156
23500
|
</article>`;
|
|
23157
23501
|
}).join("");
|
|
23158
|
-
const snippet =
|
|
23502
|
+
const snippet = escapeHtml38(`createVoiceProductionReadinessRoutes({
|
|
23159
23503
|
htmlPath: '/production-readiness',
|
|
23160
23504
|
path: '/api/production-readiness',
|
|
23161
23505
|
gatePath: '/api/production-readiness/gate',
|
|
@@ -23171,13 +23515,13 @@ var renderVoiceProductionReadinessHTML = (report, options = {}) => {
|
|
|
23171
23515
|
providerRoutingContracts: loadProviderRoutingContracts,
|
|
23172
23516
|
store: traceStore
|
|
23173
23517
|
});`);
|
|
23174
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
23518
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml38(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero,.primitive,.profile{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.primitive,.profile{background:#111722}.primitive{border-color:#3a3f2d}.eyebrow{color:#fbbf24;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{display:inline-flex;border:1px solid #3f3f46;border-radius:999px;padding:8px 12px}.primitive code{color:#fde68a}.primitive p{color:#c8ccd3;line-height:1.55;margin:.45rem 0 0}.primitive pre{background:#0b0f16;border:1px solid #2c3440;border-radius:18px;color:#fef3c7;margin:16px 0 0;overflow:auto;padding:16px}.status.pass,.check.pass,.profile-surfaces .pass{border-color:rgba(34,197,94,.55)}.status.warn,.check.warn,.profile-surfaces .warn{border-color:rgba(245,158,11,.65)}.status.fail,.check.fail{border-color:rgba(239,68,68,.75)}.checks{display:grid;gap:14px}.check{align-items:center;background:#141922;border:1px solid #26313d;border-radius:22px;display:grid;gap:16px;grid-template-columns:1fr auto auto;padding:18px}.check span,.profile-surfaces span{color:#a8b0b8;font-size:.78rem;font-weight:900;letter-spacing:.08em}.check h2{margin:.2rem 0}.check p,.profile p{color:#b9c0c8;margin:.2rem 0 0}.check .proof-source{color:#f9d77e;font-weight:800}.check strong{font-size:1.5rem}.profile-surfaces{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));margin-top:16px}.profile-surfaces article{background:#141922;border:1px solid #26313d;border-radius:16px;padding:14px}.profile-surfaces strong{display:block;margin-top:6px}.actions{display:flex;flex-wrap:wrap;gap:10px}.check a,a{color:#fbbf24}button{background:#fbbf24;border:0;border-radius:999px;color:#111827;cursor:pointer;font-weight:800;padding:9px 12px}button:disabled{cursor:wait;opacity:.65}@media(max-width:760px){main{padding:20px}.check{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted readiness</p><h1>${escapeHtml38(title)}</h1><p>One deployable pass/fail report for quality gates, provider failover, session health, handoffs, routing evidence, and optional carrier readiness.</p><p class="status ${escapeHtml38(report.status)}">Overall: ${escapeHtml38(report.status.toUpperCase())}</p><p>Checked ${escapeHtml38(new Date(report.checkedAt).toLocaleString())}</p></section>${profile}<section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceProductionReadinessRoutes(...)</code> builds this deploy gate</h2><p>Mount one package primitive to expose JSON readiness, HTML readiness, and a machine-readable gate route. Feed it the proof stores and contract reports your app already owns.</p><pre><code>${snippet}</code></pre></section><section class="checks">${checks}</section></main><script>document.querySelectorAll("[data-readiness-action]").forEach((button)=>{button.addEventListener("click",async()=>{const url=button.getAttribute("data-action-url");if(!url)return;button.disabled=true;const original=button.textContent;button.textContent="Running...";try{const response=await fetch(url,{method:"POST"});button.textContent=response.ok?"Done. Reloading...":"Failed";if(response.ok)setTimeout(()=>location.reload(),500)}catch{button.textContent="Failed"}finally{setTimeout(()=>{button.disabled=false;button.textContent=original},1500)}})});</script></body></html>`;
|
|
23175
23519
|
};
|
|
23176
23520
|
var createVoiceProductionReadinessRoutes = (options) => {
|
|
23177
23521
|
const path = options.path ?? "/api/production-readiness";
|
|
23178
23522
|
const gatePath = options.gatePath === undefined ? "/api/production-readiness/gate" : options.gatePath;
|
|
23179
23523
|
const htmlPath = options.htmlPath ?? "/production-readiness";
|
|
23180
|
-
const routes = new
|
|
23524
|
+
const routes = new Elysia37({
|
|
23181
23525
|
name: options.name ?? "absolutejs-voice-production-readiness"
|
|
23182
23526
|
});
|
|
23183
23527
|
routes.get(path, async ({ query, request }) => buildVoiceProductionReadinessReport(options, { query, request }));
|
|
@@ -23541,8 +23885,8 @@ var recommendVoiceReadinessProfile = (options) => {
|
|
|
23541
23885
|
};
|
|
23542
23886
|
};
|
|
23543
23887
|
// src/providerStackRecommendations.ts
|
|
23544
|
-
import { Elysia as
|
|
23545
|
-
var
|
|
23888
|
+
import { Elysia as Elysia38 } from "elysia";
|
|
23889
|
+
var escapeHtml39 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23546
23890
|
var profileProviderPriorities = {
|
|
23547
23891
|
"meeting-recorder": {
|
|
23548
23892
|
llm: ["openai", "anthropic", "gemini"],
|
|
@@ -23785,17 +24129,17 @@ var resolveProviderContractMatrixInput = async (matrix) => typeof matrix === "fu
|
|
|
23785
24129
|
var renderVoiceProviderContractMatrixHTML = (report, options = {}) => {
|
|
23786
24130
|
const title = options.title ?? "Voice Provider Contract Matrix";
|
|
23787
24131
|
const rows = report.rows.map((row) => {
|
|
23788
|
-
const checks = row.checks.map((check) => `<li class="${
|
|
23789
|
-
return `<article class="row ${
|
|
24132
|
+
const checks = row.checks.map((check) => `<li class="${escapeHtml39(check.status)}"><strong>${escapeHtml39(check.label)}</strong><span>${escapeHtml39(check.detail ?? check.status)}</span>${check.remediation ? `<em>${check.remediation.href ? `<a href="${escapeHtml39(check.remediation.href)}">${escapeHtml39(check.remediation.label)}</a>` : escapeHtml39(check.remediation.label)}: ${escapeHtml39(check.remediation.detail)}</em>` : ""}</li>`).join("");
|
|
24133
|
+
return `<article class="row ${escapeHtml39(row.status)}">
|
|
23790
24134
|
<div>
|
|
23791
|
-
<p class="eyebrow">${
|
|
23792
|
-
<h2>${
|
|
23793
|
-
<p class="status ${
|
|
24135
|
+
<p class="eyebrow">${escapeHtml39(row.kind)}${row.selected ? " \xB7 selected" : ""}</p>
|
|
24136
|
+
<h2>${escapeHtml39(row.provider)}</h2>
|
|
24137
|
+
<p class="status ${escapeHtml39(row.status)}">${escapeHtml39(row.status.toUpperCase())}</p>
|
|
23794
24138
|
</div>
|
|
23795
24139
|
<ul>${checks}</ul>
|
|
23796
24140
|
</article>`;
|
|
23797
24141
|
}).join("");
|
|
23798
|
-
const snippet =
|
|
24142
|
+
const snippet = escapeHtml39(`const providerContracts = () =>
|
|
23799
24143
|
createVoiceProviderContractMatrixPreset('phone-agent', {
|
|
23800
24144
|
env: process.env,
|
|
23801
24145
|
providers: {
|
|
@@ -23816,7 +24160,7 @@ createVoiceProductionReadinessRoutes({
|
|
|
23816
24160
|
providerContractMatrix: () =>
|
|
23817
24161
|
buildVoiceProviderContractMatrix(providerContracts())
|
|
23818
24162
|
});`);
|
|
23819
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24163
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml39(title)}</title><style>body{background:#0f1412;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.primitive,.row{background:#17201b;border:1px solid #2d3b32;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.16),rgba(125,211,252,.12))}.primitive{background:#111814;border-color:#41604a}.eyebrow{color:#86efac;font-size:.78rem;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill,.status{border:1px solid #3f4f45;border-radius:999px;display:inline-flex;padding:8px 12px}.primitive code{color:#bbf7d0}.primitive p{color:#c8d8ca;line-height:1.55;margin:.45rem 0 0}.primitive pre{background:#08110d;border:1px solid #294132;border-radius:18px;color:#d9f99d;margin:16px 0 0;overflow:auto;padding:16px}.status.pass,.row.pass,.pass{border-color:rgba(34,197,94,.65)}.status.warn,.row.warn,.warn{border-color:rgba(245,158,11,.7)}.status.fail,.row.fail,.fail{border-color:rgba(239,68,68,.75)}.row{display:grid;gap:20px;grid-template-columns:minmax(180px,.45fr) 1fr}.row ul{display:grid;gap:10px;list-style:none;margin:0;padding:0}.row li{background:#111814;border:1px solid #2d3b32;border-radius:16px;display:grid;gap:4px;padding:12px}.row li span{color:#b8c2ba}.row li em{color:#f9d77e;font-style:normal}.row li a{color:#86efac}@media(max-width:760px){main{padding:18px}.row{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Provider contracts</p><h1>${escapeHtml39(title)}</h1><p>Self-hosted provider proof for configured state, required env, latency budgets, fallback, streaming, and declared capabilities.</p><div class="summary"><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.warned)} warning</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} total</span></div></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceProviderContractMatrixPreset(...)</code> builds this matrix</h2><p>Give AbsoluteJS your configured LLM, STT, and TTS providers once. It turns them into deploy-checkable proof for env, fallback, streaming, latency budgets, selected providers, and profile-required capabilities without a hosted dashboard.</p><pre><code>${snippet}</code></pre></section>${rows || '<article class="row"><p>No provider contracts configured.</p></article>'}</main></body></html>`;
|
|
23820
24164
|
};
|
|
23821
24165
|
var createVoiceProviderContractMatrixJSONHandler = (matrix) => async () => buildVoiceProviderContractMatrix(await resolveProviderContractMatrixInput(matrix));
|
|
23822
24166
|
var createVoiceProviderContractMatrixHTMLHandler = (options) => async () => {
|
|
@@ -23831,7 +24175,7 @@ var createVoiceProviderContractMatrixHTMLHandler = (options) => async () => {
|
|
|
23831
24175
|
var createVoiceProviderContractMatrixRoutes = (options) => {
|
|
23832
24176
|
const path = options.path ?? "/api/provider-contracts";
|
|
23833
24177
|
const htmlPath = options.htmlPath ?? "/provider-contracts";
|
|
23834
|
-
const routes = new
|
|
24178
|
+
const routes = new Elysia38({
|
|
23835
24179
|
name: options.name ?? "absolutejs-voice-provider-contract-matrix"
|
|
23836
24180
|
});
|
|
23837
24181
|
const jsonHandler = createVoiceProviderContractMatrixJSONHandler(options.matrix);
|
|
@@ -23890,7 +24234,7 @@ var evaluateVoiceProviderStackGaps = (input) => {
|
|
|
23890
24234
|
};
|
|
23891
24235
|
};
|
|
23892
24236
|
// src/opsConsoleRoutes.ts
|
|
23893
|
-
import { Elysia as
|
|
24237
|
+
import { Elysia as Elysia39 } from "elysia";
|
|
23894
24238
|
var DEFAULT_LINKS = [
|
|
23895
24239
|
{
|
|
23896
24240
|
description: "Quality gates for CI, deploy checks, and production readiness.",
|
|
@@ -23925,7 +24269,7 @@ var DEFAULT_LINKS = [
|
|
|
23925
24269
|
label: "Handoffs"
|
|
23926
24270
|
}
|
|
23927
24271
|
];
|
|
23928
|
-
var
|
|
24272
|
+
var escapeHtml40 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23929
24273
|
var countProviderStatuses = (providers) => {
|
|
23930
24274
|
const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
|
|
23931
24275
|
const healthy = providers.filter((provider) => provider.status === "healthy").length;
|
|
@@ -23994,20 +24338,20 @@ var buildVoiceOpsConsoleReport = async (options) => {
|
|
|
23994
24338
|
trace
|
|
23995
24339
|
};
|
|
23996
24340
|
};
|
|
23997
|
-
var renderMetricCard = (input) => `<article class="metric"><span>${
|
|
24341
|
+
var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml40(input.label)}</span><strong>${escapeHtml40(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml40(input.status)}">${escapeHtml40(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml40(input.href)}">Open</a>` : ""}</article>`;
|
|
23998
24342
|
var renderVoiceOpsConsoleHTML = (report, options = {}) => {
|
|
23999
24343
|
const links = report.links.map((link) => `<article class="surface">
|
|
24000
|
-
<div><h2>${
|
|
24001
|
-
<p><a href="${
|
|
24344
|
+
<div><h2>${escapeHtml40(link.label)}</h2>${link.description ? `<p>${escapeHtml40(link.description)}</p>` : ""}</div>
|
|
24345
|
+
<p><a href="${escapeHtml40(link.href)}">Open ${escapeHtml40(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml40(link.statusHref)}">Status</a>` : ""}</p>
|
|
24002
24346
|
</article>`).join("");
|
|
24003
|
-
const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${
|
|
24004
|
-
const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${
|
|
24347
|
+
const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml40(session.sessionId)}</td><td>${escapeHtml40(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml40(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
|
|
24348
|
+
const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml40(event.kind)}</td><td>${escapeHtml40(event.provider ?? "unknown")}</td><td>${escapeHtml40(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml40(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
|
|
24005
24349
|
const title = options.title ?? "AbsoluteJS Voice Ops Console";
|
|
24006
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24350
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml40(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml40(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml40(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
|
|
24007
24351
|
};
|
|
24008
24352
|
var createVoiceOpsConsoleRoutes = (options) => {
|
|
24009
24353
|
const path = options.path ?? "/ops-console";
|
|
24010
|
-
const routes = new
|
|
24354
|
+
const routes = new Elysia39({
|
|
24011
24355
|
name: options.name ?? "absolutejs-voice-ops-console"
|
|
24012
24356
|
});
|
|
24013
24357
|
const getReport = () => buildVoiceOpsConsoleReport(options);
|
|
@@ -24024,11 +24368,11 @@ var createVoiceOpsConsoleRoutes = (options) => {
|
|
|
24024
24368
|
return routes;
|
|
24025
24369
|
};
|
|
24026
24370
|
// src/operationsRecord.ts
|
|
24027
|
-
import { Elysia as
|
|
24371
|
+
import { Elysia as Elysia41 } from "elysia";
|
|
24028
24372
|
|
|
24029
24373
|
// src/traceTimeline.ts
|
|
24030
|
-
import { Elysia as
|
|
24031
|
-
var
|
|
24374
|
+
import { Elysia as Elysia40 } from "elysia";
|
|
24375
|
+
var escapeHtml41 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
24032
24376
|
var getString16 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
24033
24377
|
var getNumber9 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
24034
24378
|
var firstString3 = (payload, keys) => {
|
|
@@ -24211,17 +24555,17 @@ var summarizeVoiceTraceTimeline = (events, options = {}) => {
|
|
|
24211
24555
|
};
|
|
24212
24556
|
};
|
|
24213
24557
|
var formatMs3 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
24214
|
-
var renderProviderCards2 = (session) => session.providers.length === 0 ? '<p class="muted">No provider events recorded for this session.</p>' : `<div class="providers">${session.providers.map((provider) => `<article><strong>${
|
|
24558
|
+
var renderProviderCards2 = (session) => session.providers.length === 0 ? '<p class="muted">No provider events recorded for this session.</p>' : `<div class="providers">${session.providers.map((provider) => `<article><strong>${escapeHtml41(provider.provider)}</strong><dl><div><dt>Events</dt><dd>${String(provider.eventCount)}</dd></div><div><dt>Avg</dt><dd>${formatMs3(provider.averageElapsedMs)}</dd></div><div><dt>Max</dt><dd>${formatMs3(provider.maxElapsedMs)}</dd></div><div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div><div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div><div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div></dl></article>`).join("")}</div>`;
|
|
24215
24559
|
var renderVoiceTraceTimelineSessionHTML = (session, options = {}) => {
|
|
24216
|
-
const events = session.events.map((event) => `<tr class="${
|
|
24217
|
-
const issues = session.evaluation.issues.length ? session.evaluation.issues.map((issue) => `<li class="${
|
|
24218
|
-
const supportLinks = session.operationsRecordHref ? `<p><a href="${
|
|
24219
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24560
|
+
const events = session.events.map((event) => `<tr class="${escapeHtml41(event.status ?? "")}"><td>+${String(event.offsetMs)}ms</td><td>${escapeHtml41(event.type)}</td><td>${escapeHtml41(event.label)}</td><td>${escapeHtml41(event.provider ?? "")}</td><td>${escapeHtml41(event.status ?? "")}</td><td>${formatMs3(event.elapsedMs)}</td></tr>`).join("");
|
|
24561
|
+
const issues = session.evaluation.issues.length ? session.evaluation.issues.map((issue) => `<li class="${escapeHtml41(issue.severity)}">${escapeHtml41(issue.code)}: ${escapeHtml41(issue.message)}</li>`).join("") : "<li>none</li>";
|
|
24562
|
+
const supportLinks = session.operationsRecordHref ? `<p><a href="${escapeHtml41(session.operationsRecordHref)}">Open operations record</a></p>` : "";
|
|
24563
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml41(options.title ?? "Voice Trace Timeline")}</title><style>${timelineCSS}</style></head><body><main><a href="/traces">Back to traces</a><header><p class="eyebrow">Call timeline</p><h1>${escapeHtml41(session.sessionId)}</h1><p class="status ${escapeHtml41(session.status)}">${escapeHtml41(session.status)}</p>${supportLinks}</header><section class="metrics"><article><span>Events</span><strong>${String(session.summary.eventCount)}</strong></article><article><span>Turns</span><strong>${String(session.summary.turnCount)}</strong></article><article><span>Errors</span><strong>${String(session.summary.errorCount)}</strong></article><article><span>Duration</span><strong>${formatMs3(session.summary.callDurationMs)}</strong></article></section><section><h2>Providers</h2>${renderProviderCards2(session)}</section><section><h2>Issues</h2><ul>${issues}</ul></section><section><h2>Timeline</h2><table><thead><tr><th>Offset</th><th>Type</th><th>Event</th><th>Provider</th><th>Status</th><th>Latency</th></tr></thead><tbody>${events}</tbody></table></section></main></body></html>`;
|
|
24220
24564
|
};
|
|
24221
|
-
var renderSessionRows = (report) => report.sessions.length === 0 ? '<tr><td colspan="7">No trace events recorded yet.</td></tr>' : report.sessions.map((session) => `<tr class="${
|
|
24565
|
+
var renderSessionRows = (report) => report.sessions.length === 0 ? '<tr><td colspan="7">No trace events recorded yet.</td></tr>' : report.sessions.map((session) => `<tr class="${escapeHtml41(session.status)}"><td>${session.operationsRecordHref ? `<a href="${escapeHtml41(session.operationsRecordHref)}">${escapeHtml41(session.sessionId)}</a>` : `<a href="/traces/${encodeURIComponent(session.sessionId)}">${escapeHtml41(session.sessionId)}</a>`}</td><td>${escapeHtml41(session.status)}</td><td>${String(session.summary.eventCount)}</td><td>${String(session.summary.turnCount)}</td><td>${String(session.summary.errorCount)}</td><td>${formatMs3(session.summary.callDurationMs)}</td><td>${session.providers.map((provider) => escapeHtml41(provider.provider)).join(", ")}</td></tr>`).join("");
|
|
24222
24566
|
var timelineCSS = "body{background:#0f1318;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}a{color:#fbbf24}.eyebrow{color:#fbbf24;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.5rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.metrics,.providers{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article,.providers article{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span,dt,.muted{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:12px 0 0}dd{font-weight:800;margin:4px 0 0}table{background:#181f27;border-collapse:collapse;border-radius:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #2b3642;padding:12px;text-align:left}section{margin-top:28px}@media(max-width:760px){main{padding:20px}table{font-size:.9rem}}";
|
|
24223
24567
|
var renderVoiceTraceTimelineHTML = (report, options = {}) => {
|
|
24224
|
-
const snippet =
|
|
24568
|
+
const snippet = escapeHtml41(`const traceStore = createVoiceTraceSinkStore({
|
|
24225
24569
|
store: runtimeStorage.traces,
|
|
24226
24570
|
sinks: [
|
|
24227
24571
|
createVoiceTraceHTTPSink({
|
|
@@ -24247,13 +24591,13 @@ app.use(
|
|
|
24247
24591
|
traceDeliveries: runtimeStorage.traceDeliveries
|
|
24248
24592
|
})
|
|
24249
24593
|
);`);
|
|
24250
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24594
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml41(options.title ?? "Voice Trace Timelines")}</title><style>${timelineCSS}.primitive{background:#181f27;border:1px solid #334155;border-radius:20px;margin:20px 0;padding:18px}.primitive p{line-height:1.55}.primitive pre{background:#0b1118;border:1px solid #2b3642;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.primitive code{color:#bfdbfe}</style></head><body><main><header><p class="eyebrow">Self-hosted voice debugging</p><h1>${escapeHtml41(options.title ?? "Voice Trace Timelines")}</h1><p class="muted">Per-call event timelines with provider latency, fallback, timeout, handoff, and error context.</p></header><section class="metrics"><article><span>Sessions</span><strong>${String(report.total)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article><article><span>Warnings</span><strong>${String(report.warnings)}</strong></article></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceTraceTimelineRoutes(...)</code> makes traces the proof backbone</h2><p class="muted">Mount trace timelines from the same trace store used by readiness, simulations, provider recovery, delivery sinks, and phone-agent smoke proof.</p><pre><code>${snippet}</code></pre></section><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Duration</th><th>Providers</th></tr></thead><tbody>${renderSessionRows(report)}</tbody></table></main></body></html>`;
|
|
24251
24595
|
};
|
|
24252
24596
|
var createVoiceTraceTimelineRoutes = (options) => {
|
|
24253
24597
|
const path = options.path ?? "/api/voice-traces";
|
|
24254
24598
|
const htmlPath = options.htmlPath ?? "/traces";
|
|
24255
24599
|
const title = options.title ?? "AbsoluteJS Voice Trace Timelines";
|
|
24256
|
-
const routes = new
|
|
24600
|
+
const routes = new Elysia40({
|
|
24257
24601
|
name: options.name ?? "absolutejs-voice-trace-timelines"
|
|
24258
24602
|
});
|
|
24259
24603
|
const buildReport = async () => summarizeVoiceTraceTimeline(await options.store.list(), {
|
|
@@ -24440,7 +24784,7 @@ var buildVoiceOperationsRecord = async (options) => {
|
|
|
24440
24784
|
transcript: buildTranscript(replay)
|
|
24441
24785
|
};
|
|
24442
24786
|
};
|
|
24443
|
-
var
|
|
24787
|
+
var escapeHtml42 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
24444
24788
|
var formatMs4 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
24445
24789
|
var outcomeLabels = (outcome) => [
|
|
24446
24790
|
outcome.complete ? "complete" : undefined,
|
|
@@ -24473,15 +24817,15 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
|
|
|
24473
24817
|
`);
|
|
24474
24818
|
};
|
|
24475
24819
|
var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
24476
|
-
const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${
|
|
24477
|
-
const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${
|
|
24478
|
-
const providerDecisions = record.providerDecisions.length ? record.providerDecisions.map((decision) => `<li><strong>${
|
|
24479
|
-
const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${
|
|
24480
|
-
const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${
|
|
24481
|
-
const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${
|
|
24482
|
-
const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${
|
|
24483
|
-
const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${
|
|
24484
|
-
const snippet =
|
|
24820
|
+
const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml42(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs4(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
|
|
24821
|
+
const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${escapeHtml42(turn.id)}</strong>${turn.committedText ? `<p><span class="label">Caller</span>${escapeHtml42(turn.committedText)}</p>` : ""}${turn.assistantReplies.map((reply) => `<p><span class="label">Assistant</span>${escapeHtml42(reply)}</p>`).join("")}${turn.errors.map((error) => `<p class="error"><span class="label">Error</span>${escapeHtml42(error)}</p>`).join("")}</li>`).join("") : "<li>No transcript turns recorded.</li>";
|
|
24822
|
+
const providerDecisions = record.providerDecisions.length ? record.providerDecisions.map((decision) => `<li><strong>${escapeHtml42(decision.provider ?? decision.selectedProvider ?? decision.fallbackProvider ?? "provider")}</strong> <span>${escapeHtml42(decision.status ?? decision.type)}</span> ${formatMs4(decision.elapsedMs)}${decision.fallbackProvider ? `<p>Fallback: ${escapeHtml42(decision.fallbackProvider)}</p>` : ""}${decision.error ? `<p class="error">${escapeHtml42(decision.error)}</p>` : ""}${decision.reason ? `<p>${escapeHtml42(decision.reason)}</p>` : ""}</li>`).join("") : "<li>No provider decisions recorded.</li>";
|
|
24823
|
+
const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${escapeHtml42(handoff.fromAgentId ?? "unknown")}</strong> to <strong>${escapeHtml42(handoff.targetAgentId ?? "unknown")}</strong> <span>${escapeHtml42(handoff.status ?? "")}</span><p>${escapeHtml42(handoff.summary ?? handoff.reason ?? "")}</p></li>`).join("") : "<li>No agent handoffs recorded.</li>";
|
|
24824
|
+
const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${escapeHtml42(tool.toolName ?? "tool")}</strong> <span>${escapeHtml42(tool.status ?? "")}</span> ${formatMs4(tool.elapsedMs)} ${tool.error ? `<p>${escapeHtml42(tool.error)}</p>` : ""}</li>`).join("") : "<li>No tool calls recorded.</li>";
|
|
24825
|
+
const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml42(review.title)}</strong> <span>${escapeHtml42(review.summary.outcome ?? "")}</span><p>${escapeHtml42(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
|
|
24826
|
+
const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${escapeHtml42(task.title)}</strong> <span>${escapeHtml42(task.status)}</span><p>${escapeHtml42(task.recommendedAction)}</p></li>`).join("") : "<li>No ops tasks recorded.</li>";
|
|
24827
|
+
const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${escapeHtml42(event.type)}</strong> <span>${escapeHtml42(event.deliveryStatus ?? "local")}</span><p>${escapeHtml42(event.deliveryError ?? event.deliveredTo ?? "")}</p></li>`).join("") : "<li>No integration events recorded.</li>";
|
|
24828
|
+
const snippet = escapeHtml42(`app.use(
|
|
24485
24829
|
createVoiceOperationsRecordRoutes({
|
|
24486
24830
|
audit: auditStore,
|
|
24487
24831
|
integrationEvents: opsEvents,
|
|
@@ -24495,16 +24839,16 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
|
24495
24839
|
tasks: opsTasks
|
|
24496
24840
|
})
|
|
24497
24841
|
);`);
|
|
24498
|
-
const incidentMarkdown =
|
|
24499
|
-
const incidentLink = options.incidentHref ? `<a href="${
|
|
24500
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24842
|
+
const incidentMarkdown = escapeHtml42(renderVoiceOperationsRecordIncidentMarkdown(record));
|
|
24843
|
+
const incidentLink = options.incidentHref ? `<a href="${escapeHtml42(options.incidentHref)}">Download incident.md</a>` : "";
|
|
24844
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml42(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml42(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml42(record.status)}">${escapeHtml42(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#incident-handoff">Incident handoff</a>${incidentLink}</div><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs4(record.summary.callDurationMs)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div></section><section class="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review. ${incidentLink}</p><pre><code>${incidentMarkdown}</code></pre></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, audit, latency, replay, reviews, tasks, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
|
|
24501
24845
|
};
|
|
24502
24846
|
var createVoiceOperationsRecordRoutes = (options) => {
|
|
24503
24847
|
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
24504
24848
|
const htmlPath = options.htmlPath === undefined ? "/voice-operations/:sessionId" : options.htmlPath;
|
|
24505
24849
|
const incidentPath = options.incidentPath === undefined ? `${path}/incident.md` : options.incidentPath;
|
|
24506
24850
|
const incidentHtmlPath = options.incidentHtmlPath === undefined && htmlPath ? `${htmlPath}/incident.md` : options.incidentHtmlPath;
|
|
24507
|
-
const routes = new
|
|
24851
|
+
const routes = new Elysia41({
|
|
24508
24852
|
name: options.name ?? "absolutejs-voice-operations-record"
|
|
24509
24853
|
});
|
|
24510
24854
|
const buildRecord = (sessionId) => buildVoiceOperationsRecord({
|
|
@@ -24556,7 +24900,7 @@ var createVoiceOperationsRecordRoutes = (options) => {
|
|
|
24556
24900
|
return routes;
|
|
24557
24901
|
};
|
|
24558
24902
|
// src/incidentBundle.ts
|
|
24559
|
-
import { Elysia as
|
|
24903
|
+
import { Elysia as Elysia42 } from "elysia";
|
|
24560
24904
|
var filterIncidentBundleArtifacts = (artifacts, filter = {}) => artifacts.filter((artifact) => {
|
|
24561
24905
|
if (filter.sessionId && artifact.sessionId !== filter.sessionId) {
|
|
24562
24906
|
return false;
|
|
@@ -24755,7 +25099,7 @@ var buildVoiceIncidentBundle = async (options) => {
|
|
|
24755
25099
|
var createVoiceIncidentBundleRoutes = (options) => {
|
|
24756
25100
|
const path = options.path ?? "/api/voice-incidents/:sessionId";
|
|
24757
25101
|
const markdownPath = options.markdownPath === undefined ? "/voice-incidents/:sessionId/markdown" : options.markdownPath;
|
|
24758
|
-
const routes = new
|
|
25102
|
+
const routes = new Elysia42({
|
|
24759
25103
|
name: options.name ?? "absolutejs-voice-incident-bundle"
|
|
24760
25104
|
});
|
|
24761
25105
|
const getSessionId = (params) => params.sessionId ?? "";
|
|
@@ -24956,19 +25300,19 @@ var summarizeVoiceOpsStatus = async (options) => {
|
|
|
24956
25300
|
};
|
|
24957
25301
|
};
|
|
24958
25302
|
// src/opsStatusRoutes.ts
|
|
24959
|
-
import { Elysia as
|
|
24960
|
-
var
|
|
25303
|
+
import { Elysia as Elysia43 } from "elysia";
|
|
25304
|
+
var escapeHtml43 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
24961
25305
|
var renderVoiceOpsStatusHTML = (report, options = {}) => {
|
|
24962
25306
|
const title = options.title ?? "AbsoluteJS Voice Ops Status";
|
|
24963
25307
|
const surfaces = Object.entries(report.surfaces).map(([key, surface]) => {
|
|
24964
25308
|
const value = "recovered" in surface ? surface.total === 0 ? "0 events" : `${surface.recovered}/${surface.total}` : ("auditTotal" in surface) ? `${surface.auditTotal + surface.traceTotal} deliveries` : ("total" in surface) ? `${Math.max(surface.total - ("failed" in surface ? surface.failed : ("degraded" in surface) ? surface.degraded : 0), 0)}/${surface.total}` : surface.status;
|
|
24965
|
-
return `<article class="surface ${
|
|
25309
|
+
return `<article class="surface ${escapeHtml43(surface.status)}"><span>${escapeHtml43(surface.status.toUpperCase())}</span><h2>${escapeHtml43(key)}</h2><strong>${escapeHtml43(value)}</strong></article>`;
|
|
24966
25310
|
}).join("");
|
|
24967
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
25311
|
+
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:#0d141b;color:#f8f3e7;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:980px;padding:32px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.2),rgba(245,158,11,.12));border:1px solid #283544;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.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.surfaces{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.surface{background:#151d26;border:1px solid #283544;border-radius:20px;padding:18px}.surface span{color:#aab5c0;font-size:.78rem;font-weight:900;letter-spacing:.08em}.surface strong{font-size:1.5rem}.pass{border-color:rgba(34,197,94,.55)}.fail{border-color:rgba(239,68,68,.75)}a{color:#5eead4}</style></head><body><main><section class="hero"><p class="eyebrow">Ops status</p><h1>${escapeHtml43(title)}</h1><p>Compact pass/fail status for framework widgets, demos, and small customer-facing health badges.</p><p class="status ${escapeHtml43(report.status)}">Overall: ${escapeHtml43(report.status.toUpperCase())}</p><p>${report.passed}/${report.total} checks passing</p></section><section class="surfaces">${surfaces || '<article class="surface pass"><span>PASS</span><h2>No checks configured</h2><strong>0/0</strong></article>'}</section></main></body></html>`;
|
|
24968
25312
|
};
|
|
24969
25313
|
var createVoiceOpsStatusRoutes = (options) => {
|
|
24970
25314
|
const path = options.path ?? "/api/voice/ops-status";
|
|
24971
|
-
const routes = new
|
|
25315
|
+
const routes = new Elysia43({
|
|
24972
25316
|
name: options.name ?? "absolutejs-voice-ops-status"
|
|
24973
25317
|
});
|
|
24974
25318
|
routes.get(path, async () => summarizeVoiceOpsStatus(options));
|
|
@@ -25401,8 +25745,8 @@ var createVoiceTTSProviderRouter = (options) => {
|
|
|
25401
25745
|
};
|
|
25402
25746
|
};
|
|
25403
25747
|
// src/traceDeliveryRoutes.ts
|
|
25404
|
-
import { Elysia as
|
|
25405
|
-
var
|
|
25748
|
+
import { Elysia as Elysia44 } from "elysia";
|
|
25749
|
+
var escapeHtml44 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
25406
25750
|
var getString18 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
25407
25751
|
var getNumber11 = (value) => {
|
|
25408
25752
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -25483,14 +25827,14 @@ var renderSinkResults2 = (delivery) => {
|
|
|
25483
25827
|
if (entries.length === 0) {
|
|
25484
25828
|
return "<p>No sink delivery attempts recorded yet.</p>";
|
|
25485
25829
|
}
|
|
25486
|
-
return `<ul>${entries.map(([sinkId, result]) => `<li><strong>${
|
|
25830
|
+
return `<ul>${entries.map(([sinkId, result]) => `<li><strong>${escapeHtml44(sinkId)}</strong>: ${escapeHtml44(result.status)}${result.deliveredTo ? ` to ${escapeHtml44(result.deliveredTo)}` : ""}${result.error ? ` (${escapeHtml44(result.error)})` : ""}</li>`).join("")}</ul>`;
|
|
25487
25831
|
};
|
|
25488
|
-
var renderEventList2 = (delivery) => delivery.events.length === 0 ? "<p>No trace events in this delivery.</p>" : `<ul>${delivery.events.map((event) => `<li>${
|
|
25832
|
+
var renderEventList2 = (delivery) => delivery.events.length === 0 ? "<p>No trace events in this delivery.</p>" : `<ul>${delivery.events.map((event) => `<li>${escapeHtml44(event.type)} <small>${escapeHtml44(event.id)}</small>${event.sessionId ? ` session=${escapeHtml44(event.sessionId)}` : ""}</li>`).join("")}</ul>`;
|
|
25489
25833
|
var renderVoiceTraceDeliveryHTML = (report, options = {}) => {
|
|
25490
25834
|
const title = options.title ?? "AbsoluteJS Voice Trace Deliveries";
|
|
25491
|
-
const drainAction = options.workerPath === false ? "" : `<form method="post" action="${
|
|
25492
|
-
const rows = report.deliveries.map((delivery) => `<article class="delivery ${
|
|
25493
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
25835
|
+
const drainAction = options.workerPath === false ? "" : `<form method="post" action="${escapeHtml44(options.workerPath ?? "/api/voice-trace-deliveries/drain")}"><button type="submit">Drain trace deliveries</button></form>`;
|
|
25836
|
+
const rows = report.deliveries.map((delivery) => `<article class="delivery ${escapeHtml44(delivery.deliveryStatus)}"><div class="head"><div><span>${escapeHtml44(delivery.deliveryStatus)}</span><h2>${escapeHtml44(delivery.id)}</h2><p>${escapeHtml44(new Date(delivery.createdAt).toLocaleString())}${delivery.deliveredAt ? ` \xB7 delivered ${escapeHtml44(new Date(delivery.deliveredAt).toLocaleString())}` : ""}</p></div><strong>${String(delivery.deliveryAttempts ?? 0)} attempt(s)</strong></div>${delivery.deliveryError ? `<p class="error">${escapeHtml44(delivery.deliveryError)}</p>` : ""}<h3>Sinks</h3>${renderSinkResults2(delivery)}<h3>Events</h3>${renderEventList2(delivery)}</article>`).join("");
|
|
25837
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml44(title)}</title><style>body{background:#0f1318;color:#f4efe1;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(34,197,94,.16),rgba(14,165,233,.14));border:1px solid #26313d;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}.grid{display:grid;gap:12px;grid-template-columns:repeat(4,1fr);margin-bottom:16px}.grid article,.delivery{background:#151b22;border:1px solid #26313d;border-radius:22px;padding:18px}.grid span,.delivery span{color:#86efac;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.grid strong{display:block;font-size:2rem}.deliveries{display:grid;gap:14px}.delivery.failed{border-color:rgba(239,68,68,.75)}.delivery.pending{border-color:rgba(245,158,11,.7)}.delivery.delivered{border-color:rgba(34,197,94,.55)}.delivery.skipped{border-color:rgba(148,163,184,.6)}.head{align-items:start;display:flex;gap:14px;justify-content:space-between}.delivery h2{font-size:1.05rem;margin:.3rem 0;overflow-wrap:anywhere}.delivery h3{margin:1rem 0 .3rem}.delivery p,.delivery li{color:#c8d0d8}.error{color:#fecaca!important}button{background:#86efac;border:0;border-radius:999px;color:#07111f;cursor:pointer;font-weight:900;margin-top:12px;padding:10px 14px}@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:1fr 1fr}.head{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Trace export health</p><h1>${escapeHtml44(title)}</h1><p>Checked ${escapeHtml44(new Date(report.checkedAt).toLocaleString())}. Showing ${String(report.deliveries.length)} delivery item(s).</p>${drainAction}</section>${renderMetricGrid3(report)}<section class="deliveries">${rows || "<p>No trace deliveries match this filter.</p>"}</section></main></body></html>`;
|
|
25494
25838
|
};
|
|
25495
25839
|
var createVoiceTraceDeliveryJSONHandler = (options) => async ({ query }) => buildVoiceTraceDeliveryReport(options, resolveVoiceTraceDeliveryFilter(query, options.filter));
|
|
25496
25840
|
var createVoiceTraceDeliveryHTMLHandler = (options) => async ({ query }) => {
|
|
@@ -25510,7 +25854,7 @@ var createVoiceTraceDeliveryRoutes = (options) => {
|
|
|
25510
25854
|
const path = options.path ?? "/api/voice-trace-deliveries";
|
|
25511
25855
|
const htmlPath = options.htmlPath === undefined ? "/traces/deliveries" : options.htmlPath;
|
|
25512
25856
|
const workerPath = options.workerPath === undefined ? `${path}/drain` : options.workerPath;
|
|
25513
|
-
const routes = new
|
|
25857
|
+
const routes = new Elysia44({
|
|
25514
25858
|
name: options.name ?? "absolutejs-voice-trace-deliveries"
|
|
25515
25859
|
}).get(path, createVoiceTraceDeliveryJSONHandler(options));
|
|
25516
25860
|
if (htmlPath !== false) {
|
|
@@ -26134,7 +26478,7 @@ var createVoiceMemoryStore = () => {
|
|
|
26134
26478
|
return { get, getOrCreate, list, remove, set };
|
|
26135
26479
|
};
|
|
26136
26480
|
// src/opsWebhook.ts
|
|
26137
|
-
import { Elysia as
|
|
26481
|
+
import { Elysia as Elysia45 } from "elysia";
|
|
26138
26482
|
var toHex6 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
26139
26483
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
26140
26484
|
const encoder = new TextEncoder;
|
|
@@ -26264,7 +26608,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
26264
26608
|
};
|
|
26265
26609
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
26266
26610
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
26267
|
-
return new
|
|
26611
|
+
return new Elysia45().post(path, async ({ body, request, set }) => {
|
|
26268
26612
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
26269
26613
|
if (options.signingSecret) {
|
|
26270
26614
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -27200,6 +27544,8 @@ export {
|
|
|
27200
27544
|
renderVoiceResilienceHTML,
|
|
27201
27545
|
renderVoiceReconnectContractHTML,
|
|
27202
27546
|
renderVoiceQualityHTML,
|
|
27547
|
+
renderVoiceProviderSloMarkdown,
|
|
27548
|
+
renderVoiceProviderSloHTML,
|
|
27203
27549
|
renderVoiceProviderHealthHTML,
|
|
27204
27550
|
renderVoiceProviderContractMatrixHTML,
|
|
27205
27551
|
renderVoiceProviderCapabilityHTML,
|
|
@@ -27365,6 +27711,7 @@ export {
|
|
|
27365
27711
|
createVoiceReconnectContractRoutes,
|
|
27366
27712
|
createVoiceReadinessProfile,
|
|
27367
27713
|
createVoiceQualityRoutes,
|
|
27714
|
+
createVoiceProviderSloRoutes,
|
|
27368
27715
|
createVoiceProviderRouter,
|
|
27369
27716
|
createVoiceProviderHealthRoutes,
|
|
27370
27717
|
createVoiceProviderHealthJSONHandler,
|
|
@@ -27535,6 +27882,7 @@ export {
|
|
|
27535
27882
|
claimVoiceOpsTask,
|
|
27536
27883
|
buildVoiceTraceReplay,
|
|
27537
27884
|
buildVoiceTraceDeliveryReport,
|
|
27885
|
+
buildVoiceProviderSloReport,
|
|
27538
27886
|
buildVoiceProviderContractMatrix,
|
|
27539
27887
|
buildVoiceProductionReadinessReport,
|
|
27540
27888
|
buildVoiceProductionReadinessGate,
|
|
@@ -12,6 +12,7 @@ import type { VoiceReconnectContractReport } from './reconnectContract';
|
|
|
12
12
|
import type { VoiceAuditEventStore, VoiceAuditEventType, VoiceAuditOutcome } from './audit';
|
|
13
13
|
import { type VoiceAuditSinkDeliveryStore } from './auditSinks';
|
|
14
14
|
import type { VoiceProviderContractMatrixReport, VoiceProviderStackCapabilityGapReport } from './providerStackRecommendations';
|
|
15
|
+
import { type VoiceProviderSloReport, type VoiceProviderSloReportOptions } from './providerSlo';
|
|
15
16
|
import type { VoiceCampaignReadinessProofReport } from './campaign';
|
|
16
17
|
import { type VoiceOpsRecoveryReport } from './opsRecovery';
|
|
17
18
|
export type VoiceProductionReadinessStatus = 'fail' | 'pass' | 'warn';
|
|
@@ -95,6 +96,7 @@ export type VoiceProductionReadinessReport = {
|
|
|
95
96
|
phoneAgentSmoke?: string;
|
|
96
97
|
providerContracts?: string;
|
|
97
98
|
providerRoutingContracts?: string;
|
|
99
|
+
providerSlo?: string;
|
|
98
100
|
quality?: string;
|
|
99
101
|
reconnectContracts?: string;
|
|
100
102
|
resilience?: string;
|
|
@@ -172,6 +174,12 @@ export type VoiceProductionReadinessReport = {
|
|
|
172
174
|
status: VoiceProductionReadinessStatus;
|
|
173
175
|
total: number;
|
|
174
176
|
};
|
|
177
|
+
providerSlo?: {
|
|
178
|
+
events: number;
|
|
179
|
+
eventsWithLatency: number;
|
|
180
|
+
issues: number;
|
|
181
|
+
status: VoiceProductionReadinessStatus;
|
|
182
|
+
};
|
|
175
183
|
reconnectContracts?: {
|
|
176
184
|
failed: number;
|
|
177
185
|
passed: number;
|
|
@@ -339,6 +347,10 @@ export type VoiceProductionReadinessRoutesOptions = {
|
|
|
339
347
|
query: Record<string, unknown>;
|
|
340
348
|
request: Request;
|
|
341
349
|
}) => Promise<readonly VoiceProviderRoutingContractReport[]> | readonly VoiceProviderRoutingContractReport[]);
|
|
350
|
+
providerSlo?: false | VoiceProviderSloReport | VoiceProviderSloReportOptions | ((input: {
|
|
351
|
+
query: Record<string, unknown>;
|
|
352
|
+
request: Request;
|
|
353
|
+
}) => Promise<VoiceProviderSloReport | VoiceProviderSloReportOptions> | VoiceProviderSloReport | VoiceProviderSloReportOptions);
|
|
342
354
|
providerStack?: false | VoiceProviderStackCapabilityGapReport | ((input: {
|
|
343
355
|
query: Record<string, unknown>;
|
|
344
356
|
request: Request;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import { type VoiceRoutingEvent, type VoiceRoutingEventKind } from './resilienceRoutes';
|
|
3
|
+
import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
|
|
4
|
+
export type VoiceProviderSloStatus = 'fail' | 'pass' | 'warn';
|
|
5
|
+
export type VoiceProviderSloThresholds = {
|
|
6
|
+
maxAverageElapsedMs?: number;
|
|
7
|
+
maxErrorRate?: number;
|
|
8
|
+
maxFallbackRate?: number;
|
|
9
|
+
maxP95ElapsedMs?: number;
|
|
10
|
+
maxTimeoutRate?: number;
|
|
11
|
+
minSamples?: number;
|
|
12
|
+
};
|
|
13
|
+
export type VoiceProviderSloThresholdConfig = Partial<Record<VoiceRoutingEventKind, VoiceProviderSloThresholds>>;
|
|
14
|
+
export type VoiceProviderSloMetric = {
|
|
15
|
+
actual: number;
|
|
16
|
+
label: string;
|
|
17
|
+
pass: boolean;
|
|
18
|
+
threshold: number;
|
|
19
|
+
unit: 'count' | 'ms' | 'rate';
|
|
20
|
+
};
|
|
21
|
+
export type VoiceProviderSloIssue = {
|
|
22
|
+
code: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
kind?: VoiceRoutingEventKind;
|
|
25
|
+
label: string;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
status: Exclude<VoiceProviderSloStatus, 'pass'>;
|
|
28
|
+
value?: number | string;
|
|
29
|
+
};
|
|
30
|
+
export type VoiceProviderSloKindReport = {
|
|
31
|
+
events: number;
|
|
32
|
+
eventsWithLatency: number;
|
|
33
|
+
fallbacks: number;
|
|
34
|
+
issues: VoiceProviderSloIssue[];
|
|
35
|
+
kind: VoiceRoutingEventKind;
|
|
36
|
+
metrics: Record<string, VoiceProviderSloMetric>;
|
|
37
|
+
providers: string[];
|
|
38
|
+
status: VoiceProviderSloStatus;
|
|
39
|
+
thresholds: Required<VoiceProviderSloThresholds>;
|
|
40
|
+
timeouts: number;
|
|
41
|
+
unresolvedErrors: number;
|
|
42
|
+
};
|
|
43
|
+
export type VoiceProviderSloSessionReport = {
|
|
44
|
+
errorCount: number;
|
|
45
|
+
fallbackCount: number;
|
|
46
|
+
kinds: VoiceRoutingEventKind[];
|
|
47
|
+
lastEventAt: number;
|
|
48
|
+
maxElapsedMs?: number;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
status: VoiceProviderSloStatus;
|
|
51
|
+
timeoutCount: number;
|
|
52
|
+
};
|
|
53
|
+
export type VoiceProviderSloReport = {
|
|
54
|
+
checkedAt: number;
|
|
55
|
+
events: number;
|
|
56
|
+
eventsWithLatency: number;
|
|
57
|
+
issues: VoiceProviderSloIssue[];
|
|
58
|
+
kinds: Record<VoiceRoutingEventKind, VoiceProviderSloKindReport>;
|
|
59
|
+
sessions: VoiceProviderSloSessionReport[];
|
|
60
|
+
status: VoiceProviderSloStatus;
|
|
61
|
+
thresholds: Record<VoiceRoutingEventKind, Required<VoiceProviderSloThresholds>>;
|
|
62
|
+
};
|
|
63
|
+
export type VoiceProviderSloReportOptions = {
|
|
64
|
+
events?: StoredVoiceTraceEvent[] | VoiceRoutingEvent[];
|
|
65
|
+
requiredKinds?: readonly VoiceRoutingEventKind[];
|
|
66
|
+
store?: VoiceTraceEventStore;
|
|
67
|
+
thresholds?: VoiceProviderSloThresholdConfig;
|
|
68
|
+
};
|
|
69
|
+
export type VoiceProviderSloRoutesOptions = VoiceProviderSloReportOptions & {
|
|
70
|
+
headers?: HeadersInit;
|
|
71
|
+
htmlPath?: false | string;
|
|
72
|
+
markdownPath?: false | string;
|
|
73
|
+
name?: string;
|
|
74
|
+
path?: string;
|
|
75
|
+
render?: (report: VoiceProviderSloReport) => string | Promise<string>;
|
|
76
|
+
title?: string;
|
|
77
|
+
};
|
|
78
|
+
export declare const buildVoiceProviderSloReport: (options?: VoiceProviderSloReportOptions) => Promise<VoiceProviderSloReport>;
|
|
79
|
+
export declare const renderVoiceProviderSloMarkdown: (report: VoiceProviderSloReport) => string;
|
|
80
|
+
export declare const renderVoiceProviderSloHTML: (report: VoiceProviderSloReport, options?: {
|
|
81
|
+
title?: string;
|
|
82
|
+
}) => string;
|
|
83
|
+
export declare const createVoiceProviderSloRoutes: (options: VoiceProviderSloRoutesOptions) => Elysia<"", {
|
|
84
|
+
decorator: {};
|
|
85
|
+
store: {};
|
|
86
|
+
derive: {};
|
|
87
|
+
resolve: {};
|
|
88
|
+
}, {
|
|
89
|
+
typebox: {};
|
|
90
|
+
error: {};
|
|
91
|
+
}, {
|
|
92
|
+
schema: {};
|
|
93
|
+
standaloneSchema: {};
|
|
94
|
+
macro: {};
|
|
95
|
+
macroFn: {};
|
|
96
|
+
parser: {};
|
|
97
|
+
response: {};
|
|
98
|
+
}, {}, {
|
|
99
|
+
derive: {};
|
|
100
|
+
resolve: {};
|
|
101
|
+
schema: {};
|
|
102
|
+
standaloneSchema: {};
|
|
103
|
+
response: {};
|
|
104
|
+
}, {
|
|
105
|
+
derive: {};
|
|
106
|
+
resolve: {};
|
|
107
|
+
schema: {};
|
|
108
|
+
standaloneSchema: {};
|
|
109
|
+
response: {};
|
|
110
|
+
}>;
|