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.
- wxcli/__init__.py +4 -0
- wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
- wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
- wxcli/_playbook/.claude/rules/cleanup.md +29 -0
- wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
- wxcli/_playbook/.claude/rules/org-health.md +14 -0
- wxcli/_playbook/.claude/settings.json +8 -0
- wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
- wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
- wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
- wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
- wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
- wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
- wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
- wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
- wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
- wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
- wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
- wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
- wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
- wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
- wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
- wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
- wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
- wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
- wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
- wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
- wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
- wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
- wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
- wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
- wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
- wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
- wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
- wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
- wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
- wxcli/_playbook/CLAUDE.md +373 -0
- wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
- wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
- wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
- wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
- wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
- wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
- wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
- wxcli/_playbook/docs/reference/admin-partner.md +357 -0
- wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
- wxcli/_playbook/docs/reference/authentication.md +965 -0
- wxcli/_playbook/docs/reference/call-control.md +1205 -0
- wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
- wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
- wxcli/_playbook/docs/reference/call-routing.md +2304 -0
- wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
- wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
- wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
- wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
- wxcli/_playbook/docs/reference/devices-core.md +1869 -0
- wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
- wxcli/_playbook/docs/reference/devices-platform.md +467 -0
- wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
- wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
- wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
- wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
- wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
- wxcli/_playbook/docs/reference/meetings-content.md +473 -0
- wxcli/_playbook/docs/reference/meetings-core.md +903 -0
- wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
- wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
- wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
- wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
- wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
- wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
- wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
- wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
- wxcli/_playbook/docs/reference/provisioning.md +1366 -0
- wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
- wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
- wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
- wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
- wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
- wxcli/_version.py +24 -0
- wxcli/auth.py +137 -0
- wxcli/commands/__init__.py +0 -0
- wxcli/commands/_registry.py +181 -0
- wxcli/commands/activation_email.py +103 -0
- wxcli/commands/admin_recordings.py +585 -0
- wxcli/commands/analytics.py +105 -0
- wxcli/commands/announcement_playlists.py +219 -0
- wxcli/commands/announcements.py +519 -0
- wxcli/commands/archive_users.py +36 -0
- wxcli/commands/attachment_actions.py +60 -0
- wxcli/commands/audit_events.py +83 -0
- wxcli/commands/authorizations.py +113 -0
- wxcli/commands/auto_attendant.py +642 -0
- wxcli/commands/broadworks_billing_reports.py +126 -0
- wxcli/commands/broadworks_enterprises.py +160 -0
- wxcli/commands/broadworks_subscribers.py +319 -0
- wxcli/commands/broadworks_workspaces.py +108 -0
- wxcli/commands/call_controls.py +1304 -0
- wxcli/commands/call_park.py +494 -0
- wxcli/commands/call_pickup.py +229 -0
- wxcli/commands/call_queue.py +1878 -0
- wxcli/commands/call_recording.py +707 -0
- wxcli/commands/call_routing.py +1610 -0
- wxcli/commands/call_settings_for_me_phase_5.py +212 -0
- wxcli/commands/caller_reputation.py +169 -0
- wxcli/commands/calling_service.py +305 -0
- wxcli/commands/cc_address_book.py +770 -0
- wxcli/commands/cc_agent_greetings.py +242 -0
- wxcli/commands/cc_agent_summaries.py +39 -0
- wxcli/commands/cc_agent_wellbeing.py +216 -0
- wxcli/commands/cc_agents.py +425 -0
- wxcli/commands/cc_ai_assistant.py +55 -0
- wxcli/commands/cc_ai_feature.py +294 -0
- wxcli/commands/cc_audio_files.py +149 -0
- wxcli/commands/cc_auto_csat.py +311 -0
- wxcli/commands/cc_aux_code.py +458 -0
- wxcli/commands/cc_business_hour.py +353 -0
- wxcli/commands/cc_call_monitoring.py +228 -0
- wxcli/commands/cc_callbacks.py +194 -0
- wxcli/commands/cc_campaign.py +212 -0
- wxcli/commands/cc_captures.py +39 -0
- wxcli/commands/cc_contact_list.py +169 -0
- wxcli/commands/cc_contact_number.py +295 -0
- wxcli/commands/cc_data_sources.py +235 -0
- wxcli/commands/cc_desktop_layout.py +380 -0
- wxcli/commands/cc_desktop_profile.py +322 -0
- wxcli/commands/cc_dial_number.py +511 -0
- wxcli/commands/cc_dial_plan.py +347 -0
- wxcli/commands/cc_dnc.py +101 -0
- wxcli/commands/cc_entry_point.py +545 -0
- wxcli/commands/cc_ewt.py +51 -0
- wxcli/commands/cc_flow.py +477 -0
- wxcli/commands/cc_global_vars.py +430 -0
- wxcli/commands/cc_holiday_list.py +335 -0
- wxcli/commands/cc_journey.py +1261 -0
- wxcli/commands/cc_legacy_flows.py +49 -0
- wxcli/commands/cc_multimedia_profile.py +436 -0
- wxcli/commands/cc_notification.py +51 -0
- wxcli/commands/cc_outdial_ani.py +604 -0
- wxcli/commands/cc_overrides.py +341 -0
- wxcli/commands/cc_queue.py +1083 -0
- wxcli/commands/cc_queue_stats.py +55 -0
- wxcli/commands/cc_realtime.py +51 -0
- wxcli/commands/cc_resource_collection.py +312 -0
- wxcli/commands/cc_search.py +46 -0
- wxcli/commands/cc_site.py +400 -0
- wxcli/commands/cc_skill.py +350 -0
- wxcli/commands/cc_skill_profile.py +289 -0
- wxcli/commands/cc_subscriptions.py +426 -0
- wxcli/commands/cc_summaries.py +130 -0
- wxcli/commands/cc_tasks.py +844 -0
- wxcli/commands/cc_team.py +325 -0
- wxcli/commands/cc_user_profiles.py +349 -0
- wxcli/commands/cc_users.py +701 -0
- wxcli/commands/cc_work_types.py +363 -0
- wxcli/commands/cdr.py +89 -0
- wxcli/commands/classifications.py +38 -0
- wxcli/commands/cleanup.py +1427 -0
- wxcli/commands/client_settings.py +72 -0
- wxcli/commands/conference.py +328 -0
- wxcli/commands/configure.py +72 -0
- wxcli/commands/converged_recordings.py +347 -0
- wxcli/commands/converged_recordings_export.py +310 -0
- wxcli/commands/cq_playlists.py +42 -0
- wxcli/commands/cucm.py +2794 -0
- wxcli/commands/cucm_config.py +129 -0
- wxcli/commands/customer_assist.py +373 -0
- wxcli/commands/data_sources.py +227 -0
- wxcli/commands/dect_devices.py +787 -0
- wxcli/commands/device_configurations.py +77 -0
- wxcli/commands/device_dynamic_settings.py +320 -0
- wxcli/commands/device_settings.py +1489 -0
- wxcli/commands/devices.py +280 -0
- wxcli/commands/domains.py +142 -0
- wxcli/commands/ecm.py +178 -0
- wxcli/commands/emergency_services.py +827 -0
- wxcli/commands/events.py +83 -0
- wxcli/commands/external_voicemail.py +52 -0
- wxcli/commands/groups.py +206 -0
- wxcli/commands/guest_management.py +76 -0
- wxcli/commands/hds.py +340 -0
- wxcli/commands/hot_desk.py +67 -0
- wxcli/commands/hot_desking_members.py +118 -0
- wxcli/commands/hot_desking_portal.py +124 -0
- wxcli/commands/hunt_group.py +577 -0
- wxcli/commands/hybrid_clusters.py +71 -0
- wxcli/commands/hybrid_connectors.py +67 -0
- wxcli/commands/identity_org.py +159 -0
- wxcli/commands/init_playbook.py +155 -0
- wxcli/commands/licenses.py +106 -0
- wxcli/commands/live_monitoring.py +40 -0
- wxcli/commands/location_call_handling.py +577 -0
- wxcli/commands/location_schedules.py +341 -0
- wxcli/commands/location_settings.py +1297 -0
- wxcli/commands/location_voicemail.py +494 -0
- wxcli/commands/locations.py +336 -0
- wxcli/commands/meeting_captions.py +110 -0
- wxcli/commands/meeting_chats.py +63 -0
- wxcli/commands/meeting_invitees.py +234 -0
- wxcli/commands/meeting_messages.py +28 -0
- wxcli/commands/meeting_participants.py +278 -0
- wxcli/commands/meeting_polls.py +111 -0
- wxcli/commands/meeting_preferences.py +515 -0
- wxcli/commands/meeting_qa.py +78 -0
- wxcli/commands/meeting_qualities.py +43 -0
- wxcli/commands/meeting_reports.py +101 -0
- wxcli/commands/meeting_session_types.py +105 -0
- wxcli/commands/meeting_site.py +57 -0
- wxcli/commands/meeting_slido.py +44 -0
- wxcli/commands/meeting_summaries.py +92 -0
- wxcli/commands/meeting_tracking_codes.py +233 -0
- wxcli/commands/meeting_transcripts.py +230 -0
- wxcli/commands/meetings.py +1997 -0
- wxcli/commands/memberships.py +164 -0
- wxcli/commands/messages.py +213 -0
- wxcli/commands/mode_management.py +244 -0
- wxcli/commands/my_call_settings.py +3577 -0
- wxcli/commands/numbers.py +405 -0
- wxcli/commands/operating_modes.py +410 -0
- wxcli/commands/org_contacts.py +284 -0
- wxcli/commands/org_health_cli.py +55 -0
- wxcli/commands/org_settings.py +71 -0
- wxcli/commands/organizations.py +83 -0
- wxcli/commands/paging_group.py +257 -0
- wxcli/commands/partner_admins.py +141 -0
- wxcli/commands/partner_reports.py +188 -0
- wxcli/commands/partner_tags.py +216 -0
- wxcli/commands/people.py +279 -0
- wxcli/commands/person_call_settings.py +71 -0
- wxcli/commands/pstn.py +280 -0
- wxcli/commands/recording_report.py +154 -0
- wxcli/commands/report_templates.py +38 -0
- wxcli/commands/reports.py +144 -0
- wxcli/commands/resource_group_memberships.py +160 -0
- wxcli/commands/resource_groups.py +67 -0
- wxcli/commands/roles.py +63 -0
- wxcli/commands/room_tabs.py +160 -0
- wxcli/commands/rooms.py +225 -0
- wxcli/commands/scim_bulk.py +46 -0
- wxcli/commands/scim_groups.py +246 -0
- wxcli/commands/scim_schemas.py +82 -0
- wxcli/commands/scim_users.py +283 -0
- wxcli/commands/security_audit.py +54 -0
- wxcli/commands/service_apps.py +51 -0
- wxcli/commands/single_number_reach.py +209 -0
- wxcli/commands/team_memberships.py +155 -0
- wxcli/commands/teams.py +153 -0
- wxcli/commands/ucm_profile.py +42 -0
- wxcli/commands/update.py +219 -0
- wxcli/commands/user_settings.py +5116 -0
- wxcli/commands/video_mesh.py +1090 -0
- wxcli/commands/virtual_extensions.py +502 -0
- wxcli/commands/virtual_line_settings.py +2072 -0
- wxcli/commands/webhooks.py +176 -0
- wxcli/commands/wholesale_billing_reports.py +147 -0
- wxcli/commands/wholesale_provisioning.py +523 -0
- wxcli/commands/workspace_locations.py +339 -0
- wxcli/commands/workspace_metrics.py +103 -0
- wxcli/commands/workspace_personalization.py +70 -0
- wxcli/commands/workspace_settings.py +3347 -0
- wxcli/commands/workspaces.py +279 -0
- wxcli/commands/xapi.py +114 -0
- wxcli/config.py +139 -0
- wxcli/errors.py +77 -0
- wxcli/main.py +210 -0
- wxcli/migration/CLAUDE.md +106 -0
- wxcli/migration/__init__.py +1 -0
- wxcli/migration/advisory/CLAUDE.md +196 -0
- wxcli/migration/advisory/__init__.py +46 -0
- wxcli/migration/advisory/advisor.py +107 -0
- wxcli/migration/advisory/advisory_patterns.py +2552 -0
- wxcli/migration/advisory/recommendation_rules.py +835 -0
- wxcli/migration/cucm/CLAUDE.md +74 -0
- wxcli/migration/cucm/__init__.py +5 -0
- wxcli/migration/cucm/connection.py +265 -0
- wxcli/migration/cucm/discovery.py +225 -0
- wxcli/migration/cucm/extractors/__init__.py +1 -0
- wxcli/migration/cucm/extractors/announcements.py +92 -0
- wxcli/migration/cucm/extractors/base.py +90 -0
- wxcli/migration/cucm/extractors/device_profiles.py +98 -0
- wxcli/migration/cucm/extractors/devices.py +193 -0
- wxcli/migration/cucm/extractors/e911.py +103 -0
- wxcli/migration/cucm/extractors/features.py +309 -0
- wxcli/migration/cucm/extractors/helpers.py +73 -0
- wxcli/migration/cucm/extractors/informational.py +295 -0
- wxcli/migration/cucm/extractors/locations.py +221 -0
- wxcli/migration/cucm/extractors/moh.py +100 -0
- wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
- wxcli/migration/cucm/extractors/routing.py +493 -0
- wxcli/migration/cucm/extractors/shared_lines.py +146 -0
- wxcli/migration/cucm/extractors/templates.py +229 -0
- wxcli/migration/cucm/extractors/tier4.py +173 -0
- wxcli/migration/cucm/extractors/users.py +240 -0
- wxcli/migration/cucm/extractors/voicemail.py +243 -0
- wxcli/migration/cucm/extractors/workspaces.py +88 -0
- wxcli/migration/cucm/unity_connection.py +304 -0
- wxcli/migration/execute/CLAUDE.md +372 -0
- wxcli/migration/execute/__init__.py +303 -0
- wxcli/migration/execute/batch.py +277 -0
- wxcli/migration/execute/dependency.py +483 -0
- wxcli/migration/execute/engine.py +964 -0
- wxcli/migration/execute/handlers.py +2232 -0
- wxcli/migration/execute/planner.py +1939 -0
- wxcli/migration/execute/runtime.py +571 -0
- wxcli/migration/export/CLAUDE.md +49 -0
- wxcli/migration/export/__init__.py +8 -0
- wxcli/migration/export/csv_export.py +126 -0
- wxcli/migration/export/deployment_plan.py +479 -0
- wxcli/migration/export/json_export.py +60 -0
- wxcli/migration/models.py +941 -0
- wxcli/migration/phone_models.py +50 -0
- wxcli/migration/preflight/CLAUDE.md +52 -0
- wxcli/migration/preflight/__init__.py +89 -0
- wxcli/migration/preflight/checks.py +911 -0
- wxcli/migration/preflight/runner.py +293 -0
- wxcli/migration/rate_limiter.py +144 -0
- wxcli/migration/report/CLAUDE.md +245 -0
- wxcli/migration/report/__init__.py +7 -0
- wxcli/migration/report/appendix.py +2437 -0
- wxcli/migration/report/assembler.py +183 -0
- wxcli/migration/report/charts.py +263 -0
- wxcli/migration/report/executive.py +776 -0
- wxcli/migration/report/explainer.py +704 -0
- wxcli/migration/report/helpers.py +74 -0
- wxcli/migration/report/ingest.py +152 -0
- wxcli/migration/report/notice_templates.py +192 -0
- wxcli/migration/report/score.py +375 -0
- wxcli/migration/report/styles.py +1052 -0
- wxcli/migration/report/user_diff.py +1052 -0
- wxcli/migration/report/user_notice.py +425 -0
- wxcli/migration/state.py +139 -0
- wxcli/migration/store.py +860 -0
- wxcli/migration/transform/CLAUDE.md +107 -0
- wxcli/migration/transform/__init__.py +12 -0
- wxcli/migration/transform/analysis_pipeline.py +453 -0
- wxcli/migration/transform/analyzers/__init__.py +142 -0
- wxcli/migration/transform/analyzers/css_permission.py +189 -0
- wxcli/migration/transform/analyzers/css_routing.py +335 -0
- wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
- wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
- wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
- wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
- wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
- wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
- wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
- wxcli/migration/transform/analyzers/missing_data.py +300 -0
- wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
- wxcli/migration/transform/analyzers/shared_line.py +170 -0
- wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
- wxcli/migration/transform/analyzers/workspace_license.py +212 -0
- wxcli/migration/transform/cross_reference.py +1136 -0
- wxcli/migration/transform/cucm_pattern.py +246 -0
- wxcli/migration/transform/decisions.py +301 -0
- wxcli/migration/transform/e164.py +145 -0
- wxcli/migration/transform/engine.py +256 -0
- wxcli/migration/transform/mappers/CLAUDE.md +123 -0
- wxcli/migration/transform/mappers/__init__.py +76 -0
- wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
- wxcli/migration/transform/mappers/base.py +190 -0
- wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
- wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
- wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
- wxcli/migration/transform/mappers/css_mapper.py +868 -0
- wxcli/migration/transform/mappers/dect_mapper.py +367 -0
- wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
- wxcli/migration/transform/mappers/device_mapper.py +227 -0
- wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
- wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
- wxcli/migration/transform/mappers/e911_mapper.py +129 -0
- wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
- wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
- wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
- wxcli/migration/transform/mappers/line_mapper.py +310 -0
- wxcli/migration/transform/mappers/location_mapper.py +235 -0
- wxcli/migration/transform/mappers/moh_mapper.py +114 -0
- wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
- wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
- wxcli/migration/transform/mappers/routing_mapper.py +821 -0
- wxcli/migration/transform/mappers/snr_mapper.py +201 -0
- wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
- wxcli/migration/transform/mappers/user_mapper.py +264 -0
- wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
- wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
- wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
- wxcli/migration/transform/normalizers.py +2008 -0
- wxcli/migration/transform/pattern_converter.py +106 -0
- wxcli/migration/transform/pipeline.py +271 -0
- wxcli/migration/transform/rules.py +255 -0
- wxcli/org_health/CLAUDE.md +63 -0
- wxcli/org_health/__init__.py +7 -0
- wxcli/org_health/analyze.py +93 -0
- wxcli/org_health/checks.py +458 -0
- wxcli/org_health/collector.py +62 -0
- wxcli/org_health/models.py +78 -0
- wxcli/org_health/report.py +300 -0
- wxcli/output.py +101 -0
- wxcli-1.2.0.dist-info/METADATA +505 -0
- wxcli-1.2.0.dist-info/RECORD +413 -0
- wxcli-1.2.0.dist-info/WHEEL +5 -0
- wxcli-1.2.0.dist-info/entry_points.txt +2 -0
- wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
- wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
- wxcli-1.2.0.dist-info/scm_version.json +8 -0
- 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
|
+
|