@absolutejs/voice 0.0.22-beta.375 → 0.0.22-beta.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1545,6 +1545,16 @@ const recoveryJobs = createVoiceSQLiteRealCallProfileRecoveryJobStore({
1545
1545
  });
1546
1546
  ```
1547
1547
 
1548
+ Recovery routes expose recent persisted jobs at `${path}/actions/jobs`. Stores can implement `list({ limit, actionId, status })`; the bundled memory and SQLite stores both support it.
1549
+
1550
+ Use `buildVoiceRealCallProfileRecoveryJobHistoryCheck(...)` in production readiness to prove recovery jobs are persisted and inspectable:
1551
+
1552
+ ```ts
1553
+ additionalChecks: async () => [
1554
+ await buildVoiceRealCallProfileRecoveryJobHistoryCheck(recoveryJobs)
1555
+ ];
1556
+ ```
1557
+
1548
1558
  Use `createVoiceProfileTraceTagger(...)` when the app already has a trace store and needs every appended trace to carry a benchmark profile label. It wraps any `VoiceTraceEventStore`, preserves the underlying store behavior, and adds `profileId`/`benchmarkProfileId` metadata and payload fields that real-call profile history can ingest later.
1549
1559
 
1550
1560
  ```ts
@@ -4649,6 +4649,13 @@ var createVoiceInMemoryRealCallProfileRecoveryJobStore = (options = {}) => {
4649
4649
  get(id) {
4650
4650
  return jobs.get(id);
4651
4651
  },
4652
+ list(input = {}) {
4653
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
4654
+ return [...jobs.values()].filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).sort((first, second) => {
4655
+ const updatedDelta = Date.parse(second.updatedAt) - Date.parse(first.updatedAt);
4656
+ return updatedDelta === 0 ? second.id.localeCompare(first.id) : updatedDelta;
4657
+ }).slice(0, limit);
4658
+ },
4652
4659
  update(id, update) {
4653
4660
  const existing = jobs.get(id);
4654
4661
  if (!existing) {
@@ -4682,6 +4689,7 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
4682
4689
  payload TEXT NOT NULL
4683
4690
  )`);
4684
4691
  const selectStatement = database.query(`SELECT payload FROM "${tableName}" WHERE id = ?1 LIMIT 1`);
4692
+ const listStatement = database.query(`SELECT payload FROM "${tableName}" ORDER BY sort_at DESC, id DESC`);
4685
4693
  const upsertStatement = database.query(`INSERT INTO "${tableName}" (id, sort_at, payload)
4686
4694
  VALUES (?1, ?2, ?3)
4687
4695
  ON CONFLICT(id) DO UPDATE SET sort_at = excluded.sort_at, payload = excluded.payload`);
@@ -4708,6 +4716,10 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
4708
4716
  get(id) {
4709
4717
  return readJob(id);
4710
4718
  },
4719
+ list(input = {}) {
4720
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
4721
+ return listStatement.all().map((row) => JSON.parse(row.payload)).filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).slice(0, limit);
4722
+ },
4711
4723
  update(id, update) {
4712
4724
  const existing = readJob(id);
4713
4725
  if (!existing) {
@@ -4748,6 +4760,94 @@ var buildVoiceRealCallProfileReadinessCheck = (report, options = {}) => {
4748
4760
  value: `${String(report.defaults.summary.actionableProfiles)}/${String(report.summary.profileCount)} actionable`
4749
4761
  };
4750
4762
  };
4763
+ var buildVoiceRealCallProfileRecoveryJobHistoryCheck = async (store, options = {}) => {
4764
+ const href = options.href ?? "/voice/real-call-profile-recovery";
4765
+ const sourceHref = options.sourceHref ?? "/api/voice/real-call-profile-history/actions/jobs";
4766
+ const minCompletedJobs = options.minCompletedJobs ?? 1;
4767
+ const label = options.label ?? "Real-call recovery job history";
4768
+ if (!store?.list) {
4769
+ return {
4770
+ actions: [
4771
+ {
4772
+ description: "Configure a recovery job store with list support so operators can inspect proof repair history.",
4773
+ href,
4774
+ label: "Configure recovery job history"
4775
+ }
4776
+ ],
4777
+ detail: "No real-call profile recovery job store with list support is configured.",
4778
+ gateExplanation: {
4779
+ evidenceHref: sourceHref,
4780
+ observed: "missing",
4781
+ remediation: "Use the bundled memory or SQLite recovery job store, or implement list() on the custom store.",
4782
+ sourceHref,
4783
+ threshold: "list support",
4784
+ thresholdLabel: "Inspectable recovery jobs",
4785
+ unit: "status"
4786
+ },
4787
+ href,
4788
+ label,
4789
+ status: "fail",
4790
+ value: "missing"
4791
+ };
4792
+ }
4793
+ const jobs = await store.list({ limit: 50 });
4794
+ const now = Date.now();
4795
+ const recentJobs = options.maxAgeMs === undefined ? jobs : jobs.filter((job) => now - Date.parse(job.updatedAt) <= options.maxAgeMs);
4796
+ const completedJobs = recentJobs.filter((job) => job.status === "pass" || job.status === "fail");
4797
+ const failedJobs = recentJobs.filter((job) => job.status === "fail");
4798
+ const runningJobs = recentJobs.filter((job) => job.status === "queued" || job.status === "running");
4799
+ const issues = [];
4800
+ const warnings = [];
4801
+ if (completedJobs.length < minCompletedJobs) {
4802
+ issues.push(`Expected at least ${String(minCompletedJobs)} completed recovery job(s), found ${String(completedJobs.length)}.`);
4803
+ }
4804
+ if (!options.allowFailedJobs && failedJobs.length > 0) {
4805
+ issues.push(`${String(failedJobs.length)} recent recovery job(s) failed.`);
4806
+ }
4807
+ if (runningJobs.length > 0) {
4808
+ const message = `${String(runningJobs.length)} recovery job(s) are still queued or running.`;
4809
+ if (options.failOnRunningJobs) {
4810
+ issues.push(message);
4811
+ } else {
4812
+ warnings.push(message);
4813
+ }
4814
+ }
4815
+ const status = issues.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass";
4816
+ const detail = status === "pass" ? `${String(completedJobs.length)} completed recovery job(s), ${String(failedJobs.length)} failed, ${String(runningJobs.length)} running.` : [...issues, ...warnings].join(" ");
4817
+ return {
4818
+ actions: [
4819
+ {
4820
+ description: "Open the recovery job UI and run or inspect proof repair jobs.",
4821
+ href,
4822
+ label: "Open recovery job history"
4823
+ },
4824
+ {
4825
+ description: "Open the recovery job history API.",
4826
+ href: sourceHref,
4827
+ label: "Open recovery jobs API"
4828
+ }
4829
+ ],
4830
+ detail,
4831
+ gateExplanation: {
4832
+ evidenceHref: sourceHref,
4833
+ observed: completedJobs.length,
4834
+ remediation: "Run a browser or phone recovery proof job and ensure recent recovery jobs complete without failure.",
4835
+ sourceHref,
4836
+ threshold: minCompletedJobs,
4837
+ thresholdLabel: "Minimum completed recovery jobs",
4838
+ unit: "count"
4839
+ },
4840
+ href,
4841
+ label,
4842
+ proofSource: {
4843
+ href: sourceHref,
4844
+ source: "recovery-job-store",
4845
+ sourceLabel: "Real-call recovery jobs"
4846
+ },
4847
+ status,
4848
+ value: `${String(completedJobs.length)} completed / ${String(failedJobs.length)} failed / ${String(runningJobs.length)} running`
4849
+ };
4850
+ };
4751
4851
  var resolveVoiceRealCallProfileProviderRoute = (options) => {
4752
4852
  const defaults = readRealCallProfileDefaultsReport(options.defaults);
4753
4853
  const profile = defaults.profiles.find((item) => item.profileId === options.profileId) ?? defaults.profiles.find((item) => item.status === "pass") ?? defaults.profiles[0];
@@ -5448,6 +5548,32 @@ var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
5448
5548
  };
5449
5549
  };
5450
5550
  routes.get(`${path}/actions`, async () => Response.json(await listActions(), { headers: options.headers }));
5551
+ routes.get(`${path}/actions/jobs`, async ({
5552
+ query,
5553
+ set
5554
+ }) => {
5555
+ if (!options.jobStore?.list) {
5556
+ set.status = 501;
5557
+ return Response.json({
5558
+ jobs: [],
5559
+ message: "No real-call profile recovery job list store is configured.",
5560
+ ok: false
5561
+ }, { headers: options.headers });
5562
+ }
5563
+ const actionId = Object.keys(realCallProfileActionPaths).includes(query.actionId ?? "") ? query.actionId : undefined;
5564
+ const status = ["fail", "pass", "queued", "running"].includes(query.status ?? "") ? query.status : undefined;
5565
+ const limit = Number(query.limit);
5566
+ const jobs = await options.jobStore.list({
5567
+ actionId,
5568
+ limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
5569
+ status
5570
+ });
5571
+ return Response.json({
5572
+ generatedAt: new Date().toISOString(),
5573
+ jobs,
5574
+ ok: true
5575
+ }, { headers: options.headers });
5576
+ });
5451
5577
  routes.get(`${path}/actions/:jobId`, async ({
5452
5578
  params,
5453
5579
  set
package/dist/index.d.ts CHANGED
@@ -30,12 +30,12 @@ export { assertVoicePlatformCoverage, buildVoicePlatformCoverageSummary, createV
30
30
  export { assertVoiceCompetitiveCoverage, buildVoiceCompetitiveCoverageReport, createVoiceCompetitiveCoverageRoutes, evaluateVoiceCompetitiveCoverage, renderVoiceCompetitiveCoverageHTML, renderVoiceCompetitiveCoverageMarkdown } from './competitiveCoverage';
31
31
  export type { VoiceCompetitiveCoverageAssertionInput, VoiceCompetitiveCoverageAssertionReport, VoiceCompetitiveCoverageIssue, VoiceCompetitiveCoverageLevel, VoiceCompetitiveCoverageReport, VoiceCompetitiveCoverageReportInput, VoiceCompetitiveCoverageRoutesOptions, VoiceCompetitiveCoverageStatus, VoiceCompetitiveCoverageSummary, VoiceCompetitiveDepthLevel, VoiceCompetitiveEvidence, VoiceCompetitiveSurface } from './competitiveCoverage';
32
32
  export type { VoicePlatformCoverageAssertionInput, VoicePlatformCoverageAssertionReport, VoicePlatformCoverageEvidence, VoicePlatformCoverageRoutesOptions, VoicePlatformCoverageStatus, VoicePlatformCoverageSummary, VoicePlatformCoverageSummaryInput, VoicePlatformCoverageSurface } from './platformCoverage';
33
- export { assertVoiceProofTrendEvidence, buildEmptyVoiceProofTrendReport, buildVoiceProofTrendProfileSummaries, buildVoiceProofTrendRecommendationReport, buildVoiceProofTrendReportFromRealCallProfiles, buildVoiceProofTrendReport, buildVoiceRealCallProfileEvidenceFromTraceEvents, buildVoiceRealCallProfileDefaults, buildVoiceRealCallProfileHistoryReport, buildVoiceRealCallProfileReadinessCheck, buildVoiceRealCallProfileRecoveryActions, createVoiceInMemoryRealCallProfileRecoveryJobStore, createVoiceSQLiteRealCallProfileRecoveryJobStore, createVoiceProofTrendRecommendationRoutes, createVoiceProofTrendRoutes, createVoiceRealCallProfileHistoryRoutes, createVoiceRealCallProfileRecoveryActionRoutes, DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS, DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS, evaluateVoiceProofTrendEvidence, formatVoiceProofTrendAge, loadVoiceRealCallProfileEvidenceFromTraceStore, normalizeVoiceProofTrendReport, readVoiceProofTrendReportFile, renderVoiceProofTrendRecommendationHTML, renderVoiceProofTrendRecommendationMarkdown, renderVoiceRealCallProfileHistoryHTML, renderVoiceRealCallProfileHistoryMarkdown, resolveVoiceRealCallProfileProviderRoute } from './proofTrends';
33
+ export { assertVoiceProofTrendEvidence, buildEmptyVoiceProofTrendReport, buildVoiceProofTrendProfileSummaries, buildVoiceProofTrendRecommendationReport, buildVoiceProofTrendReportFromRealCallProfiles, buildVoiceProofTrendReport, buildVoiceRealCallProfileEvidenceFromTraceEvents, buildVoiceRealCallProfileDefaults, buildVoiceRealCallProfileHistoryReport, buildVoiceRealCallProfileReadinessCheck, buildVoiceRealCallProfileRecoveryJobHistoryCheck, buildVoiceRealCallProfileRecoveryActions, createVoiceInMemoryRealCallProfileRecoveryJobStore, createVoiceSQLiteRealCallProfileRecoveryJobStore, createVoiceProofTrendRecommendationRoutes, createVoiceProofTrendRoutes, createVoiceRealCallProfileHistoryRoutes, createVoiceRealCallProfileRecoveryActionRoutes, DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS, DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS, evaluateVoiceProofTrendEvidence, formatVoiceProofTrendAge, loadVoiceRealCallProfileEvidenceFromTraceStore, normalizeVoiceProofTrendReport, readVoiceProofTrendReportFile, renderVoiceProofTrendRecommendationHTML, renderVoiceProofTrendRecommendationMarkdown, renderVoiceRealCallProfileHistoryHTML, renderVoiceRealCallProfileHistoryMarkdown, resolveVoiceRealCallProfileProviderRoute } from './proofTrends';
34
34
  export { applyVoiceProfileSwitchGuard, buildVoiceProfileSwitchReadinessReport, buildVoiceProfileSwitchLiveDecisionReport, createVoiceProfileSwitchLiveDecisionRoutes, createVoiceProfileSwitchPolicyProofRoutes, createVoiceProfileSwitchReadinessRoutes, recommendVoiceProfileSwitch, renderVoiceProfileSwitchLiveDecisionHTML, renderVoiceProfileSwitchPolicyProofHTML, renderVoiceProfileSwitchReadinessHTML, runVoiceProfileSwitchPolicyProof } from './profileSwitchRecommendation';
35
35
  export type { VoiceProfileSwitchGuardAction, VoiceProfileSwitchGuardDecision, VoiceProfileSwitchGuardMode, VoiceProfileSwitchGuardOptions, VoiceProfileSwitchObservedSignals, VoiceProfileSwitchLiveDecisionEvidence, VoiceProfileSwitchLiveDecisionReport, VoiceProfileSwitchLiveDecisionReportOptions, VoiceProfileSwitchLiveDecisionRoutesOptions, VoiceProfileSwitchLiveDecisionSession, VoiceProfileSwitchPolicyProofCase, VoiceProfileSwitchPolicyProofCaseResult, VoiceProfileSwitchPolicyProofOptions, VoiceProfileSwitchPolicyProofReport, VoiceProfileSwitchPolicyProofRoutesOptions, VoiceProfileSwitchReadinessIssue, VoiceProfileSwitchReadinessOptions, VoiceProfileSwitchReadinessReport, VoiceProfileSwitchReadinessRoutesOptions, VoiceProfileSwitchReadinessStatus, VoiceProfileSwitchRecommendation, VoiceProfileSwitchRecommendationOptions } from './profileSwitchRecommendation';
36
36
  export { buildVoiceProviderDecisionTraceReport, createVoiceProviderDecisionTraceEvent, createVoiceProviderDecisionTraceRoutes, listVoiceProviderDecisionTraces, renderVoiceProviderDecisionTraceHTML, renderVoiceProviderDecisionTraceMarkdown } from './providerDecisionTraces';
37
37
  export type { VoiceProviderDecisionStatus, VoiceProviderDecisionSurfaceReport, VoiceProviderDecisionTrace, VoiceProviderDecisionTraceInput, VoiceProviderDecisionTraceIssue, VoiceProviderDecisionTraceReport, VoiceProviderDecisionTraceReportOptions, VoiceProviderDecisionTraceRoutesOptions } from './providerDecisionTraces';
38
- export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendProfileDefinition, VoiceProofTrendProfileRecommendation, VoiceProofTrendProfileSummaryOptions, VoiceProofTrendProfileSummary, VoiceProofTrendProviderRecommendation, VoiceProofTrendProviderSummary, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendRealCallProfileEvidence, VoiceProofTrendRealCallProfileReportOptions, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendRuntimeChannelSummary, VoiceProofTrendStatus, VoiceProofTrendSummary, VoiceRealCallProfileDefault, VoiceRealCallProfileDefaultsOptions, VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryOptions, VoiceRealCallProfileHistoryReport, VoiceRealCallProfileHistoryRoutesOptions, VoiceRealCallProfileProviderRouteOptions, VoiceRealCallProfileReadinessCheckOptions, VoiceRealCallProfileRecoveryActionOptions, VoiceRealCallProfileRecoveryAction, VoiceRealCallProfileRecoveryActionHandler, VoiceRealCallProfileRecoveryActionHandlerInput, VoiceRealCallProfileRecoveryActionId, VoiceRealCallProfileRecoveryActionResult, VoiceRealCallProfileRecoveryActionRoutesOptions, VoiceRealCallProfileRecoveryJob, VoiceRealCallProfileRecoveryJobCreateInput, VoiceRealCallProfileRecoveryJobStatus, VoiceRealCallProfileRecoveryJobStore, VoiceRealCallProfileRecoveryJobUpdate, VoiceSQLiteRealCallProfileRecoveryJobStoreOptions, VoiceRealCallProfileTraceEvidenceOptions, VoiceRealCallProfileTraceStoreEvidenceOptions } from './proofTrends';
38
+ export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendProfileDefinition, VoiceProofTrendProfileRecommendation, VoiceProofTrendProfileSummaryOptions, VoiceProofTrendProfileSummary, VoiceProofTrendProviderRecommendation, VoiceProofTrendProviderSummary, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendRealCallProfileEvidence, VoiceProofTrendRealCallProfileReportOptions, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendRuntimeChannelSummary, VoiceProofTrendStatus, VoiceProofTrendSummary, VoiceRealCallProfileDefault, VoiceRealCallProfileDefaultsOptions, VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryOptions, VoiceRealCallProfileHistoryReport, VoiceRealCallProfileHistoryRoutesOptions, VoiceRealCallProfileProviderRouteOptions, VoiceRealCallProfileReadinessCheckOptions, VoiceRealCallProfileRecoveryActionOptions, VoiceRealCallProfileRecoveryAction, VoiceRealCallProfileRecoveryActionHandler, VoiceRealCallProfileRecoveryActionHandlerInput, VoiceRealCallProfileRecoveryActionId, VoiceRealCallProfileRecoveryJobHistoryCheckOptions, VoiceRealCallProfileRecoveryActionResult, VoiceRealCallProfileRecoveryActionRoutesOptions, VoiceRealCallProfileRecoveryJob, VoiceRealCallProfileRecoveryJobCreateInput, VoiceRealCallProfileRecoveryJobListOptions, VoiceRealCallProfileRecoveryJobStatus, VoiceRealCallProfileRecoveryJobStore, VoiceRealCallProfileRecoveryJobUpdate, VoiceSQLiteRealCallProfileRecoveryJobStoreOptions, VoiceRealCallProfileTraceEvidenceOptions, VoiceRealCallProfileTraceStoreEvidenceOptions } from './proofTrends';
39
39
  export { assertVoiceSloCalibration, buildVoiceSloCalibrationReport, buildVoiceSloReadinessThresholdReport, createVoiceSloReadinessThresholdOptions, createVoiceSloReadinessThresholdRoutes, createVoiceSloThresholdProfile, createVoiceSloCalibrationRoutes, renderVoiceSloCalibrationMarkdown, renderVoiceSloReadinessThresholdHTML, renderVoiceSloReadinessThresholdMarkdown } from './sloCalibration';
40
40
  export type { VoiceSloCalibrationMetricKey, VoiceSloCalibrationOptions, VoiceSloCalibrationReport, VoiceSloCalibrationRoutesOptions, VoiceSloCalibrationSample, VoiceSloCalibrationStatus, VoiceSloCalibrationThreshold, VoiceSloCalibrationThresholds, VoiceSloReadinessThresholdReport, VoiceSloReadinessThresholdReportOptions, VoiceSloReadinessThresholdOptions, VoiceSloReadinessThresholdRoutesOptions, VoiceSloThresholdProfile } from './sloCalibration';
41
41
  export { assertVoiceLiveOpsControlEvidence, assertVoiceLiveOpsEvidence, buildVoiceLiveOpsControlState, createVoiceLiveOpsController, createVoiceLiveOpsRoutes, createVoiceMemoryLiveOpsControlStore, evaluateVoiceLiveOpsControlEvidence, evaluateVoiceLiveOpsEvidence, getVoiceLiveOpsControlStatus, VOICE_LIVE_OPS_ACTIONS } from './liveOps';
package/dist/index.js CHANGED
@@ -16118,6 +16118,13 @@ var createVoiceInMemoryRealCallProfileRecoveryJobStore = (options = {}) => {
16118
16118
  get(id) {
16119
16119
  return jobs.get(id);
16120
16120
  },
16121
+ list(input = {}) {
16122
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
16123
+ return [...jobs.values()].filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).sort((first, second) => {
16124
+ const updatedDelta = Date.parse(second.updatedAt) - Date.parse(first.updatedAt);
16125
+ return updatedDelta === 0 ? second.id.localeCompare(first.id) : updatedDelta;
16126
+ }).slice(0, limit);
16127
+ },
16121
16128
  update(id, update) {
16122
16129
  const existing = jobs.get(id);
16123
16130
  if (!existing) {
@@ -16151,6 +16158,7 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
16151
16158
  payload TEXT NOT NULL
16152
16159
  )`);
16153
16160
  const selectStatement = database.query(`SELECT payload FROM "${tableName}" WHERE id = ?1 LIMIT 1`);
16161
+ const listStatement = database.query(`SELECT payload FROM "${tableName}" ORDER BY sort_at DESC, id DESC`);
16154
16162
  const upsertStatement = database.query(`INSERT INTO "${tableName}" (id, sort_at, payload)
16155
16163
  VALUES (?1, ?2, ?3)
16156
16164
  ON CONFLICT(id) DO UPDATE SET sort_at = excluded.sort_at, payload = excluded.payload`);
@@ -16177,6 +16185,10 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
16177
16185
  get(id) {
16178
16186
  return readJob(id);
16179
16187
  },
16188
+ list(input = {}) {
16189
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
16190
+ return listStatement.all().map((row) => JSON.parse(row.payload)).filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).slice(0, limit);
16191
+ },
16180
16192
  update(id, update) {
16181
16193
  const existing = readJob(id);
16182
16194
  if (!existing) {
@@ -16217,6 +16229,94 @@ var buildVoiceRealCallProfileReadinessCheck = (report, options = {}) => {
16217
16229
  value: `${String(report.defaults.summary.actionableProfiles)}/${String(report.summary.profileCount)} actionable`
16218
16230
  };
16219
16231
  };
16232
+ var buildVoiceRealCallProfileRecoveryJobHistoryCheck = async (store, options = {}) => {
16233
+ const href = options.href ?? "/voice/real-call-profile-recovery";
16234
+ const sourceHref = options.sourceHref ?? "/api/voice/real-call-profile-history/actions/jobs";
16235
+ const minCompletedJobs = options.minCompletedJobs ?? 1;
16236
+ const label = options.label ?? "Real-call recovery job history";
16237
+ if (!store?.list) {
16238
+ return {
16239
+ actions: [
16240
+ {
16241
+ description: "Configure a recovery job store with list support so operators can inspect proof repair history.",
16242
+ href,
16243
+ label: "Configure recovery job history"
16244
+ }
16245
+ ],
16246
+ detail: "No real-call profile recovery job store with list support is configured.",
16247
+ gateExplanation: {
16248
+ evidenceHref: sourceHref,
16249
+ observed: "missing",
16250
+ remediation: "Use the bundled memory or SQLite recovery job store, or implement list() on the custom store.",
16251
+ sourceHref,
16252
+ threshold: "list support",
16253
+ thresholdLabel: "Inspectable recovery jobs",
16254
+ unit: "status"
16255
+ },
16256
+ href,
16257
+ label,
16258
+ status: "fail",
16259
+ value: "missing"
16260
+ };
16261
+ }
16262
+ const jobs = await store.list({ limit: 50 });
16263
+ const now = Date.now();
16264
+ const recentJobs = options.maxAgeMs === undefined ? jobs : jobs.filter((job) => now - Date.parse(job.updatedAt) <= options.maxAgeMs);
16265
+ const completedJobs = recentJobs.filter((job) => job.status === "pass" || job.status === "fail");
16266
+ const failedJobs = recentJobs.filter((job) => job.status === "fail");
16267
+ const runningJobs = recentJobs.filter((job) => job.status === "queued" || job.status === "running");
16268
+ const issues = [];
16269
+ const warnings = [];
16270
+ if (completedJobs.length < minCompletedJobs) {
16271
+ issues.push(`Expected at least ${String(minCompletedJobs)} completed recovery job(s), found ${String(completedJobs.length)}.`);
16272
+ }
16273
+ if (!options.allowFailedJobs && failedJobs.length > 0) {
16274
+ issues.push(`${String(failedJobs.length)} recent recovery job(s) failed.`);
16275
+ }
16276
+ if (runningJobs.length > 0) {
16277
+ const message = `${String(runningJobs.length)} recovery job(s) are still queued or running.`;
16278
+ if (options.failOnRunningJobs) {
16279
+ issues.push(message);
16280
+ } else {
16281
+ warnings.push(message);
16282
+ }
16283
+ }
16284
+ const status = issues.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass";
16285
+ const detail = status === "pass" ? `${String(completedJobs.length)} completed recovery job(s), ${String(failedJobs.length)} failed, ${String(runningJobs.length)} running.` : [...issues, ...warnings].join(" ");
16286
+ return {
16287
+ actions: [
16288
+ {
16289
+ description: "Open the recovery job UI and run or inspect proof repair jobs.",
16290
+ href,
16291
+ label: "Open recovery job history"
16292
+ },
16293
+ {
16294
+ description: "Open the recovery job history API.",
16295
+ href: sourceHref,
16296
+ label: "Open recovery jobs API"
16297
+ }
16298
+ ],
16299
+ detail,
16300
+ gateExplanation: {
16301
+ evidenceHref: sourceHref,
16302
+ observed: completedJobs.length,
16303
+ remediation: "Run a browser or phone recovery proof job and ensure recent recovery jobs complete without failure.",
16304
+ sourceHref,
16305
+ threshold: minCompletedJobs,
16306
+ thresholdLabel: "Minimum completed recovery jobs",
16307
+ unit: "count"
16308
+ },
16309
+ href,
16310
+ label,
16311
+ proofSource: {
16312
+ href: sourceHref,
16313
+ source: "recovery-job-store",
16314
+ sourceLabel: "Real-call recovery jobs"
16315
+ },
16316
+ status,
16317
+ value: `${String(completedJobs.length)} completed / ${String(failedJobs.length)} failed / ${String(runningJobs.length)} running`
16318
+ };
16319
+ };
16220
16320
  var resolveVoiceRealCallProfileProviderRoute = (options) => {
16221
16321
  const defaults = readRealCallProfileDefaultsReport(options.defaults);
16222
16322
  const profile = defaults.profiles.find((item) => item.profileId === options.profileId) ?? defaults.profiles.find((item) => item.status === "pass") ?? defaults.profiles[0];
@@ -16917,6 +17017,32 @@ var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
16917
17017
  };
16918
17018
  };
16919
17019
  routes.get(`${path}/actions`, async () => Response.json(await listActions(), { headers: options.headers }));
17020
+ routes.get(`${path}/actions/jobs`, async ({
17021
+ query,
17022
+ set
17023
+ }) => {
17024
+ if (!options.jobStore?.list) {
17025
+ set.status = 501;
17026
+ return Response.json({
17027
+ jobs: [],
17028
+ message: "No real-call profile recovery job list store is configured.",
17029
+ ok: false
17030
+ }, { headers: options.headers });
17031
+ }
17032
+ const actionId = Object.keys(realCallProfileActionPaths).includes(query.actionId ?? "") ? query.actionId : undefined;
17033
+ const status = ["fail", "pass", "queued", "running"].includes(query.status ?? "") ? query.status : undefined;
17034
+ const limit = Number(query.limit);
17035
+ const jobs = await options.jobStore.list({
17036
+ actionId,
17037
+ limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
17038
+ status
17039
+ });
17040
+ return Response.json({
17041
+ generatedAt: new Date().toISOString(),
17042
+ jobs,
17043
+ ok: true
17044
+ }, { headers: options.headers });
17045
+ });
16920
17046
  routes.get(`${path}/actions/:jobId`, async ({
16921
17047
  params,
16922
17048
  set
@@ -39236,6 +39362,7 @@ export {
39236
39362
  buildVoiceRealtimeProviderContractMatrix,
39237
39363
  buildVoiceRealtimeChannelRuntimeSamplesFromTrace,
39238
39364
  buildVoiceRealtimeChannelReport,
39365
+ buildVoiceRealCallProfileRecoveryJobHistoryCheck,
39239
39366
  buildVoiceRealCallProfileRecoveryActions,
39240
39367
  buildVoiceRealCallProfileReadinessCheck,
39241
39368
  buildVoiceRealCallProfileHistoryReport,
@@ -378,6 +378,15 @@ export type VoiceRealCallProfileReadinessCheckOptions = {
378
378
  sourceHref?: string;
379
379
  };
380
380
  export type VoiceRealCallProfileRecoveryActionOptions = VoiceRealCallProfileReadinessCheckOptions;
381
+ export type VoiceRealCallProfileRecoveryJobHistoryCheckOptions = {
382
+ allowFailedJobs?: boolean;
383
+ failOnRunningJobs?: boolean;
384
+ href?: string;
385
+ label?: string;
386
+ maxAgeMs?: number;
387
+ minCompletedJobs?: number;
388
+ sourceHref?: string;
389
+ };
381
390
  export type VoiceRealCallProfileRecoveryActionHandlerInput = {
382
391
  actionId: VoiceRealCallProfileRecoveryActionId;
383
392
  report: VoiceRealCallProfileHistoryReport;
@@ -414,9 +423,15 @@ export type VoiceRealCallProfileRecoveryJobCreateInput = {
414
423
  status?: VoiceRealCallProfileRecoveryJobStatus;
415
424
  };
416
425
  export type VoiceRealCallProfileRecoveryJobUpdate = Partial<Pick<VoiceRealCallProfileRecoveryJob, 'completedAt' | 'message' | 'ok' | 'report' | 'startedAt' | 'status' | 'updatedAt'>>;
426
+ export type VoiceRealCallProfileRecoveryJobListOptions = {
427
+ actionId?: VoiceRealCallProfileRecoveryActionId;
428
+ limit?: number;
429
+ status?: VoiceRealCallProfileRecoveryJobStatus;
430
+ };
417
431
  export type VoiceRealCallProfileRecoveryJobStore = {
418
432
  create(input: VoiceRealCallProfileRecoveryJobCreateInput): Promise<VoiceRealCallProfileRecoveryJob> | VoiceRealCallProfileRecoveryJob;
419
433
  get(id: string): Promise<VoiceRealCallProfileRecoveryJob | undefined> | VoiceRealCallProfileRecoveryJob | undefined;
434
+ list?(options?: VoiceRealCallProfileRecoveryJobListOptions): Promise<VoiceRealCallProfileRecoveryJob[]> | VoiceRealCallProfileRecoveryJob[];
420
435
  update(id: string, update: VoiceRealCallProfileRecoveryJobUpdate): Promise<VoiceRealCallProfileRecoveryJob | undefined> | VoiceRealCallProfileRecoveryJob | undefined;
421
436
  };
422
437
  export type VoiceSQLiteRealCallProfileRecoveryJobStoreOptions = {
@@ -472,6 +487,7 @@ export declare const createVoiceInMemoryRealCallProfileRecoveryJobStore: (option
472
487
  }) => VoiceRealCallProfileRecoveryJobStore;
473
488
  export declare const createVoiceSQLiteRealCallProfileRecoveryJobStore: (options?: VoiceSQLiteRealCallProfileRecoveryJobStoreOptions) => VoiceRealCallProfileRecoveryJobStore;
474
489
  export declare const buildVoiceRealCallProfileReadinessCheck: (report: VoiceRealCallProfileHistoryReport, options?: VoiceRealCallProfileReadinessCheckOptions) => VoiceProductionReadinessCheck;
490
+ export declare const buildVoiceRealCallProfileRecoveryJobHistoryCheck: (store: Pick<VoiceRealCallProfileRecoveryJobStore, "list"> | undefined, options?: VoiceRealCallProfileRecoveryJobHistoryCheckOptions) => Promise<VoiceProductionReadinessCheck>;
475
491
  export declare const resolveVoiceRealCallProfileProviderRoute: <TProvider extends string = string>(options: VoiceRealCallProfileProviderRouteOptions<TProvider>) => TProvider | undefined;
476
492
  export declare const buildVoiceRealCallProfileHistoryReport: (options?: VoiceRealCallProfileHistoryOptions) => VoiceRealCallProfileHistoryReport;
477
493
  export declare const evaluateVoiceProofTrendEvidence: (report: VoiceProofTrendReport, input?: VoiceProofTrendAssertionInput) => VoiceProofTrendAssertionReport;
@@ -2236,6 +2236,13 @@ var createVoiceInMemoryRealCallProfileRecoveryJobStore = (options = {}) => {
2236
2236
  get(id) {
2237
2237
  return jobs.get(id);
2238
2238
  },
2239
+ list(input = {}) {
2240
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
2241
+ return [...jobs.values()].filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).sort((first, second) => {
2242
+ const updatedDelta = Date.parse(second.updatedAt) - Date.parse(first.updatedAt);
2243
+ return updatedDelta === 0 ? second.id.localeCompare(first.id) : updatedDelta;
2244
+ }).slice(0, limit);
2245
+ },
2239
2246
  update(id, update) {
2240
2247
  const existing = jobs.get(id);
2241
2248
  if (!existing) {
@@ -2269,6 +2276,7 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
2269
2276
  payload TEXT NOT NULL
2270
2277
  )`);
2271
2278
  const selectStatement = database.query(`SELECT payload FROM "${tableName}" WHERE id = ?1 LIMIT 1`);
2279
+ const listStatement = database.query(`SELECT payload FROM "${tableName}" ORDER BY sort_at DESC, id DESC`);
2272
2280
  const upsertStatement = database.query(`INSERT INTO "${tableName}" (id, sort_at, payload)
2273
2281
  VALUES (?1, ?2, ?3)
2274
2282
  ON CONFLICT(id) DO UPDATE SET sort_at = excluded.sort_at, payload = excluded.payload`);
@@ -2295,6 +2303,10 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
2295
2303
  get(id) {
2296
2304
  return readJob(id);
2297
2305
  },
2306
+ list(input = {}) {
2307
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
2308
+ return listStatement.all().map((row) => JSON.parse(row.payload)).filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).slice(0, limit);
2309
+ },
2298
2310
  update(id, update) {
2299
2311
  const existing = readJob(id);
2300
2312
  if (!existing) {
@@ -2335,6 +2347,94 @@ var buildVoiceRealCallProfileReadinessCheck = (report, options = {}) => {
2335
2347
  value: `${String(report.defaults.summary.actionableProfiles)}/${String(report.summary.profileCount)} actionable`
2336
2348
  };
2337
2349
  };
2350
+ var buildVoiceRealCallProfileRecoveryJobHistoryCheck = async (store, options = {}) => {
2351
+ const href = options.href ?? "/voice/real-call-profile-recovery";
2352
+ const sourceHref = options.sourceHref ?? "/api/voice/real-call-profile-history/actions/jobs";
2353
+ const minCompletedJobs = options.minCompletedJobs ?? 1;
2354
+ const label = options.label ?? "Real-call recovery job history";
2355
+ if (!store?.list) {
2356
+ return {
2357
+ actions: [
2358
+ {
2359
+ description: "Configure a recovery job store with list support so operators can inspect proof repair history.",
2360
+ href,
2361
+ label: "Configure recovery job history"
2362
+ }
2363
+ ],
2364
+ detail: "No real-call profile recovery job store with list support is configured.",
2365
+ gateExplanation: {
2366
+ evidenceHref: sourceHref,
2367
+ observed: "missing",
2368
+ remediation: "Use the bundled memory or SQLite recovery job store, or implement list() on the custom store.",
2369
+ sourceHref,
2370
+ threshold: "list support",
2371
+ thresholdLabel: "Inspectable recovery jobs",
2372
+ unit: "status"
2373
+ },
2374
+ href,
2375
+ label,
2376
+ status: "fail",
2377
+ value: "missing"
2378
+ };
2379
+ }
2380
+ const jobs = await store.list({ limit: 50 });
2381
+ const now = Date.now();
2382
+ const recentJobs = options.maxAgeMs === undefined ? jobs : jobs.filter((job) => now - Date.parse(job.updatedAt) <= options.maxAgeMs);
2383
+ const completedJobs = recentJobs.filter((job) => job.status === "pass" || job.status === "fail");
2384
+ const failedJobs = recentJobs.filter((job) => job.status === "fail");
2385
+ const runningJobs = recentJobs.filter((job) => job.status === "queued" || job.status === "running");
2386
+ const issues = [];
2387
+ const warnings = [];
2388
+ if (completedJobs.length < minCompletedJobs) {
2389
+ issues.push(`Expected at least ${String(minCompletedJobs)} completed recovery job(s), found ${String(completedJobs.length)}.`);
2390
+ }
2391
+ if (!options.allowFailedJobs && failedJobs.length > 0) {
2392
+ issues.push(`${String(failedJobs.length)} recent recovery job(s) failed.`);
2393
+ }
2394
+ if (runningJobs.length > 0) {
2395
+ const message = `${String(runningJobs.length)} recovery job(s) are still queued or running.`;
2396
+ if (options.failOnRunningJobs) {
2397
+ issues.push(message);
2398
+ } else {
2399
+ warnings.push(message);
2400
+ }
2401
+ }
2402
+ const status = issues.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass";
2403
+ const detail = status === "pass" ? `${String(completedJobs.length)} completed recovery job(s), ${String(failedJobs.length)} failed, ${String(runningJobs.length)} running.` : [...issues, ...warnings].join(" ");
2404
+ return {
2405
+ actions: [
2406
+ {
2407
+ description: "Open the recovery job UI and run or inspect proof repair jobs.",
2408
+ href,
2409
+ label: "Open recovery job history"
2410
+ },
2411
+ {
2412
+ description: "Open the recovery job history API.",
2413
+ href: sourceHref,
2414
+ label: "Open recovery jobs API"
2415
+ }
2416
+ ],
2417
+ detail,
2418
+ gateExplanation: {
2419
+ evidenceHref: sourceHref,
2420
+ observed: completedJobs.length,
2421
+ remediation: "Run a browser or phone recovery proof job and ensure recent recovery jobs complete without failure.",
2422
+ sourceHref,
2423
+ threshold: minCompletedJobs,
2424
+ thresholdLabel: "Minimum completed recovery jobs",
2425
+ unit: "count"
2426
+ },
2427
+ href,
2428
+ label,
2429
+ proofSource: {
2430
+ href: sourceHref,
2431
+ source: "recovery-job-store",
2432
+ sourceLabel: "Real-call recovery jobs"
2433
+ },
2434
+ status,
2435
+ value: `${String(completedJobs.length)} completed / ${String(failedJobs.length)} failed / ${String(runningJobs.length)} running`
2436
+ };
2437
+ };
2338
2438
  var resolveVoiceRealCallProfileProviderRoute = (options) => {
2339
2439
  const defaults = readRealCallProfileDefaultsReport(options.defaults);
2340
2440
  const profile = defaults.profiles.find((item) => item.profileId === options.profileId) ?? defaults.profiles.find((item) => item.status === "pass") ?? defaults.profiles[0];
@@ -3035,6 +3135,32 @@ var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
3035
3135
  };
3036
3136
  };
3037
3137
  routes.get(`${path}/actions`, async () => Response.json(await listActions(), { headers: options.headers }));
3138
+ routes.get(`${path}/actions/jobs`, async ({
3139
+ query,
3140
+ set
3141
+ }) => {
3142
+ if (!options.jobStore?.list) {
3143
+ set.status = 501;
3144
+ return Response.json({
3145
+ jobs: [],
3146
+ message: "No real-call profile recovery job list store is configured.",
3147
+ ok: false
3148
+ }, { headers: options.headers });
3149
+ }
3150
+ const actionId = Object.keys(realCallProfileActionPaths).includes(query.actionId ?? "") ? query.actionId : undefined;
3151
+ const status = ["fail", "pass", "queued", "running"].includes(query.status ?? "") ? query.status : undefined;
3152
+ const limit = Number(query.limit);
3153
+ const jobs = await options.jobStore.list({
3154
+ actionId,
3155
+ limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
3156
+ status
3157
+ });
3158
+ return Response.json({
3159
+ generatedAt: new Date().toISOString(),
3160
+ jobs,
3161
+ ok: true
3162
+ }, { headers: options.headers });
3163
+ });
3038
3164
  routes.get(`${path}/actions/:jobId`, async ({
3039
3165
  params,
3040
3166
  set
package/dist/vue/index.js CHANGED
@@ -2157,6 +2157,13 @@ var createVoiceInMemoryRealCallProfileRecoveryJobStore = (options = {}) => {
2157
2157
  get(id) {
2158
2158
  return jobs.get(id);
2159
2159
  },
2160
+ list(input = {}) {
2161
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
2162
+ return [...jobs.values()].filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).sort((first, second) => {
2163
+ const updatedDelta = Date.parse(second.updatedAt) - Date.parse(first.updatedAt);
2164
+ return updatedDelta === 0 ? second.id.localeCompare(first.id) : updatedDelta;
2165
+ }).slice(0, limit);
2166
+ },
2160
2167
  update(id, update) {
2161
2168
  const existing = jobs.get(id);
2162
2169
  if (!existing) {
@@ -2190,6 +2197,7 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
2190
2197
  payload TEXT NOT NULL
2191
2198
  )`);
2192
2199
  const selectStatement = database.query(`SELECT payload FROM "${tableName}" WHERE id = ?1 LIMIT 1`);
2200
+ const listStatement = database.query(`SELECT payload FROM "${tableName}" ORDER BY sort_at DESC, id DESC`);
2193
2201
  const upsertStatement = database.query(`INSERT INTO "${tableName}" (id, sort_at, payload)
2194
2202
  VALUES (?1, ?2, ?3)
2195
2203
  ON CONFLICT(id) DO UPDATE SET sort_at = excluded.sort_at, payload = excluded.payload`);
@@ -2216,6 +2224,10 @@ var createVoiceSQLiteRealCallProfileRecoveryJobStore = (options = {}) => {
2216
2224
  get(id) {
2217
2225
  return readJob(id);
2218
2226
  },
2227
+ list(input = {}) {
2228
+ const limit = Number.isFinite(input.limit) && input.limit !== undefined && input.limit > 0 ? Math.floor(input.limit) : 50;
2229
+ return listStatement.all().map((row) => JSON.parse(row.payload)).filter((job) => !input.actionId || job.actionId === input.actionId).filter((job) => !input.status || job.status === input.status).slice(0, limit);
2230
+ },
2219
2231
  update(id, update) {
2220
2232
  const existing = readJob(id);
2221
2233
  if (!existing) {
@@ -2256,6 +2268,94 @@ var buildVoiceRealCallProfileReadinessCheck = (report, options = {}) => {
2256
2268
  value: `${String(report.defaults.summary.actionableProfiles)}/${String(report.summary.profileCount)} actionable`
2257
2269
  };
2258
2270
  };
2271
+ var buildVoiceRealCallProfileRecoveryJobHistoryCheck = async (store, options = {}) => {
2272
+ const href = options.href ?? "/voice/real-call-profile-recovery";
2273
+ const sourceHref = options.sourceHref ?? "/api/voice/real-call-profile-history/actions/jobs";
2274
+ const minCompletedJobs = options.minCompletedJobs ?? 1;
2275
+ const label = options.label ?? "Real-call recovery job history";
2276
+ if (!store?.list) {
2277
+ return {
2278
+ actions: [
2279
+ {
2280
+ description: "Configure a recovery job store with list support so operators can inspect proof repair history.",
2281
+ href,
2282
+ label: "Configure recovery job history"
2283
+ }
2284
+ ],
2285
+ detail: "No real-call profile recovery job store with list support is configured.",
2286
+ gateExplanation: {
2287
+ evidenceHref: sourceHref,
2288
+ observed: "missing",
2289
+ remediation: "Use the bundled memory or SQLite recovery job store, or implement list() on the custom store.",
2290
+ sourceHref,
2291
+ threshold: "list support",
2292
+ thresholdLabel: "Inspectable recovery jobs",
2293
+ unit: "status"
2294
+ },
2295
+ href,
2296
+ label,
2297
+ status: "fail",
2298
+ value: "missing"
2299
+ };
2300
+ }
2301
+ const jobs = await store.list({ limit: 50 });
2302
+ const now = Date.now();
2303
+ const recentJobs = options.maxAgeMs === undefined ? jobs : jobs.filter((job) => now - Date.parse(job.updatedAt) <= options.maxAgeMs);
2304
+ const completedJobs = recentJobs.filter((job) => job.status === "pass" || job.status === "fail");
2305
+ const failedJobs = recentJobs.filter((job) => job.status === "fail");
2306
+ const runningJobs = recentJobs.filter((job) => job.status === "queued" || job.status === "running");
2307
+ const issues = [];
2308
+ const warnings = [];
2309
+ if (completedJobs.length < minCompletedJobs) {
2310
+ issues.push(`Expected at least ${String(minCompletedJobs)} completed recovery job(s), found ${String(completedJobs.length)}.`);
2311
+ }
2312
+ if (!options.allowFailedJobs && failedJobs.length > 0) {
2313
+ issues.push(`${String(failedJobs.length)} recent recovery job(s) failed.`);
2314
+ }
2315
+ if (runningJobs.length > 0) {
2316
+ const message = `${String(runningJobs.length)} recovery job(s) are still queued or running.`;
2317
+ if (options.failOnRunningJobs) {
2318
+ issues.push(message);
2319
+ } else {
2320
+ warnings.push(message);
2321
+ }
2322
+ }
2323
+ const status = issues.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass";
2324
+ const detail = status === "pass" ? `${String(completedJobs.length)} completed recovery job(s), ${String(failedJobs.length)} failed, ${String(runningJobs.length)} running.` : [...issues, ...warnings].join(" ");
2325
+ return {
2326
+ actions: [
2327
+ {
2328
+ description: "Open the recovery job UI and run or inspect proof repair jobs.",
2329
+ href,
2330
+ label: "Open recovery job history"
2331
+ },
2332
+ {
2333
+ description: "Open the recovery job history API.",
2334
+ href: sourceHref,
2335
+ label: "Open recovery jobs API"
2336
+ }
2337
+ ],
2338
+ detail,
2339
+ gateExplanation: {
2340
+ evidenceHref: sourceHref,
2341
+ observed: completedJobs.length,
2342
+ remediation: "Run a browser or phone recovery proof job and ensure recent recovery jobs complete without failure.",
2343
+ sourceHref,
2344
+ threshold: minCompletedJobs,
2345
+ thresholdLabel: "Minimum completed recovery jobs",
2346
+ unit: "count"
2347
+ },
2348
+ href,
2349
+ label,
2350
+ proofSource: {
2351
+ href: sourceHref,
2352
+ source: "recovery-job-store",
2353
+ sourceLabel: "Real-call recovery jobs"
2354
+ },
2355
+ status,
2356
+ value: `${String(completedJobs.length)} completed / ${String(failedJobs.length)} failed / ${String(runningJobs.length)} running`
2357
+ };
2358
+ };
2259
2359
  var resolveVoiceRealCallProfileProviderRoute = (options) => {
2260
2360
  const defaults = readRealCallProfileDefaultsReport(options.defaults);
2261
2361
  const profile = defaults.profiles.find((item) => item.profileId === options.profileId) ?? defaults.profiles.find((item) => item.status === "pass") ?? defaults.profiles[0];
@@ -2956,6 +3056,32 @@ var createVoiceRealCallProfileRecoveryActionRoutes = (options = {}) => {
2956
3056
  };
2957
3057
  };
2958
3058
  routes.get(`${path}/actions`, async () => Response.json(await listActions(), { headers: options.headers }));
3059
+ routes.get(`${path}/actions/jobs`, async ({
3060
+ query,
3061
+ set
3062
+ }) => {
3063
+ if (!options.jobStore?.list) {
3064
+ set.status = 501;
3065
+ return Response.json({
3066
+ jobs: [],
3067
+ message: "No real-call profile recovery job list store is configured.",
3068
+ ok: false
3069
+ }, { headers: options.headers });
3070
+ }
3071
+ const actionId = Object.keys(realCallProfileActionPaths).includes(query.actionId ?? "") ? query.actionId : undefined;
3072
+ const status = ["fail", "pass", "queued", "running"].includes(query.status ?? "") ? query.status : undefined;
3073
+ const limit = Number(query.limit);
3074
+ const jobs = await options.jobStore.list({
3075
+ actionId,
3076
+ limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
3077
+ status
3078
+ });
3079
+ return Response.json({
3080
+ generatedAt: new Date().toISOString(),
3081
+ jobs,
3082
+ ok: true
3083
+ }, { headers: options.headers });
3084
+ });
2959
3085
  routes.get(`${path}/actions/:jobId`, async ({
2960
3086
  params,
2961
3087
  set
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.375",
3
+ "version": "0.0.22-beta.377",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",