wxcli 1.2.0__py3-none-any.whl

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.
Files changed (413) hide show
  1. wxcli/__init__.py +4 -0
  2. wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
  3. wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
  4. wxcli/_playbook/.claude/rules/cleanup.md +29 -0
  5. wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
  6. wxcli/_playbook/.claude/rules/org-health.md +14 -0
  7. wxcli/_playbook/.claude/settings.json +8 -0
  8. wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
  9. wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
  10. wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
  11. wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
  12. wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
  13. wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
  14. wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
  15. wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
  16. wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
  17. wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
  18. wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
  19. wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
  20. wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
  21. wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
  22. wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
  23. wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
  24. wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
  25. wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
  26. wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
  27. wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
  28. wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
  29. wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
  30. wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
  31. wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
  32. wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
  33. wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
  34. wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
  35. wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
  36. wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
  37. wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
  38. wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
  39. wxcli/_playbook/CLAUDE.md +373 -0
  40. wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
  41. wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
  42. wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
  43. wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
  44. wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
  45. wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
  46. wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
  47. wxcli/_playbook/docs/reference/admin-partner.md +357 -0
  48. wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
  49. wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
  50. wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
  51. wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
  52. wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
  53. wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
  54. wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
  55. wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
  56. wxcli/_playbook/docs/reference/authentication.md +965 -0
  57. wxcli/_playbook/docs/reference/call-control.md +1205 -0
  58. wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
  59. wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
  60. wxcli/_playbook/docs/reference/call-routing.md +2304 -0
  61. wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
  62. wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
  63. wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
  64. wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
  65. wxcli/_playbook/docs/reference/devices-core.md +1869 -0
  66. wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
  67. wxcli/_playbook/docs/reference/devices-platform.md +467 -0
  68. wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
  69. wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
  70. wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
  71. wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
  72. wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
  73. wxcli/_playbook/docs/reference/meetings-content.md +473 -0
  74. wxcli/_playbook/docs/reference/meetings-core.md +903 -0
  75. wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
  76. wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
  77. wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
  78. wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
  79. wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
  80. wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
  81. wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
  82. wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
  83. wxcli/_playbook/docs/reference/provisioning.md +1366 -0
  84. wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
  85. wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
  86. wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
  87. wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
  88. wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
  89. wxcli/_version.py +24 -0
  90. wxcli/auth.py +137 -0
  91. wxcli/commands/__init__.py +0 -0
  92. wxcli/commands/_registry.py +181 -0
  93. wxcli/commands/activation_email.py +103 -0
  94. wxcli/commands/admin_recordings.py +585 -0
  95. wxcli/commands/analytics.py +105 -0
  96. wxcli/commands/announcement_playlists.py +219 -0
  97. wxcli/commands/announcements.py +519 -0
  98. wxcli/commands/archive_users.py +36 -0
  99. wxcli/commands/attachment_actions.py +60 -0
  100. wxcli/commands/audit_events.py +83 -0
  101. wxcli/commands/authorizations.py +113 -0
  102. wxcli/commands/auto_attendant.py +642 -0
  103. wxcli/commands/broadworks_billing_reports.py +126 -0
  104. wxcli/commands/broadworks_enterprises.py +160 -0
  105. wxcli/commands/broadworks_subscribers.py +319 -0
  106. wxcli/commands/broadworks_workspaces.py +108 -0
  107. wxcli/commands/call_controls.py +1304 -0
  108. wxcli/commands/call_park.py +494 -0
  109. wxcli/commands/call_pickup.py +229 -0
  110. wxcli/commands/call_queue.py +1878 -0
  111. wxcli/commands/call_recording.py +707 -0
  112. wxcli/commands/call_routing.py +1610 -0
  113. wxcli/commands/call_settings_for_me_phase_5.py +212 -0
  114. wxcli/commands/caller_reputation.py +169 -0
  115. wxcli/commands/calling_service.py +305 -0
  116. wxcli/commands/cc_address_book.py +770 -0
  117. wxcli/commands/cc_agent_greetings.py +242 -0
  118. wxcli/commands/cc_agent_summaries.py +39 -0
  119. wxcli/commands/cc_agent_wellbeing.py +216 -0
  120. wxcli/commands/cc_agents.py +425 -0
  121. wxcli/commands/cc_ai_assistant.py +55 -0
  122. wxcli/commands/cc_ai_feature.py +294 -0
  123. wxcli/commands/cc_audio_files.py +149 -0
  124. wxcli/commands/cc_auto_csat.py +311 -0
  125. wxcli/commands/cc_aux_code.py +458 -0
  126. wxcli/commands/cc_business_hour.py +353 -0
  127. wxcli/commands/cc_call_monitoring.py +228 -0
  128. wxcli/commands/cc_callbacks.py +194 -0
  129. wxcli/commands/cc_campaign.py +212 -0
  130. wxcli/commands/cc_captures.py +39 -0
  131. wxcli/commands/cc_contact_list.py +169 -0
  132. wxcli/commands/cc_contact_number.py +295 -0
  133. wxcli/commands/cc_data_sources.py +235 -0
  134. wxcli/commands/cc_desktop_layout.py +380 -0
  135. wxcli/commands/cc_desktop_profile.py +322 -0
  136. wxcli/commands/cc_dial_number.py +511 -0
  137. wxcli/commands/cc_dial_plan.py +347 -0
  138. wxcli/commands/cc_dnc.py +101 -0
  139. wxcli/commands/cc_entry_point.py +545 -0
  140. wxcli/commands/cc_ewt.py +51 -0
  141. wxcli/commands/cc_flow.py +477 -0
  142. wxcli/commands/cc_global_vars.py +430 -0
  143. wxcli/commands/cc_holiday_list.py +335 -0
  144. wxcli/commands/cc_journey.py +1261 -0
  145. wxcli/commands/cc_legacy_flows.py +49 -0
  146. wxcli/commands/cc_multimedia_profile.py +436 -0
  147. wxcli/commands/cc_notification.py +51 -0
  148. wxcli/commands/cc_outdial_ani.py +604 -0
  149. wxcli/commands/cc_overrides.py +341 -0
  150. wxcli/commands/cc_queue.py +1083 -0
  151. wxcli/commands/cc_queue_stats.py +55 -0
  152. wxcli/commands/cc_realtime.py +51 -0
  153. wxcli/commands/cc_resource_collection.py +312 -0
  154. wxcli/commands/cc_search.py +46 -0
  155. wxcli/commands/cc_site.py +400 -0
  156. wxcli/commands/cc_skill.py +350 -0
  157. wxcli/commands/cc_skill_profile.py +289 -0
  158. wxcli/commands/cc_subscriptions.py +426 -0
  159. wxcli/commands/cc_summaries.py +130 -0
  160. wxcli/commands/cc_tasks.py +844 -0
  161. wxcli/commands/cc_team.py +325 -0
  162. wxcli/commands/cc_user_profiles.py +349 -0
  163. wxcli/commands/cc_users.py +701 -0
  164. wxcli/commands/cc_work_types.py +363 -0
  165. wxcli/commands/cdr.py +89 -0
  166. wxcli/commands/classifications.py +38 -0
  167. wxcli/commands/cleanup.py +1427 -0
  168. wxcli/commands/client_settings.py +72 -0
  169. wxcli/commands/conference.py +328 -0
  170. wxcli/commands/configure.py +72 -0
  171. wxcli/commands/converged_recordings.py +347 -0
  172. wxcli/commands/converged_recordings_export.py +310 -0
  173. wxcli/commands/cq_playlists.py +42 -0
  174. wxcli/commands/cucm.py +2794 -0
  175. wxcli/commands/cucm_config.py +129 -0
  176. wxcli/commands/customer_assist.py +373 -0
  177. wxcli/commands/data_sources.py +227 -0
  178. wxcli/commands/dect_devices.py +787 -0
  179. wxcli/commands/device_configurations.py +77 -0
  180. wxcli/commands/device_dynamic_settings.py +320 -0
  181. wxcli/commands/device_settings.py +1489 -0
  182. wxcli/commands/devices.py +280 -0
  183. wxcli/commands/domains.py +142 -0
  184. wxcli/commands/ecm.py +178 -0
  185. wxcli/commands/emergency_services.py +827 -0
  186. wxcli/commands/events.py +83 -0
  187. wxcli/commands/external_voicemail.py +52 -0
  188. wxcli/commands/groups.py +206 -0
  189. wxcli/commands/guest_management.py +76 -0
  190. wxcli/commands/hds.py +340 -0
  191. wxcli/commands/hot_desk.py +67 -0
  192. wxcli/commands/hot_desking_members.py +118 -0
  193. wxcli/commands/hot_desking_portal.py +124 -0
  194. wxcli/commands/hunt_group.py +577 -0
  195. wxcli/commands/hybrid_clusters.py +71 -0
  196. wxcli/commands/hybrid_connectors.py +67 -0
  197. wxcli/commands/identity_org.py +159 -0
  198. wxcli/commands/init_playbook.py +155 -0
  199. wxcli/commands/licenses.py +106 -0
  200. wxcli/commands/live_monitoring.py +40 -0
  201. wxcli/commands/location_call_handling.py +577 -0
  202. wxcli/commands/location_schedules.py +341 -0
  203. wxcli/commands/location_settings.py +1297 -0
  204. wxcli/commands/location_voicemail.py +494 -0
  205. wxcli/commands/locations.py +336 -0
  206. wxcli/commands/meeting_captions.py +110 -0
  207. wxcli/commands/meeting_chats.py +63 -0
  208. wxcli/commands/meeting_invitees.py +234 -0
  209. wxcli/commands/meeting_messages.py +28 -0
  210. wxcli/commands/meeting_participants.py +278 -0
  211. wxcli/commands/meeting_polls.py +111 -0
  212. wxcli/commands/meeting_preferences.py +515 -0
  213. wxcli/commands/meeting_qa.py +78 -0
  214. wxcli/commands/meeting_qualities.py +43 -0
  215. wxcli/commands/meeting_reports.py +101 -0
  216. wxcli/commands/meeting_session_types.py +105 -0
  217. wxcli/commands/meeting_site.py +57 -0
  218. wxcli/commands/meeting_slido.py +44 -0
  219. wxcli/commands/meeting_summaries.py +92 -0
  220. wxcli/commands/meeting_tracking_codes.py +233 -0
  221. wxcli/commands/meeting_transcripts.py +230 -0
  222. wxcli/commands/meetings.py +1997 -0
  223. wxcli/commands/memberships.py +164 -0
  224. wxcli/commands/messages.py +213 -0
  225. wxcli/commands/mode_management.py +244 -0
  226. wxcli/commands/my_call_settings.py +3577 -0
  227. wxcli/commands/numbers.py +405 -0
  228. wxcli/commands/operating_modes.py +410 -0
  229. wxcli/commands/org_contacts.py +284 -0
  230. wxcli/commands/org_health_cli.py +55 -0
  231. wxcli/commands/org_settings.py +71 -0
  232. wxcli/commands/organizations.py +83 -0
  233. wxcli/commands/paging_group.py +257 -0
  234. wxcli/commands/partner_admins.py +141 -0
  235. wxcli/commands/partner_reports.py +188 -0
  236. wxcli/commands/partner_tags.py +216 -0
  237. wxcli/commands/people.py +279 -0
  238. wxcli/commands/person_call_settings.py +71 -0
  239. wxcli/commands/pstn.py +280 -0
  240. wxcli/commands/recording_report.py +154 -0
  241. wxcli/commands/report_templates.py +38 -0
  242. wxcli/commands/reports.py +144 -0
  243. wxcli/commands/resource_group_memberships.py +160 -0
  244. wxcli/commands/resource_groups.py +67 -0
  245. wxcli/commands/roles.py +63 -0
  246. wxcli/commands/room_tabs.py +160 -0
  247. wxcli/commands/rooms.py +225 -0
  248. wxcli/commands/scim_bulk.py +46 -0
  249. wxcli/commands/scim_groups.py +246 -0
  250. wxcli/commands/scim_schemas.py +82 -0
  251. wxcli/commands/scim_users.py +283 -0
  252. wxcli/commands/security_audit.py +54 -0
  253. wxcli/commands/service_apps.py +51 -0
  254. wxcli/commands/single_number_reach.py +209 -0
  255. wxcli/commands/team_memberships.py +155 -0
  256. wxcli/commands/teams.py +153 -0
  257. wxcli/commands/ucm_profile.py +42 -0
  258. wxcli/commands/update.py +219 -0
  259. wxcli/commands/user_settings.py +5116 -0
  260. wxcli/commands/video_mesh.py +1090 -0
  261. wxcli/commands/virtual_extensions.py +502 -0
  262. wxcli/commands/virtual_line_settings.py +2072 -0
  263. wxcli/commands/webhooks.py +176 -0
  264. wxcli/commands/wholesale_billing_reports.py +147 -0
  265. wxcli/commands/wholesale_provisioning.py +523 -0
  266. wxcli/commands/workspace_locations.py +339 -0
  267. wxcli/commands/workspace_metrics.py +103 -0
  268. wxcli/commands/workspace_personalization.py +70 -0
  269. wxcli/commands/workspace_settings.py +3347 -0
  270. wxcli/commands/workspaces.py +279 -0
  271. wxcli/commands/xapi.py +114 -0
  272. wxcli/config.py +139 -0
  273. wxcli/errors.py +77 -0
  274. wxcli/main.py +210 -0
  275. wxcli/migration/CLAUDE.md +106 -0
  276. wxcli/migration/__init__.py +1 -0
  277. wxcli/migration/advisory/CLAUDE.md +196 -0
  278. wxcli/migration/advisory/__init__.py +46 -0
  279. wxcli/migration/advisory/advisor.py +107 -0
  280. wxcli/migration/advisory/advisory_patterns.py +2552 -0
  281. wxcli/migration/advisory/recommendation_rules.py +835 -0
  282. wxcli/migration/cucm/CLAUDE.md +74 -0
  283. wxcli/migration/cucm/__init__.py +5 -0
  284. wxcli/migration/cucm/connection.py +265 -0
  285. wxcli/migration/cucm/discovery.py +225 -0
  286. wxcli/migration/cucm/extractors/__init__.py +1 -0
  287. wxcli/migration/cucm/extractors/announcements.py +92 -0
  288. wxcli/migration/cucm/extractors/base.py +90 -0
  289. wxcli/migration/cucm/extractors/device_profiles.py +98 -0
  290. wxcli/migration/cucm/extractors/devices.py +193 -0
  291. wxcli/migration/cucm/extractors/e911.py +103 -0
  292. wxcli/migration/cucm/extractors/features.py +309 -0
  293. wxcli/migration/cucm/extractors/helpers.py +73 -0
  294. wxcli/migration/cucm/extractors/informational.py +295 -0
  295. wxcli/migration/cucm/extractors/locations.py +221 -0
  296. wxcli/migration/cucm/extractors/moh.py +100 -0
  297. wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
  298. wxcli/migration/cucm/extractors/routing.py +493 -0
  299. wxcli/migration/cucm/extractors/shared_lines.py +146 -0
  300. wxcli/migration/cucm/extractors/templates.py +229 -0
  301. wxcli/migration/cucm/extractors/tier4.py +173 -0
  302. wxcli/migration/cucm/extractors/users.py +240 -0
  303. wxcli/migration/cucm/extractors/voicemail.py +243 -0
  304. wxcli/migration/cucm/extractors/workspaces.py +88 -0
  305. wxcli/migration/cucm/unity_connection.py +304 -0
  306. wxcli/migration/execute/CLAUDE.md +372 -0
  307. wxcli/migration/execute/__init__.py +303 -0
  308. wxcli/migration/execute/batch.py +277 -0
  309. wxcli/migration/execute/dependency.py +483 -0
  310. wxcli/migration/execute/engine.py +964 -0
  311. wxcli/migration/execute/handlers.py +2232 -0
  312. wxcli/migration/execute/planner.py +1939 -0
  313. wxcli/migration/execute/runtime.py +571 -0
  314. wxcli/migration/export/CLAUDE.md +49 -0
  315. wxcli/migration/export/__init__.py +8 -0
  316. wxcli/migration/export/csv_export.py +126 -0
  317. wxcli/migration/export/deployment_plan.py +479 -0
  318. wxcli/migration/export/json_export.py +60 -0
  319. wxcli/migration/models.py +941 -0
  320. wxcli/migration/phone_models.py +50 -0
  321. wxcli/migration/preflight/CLAUDE.md +52 -0
  322. wxcli/migration/preflight/__init__.py +89 -0
  323. wxcli/migration/preflight/checks.py +911 -0
  324. wxcli/migration/preflight/runner.py +293 -0
  325. wxcli/migration/rate_limiter.py +144 -0
  326. wxcli/migration/report/CLAUDE.md +245 -0
  327. wxcli/migration/report/__init__.py +7 -0
  328. wxcli/migration/report/appendix.py +2437 -0
  329. wxcli/migration/report/assembler.py +183 -0
  330. wxcli/migration/report/charts.py +263 -0
  331. wxcli/migration/report/executive.py +776 -0
  332. wxcli/migration/report/explainer.py +704 -0
  333. wxcli/migration/report/helpers.py +74 -0
  334. wxcli/migration/report/ingest.py +152 -0
  335. wxcli/migration/report/notice_templates.py +192 -0
  336. wxcli/migration/report/score.py +375 -0
  337. wxcli/migration/report/styles.py +1052 -0
  338. wxcli/migration/report/user_diff.py +1052 -0
  339. wxcli/migration/report/user_notice.py +425 -0
  340. wxcli/migration/state.py +139 -0
  341. wxcli/migration/store.py +860 -0
  342. wxcli/migration/transform/CLAUDE.md +107 -0
  343. wxcli/migration/transform/__init__.py +12 -0
  344. wxcli/migration/transform/analysis_pipeline.py +453 -0
  345. wxcli/migration/transform/analyzers/__init__.py +142 -0
  346. wxcli/migration/transform/analyzers/css_permission.py +189 -0
  347. wxcli/migration/transform/analyzers/css_routing.py +335 -0
  348. wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
  349. wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
  350. wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
  351. wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
  352. wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
  353. wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
  354. wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
  355. wxcli/migration/transform/analyzers/missing_data.py +300 -0
  356. wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
  357. wxcli/migration/transform/analyzers/shared_line.py +170 -0
  358. wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
  359. wxcli/migration/transform/analyzers/workspace_license.py +212 -0
  360. wxcli/migration/transform/cross_reference.py +1136 -0
  361. wxcli/migration/transform/cucm_pattern.py +246 -0
  362. wxcli/migration/transform/decisions.py +301 -0
  363. wxcli/migration/transform/e164.py +145 -0
  364. wxcli/migration/transform/engine.py +256 -0
  365. wxcli/migration/transform/mappers/CLAUDE.md +123 -0
  366. wxcli/migration/transform/mappers/__init__.py +76 -0
  367. wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
  368. wxcli/migration/transform/mappers/base.py +190 -0
  369. wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
  370. wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
  371. wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
  372. wxcli/migration/transform/mappers/css_mapper.py +868 -0
  373. wxcli/migration/transform/mappers/dect_mapper.py +367 -0
  374. wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
  375. wxcli/migration/transform/mappers/device_mapper.py +227 -0
  376. wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
  377. wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
  378. wxcli/migration/transform/mappers/e911_mapper.py +129 -0
  379. wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
  380. wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
  381. wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
  382. wxcli/migration/transform/mappers/line_mapper.py +310 -0
  383. wxcli/migration/transform/mappers/location_mapper.py +235 -0
  384. wxcli/migration/transform/mappers/moh_mapper.py +114 -0
  385. wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
  386. wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
  387. wxcli/migration/transform/mappers/routing_mapper.py +821 -0
  388. wxcli/migration/transform/mappers/snr_mapper.py +201 -0
  389. wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
  390. wxcli/migration/transform/mappers/user_mapper.py +264 -0
  391. wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
  392. wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
  393. wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
  394. wxcli/migration/transform/normalizers.py +2008 -0
  395. wxcli/migration/transform/pattern_converter.py +106 -0
  396. wxcli/migration/transform/pipeline.py +271 -0
  397. wxcli/migration/transform/rules.py +255 -0
  398. wxcli/org_health/CLAUDE.md +63 -0
  399. wxcli/org_health/__init__.py +7 -0
  400. wxcli/org_health/analyze.py +93 -0
  401. wxcli/org_health/checks.py +458 -0
  402. wxcli/org_health/collector.py +62 -0
  403. wxcli/org_health/models.py +78 -0
  404. wxcli/org_health/report.py +300 -0
  405. wxcli/output.py +101 -0
  406. wxcli-1.2.0.dist-info/METADATA +505 -0
  407. wxcli-1.2.0.dist-info/RECORD +413 -0
  408. wxcli-1.2.0.dist-info/WHEEL +5 -0
  409. wxcli-1.2.0.dist-info/entry_points.txt +2 -0
  410. wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
  411. wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
  412. wxcli-1.2.0.dist-info/scm_version.json +8 -0
  413. wxcli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1628 @@
1
+ # CDR Recipe Catalog — 75 Recipes
2
+
3
+ Lookup catalog of ready-to-run CDR query recipes for the `reporting` skill. Each recipe is a self-contained `wxcli cdr list ... | python3.11` snippet answering one common question.
4
+
5
+ **How to use this file:**
6
+ - This catalog is the *recipes only*. The **composition method** — Call Leg Model, Field Taxonomy, Composition Rules, and Output Patterns — lives in `../SKILL.md` (§ CDR Query Composition Guide). Read that first; it tells you how to read, adapt, and compose any recipe.
7
+ - **Find a recipe fast:** list them all with `grep -nE '^### Category|^\*\*Recipe' references/cdr-recipes.md`, or grep by keyword — e.g. `grep -ni "emergency" references/cdr-recipes.md`, `grep -ni "hold" references/cdr-recipes.md`.
8
+ - **No recipe fits?** Compose a new query from the Field Taxonomy + Output Patterns in `../SKILL.md`. The recipes here are examples of those patterns, not an exhaustive list.
9
+ - All recipes use the **CDR Feed** (`wxcli cdr list`, 12-hour max window, 30-day retention) unless a recipe says otherwise. Replace `START`/`END` with ISO 8601 timestamps (`2026-04-10T14:00:00.000Z`).
10
+
11
+ ## Categories
12
+
13
+ | # | Category | Recipes | Covers |
14
+ |---|----------|---------|--------|
15
+ | 1 | Call Volume & Traffic | 1–8 | total count, by location/hour/day/type, inbound vs outbound, peak hour, trend |
16
+ | 2 | Call Outcomes & Quality | 9–16 | missed/failed/abandoned, answer rate, outcome reason, short calls, long-ring no-answer |
17
+ | 3 | Hold & Wait Time | 17–24 | avg hold, excessive hold (>30s/>60s), hold-then-abandon, ring duration, hold by user |
18
+ | 4 | User & Agent Performance | 25–32 | top talkers, calls/duration per user, missed per user, answer rate, international, after-hours |
19
+ | 5 | Trunk & Routing | 33–39 | calls per trunk, utilization by hour, route groups, redirect/original reason, forwarding loops |
20
+ | 6 | PSTN & Billing | 40–46 | by vendor, duration by vendor, international by country, call-type mix, auth codes, vendor comparison |
21
+ | 7 | Device & Client | 47–52 | client type, device model inventory, OS type, softphone vs desk, Webex Go mobile, client version |
22
+ | 8 | Spam & Reputation | 53–57 | reputation score distribution, blocked vs allowed, top blocked numbers, spam by location, captcha |
23
+ | 9 | Recording Compliance | 58–63 | success rate, by platform, by trigger, failed recordings, unrecorded analysis, compliance by location |
24
+ | 10 | Call Tracing & Diagnostics | 64–69 | trace by correlation ID, transfer chains, park/retrieve, forwarding path, quality proxy, repeat callers |
25
+ | 11 | Cross-Category Compound | 70–75 | multi-filter queries combining dimensions from other categories: spam→queue→abandon, international calls on a trunk during business hours, forwarded-twice→voicemail, missed calls from repeat callers, long-hold calls that were transferred, after-hours emergency (look here when a question crosses trunk + call-type + time, etc., not just one category) |
26
+
27
+ ---
28
+
29
+ ### Category 1: Call Volume & Traffic
30
+
31
+ **Recipe 1 — Total call count**
32
+ Question: "How many calls did we get?"
33
+ Note: Unique Correlation ID count = best proxy for "number of calls." Raw CDR count and ORIGINATING count both overcount — HG calls generate multiple ORIGINATING CDRs (one per ring attempt) and multiple TERMINATING CDRs (HG + agent per attempt).
34
+ # Output: Metric | Value
35
+ ```bash
36
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
37
+ import json, sys
38
+ data = json.load(sys.stdin)
39
+ orig = len([r for r in data if r.get('Direction') == 'ORIGINATING'])
40
+ term = len([r for r in data if r.get('Direction') == 'TERMINATING'])
41
+ unique = len(set(r.get('Correlation ID') for r in data if r.get('Correlation ID')))
42
+ # Raw CDR counts are inflated for HG calls (multiple O+T CDRs per ring attempt).
43
+ # Unique Correlation ID count is the best proxy for unique calls.
44
+ print(f'Total CDR legs: {len(data)} (ORIGINATING: {orig}, TERMINATING: {term})')
45
+ print(f'Unique calls (by Correlation ID): {unique}')
46
+ "
47
+ ```
48
+
49
+ **Recipe 2 — Calls by location**
50
+ Question: "Which office is busiest?"
51
+ # Output: Location | Total | Answered | Missed | Answer% | Avg Duration(s)
52
+ ```bash
53
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
54
+ import json, sys
55
+ from collections import defaultdict
56
+ data = json.load(sys.stdin)
57
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
58
+ for r in data:
59
+ loc = r.get('Location','Unknown')
60
+ stats[loc]['total'] += 1
61
+ if r.get('Answer indicator') == 'Yes':
62
+ stats[loc]['answered'] += 1
63
+ stats[loc]['dur'] += int(r.get('Duration',0))
64
+ print(f\"{'Location':<25} {'Total':>6} {'Answered':>9} {'Missed':>7} {'Ans%':>6} {'AvgDur':>7}\")
65
+ for loc, s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
66
+ missed = s['total'] - s['answered']
67
+ rate = s['answered']/s['total']*100 if s['total'] else 0
68
+ avg = s['dur']/s['answered'] if s['answered'] else 0
69
+ print(f\"{loc:<25} {s['total']:>6} {s['answered']:>9} {missed:>7} {rate:>5.1f}% {avg:>6.0f}s\")
70
+ "
71
+ ```
72
+
73
+ **Recipe 3 — Calls by hour**
74
+ Question: "When is our peak call time?"
75
+ # Output: Hour | Total | Answered | Missed Rate%
76
+ ```bash
77
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
78
+ import json, sys
79
+ from collections import defaultdict
80
+ data = json.load(sys.stdin)
81
+ stats = defaultdict(lambda: {'total':0,'answered':0})
82
+ for r in data:
83
+ h = r.get('Start time','')[:13]
84
+ if h:
85
+ stats[h]['total'] += 1
86
+ if r.get('Answer indicator') == 'Yes':
87
+ stats[h]['answered'] += 1
88
+ print(f\"{'Hour':<14} {'Total':>6} {'Answered':>9} {'Missed%':>8}\")
89
+ for h, s in sorted(stats.items()):
90
+ missed_rate = (s['total']-s['answered'])/s['total']*100 if s['total'] else 0
91
+ print(f\"{h:<14} {s['total']:>6} {s['answered']:>9} {missed_rate:>7.1f}%\")
92
+ "
93
+ ```
94
+
95
+ **Recipe 4 — Calls by day of week**
96
+ Question: "Which day of the week is busiest?"
97
+ # Output: Day | Total | Answered | Missed Rate%
98
+ ```bash
99
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
100
+ import json, sys
101
+ from collections import defaultdict
102
+ from datetime import datetime
103
+ data = json.load(sys.stdin)
104
+ stats = defaultdict(lambda: {'total':0,'answered':0})
105
+ for r in data:
106
+ ts = r.get('Start time','')
107
+ if ts:
108
+ try:
109
+ dt = datetime.fromisoformat(ts.replace('Z','+00:00'))
110
+ day = dt.strftime('%A')
111
+ stats[day]['total'] += 1
112
+ if r.get('Answer indicator') == 'Yes':
113
+ stats[day]['answered'] += 1
114
+ except: pass
115
+ print(f\"{'Day':<12} {'Total':>6} {'Answered':>9} {'Missed%':>8}\")
116
+ for day in ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']:
117
+ if stats[day]['total'] > 0:
118
+ s = stats[day]
119
+ missed_rate = (s['total']-s['answered'])/s['total']*100 if s['total'] else 0
120
+ print(f\"{day:<12} {s['total']:>6} {s['answered']:>9} {missed_rate:>7.1f}%\")
121
+ "
122
+ ```
123
+
124
+ **Recipe 5 — Calls by type**
125
+ Question: "How many internal vs external vs international calls?"
126
+ # Output: Call Type | Count | %
127
+ ```bash
128
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
129
+ import json, sys
130
+ from collections import Counter
131
+ data = json.load(sys.stdin)
132
+ types = Counter(r.get('Call type', 'Unknown') for r in data)
133
+ for t, c in types.most_common():
134
+ print(f'{t}: {c} calls ({c/len(data)*100:.1f}%)')
135
+ "
136
+ ```
137
+
138
+ **Recipe 6 — Inbound vs outbound ratio**
139
+ Question: "What's our inbound/outbound split?"
140
+ # Output: Metric | Value
141
+ ```bash
142
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
143
+ import json, sys
144
+ data = json.load(sys.stdin)
145
+ total = len(data)
146
+ inb = len([r for r in data if r.get('Direction') == 'TERMINATING'])
147
+ outb = len([r for r in data if r.get('Direction') == 'ORIGINATING'])
148
+ print(f'Total: {total}')
149
+ print(f'Inbound: {inb} ({inb/total*100:.1f}%)' if total else 'No data')
150
+ print(f'Outbound: {outb} ({outb/total*100:.1f}%)' if total else '')
151
+ "
152
+ ```
153
+
154
+ **Recipe 7 — Peak hour identification**
155
+ Question: "What's the single busiest hour?"
156
+ # Output: Metric | Value
157
+ ```bash
158
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
159
+ import json, sys
160
+ from collections import Counter
161
+ data = json.load(sys.stdin)
162
+ hours = Counter(r.get('Start time', '')[:13] for r in data)
163
+ if hours:
164
+ peak, count = hours.most_common(1)[0]
165
+ print(f'Peak hour: {peak} with {count} calls')
166
+ print(f'Average: {len(data)/len(hours):.1f} calls/hour')
167
+ "
168
+ ```
169
+
170
+ **Recipe 8 — Volume trend comparison**
171
+ Question: "Are calls up or down vs last week?"
172
+ Note: Requires two CDR pulls — one for this week, one for last week. Replace THIS_START/THIS_END and LAST_START/LAST_END accordingly.
173
+ # Output: Period | Calls | Change | % Change
174
+ ```bash
175
+ wxcli cdr list --start-time THIS_START --end-time THIS_END -o json > /tmp/cdr_this.json
176
+ wxcli cdr list --start-time LAST_START --end-time LAST_END -o json > /tmp/cdr_last.json
177
+ python3.11 -c "
178
+ import json
179
+ this_week = json.load(open('/tmp/cdr_this.json'))
180
+ last_week = json.load(open('/tmp/cdr_last.json'))
181
+ diff = len(this_week) - len(last_week)
182
+ pct = (diff / len(last_week) * 100) if last_week else 0
183
+ direction = 'up' if diff > 0 else 'down'
184
+ print(f'This period: {len(this_week)} calls')
185
+ print(f'Last period: {len(last_week)} calls')
186
+ print(f'Change: {direction} {abs(diff)} ({abs(pct):.1f}%)')
187
+ "
188
+ ```
189
+ # Note: R8 requires live CDR pulls written to /tmp/cdr_this.json and /tmp/cdr_last.json.
190
+ # Not compatible with the session cache (/tmp/cdr-session.json). See Step 6a for pull instructions.
191
+
192
+ ### Category 2: Call Outcomes & Quality
193
+
194
+ **Recipe 9 — Missed calls**
195
+ Question: "How many calls did we miss?"
196
+ Note: Filters to User-type TERMINATING CDRs only — excludes HuntGroup CDRs (which also have Answer indicator=No for unanswered ring attempts but are not human misses).
197
+ # Output: Time | User | Calling # | Called # | Ring Duration | Reason
198
+ ```bash
199
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
200
+ import json, sys
201
+ data = json.load(sys.stdin)
202
+ # Filter to User-type TERMINATING only: HG ring attempts also show Answer indicator=No
203
+ # but are not missed calls — they are internal delivery mechanics
204
+ missed = [r for r in data if r.get('Answer indicator') == 'No'
205
+ and r.get('Direction') == 'TERMINATING'
206
+ and r.get('User type') == 'User']
207
+ unique_calls = set(r.get('Correlation ID') for r in data if r.get('Correlation ID'))
208
+ print(f'Missed deliveries to users: {len(missed)} (of {len(unique_calls)} unique calls)')
209
+ if not missed:
210
+ print('No matching records found in this time window.')
211
+ else:
212
+ for c in missed[:10]:
213
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User','?')} | {c.get('Calling number','?')} -> {c.get('Called number','?')} | ring {c.get('Ring duration','?')}s | {c.get('Call outcome reason','?')}\")
214
+ "
215
+ ```
216
+
217
+ **Recipe 10 — Failed calls**
218
+ Question: "How many calls failed?"
219
+ # Output: Time | User | Calling # | Called # | Outcome | Reason
220
+ ```bash
221
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
222
+ import json, sys
223
+ data = json.load(sys.stdin)
224
+ failed = [r for r in data if r.get('Call outcome') in ('Failure', 'Refusal')]
225
+ print(f'Failed/refused: {len(failed)} of {len(data)}')
226
+ if not failed:
227
+ print('No matching records found in this time window.')
228
+ else:
229
+ for c in failed[:10]:
230
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User','?')} | {c.get('Calling number','?')} -> {c.get('Called number','?')} | {c.get('Call outcome','?')}: {c.get('Call outcome reason','?')}\")
231
+ "
232
+ ```
233
+
234
+ **Recipe 11 — Abandoned calls**
235
+ Question: "How many callers hung up before we answered?"
236
+ Note: Deduplicates by Correlation ID — a caller who abandoned while ringing through a HG generates multiple No-answer CDRs (one per ring attempt) but should count as one abandonment.
237
+ # Output: Unique abandoned calls + Time | Calling # | Called # | Ring Duration
238
+ ```bash
239
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
240
+ import json, sys
241
+ from collections import defaultdict
242
+ data = json.load(sys.stdin)
243
+ # Group by Correlation ID; a call is abandoned if all CDRs have Answer indicator=No
244
+ # and at least one has Releasing party=Local (caller hung up)
245
+ corr = defaultdict(list)
246
+ for r in data:
247
+ cid = r.get('Correlation ID')
248
+ if cid:
249
+ corr[cid].append(r)
250
+ abandoned = []
251
+ for cid, legs in corr.items():
252
+ any_answered = any(r.get('Answer indicator') == 'Yes' for r in legs)
253
+ caller_hungup = any(r.get('Releasing party') == 'Local' and r.get('Answer indicator') == 'No' for r in legs)
254
+ if not any_answered and caller_hungup:
255
+ first = sorted(legs, key=lambda x: x.get('Start time',''))[0]
256
+ abandoned.append(first)
257
+ print(f'Abandoned calls (caller hung up before answer): {len(abandoned)} of {len(corr)} unique calls')
258
+ if not abandoned:
259
+ print('No matching records found in this time window.')
260
+ else:
261
+ for c in sorted(abandoned, key=lambda x: x.get('Start time',''))[:10]:
262
+ print(f\" {c.get('Start time','')[:16]} | {c.get('Calling number','?')} -> {c.get('Called number','?')} | rang {c.get('Ring duration','?')}s\")
263
+ "
264
+ ```
265
+
266
+ **Recipe 12 — Answer rate**
267
+ Question: "What percentage of calls do we answer?"
268
+ Note: Deduplicates by Correlation ID and checks for a User-type TERMINATING answer. Raw CDR counts are NOT valid for answer rate — HG calls produce multiple Answered=Yes CDRs (HG TERMINATING + HG ORIGINATING + agent TERMINATING) per single answered call.
269
+ # Output: Metric | Value
270
+ ```bash
271
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
272
+ import json, sys
273
+ from collections import defaultdict
274
+ data = json.load(sys.stdin)
275
+ # Group CDRs by Correlation ID
276
+ corr = defaultdict(list)
277
+ for r in data:
278
+ cid = r.get('Correlation ID')
279
+ if cid:
280
+ corr[cid].append(r)
281
+ # A call is 'answered' if any User-type TERMINATING CDR has Answer indicator=Yes
282
+ answered_calls = sum(
283
+ 1 for legs in corr.values()
284
+ if any(r.get('Answer indicator') == 'Yes'
285
+ and r.get('Direction') == 'TERMINATING'
286
+ and r.get('User type') == 'User'
287
+ for r in legs)
288
+ )
289
+ total_calls = len(corr)
290
+ print(f'Answer rate: {answered_calls/total_calls*100:.1f}% ({answered_calls} of {total_calls} unique calls)' if total_calls else 'No data')
291
+ "
292
+ ```
293
+
294
+ **Recipe 13 — Calls by outcome reason**
295
+ Question: "Why are calls failing?"
296
+ # Output: Reason | Count | %
297
+ ```bash
298
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
299
+ import json, sys
300
+ from collections import Counter
301
+ data = json.load(sys.stdin)
302
+ reasons = Counter(r.get('Call outcome reason', 'Unknown') for r in data if r.get('Call outcome') != 'Success')
303
+ total_non_success = sum(reasons.values())
304
+ for reason, count in reasons.most_common():
305
+ pct = count/total_non_success*100 if total_non_success else 0
306
+ print(f'{reason}: {count} ({pct:.1f}%)')
307
+ "
308
+ ```
309
+
310
+ **Recipe 14 — Short calls (possible misroutes)**
311
+ Question: "How many calls lasted under 10 seconds?"
312
+ # Output: Time | User | Called # | Duration | Outcome
313
+ ```bash
314
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
315
+ import json, sys
316
+ data = json.load(sys.stdin)
317
+ short = [r for r in data if r.get('Answer indicator') == 'Yes' and int(r.get('Duration', 0)) < 10]
318
+ print(f'Short calls (<10s, answered): {len(short)} of {len(data)}')
319
+ if not short:
320
+ print('No matching records found in this time window.')
321
+ else:
322
+ for c in short[:10]:
323
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User','?')} | {c.get('Called number','?')} | {c.get('Duration','?')}s | {c.get('Call outcome','?')}\")
324
+ "
325
+ ```
326
+
327
+ **Recipe 15 — Long-ring no-answer**
328
+ Question: "Calls that rang for over 30 seconds and weren't answered?"
329
+ # Output: Time | User | Calling # | Ring Duration
330
+ ```bash
331
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
332
+ import json, sys
333
+ data = json.load(sys.stdin)
334
+ long_ring = [r for r in data if r.get('Answer indicator') == 'No' and int(r.get('Ring duration', 0)) > 30]
335
+ print(f'Long-ring no-answer (>30s ring): {len(long_ring)}')
336
+ if not long_ring:
337
+ print('No matching records found in this time window.')
338
+ else:
339
+ for c in long_ring[:10]:
340
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | rang {c.get('Ring duration')}s | from {c.get('Calling number')}\")
341
+ "
342
+ ```
343
+
344
+ **Recipe 16 — Call outcome by location**
345
+ Question: "Which office has the worst answer rate?"
346
+ Note: Uses Correlation ID deduplication — HG calls produce multiple Answered=Yes CDRs per call so raw CDR counts would inflate answer rates. Each unique Correlation ID = one call.
347
+ # Output: Location | Unique Calls | Answered | Answer%
348
+ ```bash
349
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
350
+ import json, sys
351
+ from collections import defaultdict
352
+ data = json.load(sys.stdin)
353
+ # Group by Correlation ID, track location and whether a User answered
354
+ corr = defaultdict(lambda: {'loc': 'Unknown', 'answered': False})
355
+ for r in data:
356
+ cid = r.get('Correlation ID')
357
+ if not cid:
358
+ continue
359
+ if r.get('Location'):
360
+ corr[cid]['loc'] = r.get('Location')
361
+ if (r.get('Answer indicator') == 'Yes'
362
+ and r.get('Direction') == 'TERMINATING'
363
+ and r.get('User type') == 'User'):
364
+ corr[cid]['answered'] = True
365
+ stats = defaultdict(lambda: {'total': 0, 'answered': 0})
366
+ for info in corr.values():
367
+ loc = info['loc']
368
+ stats[loc]['total'] += 1
369
+ if info['answered']:
370
+ stats[loc]['answered'] += 1
371
+ print(f\"{'Location':<25} {'Calls':>6} {'Answered':>9} {'Ans%':>6}\")
372
+ for loc, s in sorted(stats.items(), key=lambda x: x[1]['answered']/max(x[1]['total'],1)):
373
+ rate = s['answered']/s['total']*100 if s['total'] else 0
374
+ print(f\"{loc:<25} {s['total']:>6} {s['answered']:>9} {rate:>5.1f}%\")
375
+ "
376
+ ```
377
+
378
+ **Recipe 16b — Inbound answer rate for a specific location**
379
+ Question: "What percentage of inbound calls to [Location] were answered vs missed?"
380
+ Note: Deduplicates by Correlation ID and checks for a User-type answer. HG TERMINATING CDRs also show Answered=Yes (HG processed the call) — filtering to User type=User gives the human answer rate. Pass location name via --locations flag at pull time.
381
+ # Output: Metric | Value
382
+ ```bash
383
+ wxcli cdr list --start-time START --end-time END --locations "Location Name" -o json | python3.11 -c "
384
+ import json, sys
385
+ from collections import defaultdict
386
+ data = json.load(sys.stdin)
387
+ corr = defaultdict(list)
388
+ for r in data:
389
+ cid = r.get('Correlation ID')
390
+ if cid:
391
+ corr[cid].append(r)
392
+ answered_calls = sum(
393
+ 1 for legs in corr.values()
394
+ if any(r.get('Answer indicator') == 'Yes'
395
+ and r.get('Direction') == 'TERMINATING'
396
+ and r.get('User type') == 'User'
397
+ for r in legs)
398
+ )
399
+ total = len(corr)
400
+ missed = total - answered_calls
401
+ print(f'Unique calls: {total}')
402
+ print(f'Answered: {answered_calls} ({answered_calls/total*100:.1f}%)' if total else 'No data')
403
+ print(f'Missed: {missed} ({missed/total*100:.1f}%)' if total else '')
404
+ "
405
+ ```
406
+ Note: If the window exceeds 12 hours (e.g., "yesterday" = 24h), issue 2 sequential CDR pulls and concatenate the JSON arrays before piping to Python. See the multi-pull merge pattern in Step 6a.
407
+
408
+ ### Category 3: Hold & Wait Time
409
+
410
+ **Recipe 17 — Average hold time**
411
+ Question: "What's our average hold time?"
412
+ # Output: Metric | Value
413
+ ```bash
414
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
415
+ import json, sys
416
+ data = json.load(sys.stdin)
417
+ holds = [int(r.get('Hold duration', 0)) for r in data if int(r.get('Hold duration', 0)) > 0]
418
+ if holds:
419
+ print(f'Calls with hold: {len(holds)}')
420
+ print(f'Average hold: {sum(holds)/len(holds):.1f}s')
421
+ print(f'Max hold: {max(holds)}s')
422
+ else:
423
+ print('No calls with hold time found')
424
+ "
425
+ ```
426
+
427
+ **Recipe 18 — Calls with excessive hold (>30s)**
428
+ Question: "How many calls had over 30 seconds of hold?"
429
+ # Output: Time | User | Hold Duration | Calling #
430
+ ```bash
431
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
432
+ import json, sys
433
+ data = json.load(sys.stdin)
434
+ over30 = [r for r in data if int(r.get('Hold duration', 0)) > 30]
435
+ print(f'Calls with >30s hold: {len(over30)}')
436
+ if not over30:
437
+ print('No matching records found in this time window.')
438
+ else:
439
+ for c in over30[:10]:
440
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | hold {c.get('Hold duration')}s | {c.get('Calling number')}\")
441
+ "
442
+ ```
443
+
444
+ **Recipe 19 — Calls with excessive hold (>60s)**
445
+ Question: "How many calls had over a minute of hold?"
446
+ # Output: Time | User | Hold Duration | Calling #
447
+ ```bash
448
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
449
+ import json, sys
450
+ data = json.load(sys.stdin)
451
+ over60 = [r for r in data if int(r.get('Hold duration', 0)) > 60]
452
+ print(f'Calls with >60s hold: {len(over60)}')
453
+ if not over60:
454
+ print('No matching records found in this time window.')
455
+ else:
456
+ for c in over60[:10]:
457
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | hold {c.get('Hold duration')}s | {c.get('Calling number')}\")
458
+ "
459
+ ```
460
+
461
+ **Recipe 20 — Hold-then-abandoned (>30s)**
462
+ Question: "Calls we answered, put on hold >30s, and the caller hung up?"
463
+ # Output: Time | User | Hold Duration | Total Duration
464
+ ```bash
465
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
466
+ import json, sys
467
+ data = json.load(sys.stdin)
468
+ hold_abandoned = [r for r in data
469
+ if r.get('Answer indicator') == 'Yes'
470
+ and int(r.get('Hold duration', 0)) > 30
471
+ and r.get('Releasing party') == 'Remote']
472
+ print(f'Answered -> held >30s -> caller hung up: {len(hold_abandoned)}')
473
+ if not hold_abandoned:
474
+ print('No matching records found in this time window.')
475
+ else:
476
+ for c in hold_abandoned[:10]:
477
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | hold {c.get('Hold duration')}s | {c.get('Duration')}s total\")
478
+ "
479
+ ```
480
+
481
+ **Recipe 21 — Hold-then-abandoned (>60s)**
482
+ Question: "Calls we answered, put on hold >60s, and the caller hung up?"
483
+ # Output: Time | User | Hold Duration | Total Duration
484
+ ```bash
485
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
486
+ import json, sys
487
+ data = json.load(sys.stdin)
488
+ hold_abandoned = [r for r in data
489
+ if r.get('Answer indicator') == 'Yes'
490
+ and int(r.get('Hold duration', 0)) > 60
491
+ and r.get('Releasing party') == 'Remote']
492
+ print(f'Answered -> held >60s -> caller hung up: {len(hold_abandoned)}')
493
+ if not hold_abandoned:
494
+ print('No matching records found in this time window.')
495
+ else:
496
+ for c in hold_abandoned[:10]:
497
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | hold {c.get('Hold duration')}s | {c.get('Duration')}s total\")
498
+ "
499
+ ```
500
+
501
+ **Recipe 22 — Average ring duration**
502
+ Question: "How long are callers waiting before we pick up?"
503
+ # Output: Metric | Value
504
+ ```bash
505
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
506
+ import json, sys
507
+ data = json.load(sys.stdin)
508
+ rings = [int(r.get('Ring duration', 0)) for r in data if r.get('Answer indicator') == 'Yes' and int(r.get('Ring duration', 0)) > 0]
509
+ if rings:
510
+ print(f'Answered calls with ring data: {len(rings)}')
511
+ print(f'Average ring: {sum(rings)/len(rings):.1f}s')
512
+ print(f'Max ring: {max(rings)}s')
513
+ "
514
+ ```
515
+
516
+ **Recipe 23 — Ring duration by location**
517
+ Question: "Which office is slowest to answer?"
518
+ # Output: Location | Avg Ring(s) | Calls
519
+ ```bash
520
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
521
+ import json, sys
522
+ from collections import defaultdict
523
+ data = json.load(sys.stdin)
524
+ stats = defaultdict(list)
525
+ for r in data:
526
+ if r.get('Answer indicator') == 'Yes' and int(r.get('Ring duration', 0)) > 0:
527
+ stats[r.get('Location', 'Unknown')].append(int(r.get('Ring duration', 0)))
528
+ for loc, rings in sorted(stats.items(), key=lambda x: sum(x[1])/len(x[1]), reverse=True):
529
+ avg = sum(rings)/len(rings)
530
+ print(f'{loc}: avg {avg:.1f}s ring ({len(rings)} calls)')
531
+ "
532
+ ```
533
+
534
+ **Recipe 24 — Hold time by user**
535
+ Question: "Which agents put callers on hold the longest?"
536
+ # Output: User | Avg Hold(s) | Calls with Hold | Max Hold(s)
537
+ ```bash
538
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
539
+ import json, sys
540
+ from collections import defaultdict
541
+ data = json.load(sys.stdin)
542
+ stats = defaultdict(list)
543
+ for r in data:
544
+ hold = int(r.get('Hold duration', 0))
545
+ if hold > 0:
546
+ stats[r.get('User', 'Unknown')].append(hold)
547
+ for user, holds in sorted(stats.items(), key=lambda x: sum(x[1])/len(x[1]), reverse=True)[:15]:
548
+ avg = sum(holds)/len(holds)
549
+ print(f'{user}: avg {avg:.1f}s hold, {len(holds)} calls with hold, max {max(holds)}s')
550
+ "
551
+ ```
552
+
553
+ ### Category 4: User & Agent Performance
554
+
555
+ **Recipe 25 — Top talkers (by call count)**
556
+ Question: "Who makes/receives the most calls?"
557
+ # Output: User | Total | Answered | Missed | Answer% | Avg Duration(s)
558
+ ```bash
559
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
560
+ import json, sys
561
+ from collections import defaultdict
562
+ data = json.load(sys.stdin)
563
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
564
+ for r in data:
565
+ user = r.get('User','Unknown')
566
+ stats[user]['total'] += 1
567
+ if r.get('Answer indicator') == 'Yes':
568
+ stats[user]['answered'] += 1
569
+ stats[user]['dur'] += int(r.get('Duration',0))
570
+ print(f\"{'User':<30} {'Total':>6} {'Answered':>9} {'Missed':>7} {'Ans%':>6} {'AvgDur':>7}\")
571
+ for user, s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True)[:15]:
572
+ missed = s['total'] - s['answered']
573
+ rate = s['answered']/s['total']*100 if s['total'] else 0
574
+ avg = s['dur']/s['answered'] if s['answered'] else 0
575
+ print(f\"{user:<30} {s['total']:>6} {s['answered']:>9} {missed:>7} {rate:>5.1f}% {avg:>6.0f}s\")
576
+ "
577
+ ```
578
+
579
+ **Recipe 26 — Top talkers (by total duration)**
580
+ Question: "Who spends the most time on the phone?"
581
+ # Output: User | Total Min | Avg Duration(s) | Calls | Answer%
582
+ ```bash
583
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
584
+ import json, sys
585
+ from collections import defaultdict
586
+ data = json.load(sys.stdin)
587
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
588
+ for r in data:
589
+ user = r.get('User','Unknown')
590
+ stats[user]['total'] += 1
591
+ if r.get('Answer indicator') == 'Yes':
592
+ stats[user]['answered'] += 1
593
+ stats[user]['dur'] += int(r.get('Duration',0))
594
+ print(f\"{'User':<30} {'Total Min':>10} {'Avg Dur':>8} {'Calls':>6} {'Ans%':>6}\")
595
+ for user, s in sorted(stats.items(), key=lambda x: x[1]['dur'], reverse=True)[:15]:
596
+ rate = s['answered']/s['total']*100 if s['total'] else 0
597
+ avg = s['dur']/s['total'] if s['total'] else 0
598
+ print(f\"{user:<30} {s['dur']/60:>10.1f} {avg:>7.0f}s {s['total']:>6} {rate:>5.1f}%\")
599
+ "
600
+ ```
601
+
602
+ **Recipe 27 — Calls per user**
603
+ Question: "How many calls does each user handle?"
604
+ # Output: User | Total | Answered | Missed | Answer%
605
+ ```bash
606
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
607
+ import json, sys
608
+ from collections import defaultdict
609
+ data = json.load(sys.stdin)
610
+ stats = defaultdict(lambda: {'total':0,'answered':0})
611
+ for r in data:
612
+ user = r.get('User','Unknown')
613
+ stats[user]['total'] += 1
614
+ if r.get('Answer indicator') == 'Yes':
615
+ stats[user]['answered'] += 1
616
+ print(f\"{'User':<30} {'Total':>6} {'Answered':>9} {'Missed':>7} {'Ans%':>6}\")
617
+ for user, s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
618
+ missed = s['total'] - s['answered']
619
+ rate = s['answered']/s['total']*100 if s['total'] else 0
620
+ print(f\"{user:<30} {s['total']:>6} {s['answered']:>9} {missed:>7} {rate:>5.1f}%\")
621
+ "
622
+ ```
623
+
624
+ **Recipe 28 — Average duration per user**
625
+ Question: "Who has the longest average call?"
626
+ # Output: User | Avg Duration(s) | Calls | Answer%
627
+ ```bash
628
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
629
+ import json, sys
630
+ from collections import defaultdict
631
+ data = json.load(sys.stdin)
632
+ stats = defaultdict(lambda: {'durs':[], 'total':0, 'answered':0})
633
+ for r in data:
634
+ user = r.get('User','Unknown')
635
+ stats[user]['total'] += 1
636
+ if r.get('Answer indicator') == 'Yes':
637
+ stats[user]['answered'] += 1
638
+ stats[user]['durs'].append(int(r.get('Duration',0)))
639
+ print(f\"{'User':<30} {'Avg Dur':>8} {'Calls':>6} {'Ans%':>6}\")
640
+ for user, s in sorted(stats.items(), key=lambda x: sum(x[1]['durs'])/len(x[1]['durs']) if x[1]['durs'] else 0, reverse=True)[:15]:
641
+ avg = sum(s['durs'])/len(s['durs']) if s['durs'] else 0
642
+ rate = s['answered']/s['total']*100 if s['total'] else 0
643
+ print(f\"{user:<30} {avg:>7.0f}s {s['total']:>6} {rate:>5.1f}%\")
644
+ "
645
+ ```
646
+
647
+ **Recipe 29 — Users with most missed calls**
648
+ Question: "Who misses the most calls?"
649
+ # Output: User | Missed | Total | Miss%
650
+ ```bash
651
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
652
+ import json, sys
653
+ from collections import defaultdict
654
+ data = json.load(sys.stdin)
655
+ stats = defaultdict(lambda: {'total':0,'missed':0})
656
+ for r in data:
657
+ user = r.get('User','Unknown')
658
+ stats[user]['total'] += 1
659
+ if r.get('Answer indicator') == 'No':
660
+ stats[user]['missed'] += 1
661
+ print(f\"{'User':<30} {'Missed':>7} {'Total':>6} {'Miss%':>6}\")
662
+ for user, s in sorted(stats.items(), key=lambda x: x[1]['missed'], reverse=True)[:15]:
663
+ rate = s['missed']/s['total']*100 if s['total'] else 0
664
+ print(f\"{user:<30} {s['missed']:>7} {s['total']:>6} {rate:>5.1f}%\")
665
+ "
666
+ ```
667
+
668
+ **Recipe 30 — User answer rate**
669
+ Question: "What's each user's answer rate?"
670
+ # Output: User | Total | Answered | Missed | Answer% | Avg Duration(s)
671
+ ```bash
672
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
673
+ import json, sys
674
+ from collections import defaultdict
675
+ data = json.load(sys.stdin)
676
+ stats = defaultdict(lambda: {'total': 0, 'answered': 0, 'dur': 0})
677
+ for r in data:
678
+ user = r.get('User', 'Unknown')
679
+ stats[user]['total'] += 1
680
+ if r.get('Answer indicator') == 'Yes':
681
+ stats[user]['answered'] += 1
682
+ stats[user]['dur'] += int(r.get('Duration', 0))
683
+ print(f\"{'User':<30} {'Total':>6} {'Answered':>9} {'Missed':>7} {'Ans%':>6} {'AvgDur':>7}\")
684
+ for user, s in sorted(stats.items(), key=lambda x: x[1]['answered']/max(x[1]['total'],1)):
685
+ missed = s['total'] - s['answered']
686
+ rate = s['answered']/s['total']*100 if s['total'] else 0
687
+ avg = s['dur']/s['answered'] if s['answered'] else 0
688
+ print(f\"{user:<30} {s['total']:>6} {s['answered']:>9} {missed:>7} {rate:>5.1f}% {avg:>6.0f}s\")
689
+ "
690
+ ```
691
+
692
+ **Recipe 31 — International calls per user**
693
+ Question: "Who makes the most international calls?"
694
+ # Output: User | Intl Calls | Total Calls | Intl%
695
+ ```bash
696
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
697
+ import json, sys
698
+ from collections import defaultdict
699
+ data = json.load(sys.stdin)
700
+ stats = defaultdict(lambda: {'total':0,'intl':0})
701
+ for r in data:
702
+ user = r.get('User','Unknown')
703
+ stats[user]['total'] += 1
704
+ if r.get('Call type') == 'SIP_INTERNATIONAL':
705
+ stats[user]['intl'] += 1
706
+ intl_users = {u: s for u, s in stats.items() if s['intl'] > 0}
707
+ print(f\"{'User':<30} {'Intl':>5} {'Total':>6} {'Intl%':>6}\")
708
+ for user, s in sorted(intl_users.items(), key=lambda x: x[1]['intl'], reverse=True)[:15]:
709
+ rate = s['intl']/s['total']*100 if s['total'] else 0
710
+ print(f\"{user:<30} {s['intl']:>5} {s['total']:>6} {rate:>5.1f}%\")
711
+ "
712
+ ```
713
+
714
+ **Recipe 32 — After-hours activity**
715
+ Question: "Who's making calls outside business hours (before 8am or after 6pm)?"
716
+ # Output: User | After-Hours | Total | AH%
717
+ ```bash
718
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
719
+ import json, sys
720
+ from collections import defaultdict
721
+ data = json.load(sys.stdin)
722
+ stats = defaultdict(lambda: {'total':0,'ah':0})
723
+ for r in data:
724
+ user = r.get('User','Unknown')
725
+ stats[user]['total'] += 1
726
+ ts = r.get('Start time','')
727
+ if ts:
728
+ try:
729
+ hour = int(ts[11:13])
730
+ if hour < 8 or hour >= 18:
731
+ stats[user]['ah'] += 1
732
+ except: pass
733
+ ah_users = {u: s for u, s in stats.items() if s['ah'] > 0}
734
+ print(f\"{'User':<30} {'After-Hours':>12} {'Total':>6} {'AH%':>5}\")
735
+ for user, s in sorted(ah_users.items(), key=lambda x: x[1]['ah'], reverse=True)[:15]:
736
+ rate = s['ah']/s['total']*100 if s['total'] else 0
737
+ print(f\"{user:<30} {s['ah']:>12} {s['total']:>6} {rate:>4.1f}%\")
738
+ "
739
+ ```
740
+
741
+ ### Category 5: Trunk & Routing
742
+
743
+ **Recipe 33 — Calls per trunk**
744
+ Question: "How much traffic on each trunk?"
745
+ # Output: Trunk | Direction | Total | Duration(min) | Answer%
746
+ ```bash
747
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
748
+ import json, sys
749
+ from collections import defaultdict
750
+ data = json.load(sys.stdin)
751
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
752
+ for r in data:
753
+ for field, direction in [('Inbound trunk','Inbound'),('Outbound trunk','Outbound')]:
754
+ trunk = r.get(field)
755
+ if trunk:
756
+ key = (trunk, direction)
757
+ stats[key]['total'] += 1
758
+ if r.get('Answer indicator') == 'Yes':
759
+ stats[key]['answered'] += 1
760
+ stats[key]['dur'] += int(r.get('Duration',0))
761
+ print(f\"{'Trunk':<30} {'Dir':>8} {'Total':>6} {'Min':>7} {'Ans%':>6}\")
762
+ for (trunk, direction), s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
763
+ rate = s['answered']/s['total']*100 if s['total'] else 0
764
+ print(f\"{trunk:<30} {direction:>8} {s['total']:>6} {s['dur']/60:>7.1f} {rate:>5.1f}%\")
765
+ "
766
+ ```
767
+
768
+ **Recipe 34 — Trunk utilization by hour**
769
+ Question: "When are our trunks busiest?"
770
+ # Output: Trunk | Hour | Calls
771
+ ```bash
772
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
773
+ import json, sys
774
+ from collections import Counter
775
+ data = json.load(sys.stdin)
776
+ trunk_hours = Counter()
777
+ for r in data:
778
+ trunk = r.get('Outbound trunk') or r.get('Inbound trunk')
779
+ hour = r.get('Start time', '')[:13]
780
+ if trunk and hour:
781
+ trunk_hours[(trunk, hour)] += 1
782
+ for (trunk, hour), count in sorted(trunk_hours.items()):
783
+ print(f'{trunk} | {hour}: {count} calls')
784
+ "
785
+ ```
786
+
787
+ **Recipe 35 — Route group distribution**
788
+ Question: "How is traffic distributed across route groups?"
789
+ # Output: Route Group | Count | %
790
+ ```bash
791
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
792
+ import json, sys
793
+ from collections import Counter
794
+ data = json.load(sys.stdin)
795
+ groups = Counter(r.get('Route group') for r in data if r.get('Route group'))
796
+ total = sum(groups.values())
797
+ for group, count in groups.most_common():
798
+ print(f\"{group}: {count} ({count/total*100:.1f}%)\")
799
+ "
800
+ ```
801
+
802
+ **Recipe 36 — Redirect chain analysis**
803
+ Question: "How many calls were forwarded/redirected?"
804
+ # Output: Summary + Reason | Count | %
805
+ ```bash
806
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
807
+ import json, sys
808
+ from collections import Counter
809
+ data = json.load(sys.stdin)
810
+ redirected = [r for r in data if r.get('Redirect reason')]
811
+ print(f'Redirected calls: {len(redirected)} of {len(data)} ({len(redirected)/len(data)*100:.1f}%)' if data else 'No data')
812
+ reasons = Counter(r.get('Redirect reason') for r in redirected)
813
+ total_r = sum(reasons.values())
814
+ for reason, count in reasons.most_common():
815
+ print(f\" {reason}: {count} ({count/total_r*100:.1f}%)\")
816
+ "
817
+ ```
818
+
819
+ **Recipe 37 — Calls by redirect reason**
820
+ Question: "Why are calls being redirected?"
821
+ # Output: Reason | Count | %
822
+ ```bash
823
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
824
+ import json, sys
825
+ from collections import Counter
826
+ data = json.load(sys.stdin)
827
+ reasons = Counter(r.get('Redirect reason') for r in data if r.get('Redirect reason'))
828
+ total = sum(reasons.values())
829
+ for reason, count in reasons.most_common(10):
830
+ print(f\"{reason}: {count} ({count/total*100:.1f}%)\")
831
+ "
832
+ ```
833
+
834
+ **Recipe 38 — Calls by original reason**
835
+ Question: "What triggers the first redirect?"
836
+ # Output: Reason | Count | %
837
+ ```bash
838
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
839
+ import json, sys
840
+ from collections import Counter
841
+ data = json.load(sys.stdin)
842
+ reasons = Counter(r.get('Original reason') for r in data if r.get('Original reason'))
843
+ total = sum(reasons.values())
844
+ for reason, count in reasons.most_common(10):
845
+ print(f\"{reason}: {count} ({count/total*100:.1f}%)\")
846
+ "
847
+ ```
848
+
849
+ **Recipe 39 — Forwarding loop detection**
850
+ Question: "Any calls redirected 3+ times (possible forwarding loops)?"
851
+ # Output: Correlation ID | Legs | Start Time | Calling # | Called #
852
+ ```bash
853
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
854
+ import json, sys
855
+ from collections import defaultdict
856
+ data = json.load(sys.stdin)
857
+ corr_counts = defaultdict(int)
858
+ corr_samples = {}
859
+ for r in data:
860
+ cid = r.get('Correlation ID')
861
+ if cid:
862
+ corr_counts[cid] += 1
863
+ corr_samples[cid] = r
864
+ loops = {cid: count for cid, count in corr_counts.items() if count >= 3}
865
+ print(f'Calls with 3+ legs (possible loops): {len(loops)}')
866
+ for cid, count in sorted(loops.items(), key=lambda x: x[1], reverse=True)[:10]:
867
+ s = corr_samples[cid]
868
+ print(f\" {cid} | {count} legs | {s.get('Start time')} | {s.get('Calling number')} -> {s.get('Called number')}\")
869
+ "
870
+ ```
871
+
872
+ ### Category 6: PSTN & Billing
873
+
874
+ **Recipe 40 — Calls by PSTN vendor**
875
+ Question: "How is traffic split across carriers?"
876
+ # Output: Vendor | Total | Duration(min) | Answer%
877
+ ```bash
878
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
879
+ import json, sys
880
+ from collections import defaultdict
881
+ data = json.load(sys.stdin)
882
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
883
+ for r in data:
884
+ v = r.get('PSTN vendor name')
885
+ if v:
886
+ stats[v]['total'] += 1
887
+ if r.get('Answer indicator') == 'Yes':
888
+ stats[v]['answered'] += 1
889
+ stats[v]['dur'] += int(r.get('Duration',0))
890
+ print(f\"{'Vendor':<30} {'Total':>6} {'Min':>7} {'Ans%':>6}\")
891
+ for v, s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
892
+ rate = s['answered']/s['total']*100 if s['total'] else 0
893
+ print(f\"{v:<30} {s['total']:>6} {s['dur']/60:>7.1f} {rate:>5.1f}%\")
894
+ "
895
+ ```
896
+
897
+ **Recipe 41 — Duration by PSTN vendor**
898
+ Question: "Which carrier carries the most minutes?"
899
+ # Output: Vendor | Total | Duration(min) | Answer%
900
+ ```bash
901
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
902
+ import json, sys
903
+ from collections import defaultdict
904
+ data = json.load(sys.stdin)
905
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
906
+ for r in data:
907
+ v = r.get('PSTN vendor name')
908
+ if v:
909
+ stats[v]['total'] += 1
910
+ if r.get('Answer indicator') == 'Yes':
911
+ stats[v]['answered'] += 1
912
+ stats[v]['dur'] += int(r.get('Duration',0))
913
+ print(f\"{'Vendor':<30} {'Total':>6} {'Min':>7} {'Ans%':>6}\")
914
+ for v, s in sorted(stats.items(), key=lambda x: x[1]['dur'], reverse=True):
915
+ rate = s['answered']/s['total']*100 if s['total'] else 0
916
+ print(f\"{v:<30} {s['total']:>6} {s['dur']/60:>7.1f} {rate:>5.1f}%\")
917
+ "
918
+ ```
919
+
920
+ **Recipe 42 — International calls by country**
921
+ Question: "Which countries are we calling?"
922
+ # Output: Country | Calls | %
923
+ ```bash
924
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
925
+ import json, sys
926
+ from collections import Counter
927
+ data = json.load(sys.stdin)
928
+ countries = Counter(r.get('International country') for r in data if r.get('International country'))
929
+ total = sum(countries.values())
930
+ print(f'International calls to {len(countries)} countries:')
931
+ for country, count in countries.most_common():
932
+ print(f' {country}: {count} ({count/total*100:.1f}%)')
933
+ "
934
+ ```
935
+
936
+ **Recipe 43 — International call duration by country**
937
+ Question: "How many minutes to each country?"
938
+ # Output: Country | Duration(min) | Calls | %
939
+ ```bash
940
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
941
+ import json, sys
942
+ from collections import defaultdict
943
+ data = json.load(sys.stdin)
944
+ stats = defaultdict(lambda: {'dur':0,'count':0})
945
+ for r in data:
946
+ c = r.get('International country')
947
+ if c:
948
+ stats[c]['dur'] += int(r.get('Duration',0))
949
+ stats[c]['count'] += 1
950
+ total_calls = sum(s['count'] for s in stats.values())
951
+ print(f\"{'Country':<20} {'Min':>7} {'Calls':>6} {'%':>5}\")
952
+ for c, s in sorted(stats.items(), key=lambda x: x[1]['dur'], reverse=True):
953
+ pct = s['count']/total_calls*100 if total_calls else 0
954
+ print(f\"{c:<20} {s['dur']/60:>7.1f} {s['count']:>6} {pct:>4.1f}%\")
955
+ "
956
+ ```
957
+
958
+ **Recipe 44 — Call type mix**
959
+ Question: "What's our toll-free vs premium vs national breakdown?"
960
+ # Output: Call Type | Count | %
961
+ ```bash
962
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
963
+ import json, sys
964
+ from collections import Counter
965
+ data = json.load(sys.stdin)
966
+ types = Counter(r.get('Call type', 'Unknown') for r in data)
967
+ total = len(data)
968
+ for t, c in types.most_common():
969
+ print(f'{t}: {c} ({c/total*100:.1f}%)')
970
+ "
971
+ ```
972
+
973
+ **Recipe 45 — Authorization code usage**
974
+ Question: "Which auth codes are being used and how often?"
975
+ # Output: Code | Calls | Distinct Users
976
+ ```bash
977
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
978
+ import json, sys
979
+ from collections import defaultdict
980
+ data = json.load(sys.stdin)
981
+ stats = defaultdict(lambda: {'calls':0,'users':set()})
982
+ for r in data:
983
+ code = r.get('Authorization code')
984
+ if code:
985
+ stats[code]['calls'] += 1
986
+ user = r.get('User')
987
+ if user:
988
+ stats[code]['users'].add(user)
989
+ if stats:
990
+ print(f'{len(stats)} auth codes in use:')
991
+ print(f\"{'Code':<20} {'Calls':>6} {'Users':>6}\")
992
+ for code, s in sorted(stats.items(), key=lambda x: x[1]['calls'], reverse=True):
993
+ print(f\"{code:<20} {s['calls']:>6} {len(s['users']):>6}\")
994
+ else:
995
+ print('No calls with authorization codes found')
996
+ "
997
+ ```
998
+
999
+ **Recipe 46 — PSTN vendor comparison**
1000
+ Question: "Compare carriers on volume and duration"
1001
+ # Output: Vendor | Calls | Minutes | Answer%
1002
+ ```bash
1003
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1004
+ import json, sys
1005
+ from collections import defaultdict
1006
+ data = json.load(sys.stdin)
1007
+ stats = defaultdict(lambda: {'calls': 0, 'duration': 0, 'answered': 0})
1008
+ for r in data:
1009
+ v = r.get('PSTN vendor name')
1010
+ if v:
1011
+ stats[v]['calls'] += 1
1012
+ stats[v]['duration'] += int(r.get('Duration', 0))
1013
+ if r.get('Answer indicator') == 'Yes':
1014
+ stats[v]['answered'] += 1
1015
+ print(f\"{'Vendor':<30} {'Calls':>6} {'Minutes':>8} {'Answer%':>8}\")
1016
+ print('-' * 55)
1017
+ for v, s in sorted(stats.items(), key=lambda x: x[1]['calls'], reverse=True):
1018
+ rate = s['answered']/s['calls']*100 if s['calls'] else 0
1019
+ print(f\"{v:<30} {s['calls']:>6} {s['duration']/60:>8.1f} {rate:>7.1f}%\")
1020
+ "
1021
+ ```
1022
+
1023
+ ### Category 7: Device & Client
1024
+
1025
+ **Recipe 47 — Client type distribution**
1026
+ Question: "What are people using to make calls?"
1027
+ # Output: Client Type | Total | % | Duration(min) | Answer%
1028
+ ```bash
1029
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1030
+ import json, sys
1031
+ from collections import defaultdict
1032
+ data = json.load(sys.stdin)
1033
+ stats = defaultdict(lambda: {'total':0,'answered':0,'dur':0})
1034
+ for r in data:
1035
+ ct = r.get('Client type','Unknown')
1036
+ stats[ct]['total'] += 1
1037
+ if r.get('Answer indicator') == 'Yes':
1038
+ stats[ct]['answered'] += 1
1039
+ stats[ct]['dur'] += int(r.get('Duration',0))
1040
+ total = len(data)
1041
+ print(f\"{'Client Type':<25} {'Total':>6} {'%':>5} {'Min':>7} {'Ans%':>6}\")
1042
+ for ct, s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
1043
+ pct = s['total']/total*100 if total else 0
1044
+ rate = s['answered']/s['total']*100 if s['total'] else 0
1045
+ print(f\"{ct:<25} {s['total']:>6} {pct:>4.1f}% {s['dur']/60:>7.1f} {rate:>5.1f}%\")
1046
+ "
1047
+ ```
1048
+
1049
+ **Recipe 48 — Device model inventory**
1050
+ Question: "What phone models are active in CDR?"
1051
+ # Output: Model | Calls | %
1052
+ ```bash
1053
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1054
+ import json, sys
1055
+ from collections import Counter
1056
+ data = json.load(sys.stdin)
1057
+ models = Counter(r.get('Model') for r in data if r.get('Model'))
1058
+ total = sum(models.values())
1059
+ print(f'{len(models)} device models seen:')
1060
+ for model, count in models.most_common():
1061
+ print(f' {model}: {count} ({count/total*100:.1f}%)')
1062
+ "
1063
+ ```
1064
+
1065
+ **Recipe 49 — Calls by OS type**
1066
+ Question: "Mobile vs desktop breakdown?"
1067
+ # Output: OS Type | Count | % | Answer%
1068
+ ```bash
1069
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1070
+ import json, sys
1071
+ from collections import defaultdict
1072
+ data = json.load(sys.stdin)
1073
+ stats = defaultdict(lambda: {'total':0,'answered':0})
1074
+ for r in data:
1075
+ os = r.get('OS type')
1076
+ if os:
1077
+ stats[os]['total'] += 1
1078
+ if r.get('Answer indicator') == 'Yes':
1079
+ stats[os]['answered'] += 1
1080
+ total = sum(s['total'] for s in stats.values())
1081
+ print(f\"{'OS Type':<20} {'Count':>6} {'%':>5} {'Ans%':>6}\")
1082
+ for os, s in sorted(stats.items(), key=lambda x: x[1]['total'], reverse=True):
1083
+ pct = s['total']/total*100 if total else 0
1084
+ rate = s['answered']/s['total']*100 if s['total'] else 0
1085
+ print(f\"{os:<20} {s['total']:>6} {pct:>4.1f}% {rate:>5.1f}%\")
1086
+ "
1087
+ ```
1088
+
1089
+ **Recipe 50 — Softphone vs desk phone ratio**
1090
+ Question: "How many calls from Webex app vs physical phones?"
1091
+ # Output: Client Category | Count | %
1092
+ ```bash
1093
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1094
+ import json, sys
1095
+ data = json.load(sys.stdin)
1096
+ total = len(data)
1097
+ app = len([r for r in data if r.get('Client type') in ('WXC_CLIENT', 'TEAMS_WXC_CLIENT')])
1098
+ device = len([r for r in data if r.get('Client type') == 'WXC_DEVICE'])
1099
+ sip = len([r for r in data if r.get('Client type') == 'SIP'])
1100
+ other = total - app - device - sip
1101
+ print(f'Webex App (softphone): {app} ({app/total*100:.1f}%)')
1102
+ print(f'Desk phone (WXC_DEVICE): {device} ({device/total*100:.1f}%)')
1103
+ print(f'SIP endpoint: {sip} ({sip/total*100:.1f}%)')
1104
+ print(f'Other: {other} ({other/total*100:.1f}%)')
1105
+ "
1106
+ ```
1107
+
1108
+ **Recipe 51 — Webex Go mobile calls**
1109
+ Question: "How many calls over cellular (Webex Go)?"
1110
+ # Output: Time | User | Direction | Duration(s)
1111
+ ```bash
1112
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1113
+ import json, sys
1114
+ data = json.load(sys.stdin)
1115
+ mobile = [r for r in data if r.get('Sub client type') == 'MOBILE_NETWORK']
1116
+ print(f'Webex Go (mobile network) calls: {len(mobile)} of {len(data)}')
1117
+ if not mobile:
1118
+ print('No matching records found in this time window.')
1119
+ else:
1120
+ for c in mobile[:10]:
1121
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | {c.get('Direction')} | {c.get('Duration')}s\")
1122
+ "
1123
+ ```
1124
+
1125
+ **Recipe 52 — Client version distribution**
1126
+ Question: "Are users on the latest app version?"
1127
+ # Output: Client Version | Count | %
1128
+ ```bash
1129
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1130
+ import json, sys
1131
+ from collections import Counter
1132
+ data = json.load(sys.stdin)
1133
+ versions = Counter(r.get('Client version') for r in data if r.get('Client version'))
1134
+ total = sum(versions.values())
1135
+ for version, count in versions.most_common(15):
1136
+ print(f'{version}: {count} ({count/total*100:.1f}%)')
1137
+ "
1138
+ ```
1139
+
1140
+ ### Category 8: Spam & Reputation
1141
+
1142
+ **Recipe 53 — Reputation score distribution**
1143
+ Question: "How many calls by reputation score range?"
1144
+ # Output: Score Range | Count | %
1145
+ ```bash
1146
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1147
+ import json, sys
1148
+ from collections import Counter
1149
+ data = json.load(sys.stdin)
1150
+ buckets = Counter()
1151
+ for r in data:
1152
+ score = r.get('Caller Reputation Score')
1153
+ if score:
1154
+ s = float(score)
1155
+ if s <= 1.0: buckets['0-1 (likely spam)'] += 1
1156
+ elif s <= 2.0: buckets['1-2 (suspicious)'] += 1
1157
+ elif s <= 3.0: buckets['2-3 (neutral)'] += 1
1158
+ elif s <= 4.0: buckets['3-4 (probably ok)'] += 1
1159
+ else: buckets['4-5 (trusted)'] += 1
1160
+ has_score = sum(buckets.values())
1161
+ print(f'Calls with reputation scores: {has_score} of {len(data)}')
1162
+ total_scored = sum(buckets.values())
1163
+ for bucket, count in sorted(buckets.items()):
1164
+ pct = count/total_scored*100 if total_scored else 0
1165
+ print(f' {bucket}: {count} ({pct:.1f}%)')
1166
+ "
1167
+ ```
1168
+
1169
+ **Recipe 54 — Blocked vs allowed ratio**
1170
+ Question: "What percentage of calls are we blocking?"
1171
+ # Output: Result | Count | %
1172
+ ```bash
1173
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1174
+ import json, sys
1175
+ from collections import Counter
1176
+ data = json.load(sys.stdin)
1177
+ results = Counter(r.get('Caller Reputation Service Result') for r in data if r.get('Caller Reputation Service Result'))
1178
+ total = sum(results.values())
1179
+ for result, count in results.most_common():
1180
+ print(f'{result}: {count} ({count/total*100:.1f}%)')
1181
+ "
1182
+ ```
1183
+
1184
+ **Recipe 55 — Top blocked numbers**
1185
+ Question: "Which numbers get blocked most?"
1186
+ # Output: Calling Number | Blocked Count | %
1187
+ ```bash
1188
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1189
+ import json, sys
1190
+ from collections import Counter
1191
+ data = json.load(sys.stdin)
1192
+ blocked = Counter(r.get('Calling number') for r in data
1193
+ if r.get('Caller Reputation Service Result') in ('block', 'captcha-block'))
1194
+ total = sum(blocked.values())
1195
+ print('Top blocked callers:')
1196
+ for number, count in blocked.most_common(15):
1197
+ pct = count/total*100 if total else 0
1198
+ print(f' {number}: {count} ({pct:.1f}%)')
1199
+ "
1200
+ ```
1201
+
1202
+ **Recipe 56 — Spam rate by location**
1203
+ Question: "Which office gets the most spam?"
1204
+ # Output: Location | Spam Count | Spam% | Total
1205
+ ```bash
1206
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1207
+ import json, sys
1208
+ from collections import defaultdict
1209
+ data = json.load(sys.stdin)
1210
+ stats = defaultdict(lambda: {'total': 0, 'spam': 0})
1211
+ for r in data:
1212
+ loc = r.get('Location', 'Unknown')
1213
+ stats[loc]['total'] += 1
1214
+ result = r.get('Caller Reputation Service Result', '')
1215
+ if result in ('block', 'captcha-block', 'captcha-allow'):
1216
+ stats[loc]['spam'] += 1
1217
+ for loc, s in sorted(stats.items(), key=lambda x: x[1]['spam'], reverse=True):
1218
+ rate = s['spam']/s['total']*100 if s['total'] else 0
1219
+ print(f\"{loc}: {s['spam']} spam ({rate:.1f}%) of {s['total']} total\")
1220
+ "
1221
+ ```
1222
+
1223
+ **Recipe 57 — Captcha effectiveness**
1224
+ Question: "How many captcha challenges succeed vs fail?"
1225
+ # Output: Metric | Value
1226
+ ```bash
1227
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1228
+ import json, sys
1229
+ data = json.load(sys.stdin)
1230
+ captcha_allow = len([r for r in data if r.get('Caller Reputation Service Result') == 'captcha-allow'])
1231
+ captcha_block = len([r for r in data if r.get('Caller Reputation Service Result') == 'captcha-block'])
1232
+ total_captcha = captcha_allow + captcha_block
1233
+ if total_captcha:
1234
+ print(f'Captcha challenges: {total_captcha}')
1235
+ print(f' Passed: {captcha_allow} ({captcha_allow/total_captcha*100:.1f}%)')
1236
+ print(f' Failed: {captcha_block} ({captcha_block/total_captcha*100:.1f}%)')
1237
+ else:
1238
+ print('No captcha challenges found')
1239
+ "
1240
+ ```
1241
+
1242
+ ### Category 9: Recording Compliance
1243
+
1244
+ **Recipe 58 — Recording success rate**
1245
+ Question: "What percentage of recordings succeed?"
1246
+ # Output: Recording Result | Count | %
1247
+ ```bash
1248
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1249
+ import json, sys
1250
+ from collections import Counter
1251
+ data = json.load(sys.stdin)
1252
+ results = Counter(r.get('Call Recording Result') for r in data if r.get('Call Recording Result'))
1253
+ total = sum(results.values())
1254
+ print(f'Calls with recording data: {total}')
1255
+ for result, count in results.most_common():
1256
+ print(f' {result}: {count} ({count/total*100:.1f}%)')
1257
+ "
1258
+ ```
1259
+
1260
+ **Recipe 59 — Recording by platform**
1261
+ Question: "Which recording platforms are in use?"
1262
+ # Output: Platform | Count | %
1263
+ ```bash
1264
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1265
+ import json, sys
1266
+ from collections import Counter
1267
+ data = json.load(sys.stdin)
1268
+ platforms = Counter(r.get('Call Recording Platform Name') for r in data if r.get('Call Recording Platform Name'))
1269
+ total = sum(platforms.values())
1270
+ for platform, count in platforms.most_common():
1271
+ print(f'{platform}: {count} ({count/total*100:.1f}%)')
1272
+ "
1273
+ ```
1274
+
1275
+ **Recipe 60 — Recording by trigger type**
1276
+ Question: "How are recordings initiated?"
1277
+ # Output: Trigger | Count | %
1278
+ ```bash
1279
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1280
+ import json, sys
1281
+ from collections import Counter
1282
+ data = json.load(sys.stdin)
1283
+ triggers = Counter(r.get('Call Recording Trigger') for r in data if r.get('Call Recording Trigger'))
1284
+ total = sum(triggers.values())
1285
+ for trigger, count in triggers.most_common():
1286
+ print(f'{trigger}: {count} ({count/total*100:.1f}%)')
1287
+ "
1288
+ ```
1289
+
1290
+ **Recipe 61 — Failed recordings**
1291
+ Question: "Which calls failed to record?"
1292
+ # Output: Time | User | Platform | Calling # | Called #
1293
+ ```bash
1294
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1295
+ import json, sys
1296
+ data = json.load(sys.stdin)
1297
+ failed = [r for r in data if r.get('Call Recording Result') == 'failed']
1298
+ print(f'Failed recordings: {len(failed)}')
1299
+ if not failed:
1300
+ print('No matching records found in this time window.')
1301
+ else:
1302
+ for c in failed[:15]:
1303
+ print(f\" {c.get('Start time')} | {c.get('User')} | {c.get('Call Recording Platform Name')} | {c.get('Calling number')} -> {c.get('Called number')}\")
1304
+ "
1305
+ ```
1306
+
1307
+ **Recipe 62 — Unrecorded calls analysis**
1308
+ Question: "How many answered calls have no recording data?"
1309
+ # Output: Metric | Value (summary) + Location | Unrecorded (sub-table)
1310
+ ```bash
1311
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1312
+ import json, sys
1313
+ from collections import Counter
1314
+ data = json.load(sys.stdin)
1315
+ answered = [r for r in data if r.get('Answer indicator') == 'Yes']
1316
+ no_recording = [r for r in answered if not r.get('Call Recording Result')]
1317
+ print(f'Answered calls: {len(answered)}')
1318
+ print(f'With recording data: {len(answered) - len(no_recording)}')
1319
+ print(f'Without recording data: {len(no_recording)}')
1320
+ if no_recording:
1321
+ locs = Counter(r.get('Location', 'Unknown') for r in no_recording)
1322
+ print(f\"{'Location':<25} {'Unrecorded':>10}\")
1323
+ for loc, count in locs.most_common():
1324
+ print(f\"{loc:<25} {count:>10}\")
1325
+ "
1326
+ ```
1327
+
1328
+ **Recipe 63 — Recording compliance by location**
1329
+ Question: "Which office has the worst recording rate?"
1330
+ # Output: Location | Recorded% | Recorded | Answered | Failed
1331
+ ```bash
1332
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1333
+ import json, sys
1334
+ from collections import defaultdict
1335
+ data = json.load(sys.stdin)
1336
+ stats = defaultdict(lambda: {'answered': 0, 'recorded': 0, 'failed': 0})
1337
+ for r in data:
1338
+ if r.get('Answer indicator') == 'Yes':
1339
+ loc = r.get('Location', 'Unknown')
1340
+ stats[loc]['answered'] += 1
1341
+ result = r.get('Call Recording Result', '')
1342
+ if result == 'successful':
1343
+ stats[loc]['recorded'] += 1
1344
+ elif result == 'failed':
1345
+ stats[loc]['failed'] += 1
1346
+ print(f\"{'Location':<25} {'Recorded%':>10} {'Recorded':>10} {'Answered':>10} {'Failed':>8}\")
1347
+ for loc, s in sorted(stats.items(), key=lambda x: x[1]['recorded']/max(x[1]['answered'],1)):
1348
+ rate = s['recorded']/s['answered']*100 if s['answered'] else 0
1349
+ print(f\"{loc:<25} {rate:>9.1f}% {s['recorded']:>10} {s['answered']:>10} {s['failed']:>8}\")
1350
+ "
1351
+ ```
1352
+
1353
+ ### Category 10: Call Tracing & Diagnostics
1354
+
1355
+ **Recipe 64 — Trace call by correlation ID**
1356
+ Question: "Find all legs of this call"
1357
+ Note: Replace CORRELATION_ID with the actual value. For calls involving transfers, multiple Correlation IDs may be involved — use Recipe 65 to follow the full transfer chain instead.
1358
+ # Output: Leg # | Time | Direction | User type | User | From | To | Duration | Related Reason
1359
+ ```bash
1360
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1361
+ import json, sys
1362
+ data = json.load(sys.stdin)
1363
+ cid = 'CORRELATION_ID'
1364
+ legs = [r for r in data if r.get('Correlation ID') == cid]
1365
+ print(f'Found {len(legs)} legs for correlation ID {cid}:')
1366
+ if not legs:
1367
+ print('No matching records found in this time window.')
1368
+ else:
1369
+ for i, c in enumerate(sorted(legs, key=lambda x: x.get('Start time', '')), 1):
1370
+ print(f' Leg {i}: {c.get(\"Start time\",\"\")[:16]} | {c.get(\"Direction\")} | {c.get(\"User type\",\"?\")} | {c.get(\"User\", \"?\")} | {c.get(\"Calling number\")} -> {c.get(\"Called number\")} | {c.get(\"Duration\")}s | {c.get(\"Related reason\", \"\")}')
1371
+ "
1372
+ ```
1373
+
1374
+ **Recipe 65 — Transfer chain reconstruction**
1375
+ Question: "Show the full transfer path for this call"
1376
+ Note: Replace CORRELATION_ID with the starting Correlation ID. The recipe follows Transfer related call ID → Local call ID linkage to discover all linked Correlation IDs across the transfer chain.
1377
+ # Output: CorrID | Leg # | Time | User type | Direction | Related Reason | From | To
1378
+ ```bash
1379
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1380
+ import json, sys
1381
+ from collections import defaultdict
1382
+ data = json.load(sys.stdin)
1383
+ seed_cid = 'CORRELATION_ID'
1384
+
1385
+ # Build lookup: local_call_id -> CDR
1386
+ by_local = {r.get('Local call ID'): r for r in data if r.get('Local call ID')}
1387
+
1388
+ # Walk transfer chain: follow transfer_related_call_id -> local_call_id links
1389
+ visited = set()
1390
+ queue = [seed_cid]
1391
+ all_cdrs = []
1392
+ while queue:
1393
+ cid = queue.pop()
1394
+ if cid in visited:
1395
+ continue
1396
+ visited.add(cid)
1397
+ group = [r for r in data if r.get('Correlation ID') == cid]
1398
+ all_cdrs.extend(group)
1399
+ for r in group:
1400
+ trid = r.get('Transfer related call ID')
1401
+ if trid and trid in by_local:
1402
+ linked_cid = by_local[trid].get('Correlation ID')
1403
+ if linked_cid and linked_cid not in visited:
1404
+ queue.append(linked_cid)
1405
+
1406
+ print(f'Transfer chain: {len(visited)} Correlation IDs, {len(all_cdrs)} CDRs')
1407
+ if not all_cdrs:
1408
+ print('No matching records found in this time window.')
1409
+ else:
1410
+ for i, c in enumerate(sorted(all_cdrs, key=lambda x: x.get('Start time', '')), 1):
1411
+ cid_short = (c.get('Correlation ID') or '')[-8:]
1412
+ print(f\" {i}: ...{cid_short} | {c.get('Start time','')[:16]} | {c.get('User type','?')} | {c.get('Direction')} | {c.get('Related reason','') or c.get('Transfer related call ID','')} | {c.get('Calling number')} -> {c.get('Called number')}\")
1413
+ "
1414
+ ```
1415
+
1416
+ **Recipe 66 — Park and retrieve trace**
1417
+ Question: "How many calls were parked? Average park duration?"
1418
+ # Output: Metric | Value
1419
+ ```bash
1420
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1421
+ import json, sys
1422
+ data = json.load(sys.stdin)
1423
+ parked = [r for r in data if r.get('Related reason') and 'CallPark' in r.get('Related reason', '')]
1424
+ retrieved = [r for r in data if r.get('Related reason') and 'CallParkRetrieve' in r.get('Related reason', '')]
1425
+ print(f'Call Park events: {len(parked)}')
1426
+ print(f'Call Park Retrieve events: {len(retrieved)}')
1427
+ if not parked:
1428
+ print('No matching records found in this time window.')
1429
+ else:
1430
+ for c in parked[:10]:
1431
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | {c.get('Calling number')} -> {c.get('Called number')}\")
1432
+ "
1433
+ ```
1434
+
1435
+ **Recipe 67 — Forwarding path reconstruction**
1436
+ Question: "How did this call reach this user?"
1437
+ Note: Replace CORRELATION_ID with the actual value.
1438
+ # Output: Hop # | User | Original Reason | Redirect Reason | Related Reason
1439
+ ```bash
1440
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1441
+ import json, sys
1442
+ data = json.load(sys.stdin)
1443
+ cid = 'CORRELATION_ID'
1444
+ legs = sorted([r for r in data if r.get('Correlation ID') == cid], key=lambda x: x.get('Start time', ''))
1445
+ print(f'Forwarding path ({len(legs)} hops):')
1446
+ if not legs:
1447
+ print('No matching records found in this time window.')
1448
+ else:
1449
+ for i, c in enumerate(legs, 1):
1450
+ orig = c.get('Original reason', '-')
1451
+ redir = c.get('Redirect reason', '-')
1452
+ related = c.get('Related reason', '-')
1453
+ print(f' Hop {i}: {c.get(\"User\", \"?\")} | orig={orig} | redirect={redir} | related={related}')
1454
+ "
1455
+ ```
1456
+
1457
+ **Recipe 68 — Call quality proxy (failure patterns)**
1458
+ Question: "Find calls with likely quality issues"
1459
+ # Output: Metric | Value (summary) + Reason | Count (sub-table)
1460
+ ```bash
1461
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1462
+ import json, sys
1463
+ data = json.load(sys.stdin)
1464
+ short_fail = [r for r in data
1465
+ if r.get('Answer indicator') == 'Yes'
1466
+ and int(r.get('Duration', 0)) < 5
1467
+ and r.get('Call outcome') == 'Failure']
1468
+ sip_errors = [r for r in data if r.get('Call outcome reason', '').startswith('SIP')]
1469
+ print(f'Short answered calls that failed (<5s): {len(short_fail)}')
1470
+ print(f'SIP error outcomes: {len(sip_errors)}')
1471
+ if sip_errors:
1472
+ from collections import Counter
1473
+ reasons = Counter(r.get('Call outcome reason') for r in sip_errors)
1474
+ print(f\"{'Reason':<40} {'Count':>6}\")
1475
+ for reason, count in reasons.most_common(10):
1476
+ print(f\" {reason:<38} {count:>6}\")
1477
+ "
1478
+ ```
1479
+
1480
+ **Recipe 69 — Repeated caller detection**
1481
+ Question: "Who's calling the same number over and over?"
1482
+ # Output: Calling # | Called # | Call Count
1483
+ ```bash
1484
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1485
+ import json, sys
1486
+ from collections import Counter
1487
+ data = json.load(sys.stdin)
1488
+ pairs = Counter((r.get('Calling number', '?'), r.get('Called number', '?')) for r in data)
1489
+ repeats = {pair: count for pair, count in pairs.items() if count >= 3}
1490
+ print(f'Number pairs with 3+ calls:')
1491
+ for (caller, called), count in sorted(repeats.items(), key=lambda x: x[1], reverse=True)[:15]:
1492
+ print(f' {caller} -> {called}: {count} calls')
1493
+ "
1494
+ ```
1495
+
1496
+ ### Category 11: Cross-Category Compound Queries
1497
+
1498
+ **Recipe 70 — Spam that reached a queue and was abandoned**
1499
+ Question: "Spam calls that made it into a queue and the caller hung up?"
1500
+ # Output: Time | Rep Score | Rep Result | Calling # | Queue Type
1501
+ ```bash
1502
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1503
+ import json, sys
1504
+ data = json.load(sys.stdin)
1505
+ spam_queue_abandoned = [r for r in data
1506
+ if r.get('Caller Reputation Service Result') in ('captcha-allow', 'allow')
1507
+ and float(r.get('Caller Reputation Score') or '5') < 2.0
1508
+ and r.get('Queue type')
1509
+ and r.get('Answer indicator') == 'No']
1510
+ print(f'Low-reputation calls that reached a queue and were abandoned: {len(spam_queue_abandoned)}')
1511
+ if not spam_queue_abandoned:
1512
+ print('No matching records found in this time window.')
1513
+ else:
1514
+ for c in spam_queue_abandoned[:10]:
1515
+ print(f\" {c.get('Start time','')[:16]} | score={c.get('Caller Reputation Score','?')} | {c.get('Caller Reputation Service Result','?')} | {c.get('Calling number','?')} | queue={c.get('Queue type','?')}\")
1516
+ "
1517
+ ```
1518
+
1519
+ **Recipe 71 — International calls on a specific trunk during business hours**
1520
+ Question: "International calls on trunk X during business hours?"
1521
+ Note: Replace TRUNK_NAME with the actual trunk name.
1522
+ # Output: Time | User | Called # | Duration(s) | Trunk
1523
+ ```bash
1524
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1525
+ import json, sys
1526
+ data = json.load(sys.stdin)
1527
+ trunk_name = 'TRUNK_NAME'
1528
+ results = [r for r in data
1529
+ if r.get('Call type') == 'SIP_INTERNATIONAL'
1530
+ and (r.get('Outbound trunk') == trunk_name or r.get('Inbound trunk') == trunk_name)
1531
+ and 8 <= int(r.get('Start time', '')[11:13] or '0') < 18]
1532
+ print(f'International calls on {trunk_name} during business hours: {len(results)}')
1533
+ total_dur = sum(int(r.get('Duration', 0)) for r in results)
1534
+ print(f'Total duration: {total_dur/60:.1f} min')
1535
+ if not results:
1536
+ print('No matching records found in this time window.')
1537
+ else:
1538
+ for r in results[:10]:
1539
+ trunk_used = r.get('Outbound trunk') or r.get('Inbound trunk','?')
1540
+ print(f\" {r.get('Start time','')[:16]} | {r.get('User','?')} | {r.get('Called number','?')} | {r.get('Duration','?')}s | {trunk_used}\")
1541
+ "
1542
+ ```
1543
+
1544
+ **Recipe 72 — Calls forwarded twice that ended in voicemail**
1545
+ Question: "Calls that were forwarded multiple times and ended in voicemail?"
1546
+ # Output: Time | Calling # | Legs | Redirected Legs
1547
+ ```bash
1548
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1549
+ import json, sys
1550
+ from collections import defaultdict
1551
+ data = json.load(sys.stdin)
1552
+ corr_legs = defaultdict(list)
1553
+ for r in data:
1554
+ cid = r.get('Correlation ID')
1555
+ if cid:
1556
+ corr_legs[cid].append(r)
1557
+ multi_fwd_vm = []
1558
+ for cid, legs in corr_legs.items():
1559
+ redirected = [l for l in legs if l.get('Redirect reason')]
1560
+ vm = [l for l in legs if 'VoiceMail' in l.get('Related reason', '') or 'VoiceMailRetrieval' in l.get('Related reason', '')]
1561
+ if len(redirected) >= 2 and vm:
1562
+ multi_fwd_vm.append((cid, legs))
1563
+ print(f'Calls forwarded 2+ times ending in voicemail: {len(multi_fwd_vm)}')
1564
+ if not multi_fwd_vm:
1565
+ print('No matching records found in this time window.')
1566
+ else:
1567
+ for cid, legs in multi_fwd_vm[:5]:
1568
+ first = sorted(legs, key=lambda x: x.get('Start time',''))[0]
1569
+ redirected_count = len([l for l in legs if l.get('Redirect reason')])
1570
+ print(f\" {first.get('Start time','')[:16]} | {first.get('Calling number','?')} | {len(legs)} legs | {redirected_count} redirected\")
1571
+ "
1572
+ ```
1573
+
1574
+ **Recipe 73 — Missed calls from repeat callers**
1575
+ Question: "Repeat callers we keep missing?"
1576
+ # Output: Calling # | Missed Count
1577
+ ```bash
1578
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1579
+ import json, sys
1580
+ from collections import Counter
1581
+ data = json.load(sys.stdin)
1582
+ missed = [r for r in data if r.get('Answer indicator') == 'No']
1583
+ repeat_missed = Counter(r.get('Calling number', '?') for r in missed)
1584
+ repeats = {num: count for num, count in repeat_missed.items() if count >= 2}
1585
+ print(f'Callers with 2+ missed calls:')
1586
+ for num, count in sorted(repeats.items(), key=lambda x: x[1], reverse=True)[:15]:
1587
+ print(f' {num}: {count} missed calls')
1588
+ "
1589
+ ```
1590
+
1591
+ **Recipe 74 — Long-hold calls that were transferred**
1592
+ Question: "Calls with long hold that ended up being transferred?"
1593
+ # Output: Time | User | Hold Duration(s) | Calling # | Called # | Related Reason
1594
+ ```bash
1595
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1596
+ import json, sys
1597
+ data = json.load(sys.stdin)
1598
+ hold_transfer = [r for r in data
1599
+ if int(r.get('Hold duration', 0)) > 60
1600
+ and r.get('Related reason') and 'Transfer' in r.get('Related reason', '')]
1601
+ print(f'Calls with >60s hold then transferred: {len(hold_transfer)}')
1602
+ if not hold_transfer:
1603
+ print('No matching records found in this time window.')
1604
+ else:
1605
+ for c in hold_transfer[:10]:
1606
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User','?')} | hold {c.get('Hold duration','?')}s | {c.get('Calling number','?')} -> {c.get('Called number','?')} | {c.get('Related reason','?')}\")
1607
+ "
1608
+ ```
1609
+
1610
+ **Recipe 75 — After-hours emergency calls**
1611
+ Question: "Emergency calls outside business hours?"
1612
+ # Output: Time | User | Location | Calling # | Called #
1613
+ ```bash
1614
+ wxcli cdr list --start-time START --end-time END -o json | python3.11 -c "
1615
+ import json, sys
1616
+ data = json.load(sys.stdin)
1617
+ after_hours_emergency = [r for r in data
1618
+ if r.get('Call type') == 'SIP_EMERGENCY'
1619
+ and (int(r.get('Start time', '')[11:13] or '0') < 8 or int(r.get('Start time', '')[11:13] or '0') >= 18)]
1620
+ print(f'After-hours emergency calls: {len(after_hours_emergency)}')
1621
+ if not after_hours_emergency:
1622
+ print('No matching records found in this time window.')
1623
+ else:
1624
+ for c in after_hours_emergency[:10]:
1625
+ print(f\" {c.get('Start time','')[:16]} | {c.get('User')} | {c.get('Location')} | {c.get('Calling number')} -> {c.get('Called number')}\")
1626
+ "
1627
+ ```
1628
+