@absolutejs/voice 0.0.22-beta.214 → 0.0.22-beta.216
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 +433 -84
- package/dist/productionReadiness.d.ts +12 -0
- package/dist/providerSlo.d.ts +112 -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,309 @@ 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 now = options.now ?? Date.now();
|
|
22070
|
+
const events = normalizeEvents2(rawEvents).filter((event) => typeof options.maxAgeMs !== "number" || now - event.at <= options.maxAgeMs);
|
|
22071
|
+
const thresholds = mergeThresholds(options.thresholds);
|
|
22072
|
+
const observedKinds = new Set(events.map((event) => event.kind));
|
|
22073
|
+
const requiredKinds = new Set(options.requiredKinds ?? [...observedKinds]);
|
|
22074
|
+
const kindReports = Object.fromEntries(providerKinds.map((kind) => [
|
|
22075
|
+
kind,
|
|
22076
|
+
summarizeKind(kind, events, thresholds[kind], requiredKinds.has(kind))
|
|
22077
|
+
]));
|
|
22078
|
+
const issues = Object.values(kindReports).flatMap((report) => report.issues);
|
|
22079
|
+
const eventsWithLatency = events.filter((event) => typeof event.elapsedMs === "number").length;
|
|
22080
|
+
if (events.length === 0) {
|
|
22081
|
+
issues.push({
|
|
22082
|
+
code: "provider_slo.no_routing_events",
|
|
22083
|
+
detail: "No provider routing events are recorded yet. Run a live turn, smoke, or provider simulation before certifying latency.",
|
|
22084
|
+
label: "Provider routing evidence",
|
|
22085
|
+
status: "warn",
|
|
22086
|
+
value: 0
|
|
22087
|
+
});
|
|
22088
|
+
}
|
|
22089
|
+
return {
|
|
22090
|
+
checkedAt: Date.now(),
|
|
22091
|
+
events: events.length,
|
|
22092
|
+
eventsWithLatency,
|
|
22093
|
+
issues,
|
|
22094
|
+
kinds: kindReports,
|
|
22095
|
+
sessions: summarizeSessions(events),
|
|
22096
|
+
status: issues.some((issue) => issue.status === "fail") ? "fail" : issues.some((issue) => issue.status === "warn") ? "warn" : "pass",
|
|
22097
|
+
thresholds
|
|
22098
|
+
};
|
|
22099
|
+
};
|
|
22100
|
+
var formatMetricValue2 = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
|
|
22101
|
+
var formatMetricThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
|
|
22102
|
+
var getMetric = (report, key) => report.metrics[key];
|
|
22103
|
+
var renderVoiceProviderSloMarkdown = (report) => {
|
|
22104
|
+
const rows = providerKinds.map((kind) => {
|
|
22105
|
+
const kindReport = report.kinds[kind];
|
|
22106
|
+
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"))} |`;
|
|
22107
|
+
}).join(`
|
|
22108
|
+
`);
|
|
22109
|
+
const issues = report.issues.map((issue) => `- ${issue.status}: ${issue.kind ? `${issue.kind.toUpperCase()} ` : ""}${issue.label}${issue.detail ? ` - ${issue.detail}` : ""}`).join(`
|
|
22110
|
+
`) || "No provider SLO issues.";
|
|
22111
|
+
return `# Voice Provider SLO Report
|
|
22112
|
+
|
|
22113
|
+
Generated: ${new Date(report.checkedAt).toISOString()}
|
|
22114
|
+
|
|
22115
|
+
Overall: **${report.status}**
|
|
22116
|
+
|
|
22117
|
+
Events: ${report.events}
|
|
22118
|
+
|
|
22119
|
+
Events with latency: ${report.eventsWithLatency}
|
|
22120
|
+
|
|
22121
|
+
| Kind | Status | Events | Latency Samples | Avg | P95 | Error Rate | Timeout Rate | Fallback Rate |
|
|
22122
|
+
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
22123
|
+
${rows}
|
|
22124
|
+
|
|
22125
|
+
## Issues
|
|
22126
|
+
|
|
22127
|
+
${issues}
|
|
22128
|
+
`;
|
|
22129
|
+
};
|
|
22130
|
+
var renderVoiceProviderSloHTML = (report, options = {}) => {
|
|
22131
|
+
const title = options.title ?? "AbsoluteJS Voice Provider SLOs";
|
|
22132
|
+
const kindCards = providerKinds.map((kind) => {
|
|
22133
|
+
const kindReport = report.kinds[kind];
|
|
22134
|
+
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("");
|
|
22135
|
+
const providers = kindReport.providers.length ? kindReport.providers.join(", ") : "none recorded";
|
|
22136
|
+
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>`;
|
|
22137
|
+
}).join("");
|
|
22138
|
+
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>";
|
|
22139
|
+
const snippet = `createVoiceProviderSloRoutes({
|
|
22140
|
+
store: runtimeStorage.traces,
|
|
22141
|
+
requiredKinds: ['llm', 'stt', 'tts'],
|
|
22142
|
+
thresholds: {
|
|
22143
|
+
llm: { maxAverageElapsedMs: 2500, maxP95ElapsedMs: 4500 },
|
|
22144
|
+
stt: { maxAverageElapsedMs: 800, maxP95ElapsedMs: 1500 },
|
|
22145
|
+
tts: { maxAverageElapsedMs: 1200, maxP95ElapsedMs: 2200 }
|
|
22146
|
+
}
|
|
22147
|
+
})`;
|
|
22148
|
+
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>`;
|
|
22149
|
+
};
|
|
22150
|
+
var createVoiceProviderSloRoutes = (options) => {
|
|
22151
|
+
const path = options.path ?? "/api/voice/provider-slos";
|
|
22152
|
+
const htmlPath = options.htmlPath ?? "/voice/provider-slos";
|
|
22153
|
+
const markdownPath = options.markdownPath ?? "/voice/provider-slos.md";
|
|
22154
|
+
const headers = {
|
|
22155
|
+
"cache-control": "no-store",
|
|
22156
|
+
...options.headers ?? {}
|
|
22157
|
+
};
|
|
22158
|
+
const buildReport = () => buildVoiceProviderSloReport(options);
|
|
22159
|
+
const app = new Elysia35({ name: options.name ?? "absolute-voice-provider-slos" });
|
|
22160
|
+
app.get(path, async () => Response.json(await buildReport(), { headers }));
|
|
22161
|
+
if (markdownPath !== false) {
|
|
22162
|
+
app.get(markdownPath, async () => {
|
|
22163
|
+
const report = await buildReport();
|
|
22164
|
+
return new Response(renderVoiceProviderSloMarkdown(report), {
|
|
22165
|
+
headers: {
|
|
22166
|
+
...headers,
|
|
22167
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
22168
|
+
}
|
|
22169
|
+
});
|
|
22170
|
+
});
|
|
22171
|
+
}
|
|
22172
|
+
if (htmlPath !== false) {
|
|
22173
|
+
app.get(htmlPath, async () => {
|
|
22174
|
+
const report = await buildReport();
|
|
22175
|
+
const html = options.render ? await options.render(report) : renderVoiceProviderSloHTML(report, {
|
|
22176
|
+
title: options.title
|
|
22177
|
+
});
|
|
22178
|
+
return new Response(html, {
|
|
22179
|
+
headers: {
|
|
22180
|
+
...headers,
|
|
22181
|
+
"content-type": "text/html; charset=utf-8"
|
|
22182
|
+
}
|
|
22183
|
+
});
|
|
22184
|
+
});
|
|
22185
|
+
}
|
|
22186
|
+
return app;
|
|
22187
|
+
};
|
|
21891
22188
|
// src/productionReadiness.ts
|
|
21892
|
-
import { Elysia as
|
|
22189
|
+
import { Elysia as Elysia37 } from "elysia";
|
|
21893
22190
|
|
|
21894
22191
|
// src/opsRecovery.ts
|
|
21895
|
-
import { Elysia as
|
|
21896
|
-
var
|
|
22192
|
+
import { Elysia as Elysia36 } from "elysia";
|
|
22193
|
+
var escapeHtml37 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
21897
22194
|
var getString14 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
21898
22195
|
var hrefForSession = (value, sessionId) => {
|
|
21899
22196
|
if (typeof value === "function") {
|
|
@@ -22107,19 +22404,19 @@ ${failedSessions || "None."}
|
|
|
22107
22404
|
${report.latency ? renderVoiceLatencySLOMarkdown(report.latency, { title: "Latency SLO" }) : "Latency SLO disabled."}
|
|
22108
22405
|
`;
|
|
22109
22406
|
};
|
|
22110
|
-
var renderDeliverySummary = (label, summary) => summary ? `<article><span>${
|
|
22407
|
+
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
22408
|
var renderVoiceOpsRecoveryHTML = (report, options = {}) => {
|
|
22112
22409
|
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>${
|
|
22410
|
+
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("");
|
|
22411
|
+
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("");
|
|
22412
|
+
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("");
|
|
22413
|
+
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
22414
|
};
|
|
22118
22415
|
var createVoiceOpsRecoveryRoutes = (options = {}) => {
|
|
22119
22416
|
const path = options.path ?? "/api/voice/ops-recovery";
|
|
22120
22417
|
const htmlPath = options.htmlPath === undefined ? "/ops-recovery" : options.htmlPath;
|
|
22121
22418
|
const markdownPath = options.markdownPath === undefined ? `${path}.md` : options.markdownPath;
|
|
22122
|
-
const routes = new
|
|
22419
|
+
const routes = new Elysia36({
|
|
22123
22420
|
name: options.name ?? "absolutejs-voice-ops-recovery"
|
|
22124
22421
|
}).get(path, async () => buildVoiceOpsRecoveryReport(options));
|
|
22125
22422
|
if (htmlPath) {
|
|
@@ -22149,7 +22446,7 @@ var createVoiceOpsRecoveryRoutes = (options = {}) => {
|
|
|
22149
22446
|
};
|
|
22150
22447
|
|
|
22151
22448
|
// src/productionReadiness.ts
|
|
22152
|
-
var
|
|
22449
|
+
var escapeHtml38 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
22153
22450
|
var rollupStatus3 = (checks) => checks.some((check) => check.status === "fail") ? "fail" : checks.some((check) => check.status === "warn") ? "warn" : "pass";
|
|
22154
22451
|
var readinessGateCodes = {
|
|
22155
22452
|
"Agent squad contracts": "voice.readiness.agent_squad_contracts",
|
|
@@ -22169,6 +22466,7 @@ var readinessGateCodes = {
|
|
|
22169
22466
|
"Provider fallback recovery": "voice.readiness.provider_fallback_recovery",
|
|
22170
22467
|
"Provider health": "voice.readiness.provider_health",
|
|
22171
22468
|
"Provider routing contracts": "voice.readiness.provider_routing_contracts",
|
|
22469
|
+
"Provider SLO gates": "voice.readiness.provider_slo_gates",
|
|
22172
22470
|
"Provider stack capabilities": "voice.readiness.provider_stack_capabilities",
|
|
22173
22471
|
"Quality gates": "voice.readiness.quality_gates",
|
|
22174
22472
|
"Reconnect recovery contracts": "voice.readiness.reconnect_contracts",
|
|
@@ -22241,6 +22539,18 @@ var resolveProviderRoutingContracts = async (options, input) => {
|
|
|
22241
22539
|
}
|
|
22242
22540
|
return typeof options.providerRoutingContracts === "function" ? await options.providerRoutingContracts(input) : options.providerRoutingContracts;
|
|
22243
22541
|
};
|
|
22542
|
+
var isVoiceProviderSloReport = (value) => typeof value.status === "string" && typeof value.checkedAt === "number" && typeof value.events === "number";
|
|
22543
|
+
var resolveProviderSlo = async (options, input) => {
|
|
22544
|
+
if (options.providerSlo === false || options.providerSlo === undefined) {
|
|
22545
|
+
return;
|
|
22546
|
+
}
|
|
22547
|
+
const value = typeof options.providerSlo === "function" ? await options.providerSlo(input) : options.providerSlo;
|
|
22548
|
+
return isVoiceProviderSloReport(value) ? value : buildVoiceProviderSloReport({
|
|
22549
|
+
...value,
|
|
22550
|
+
events: value.events,
|
|
22551
|
+
store: value.store ?? options.store
|
|
22552
|
+
});
|
|
22553
|
+
};
|
|
22244
22554
|
var resolveProviderStack = async (options, input) => {
|
|
22245
22555
|
if (options.providerStack === false || options.providerStack === undefined) {
|
|
22246
22556
|
return;
|
|
@@ -22567,6 +22877,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22567
22877
|
carriers,
|
|
22568
22878
|
agentSquadContracts,
|
|
22569
22879
|
providerRoutingContracts,
|
|
22880
|
+
providerSlo,
|
|
22570
22881
|
providerStack,
|
|
22571
22882
|
providerContractMatrix,
|
|
22572
22883
|
phoneAgentSmokes,
|
|
@@ -22601,6 +22912,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22601
22912
|
resolveCarriers(options, { query, request }),
|
|
22602
22913
|
resolveAgentSquadContracts(options, { query, request }),
|
|
22603
22914
|
resolveProviderRoutingContracts(options, { query, request }),
|
|
22915
|
+
resolveProviderSlo(options, { query, request }),
|
|
22604
22916
|
resolveProviderStack(options, { query, request }),
|
|
22605
22917
|
resolveProviderContractMatrix(options, { query, request }),
|
|
22606
22918
|
resolvePhoneAgentSmokes(options, { query, request }),
|
|
@@ -22796,6 +23108,12 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22796
23108
|
status: providerRoutingContracts.some((report) => !report.pass) ? "fail" : providerRoutingContracts.length === 0 ? "warn" : "pass",
|
|
22797
23109
|
total: providerRoutingContracts.length
|
|
22798
23110
|
} : undefined;
|
|
23111
|
+
const providerSloSummary = providerSlo ? {
|
|
23112
|
+
events: providerSlo.events,
|
|
23113
|
+
eventsWithLatency: providerSlo.eventsWithLatency,
|
|
23114
|
+
issues: providerSlo.issues.length,
|
|
23115
|
+
status: providerSlo.status
|
|
23116
|
+
} : undefined;
|
|
22799
23117
|
const phoneAgentSmokeSummary = phoneAgentSmokes ? {
|
|
22800
23118
|
failed: phoneAgentSmokes.filter((report) => !report.pass).length,
|
|
22801
23119
|
passed: phoneAgentSmokes.filter((report) => report.pass).length,
|
|
@@ -22854,6 +23172,31 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
22854
23172
|
]
|
|
22855
23173
|
});
|
|
22856
23174
|
}
|
|
23175
|
+
if (providerSloSummary && providerSlo) {
|
|
23176
|
+
const firstIssue = providerSlo.issues[0];
|
|
23177
|
+
checks.push({
|
|
23178
|
+
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.`,
|
|
23179
|
+
href: firstIssue?.sessionId ? voiceOperationsRecordHref(options.links?.operationsRecords ?? "/voice-operations", firstIssue.sessionId) : options.links?.providerSlo ?? options.links?.resilience ?? "/voice/provider-slos",
|
|
23180
|
+
label: "Provider SLO gates",
|
|
23181
|
+
proofSource: proofSource("providerSlo", "providerSlos"),
|
|
23182
|
+
status: providerSloSummary.status,
|
|
23183
|
+
value: `${providerSloSummary.eventsWithLatency}/${providerSloSummary.events}`,
|
|
23184
|
+
actions: providerSloSummary.status === "pass" ? [] : [
|
|
23185
|
+
...firstIssue?.sessionId ? [
|
|
23186
|
+
{
|
|
23187
|
+
description: "Open the exact call/session operations record for the first provider SLO issue.",
|
|
23188
|
+
href: voiceOperationsRecordHref(options.links?.operationsRecords ?? "/voice-operations", firstIssue.sessionId),
|
|
23189
|
+
label: "Open impacted operations record"
|
|
23190
|
+
}
|
|
23191
|
+
] : [],
|
|
23192
|
+
{
|
|
23193
|
+
description: "Open provider SLO proof and inspect latency, timeout, fallback, and unresolved error budgets.",
|
|
23194
|
+
href: options.links?.providerSlo ?? options.links?.resilience ?? "/voice/provider-slos",
|
|
23195
|
+
label: "Open provider SLO report"
|
|
23196
|
+
}
|
|
23197
|
+
]
|
|
23198
|
+
});
|
|
23199
|
+
}
|
|
22857
23200
|
if (providerStack) {
|
|
22858
23201
|
const missingLanes = providerStack.gaps.filter((gap) => gap.status !== "pass");
|
|
22859
23202
|
checks.push({
|
|
@@ -23081,6 +23424,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
23081
23424
|
phoneAgentSmoke: "/sessions",
|
|
23082
23425
|
providerContracts: "/provider-contracts",
|
|
23083
23426
|
providerRoutingContracts: "/resilience",
|
|
23427
|
+
providerSlo: "/voice/provider-slos",
|
|
23084
23428
|
quality: "/quality",
|
|
23085
23429
|
reconnectContracts: "/sessions",
|
|
23086
23430
|
resilience: "/resilience",
|
|
@@ -23121,6 +23465,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
23121
23465
|
providerRecovery,
|
|
23122
23466
|
phoneAgentSmokes: phoneAgentSmokeSummary,
|
|
23123
23467
|
providerRoutingContracts: providerRoutingContractSummary,
|
|
23468
|
+
providerSlo: providerSloSummary,
|
|
23124
23469
|
reconnectContracts: reconnectContractSummary,
|
|
23125
23470
|
quality: {
|
|
23126
23471
|
status: quality.status
|
|
@@ -23140,22 +23485,22 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
23140
23485
|
var buildVoiceProductionReadinessGate = async (options, input = {}) => summarizeVoiceProductionReadinessGate(await buildVoiceProductionReadinessReport(options, input), options.gate || undefined);
|
|
23141
23486
|
var renderVoiceProductionReadinessHTML = (report, options = {}) => {
|
|
23142
23487
|
const title = options.title ?? "AbsoluteJS Voice Production Readiness";
|
|
23143
|
-
const profile = report.profile ? `<section class="profile"><p class="eyebrow">Readiness profile</p><h2>${
|
|
23488
|
+
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
23489
|
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 ${
|
|
23490
|
+
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("");
|
|
23491
|
+
return `<article class="check ${escapeHtml38(check.status)}">
|
|
23147
23492
|
<div>
|
|
23148
|
-
<span>${
|
|
23149
|
-
<h2>${
|
|
23150
|
-
${check.detail ? `<p>${
|
|
23151
|
-
${check.proofSource ? `<p class="proof-source">Proof source: ${check.proofSource.href ? `<a href="${
|
|
23493
|
+
<span>${escapeHtml38(check.status.toUpperCase())}</span>
|
|
23494
|
+
<h2>${escapeHtml38(check.label)}</h2>
|
|
23495
|
+
${check.detail ? `<p>${escapeHtml38(check.detail)}</p>` : ""}
|
|
23496
|
+
${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
23497
|
${actions ? `<p class="actions">${actions}</p>` : ""}
|
|
23153
23498
|
</div>
|
|
23154
|
-
<strong>${
|
|
23155
|
-
${check.href ? `<a href="${
|
|
23499
|
+
<strong>${escapeHtml38(String(check.value ?? check.status))}</strong>
|
|
23500
|
+
${check.href ? `<a href="${escapeHtml38(check.href)}">Open surface</a>` : ""}
|
|
23156
23501
|
</article>`;
|
|
23157
23502
|
}).join("");
|
|
23158
|
-
const snippet =
|
|
23503
|
+
const snippet = escapeHtml38(`createVoiceProductionReadinessRoutes({
|
|
23159
23504
|
htmlPath: '/production-readiness',
|
|
23160
23505
|
path: '/api/production-readiness',
|
|
23161
23506
|
gatePath: '/api/production-readiness/gate',
|
|
@@ -23171,13 +23516,13 @@ var renderVoiceProductionReadinessHTML = (report, options = {}) => {
|
|
|
23171
23516
|
providerRoutingContracts: loadProviderRoutingContracts,
|
|
23172
23517
|
store: traceStore
|
|
23173
23518
|
});`);
|
|
23174
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
23519
|
+
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
23520
|
};
|
|
23176
23521
|
var createVoiceProductionReadinessRoutes = (options) => {
|
|
23177
23522
|
const path = options.path ?? "/api/production-readiness";
|
|
23178
23523
|
const gatePath = options.gatePath === undefined ? "/api/production-readiness/gate" : options.gatePath;
|
|
23179
23524
|
const htmlPath = options.htmlPath ?? "/production-readiness";
|
|
23180
|
-
const routes = new
|
|
23525
|
+
const routes = new Elysia37({
|
|
23181
23526
|
name: options.name ?? "absolutejs-voice-production-readiness"
|
|
23182
23527
|
});
|
|
23183
23528
|
routes.get(path, async ({ query, request }) => buildVoiceProductionReadinessReport(options, { query, request }));
|
|
@@ -23541,8 +23886,8 @@ var recommendVoiceReadinessProfile = (options) => {
|
|
|
23541
23886
|
};
|
|
23542
23887
|
};
|
|
23543
23888
|
// src/providerStackRecommendations.ts
|
|
23544
|
-
import { Elysia as
|
|
23545
|
-
var
|
|
23889
|
+
import { Elysia as Elysia38 } from "elysia";
|
|
23890
|
+
var escapeHtml39 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23546
23891
|
var profileProviderPriorities = {
|
|
23547
23892
|
"meeting-recorder": {
|
|
23548
23893
|
llm: ["openai", "anthropic", "gemini"],
|
|
@@ -23785,17 +24130,17 @@ var resolveProviderContractMatrixInput = async (matrix) => typeof matrix === "fu
|
|
|
23785
24130
|
var renderVoiceProviderContractMatrixHTML = (report, options = {}) => {
|
|
23786
24131
|
const title = options.title ?? "Voice Provider Contract Matrix";
|
|
23787
24132
|
const rows = report.rows.map((row) => {
|
|
23788
|
-
const checks = row.checks.map((check) => `<li class="${
|
|
23789
|
-
return `<article class="row ${
|
|
24133
|
+
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("");
|
|
24134
|
+
return `<article class="row ${escapeHtml39(row.status)}">
|
|
23790
24135
|
<div>
|
|
23791
|
-
<p class="eyebrow">${
|
|
23792
|
-
<h2>${
|
|
23793
|
-
<p class="status ${
|
|
24136
|
+
<p class="eyebrow">${escapeHtml39(row.kind)}${row.selected ? " \xB7 selected" : ""}</p>
|
|
24137
|
+
<h2>${escapeHtml39(row.provider)}</h2>
|
|
24138
|
+
<p class="status ${escapeHtml39(row.status)}">${escapeHtml39(row.status.toUpperCase())}</p>
|
|
23794
24139
|
</div>
|
|
23795
24140
|
<ul>${checks}</ul>
|
|
23796
24141
|
</article>`;
|
|
23797
24142
|
}).join("");
|
|
23798
|
-
const snippet =
|
|
24143
|
+
const snippet = escapeHtml39(`const providerContracts = () =>
|
|
23799
24144
|
createVoiceProviderContractMatrixPreset('phone-agent', {
|
|
23800
24145
|
env: process.env,
|
|
23801
24146
|
providers: {
|
|
@@ -23816,7 +24161,7 @@ createVoiceProductionReadinessRoutes({
|
|
|
23816
24161
|
providerContractMatrix: () =>
|
|
23817
24162
|
buildVoiceProviderContractMatrix(providerContracts())
|
|
23818
24163
|
});`);
|
|
23819
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24164
|
+
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
24165
|
};
|
|
23821
24166
|
var createVoiceProviderContractMatrixJSONHandler = (matrix) => async () => buildVoiceProviderContractMatrix(await resolveProviderContractMatrixInput(matrix));
|
|
23822
24167
|
var createVoiceProviderContractMatrixHTMLHandler = (options) => async () => {
|
|
@@ -23831,7 +24176,7 @@ var createVoiceProviderContractMatrixHTMLHandler = (options) => async () => {
|
|
|
23831
24176
|
var createVoiceProviderContractMatrixRoutes = (options) => {
|
|
23832
24177
|
const path = options.path ?? "/api/provider-contracts";
|
|
23833
24178
|
const htmlPath = options.htmlPath ?? "/provider-contracts";
|
|
23834
|
-
const routes = new
|
|
24179
|
+
const routes = new Elysia38({
|
|
23835
24180
|
name: options.name ?? "absolutejs-voice-provider-contract-matrix"
|
|
23836
24181
|
});
|
|
23837
24182
|
const jsonHandler = createVoiceProviderContractMatrixJSONHandler(options.matrix);
|
|
@@ -23890,7 +24235,7 @@ var evaluateVoiceProviderStackGaps = (input) => {
|
|
|
23890
24235
|
};
|
|
23891
24236
|
};
|
|
23892
24237
|
// src/opsConsoleRoutes.ts
|
|
23893
|
-
import { Elysia as
|
|
24238
|
+
import { Elysia as Elysia39 } from "elysia";
|
|
23894
24239
|
var DEFAULT_LINKS = [
|
|
23895
24240
|
{
|
|
23896
24241
|
description: "Quality gates for CI, deploy checks, and production readiness.",
|
|
@@ -23925,7 +24270,7 @@ var DEFAULT_LINKS = [
|
|
|
23925
24270
|
label: "Handoffs"
|
|
23926
24271
|
}
|
|
23927
24272
|
];
|
|
23928
|
-
var
|
|
24273
|
+
var escapeHtml40 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23929
24274
|
var countProviderStatuses = (providers) => {
|
|
23930
24275
|
const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
|
|
23931
24276
|
const healthy = providers.filter((provider) => provider.status === "healthy").length;
|
|
@@ -23994,20 +24339,20 @@ var buildVoiceOpsConsoleReport = async (options) => {
|
|
|
23994
24339
|
trace
|
|
23995
24340
|
};
|
|
23996
24341
|
};
|
|
23997
|
-
var renderMetricCard = (input) => `<article class="metric"><span>${
|
|
24342
|
+
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
24343
|
var renderVoiceOpsConsoleHTML = (report, options = {}) => {
|
|
23999
24344
|
const links = report.links.map((link) => `<article class="surface">
|
|
24000
|
-
<div><h2>${
|
|
24001
|
-
<p><a href="${
|
|
24345
|
+
<div><h2>${escapeHtml40(link.label)}</h2>${link.description ? `<p>${escapeHtml40(link.description)}</p>` : ""}</div>
|
|
24346
|
+
<p><a href="${escapeHtml40(link.href)}">Open ${escapeHtml40(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml40(link.statusHref)}">Status</a>` : ""}</p>
|
|
24002
24347
|
</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>${
|
|
24348
|
+
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>';
|
|
24349
|
+
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
24350
|
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>${
|
|
24351
|
+
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
24352
|
};
|
|
24008
24353
|
var createVoiceOpsConsoleRoutes = (options) => {
|
|
24009
24354
|
const path = options.path ?? "/ops-console";
|
|
24010
|
-
const routes = new
|
|
24355
|
+
const routes = new Elysia39({
|
|
24011
24356
|
name: options.name ?? "absolutejs-voice-ops-console"
|
|
24012
24357
|
});
|
|
24013
24358
|
const getReport = () => buildVoiceOpsConsoleReport(options);
|
|
@@ -24024,11 +24369,11 @@ var createVoiceOpsConsoleRoutes = (options) => {
|
|
|
24024
24369
|
return routes;
|
|
24025
24370
|
};
|
|
24026
24371
|
// src/operationsRecord.ts
|
|
24027
|
-
import { Elysia as
|
|
24372
|
+
import { Elysia as Elysia41 } from "elysia";
|
|
24028
24373
|
|
|
24029
24374
|
// src/traceTimeline.ts
|
|
24030
|
-
import { Elysia as
|
|
24031
|
-
var
|
|
24375
|
+
import { Elysia as Elysia40 } from "elysia";
|
|
24376
|
+
var escapeHtml41 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
24032
24377
|
var getString16 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
24033
24378
|
var getNumber9 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
24034
24379
|
var firstString3 = (payload, keys) => {
|
|
@@ -24211,17 +24556,17 @@ var summarizeVoiceTraceTimeline = (events, options = {}) => {
|
|
|
24211
24556
|
};
|
|
24212
24557
|
};
|
|
24213
24558
|
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>${
|
|
24559
|
+
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
24560
|
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>${
|
|
24561
|
+
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("");
|
|
24562
|
+
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>";
|
|
24563
|
+
const supportLinks = session.operationsRecordHref ? `<p><a href="${escapeHtml41(session.operationsRecordHref)}">Open operations record</a></p>` : "";
|
|
24564
|
+
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
24565
|
};
|
|
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="${
|
|
24566
|
+
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
24567
|
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
24568
|
var renderVoiceTraceTimelineHTML = (report, options = {}) => {
|
|
24224
|
-
const snippet =
|
|
24569
|
+
const snippet = escapeHtml41(`const traceStore = createVoiceTraceSinkStore({
|
|
24225
24570
|
store: runtimeStorage.traces,
|
|
24226
24571
|
sinks: [
|
|
24227
24572
|
createVoiceTraceHTTPSink({
|
|
@@ -24247,13 +24592,13 @@ app.use(
|
|
|
24247
24592
|
traceDeliveries: runtimeStorage.traceDeliveries
|
|
24248
24593
|
})
|
|
24249
24594
|
);`);
|
|
24250
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
24595
|
+
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
24596
|
};
|
|
24252
24597
|
var createVoiceTraceTimelineRoutes = (options) => {
|
|
24253
24598
|
const path = options.path ?? "/api/voice-traces";
|
|
24254
24599
|
const htmlPath = options.htmlPath ?? "/traces";
|
|
24255
24600
|
const title = options.title ?? "AbsoluteJS Voice Trace Timelines";
|
|
24256
|
-
const routes = new
|
|
24601
|
+
const routes = new Elysia40({
|
|
24257
24602
|
name: options.name ?? "absolutejs-voice-trace-timelines"
|
|
24258
24603
|
});
|
|
24259
24604
|
const buildReport = async () => summarizeVoiceTraceTimeline(await options.store.list(), {
|
|
@@ -24440,7 +24785,7 @@ var buildVoiceOperationsRecord = async (options) => {
|
|
|
24440
24785
|
transcript: buildTranscript(replay)
|
|
24441
24786
|
};
|
|
24442
24787
|
};
|
|
24443
|
-
var
|
|
24788
|
+
var escapeHtml42 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
24444
24789
|
var formatMs4 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
24445
24790
|
var outcomeLabels = (outcome) => [
|
|
24446
24791
|
outcome.complete ? "complete" : undefined,
|
|
@@ -24473,15 +24818,15 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
|
|
|
24473
24818
|
`);
|
|
24474
24819
|
};
|
|
24475
24820
|
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 =
|
|
24821
|
+
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>';
|
|
24822
|
+
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>";
|
|
24823
|
+
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>";
|
|
24824
|
+
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>";
|
|
24825
|
+
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>";
|
|
24826
|
+
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>";
|
|
24827
|
+
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>";
|
|
24828
|
+
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>";
|
|
24829
|
+
const snippet = escapeHtml42(`app.use(
|
|
24485
24830
|
createVoiceOperationsRecordRoutes({
|
|
24486
24831
|
audit: auditStore,
|
|
24487
24832
|
integrationEvents: opsEvents,
|
|
@@ -24495,16 +24840,16 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
|
24495
24840
|
tasks: opsTasks
|
|
24496
24841
|
})
|
|
24497
24842
|
);`);
|
|
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>${
|
|
24843
|
+
const incidentMarkdown = escapeHtml42(renderVoiceOperationsRecordIncidentMarkdown(record));
|
|
24844
|
+
const incidentLink = options.incidentHref ? `<a href="${escapeHtml42(options.incidentHref)}">Download incident.md</a>` : "";
|
|
24845
|
+
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
24846
|
};
|
|
24502
24847
|
var createVoiceOperationsRecordRoutes = (options) => {
|
|
24503
24848
|
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
24504
24849
|
const htmlPath = options.htmlPath === undefined ? "/voice-operations/:sessionId" : options.htmlPath;
|
|
24505
24850
|
const incidentPath = options.incidentPath === undefined ? `${path}/incident.md` : options.incidentPath;
|
|
24506
24851
|
const incidentHtmlPath = options.incidentHtmlPath === undefined && htmlPath ? `${htmlPath}/incident.md` : options.incidentHtmlPath;
|
|
24507
|
-
const routes = new
|
|
24852
|
+
const routes = new Elysia41({
|
|
24508
24853
|
name: options.name ?? "absolutejs-voice-operations-record"
|
|
24509
24854
|
});
|
|
24510
24855
|
const buildRecord = (sessionId) => buildVoiceOperationsRecord({
|
|
@@ -24556,7 +24901,7 @@ var createVoiceOperationsRecordRoutes = (options) => {
|
|
|
24556
24901
|
return routes;
|
|
24557
24902
|
};
|
|
24558
24903
|
// src/incidentBundle.ts
|
|
24559
|
-
import { Elysia as
|
|
24904
|
+
import { Elysia as Elysia42 } from "elysia";
|
|
24560
24905
|
var filterIncidentBundleArtifacts = (artifacts, filter = {}) => artifacts.filter((artifact) => {
|
|
24561
24906
|
if (filter.sessionId && artifact.sessionId !== filter.sessionId) {
|
|
24562
24907
|
return false;
|
|
@@ -24755,7 +25100,7 @@ var buildVoiceIncidentBundle = async (options) => {
|
|
|
24755
25100
|
var createVoiceIncidentBundleRoutes = (options) => {
|
|
24756
25101
|
const path = options.path ?? "/api/voice-incidents/:sessionId";
|
|
24757
25102
|
const markdownPath = options.markdownPath === undefined ? "/voice-incidents/:sessionId/markdown" : options.markdownPath;
|
|
24758
|
-
const routes = new
|
|
25103
|
+
const routes = new Elysia42({
|
|
24759
25104
|
name: options.name ?? "absolutejs-voice-incident-bundle"
|
|
24760
25105
|
});
|
|
24761
25106
|
const getSessionId = (params) => params.sessionId ?? "";
|
|
@@ -24956,19 +25301,19 @@ var summarizeVoiceOpsStatus = async (options) => {
|
|
|
24956
25301
|
};
|
|
24957
25302
|
};
|
|
24958
25303
|
// src/opsStatusRoutes.ts
|
|
24959
|
-
import { Elysia as
|
|
24960
|
-
var
|
|
25304
|
+
import { Elysia as Elysia43 } from "elysia";
|
|
25305
|
+
var escapeHtml43 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
24961
25306
|
var renderVoiceOpsStatusHTML = (report, options = {}) => {
|
|
24962
25307
|
const title = options.title ?? "AbsoluteJS Voice Ops Status";
|
|
24963
25308
|
const surfaces = Object.entries(report.surfaces).map(([key, surface]) => {
|
|
24964
25309
|
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 ${
|
|
25310
|
+
return `<article class="surface ${escapeHtml43(surface.status)}"><span>${escapeHtml43(surface.status.toUpperCase())}</span><h2>${escapeHtml43(key)}</h2><strong>${escapeHtml43(value)}</strong></article>`;
|
|
24966
25311
|
}).join("");
|
|
24967
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${
|
|
25312
|
+
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
25313
|
};
|
|
24969
25314
|
var createVoiceOpsStatusRoutes = (options) => {
|
|
24970
25315
|
const path = options.path ?? "/api/voice/ops-status";
|
|
24971
|
-
const routes = new
|
|
25316
|
+
const routes = new Elysia43({
|
|
24972
25317
|
name: options.name ?? "absolutejs-voice-ops-status"
|
|
24973
25318
|
});
|
|
24974
25319
|
routes.get(path, async () => summarizeVoiceOpsStatus(options));
|
|
@@ -25401,8 +25746,8 @@ var createVoiceTTSProviderRouter = (options) => {
|
|
|
25401
25746
|
};
|
|
25402
25747
|
};
|
|
25403
25748
|
// src/traceDeliveryRoutes.ts
|
|
25404
|
-
import { Elysia as
|
|
25405
|
-
var
|
|
25749
|
+
import { Elysia as Elysia44 } from "elysia";
|
|
25750
|
+
var escapeHtml44 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
25406
25751
|
var getString18 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
25407
25752
|
var getNumber11 = (value) => {
|
|
25408
25753
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -25483,14 +25828,14 @@ var renderSinkResults2 = (delivery) => {
|
|
|
25483
25828
|
if (entries.length === 0) {
|
|
25484
25829
|
return "<p>No sink delivery attempts recorded yet.</p>";
|
|
25485
25830
|
}
|
|
25486
|
-
return `<ul>${entries.map(([sinkId, result]) => `<li><strong>${
|
|
25831
|
+
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
25832
|
};
|
|
25488
|
-
var renderEventList2 = (delivery) => delivery.events.length === 0 ? "<p>No trace events in this delivery.</p>" : `<ul>${delivery.events.map((event) => `<li>${
|
|
25833
|
+
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
25834
|
var renderVoiceTraceDeliveryHTML = (report, options = {}) => {
|
|
25490
25835
|
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>${
|
|
25836
|
+
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>`;
|
|
25837
|
+
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("");
|
|
25838
|
+
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
25839
|
};
|
|
25495
25840
|
var createVoiceTraceDeliveryJSONHandler = (options) => async ({ query }) => buildVoiceTraceDeliveryReport(options, resolveVoiceTraceDeliveryFilter(query, options.filter));
|
|
25496
25841
|
var createVoiceTraceDeliveryHTMLHandler = (options) => async ({ query }) => {
|
|
@@ -25510,7 +25855,7 @@ var createVoiceTraceDeliveryRoutes = (options) => {
|
|
|
25510
25855
|
const path = options.path ?? "/api/voice-trace-deliveries";
|
|
25511
25856
|
const htmlPath = options.htmlPath === undefined ? "/traces/deliveries" : options.htmlPath;
|
|
25512
25857
|
const workerPath = options.workerPath === undefined ? `${path}/drain` : options.workerPath;
|
|
25513
|
-
const routes = new
|
|
25858
|
+
const routes = new Elysia44({
|
|
25514
25859
|
name: options.name ?? "absolutejs-voice-trace-deliveries"
|
|
25515
25860
|
}).get(path, createVoiceTraceDeliveryJSONHandler(options));
|
|
25516
25861
|
if (htmlPath !== false) {
|
|
@@ -26134,7 +26479,7 @@ var createVoiceMemoryStore = () => {
|
|
|
26134
26479
|
return { get, getOrCreate, list, remove, set };
|
|
26135
26480
|
};
|
|
26136
26481
|
// src/opsWebhook.ts
|
|
26137
|
-
import { Elysia as
|
|
26482
|
+
import { Elysia as Elysia45 } from "elysia";
|
|
26138
26483
|
var toHex6 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
26139
26484
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
26140
26485
|
const encoder = new TextEncoder;
|
|
@@ -26264,7 +26609,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
26264
26609
|
};
|
|
26265
26610
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
26266
26611
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
26267
|
-
return new
|
|
26612
|
+
return new Elysia45().post(path, async ({ body, request, set }) => {
|
|
26268
26613
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
26269
26614
|
if (options.signingSecret) {
|
|
26270
26615
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -27200,6 +27545,8 @@ export {
|
|
|
27200
27545
|
renderVoiceResilienceHTML,
|
|
27201
27546
|
renderVoiceReconnectContractHTML,
|
|
27202
27547
|
renderVoiceQualityHTML,
|
|
27548
|
+
renderVoiceProviderSloMarkdown,
|
|
27549
|
+
renderVoiceProviderSloHTML,
|
|
27203
27550
|
renderVoiceProviderHealthHTML,
|
|
27204
27551
|
renderVoiceProviderContractMatrixHTML,
|
|
27205
27552
|
renderVoiceProviderCapabilityHTML,
|
|
@@ -27365,6 +27712,7 @@ export {
|
|
|
27365
27712
|
createVoiceReconnectContractRoutes,
|
|
27366
27713
|
createVoiceReadinessProfile,
|
|
27367
27714
|
createVoiceQualityRoutes,
|
|
27715
|
+
createVoiceProviderSloRoutes,
|
|
27368
27716
|
createVoiceProviderRouter,
|
|
27369
27717
|
createVoiceProviderHealthRoutes,
|
|
27370
27718
|
createVoiceProviderHealthJSONHandler,
|
|
@@ -27535,6 +27883,7 @@ export {
|
|
|
27535
27883
|
claimVoiceOpsTask,
|
|
27536
27884
|
buildVoiceTraceReplay,
|
|
27537
27885
|
buildVoiceTraceDeliveryReport,
|
|
27886
|
+
buildVoiceProviderSloReport,
|
|
27538
27887
|
buildVoiceProviderContractMatrix,
|
|
27539
27888
|
buildVoiceProductionReadinessReport,
|
|
27540
27889
|
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,112 @@
|
|
|
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
|
+
maxAgeMs?: number;
|
|
66
|
+
now?: number;
|
|
67
|
+
requiredKinds?: readonly VoiceRoutingEventKind[];
|
|
68
|
+
store?: VoiceTraceEventStore;
|
|
69
|
+
thresholds?: VoiceProviderSloThresholdConfig;
|
|
70
|
+
};
|
|
71
|
+
export type VoiceProviderSloRoutesOptions = VoiceProviderSloReportOptions & {
|
|
72
|
+
headers?: HeadersInit;
|
|
73
|
+
htmlPath?: false | string;
|
|
74
|
+
markdownPath?: false | string;
|
|
75
|
+
name?: string;
|
|
76
|
+
path?: string;
|
|
77
|
+
render?: (report: VoiceProviderSloReport) => string | Promise<string>;
|
|
78
|
+
title?: string;
|
|
79
|
+
};
|
|
80
|
+
export declare const buildVoiceProviderSloReport: (options?: VoiceProviderSloReportOptions) => Promise<VoiceProviderSloReport>;
|
|
81
|
+
export declare const renderVoiceProviderSloMarkdown: (report: VoiceProviderSloReport) => string;
|
|
82
|
+
export declare const renderVoiceProviderSloHTML: (report: VoiceProviderSloReport, options?: {
|
|
83
|
+
title?: string;
|
|
84
|
+
}) => string;
|
|
85
|
+
export declare const createVoiceProviderSloRoutes: (options: VoiceProviderSloRoutesOptions) => Elysia<"", {
|
|
86
|
+
decorator: {};
|
|
87
|
+
store: {};
|
|
88
|
+
derive: {};
|
|
89
|
+
resolve: {};
|
|
90
|
+
}, {
|
|
91
|
+
typebox: {};
|
|
92
|
+
error: {};
|
|
93
|
+
}, {
|
|
94
|
+
schema: {};
|
|
95
|
+
standaloneSchema: {};
|
|
96
|
+
macro: {};
|
|
97
|
+
macroFn: {};
|
|
98
|
+
parser: {};
|
|
99
|
+
response: {};
|
|
100
|
+
}, {}, {
|
|
101
|
+
derive: {};
|
|
102
|
+
resolve: {};
|
|
103
|
+
schema: {};
|
|
104
|
+
standaloneSchema: {};
|
|
105
|
+
response: {};
|
|
106
|
+
}, {
|
|
107
|
+
derive: {};
|
|
108
|
+
resolve: {};
|
|
109
|
+
schema: {};
|
|
110
|
+
standaloneSchema: {};
|
|
111
|
+
response: {};
|
|
112
|
+
}>;
|