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,776 @@
|
|
|
1
|
+
"""Executive summary HTML generator for CUCM assessment reports (v4).
|
|
2
|
+
|
|
3
|
+
Generates a 4-page narrative: Verdict → Environment → Scope → Next Steps.
|
|
4
|
+
Depends on score, charts, explainer, and helpers modules.
|
|
5
|
+
|
|
6
|
+
One public function: generate_executive_summary().
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import html
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from wxcli.migration.report.charts import donut_chart, gauge_chart, stacked_bar_chart
|
|
16
|
+
from wxcli.migration.report.explainer import (
|
|
17
|
+
DECISION_TYPE_DISPLAY_NAMES,
|
|
18
|
+
explain_decision,
|
|
19
|
+
generate_key_findings,
|
|
20
|
+
generate_verdict,
|
|
21
|
+
)
|
|
22
|
+
from wxcli.migration.report.helpers import friendly_site_name, strip_canonical_id
|
|
23
|
+
from wxcli.migration.report.score import compute_complexity_score
|
|
24
|
+
from wxcli.migration.store import MigrationStore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Feature type display names
|
|
28
|
+
_FEATURE_DISPLAY_NAMES = {
|
|
29
|
+
"hunt_group": "Hunt Group",
|
|
30
|
+
"call_queue": "Call Queue",
|
|
31
|
+
"auto_attendant": "Auto Attendant",
|
|
32
|
+
"call_park": "Call Park",
|
|
33
|
+
"pickup_group": "Pickup Group",
|
|
34
|
+
"paging_group": "Paging Group",
|
|
35
|
+
"call_forwarding": "Call Forwarding",
|
|
36
|
+
"monitoring_list": "BLF / Monitoring",
|
|
37
|
+
"line_key_template": "Phone Button Templates",
|
|
38
|
+
"softkey_config": "Softkey Templates",
|
|
39
|
+
"executive_assistant": "Executive/Assistant",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Webex equivalents for feature types
|
|
43
|
+
_WEBEX_EQUIVALENTS = {
|
|
44
|
+
"hunt_group": "Hunt Group",
|
|
45
|
+
"call_queue": "Call Queue",
|
|
46
|
+
"auto_attendant": "Auto Attendant",
|
|
47
|
+
"call_park": "Call Park",
|
|
48
|
+
"pickup_group": "Call Pickup",
|
|
49
|
+
"paging_group": "Paging Group",
|
|
50
|
+
"call_forwarding": "Per-person call forwarding",
|
|
51
|
+
"monitoring_list": "Per-person monitoring list",
|
|
52
|
+
"line_key_template": "Line Key Templates",
|
|
53
|
+
"softkey_config": "Programmable Softkeys (PSK)",
|
|
54
|
+
"executive_assistant": "Executive/Assistant (native)",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Object types for counts
|
|
58
|
+
_REPORT_OBJECT_TYPES = [
|
|
59
|
+
"user", "device", "location", "hunt_group", "call_queue",
|
|
60
|
+
"auto_attendant", "call_park", "pickup_group", "paging_group",
|
|
61
|
+
"trunk", "route_group", "dial_plan", "translation_pattern",
|
|
62
|
+
"css", "partition", "shared_line", "workspace", "virtual_line",
|
|
63
|
+
"operating_mode", "schedule", "voicemail_profile", "calling_permission",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _count_selective_call_handling_candidates(store: MigrationStore) -> int:
|
|
68
|
+
"""Count non-stale FEATURE_APPROXIMATION decisions tagged with the
|
|
69
|
+
selective_call_handling_pattern context key."""
|
|
70
|
+
count = 0
|
|
71
|
+
for d in store.get_all_decisions():
|
|
72
|
+
if d.get("type") != "FEATURE_APPROXIMATION":
|
|
73
|
+
continue
|
|
74
|
+
if d.get("chosen_option") == "__stale__":
|
|
75
|
+
continue
|
|
76
|
+
ctx = d.get("context", {}) or {}
|
|
77
|
+
if ctx.get("selective_call_handling_pattern"):
|
|
78
|
+
count += 1
|
|
79
|
+
return count
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def generate_executive_summary(
|
|
83
|
+
store: MigrationStore,
|
|
84
|
+
brand: str,
|
|
85
|
+
prepared_by: str,
|
|
86
|
+
cluster_name: str = "",
|
|
87
|
+
cucm_version: str = "",
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Generate the 4-page executive summary HTML from a populated store."""
|
|
90
|
+
sections = [
|
|
91
|
+
_page_verdict(store, brand, cluster_name, cucm_version),
|
|
92
|
+
_page_environment(store, brand, cluster_name, cucm_version),
|
|
93
|
+
_page_scope(store),
|
|
94
|
+
_page_next_steps(store, brand, prepared_by),
|
|
95
|
+
]
|
|
96
|
+
return "\n\n".join(sections)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Page 1 — The Verdict
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _page_verdict(store: MigrationStore, brand: str, cluster_name: str = "", cucm_version: str = "") -> str:
|
|
104
|
+
"""Page 1: Answer 'should I be worried?' in 5 seconds."""
|
|
105
|
+
result = compute_complexity_score(store)
|
|
106
|
+
gauge_svg = gauge_chart(result.score, result.color, result.label)
|
|
107
|
+
verdict_text = generate_verdict(result, store)
|
|
108
|
+
findings = generate_key_findings(store)
|
|
109
|
+
|
|
110
|
+
parts = [
|
|
111
|
+
f'<section id="score">',
|
|
112
|
+
f'<h2>Migration Complexity Assessment</h2>',
|
|
113
|
+
f'<div class="verdict">{verdict_text}</div>',
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
# Score layout: gauge left, factor bars right
|
|
117
|
+
parts.append('<div class="score-layout">')
|
|
118
|
+
parts.append(f'<div class="score-gauge">\n{gauge_svg}\n</div>')
|
|
119
|
+
|
|
120
|
+
if result.factors:
|
|
121
|
+
sorted_factors = sorted(result.factors, key=lambda f: f["raw_score"], reverse=True)
|
|
122
|
+
parts.append('<div class="score-breakdown">')
|
|
123
|
+
parts.append(
|
|
124
|
+
'<div class="factor-scale">'
|
|
125
|
+
'<span class="factor-scale-title">Complexity Impact</span>'
|
|
126
|
+
'<span class="factor-scale-low">Low</span>'
|
|
127
|
+
'<span class="factor-scale-high">High</span>'
|
|
128
|
+
'</div>'
|
|
129
|
+
)
|
|
130
|
+
for i, factor in enumerate(sorted_factors):
|
|
131
|
+
display = factor.get("display_name", factor["name"])
|
|
132
|
+
raw = factor["raw_score"]
|
|
133
|
+
color = "var(--primary)" if i == 0 and raw > 0 else "var(--slate-400)"
|
|
134
|
+
parts.append(
|
|
135
|
+
f'<div class="factor-row">'
|
|
136
|
+
f'<span class="factor-label">{html.escape(display)}</span>'
|
|
137
|
+
f'<div class="factor-bar">'
|
|
138
|
+
f'<div class="factor-fill" style="width:{raw}%;background:{color};"></div>'
|
|
139
|
+
f'</div>'
|
|
140
|
+
f'</div>'
|
|
141
|
+
)
|
|
142
|
+
parts.append('</div>')
|
|
143
|
+
parts.append('</div>') # close score-layout
|
|
144
|
+
|
|
145
|
+
# Calibration disclaimer
|
|
146
|
+
if not result.calibrated:
|
|
147
|
+
parts.append(
|
|
148
|
+
'<div class="callout info">'
|
|
149
|
+
'<p><strong>Note:</strong> This complexity score uses design-time weights '
|
|
150
|
+
'that have not yet been calibrated against completed migrations. '
|
|
151
|
+
'Use as a relative indicator, not an absolute measure.</p>'
|
|
152
|
+
'</div>'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Key findings
|
|
156
|
+
if findings:
|
|
157
|
+
parts.append('<ul class="key-findings">')
|
|
158
|
+
for f in findings:
|
|
159
|
+
icon_class = "check" if f["icon"] == "✓" else "alert"
|
|
160
|
+
icon_text = "✓" if f["icon"] == "✓" else "!"
|
|
161
|
+
parts.append(
|
|
162
|
+
f'<li>'
|
|
163
|
+
f'<span class="finding-icon {icon_class}">{icon_text}</span>'
|
|
164
|
+
f'<span>{f["text"]}</span>'
|
|
165
|
+
f'</li>'
|
|
166
|
+
)
|
|
167
|
+
parts.append('</ul>')
|
|
168
|
+
|
|
169
|
+
# Stat grid
|
|
170
|
+
total_objects = _total_object_count(store)
|
|
171
|
+
parts.append('<div class="stat-grid">')
|
|
172
|
+
parts.append(_stat_card(html.escape(brand), "Customer"))
|
|
173
|
+
if cluster_name:
|
|
174
|
+
parts.append(_stat_card(html.escape(cluster_name), "CUCM Cluster"))
|
|
175
|
+
if cucm_version:
|
|
176
|
+
parts.append(_stat_card(html.escape(cucm_version), "CUCM Version"))
|
|
177
|
+
parts.append(_stat_card(str(total_objects), "Total Objects"))
|
|
178
|
+
parts.append(_stat_card(str(store.count_by_type("location")), "Sites"))
|
|
179
|
+
parts.append(_stat_card(str(store.count_by_type("user")), "Users"))
|
|
180
|
+
parts.append(_stat_card(str(len(store.get_objects("device"))), "Devices"))
|
|
181
|
+
parts.append('</div>')
|
|
182
|
+
|
|
183
|
+
parts.append('</section>')
|
|
184
|
+
return "\n".join(parts)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Page 2 — Your Environment
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def _page_environment(
|
|
192
|
+
store: MigrationStore,
|
|
193
|
+
brand: str,
|
|
194
|
+
cluster_name: str,
|
|
195
|
+
cucm_version: str,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Page 2: Four semantic groups — People, Devices, Call Features, Sites."""
|
|
198
|
+
user_count = store.count_by_type("user")
|
|
199
|
+
device_count = len(store.get_objects("device"))
|
|
200
|
+
location_count = store.count_by_type("location")
|
|
201
|
+
|
|
202
|
+
parts = [
|
|
203
|
+
f'<section id="inventory">',
|
|
204
|
+
f'<h2>What You Have</h2>',
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
# --- Donut chart floated right alongside People + Devices ---
|
|
208
|
+
devices = store.get_objects("device")
|
|
209
|
+
donut_html = ""
|
|
210
|
+
native = convertible = incompatible = 0
|
|
211
|
+
if devices:
|
|
212
|
+
native = sum(1 for d in devices if d.get("compatibility_tier") == "native_mpp")
|
|
213
|
+
convertible = sum(1 for d in devices if d.get("compatibility_tier") == "convertible")
|
|
214
|
+
incompatible = sum(1 for d in devices if d.get("compatibility_tier") == "incompatible")
|
|
215
|
+
webex_app = sum(1 for d in devices if d.get("compatibility_tier") == "webex_app")
|
|
216
|
+
dect = sum(1 for d in devices if d.get("compatibility_tier") == "dect")
|
|
217
|
+
donut_segments = [
|
|
218
|
+
{"label": "Native MPP", "value": native, "color": "#2E7D32"},
|
|
219
|
+
{"label": "Convertible", "value": convertible, "color": "#EF6C00"},
|
|
220
|
+
{"label": "Incompatible", "value": incompatible, "color": "#C62828"},
|
|
221
|
+
]
|
|
222
|
+
if webex_app > 0:
|
|
223
|
+
donut_segments.append({"label": "Webex App", "value": webex_app, "color": "#0277BD"})
|
|
224
|
+
if dect > 0:
|
|
225
|
+
donut_segments.append({"label": "DECT", "value": dect, "color": "#42A5F5"})
|
|
226
|
+
donut_html = donut_chart(donut_segments)
|
|
227
|
+
|
|
228
|
+
if donut_html:
|
|
229
|
+
parts.append(f'<div class="env-donut">{donut_html}</div>')
|
|
230
|
+
|
|
231
|
+
# --- People ---
|
|
232
|
+
shared_line_count = store.count_by_type("shared_line")
|
|
233
|
+
line_count = store.count_by_type("line")
|
|
234
|
+
|
|
235
|
+
parts.append('<h3>People</h3>')
|
|
236
|
+
parts.append('<div class="stat-grid">')
|
|
237
|
+
parts.append(_stat_card(str(user_count), "Users"))
|
|
238
|
+
if shared_line_count:
|
|
239
|
+
parts.append(_stat_card(str(shared_line_count), "Shared Lines"))
|
|
240
|
+
if line_count:
|
|
241
|
+
parts.append(_stat_card(str(line_count), "Extensions"))
|
|
242
|
+
intercept_count = store.count_by_type("intercept_candidate")
|
|
243
|
+
if intercept_count:
|
|
244
|
+
parts.append(_stat_card(str(intercept_count), "Intercept Candidates"))
|
|
245
|
+
parts.append('</div>')
|
|
246
|
+
|
|
247
|
+
# --- Devices ---
|
|
248
|
+
if devices:
|
|
249
|
+
total = len(devices)
|
|
250
|
+
|
|
251
|
+
parts.append('<h3>Devices</h3>')
|
|
252
|
+
parts.append('<div class="stat-grid">')
|
|
253
|
+
parts.append(_stat_card(str(total), "Total Phones"))
|
|
254
|
+
parts.append(_stat_card(str(native), "Native MPP"))
|
|
255
|
+
if convertible:
|
|
256
|
+
parts.append(_stat_card(str(convertible), "Convertible"))
|
|
257
|
+
if incompatible:
|
|
258
|
+
parts.append(_stat_card(str(incompatible), "Incompatible"))
|
|
259
|
+
if dect:
|
|
260
|
+
parts.append(_stat_card(str(dect), "DECT"))
|
|
261
|
+
parts.append('</div>')
|
|
262
|
+
|
|
263
|
+
if donut_html:
|
|
264
|
+
parts.append('<div style="clear:both;"></div>')
|
|
265
|
+
|
|
266
|
+
# --- Analog Gateways (if present) ---
|
|
267
|
+
gateways = store.get_objects("gateway")
|
|
268
|
+
if gateways:
|
|
269
|
+
analog_count = 0
|
|
270
|
+
estimated_ports = 0
|
|
271
|
+
for gw in gateways:
|
|
272
|
+
state = gw.get("pre_migration_state", {})
|
|
273
|
+
product = (state.get("product", "") or "").upper()
|
|
274
|
+
protocol = (state.get("protocol", "") or "").upper()
|
|
275
|
+
is_analog = protocol in ("MGCP", "H.323", "H323") or any(
|
|
276
|
+
kw in product for kw in ("VG", "ATA", "ISR", "FXS", "FXO")
|
|
277
|
+
)
|
|
278
|
+
if is_analog:
|
|
279
|
+
analog_count += 1
|
|
280
|
+
# Quick port estimate from product name
|
|
281
|
+
for model, ports in [("ATA 191", 2), ("ATA 192", 2), ("VG310", 24),
|
|
282
|
+
("VG350", 48), ("VG400", 8), ("VG450", 200)]:
|
|
283
|
+
if model.upper() in product:
|
|
284
|
+
estimated_ports += ports
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
if analog_count > 0:
|
|
288
|
+
port_text = f" with an estimated <strong>{estimated_ports} ports</strong>" if estimated_ports else ""
|
|
289
|
+
parts.append(
|
|
290
|
+
f'<div class="callout warning">'
|
|
291
|
+
f'<p><strong>{analog_count} analog gateway{"s" if analog_count != 1 else ""}</strong>'
|
|
292
|
+
f'{port_text} serving fax, paging, and analog devices. '
|
|
293
|
+
f'Each port requires manual mapping to a Webex workspace with ATA provisioning. '
|
|
294
|
+
f'See Gateway & Analog Port Review in the Technical Appendix.</p>'
|
|
295
|
+
f'</div>'
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# --- Custom Audio Assets (if present) ---
|
|
299
|
+
moh_sources = store.get_objects("music_on_hold")
|
|
300
|
+
announcements = store.get_objects("announcement")
|
|
301
|
+
custom_moh = [m for m in moh_sources if not m.get("is_default", False)]
|
|
302
|
+
if custom_moh or announcements:
|
|
303
|
+
parts.append('<h3>Custom Audio</h3>')
|
|
304
|
+
parts.append('<div class="stat-grid">')
|
|
305
|
+
if custom_moh:
|
|
306
|
+
parts.append(_stat_card(str(len(custom_moh)), "Custom MoH Sources"))
|
|
307
|
+
if announcements:
|
|
308
|
+
parts.append(_stat_card(str(len(announcements)), "Announcement Files"))
|
|
309
|
+
parts.append('</div>')
|
|
310
|
+
parts.append(
|
|
311
|
+
'<div class="callout warning">'
|
|
312
|
+
'<p>Custom audio files must be downloaded from CUCM and uploaded to '
|
|
313
|
+
'Webex before migration. See Audio Assets in the Technical Appendix.</p>'
|
|
314
|
+
'</div>'
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# --- Call Features ---
|
|
318
|
+
feature_types = ["hunt_group", "call_queue", "auto_attendant",
|
|
319
|
+
"call_park", "pickup_group", "paging_group",
|
|
320
|
+
"call_forwarding", "monitoring_list",
|
|
321
|
+
"executive_assistant"]
|
|
322
|
+
feature_data = []
|
|
323
|
+
for ft in feature_types:
|
|
324
|
+
count = store.count_by_type(ft)
|
|
325
|
+
if count > 0:
|
|
326
|
+
feature_data.append((ft, _FEATURE_DISPLAY_NAMES.get(ft, ft), count))
|
|
327
|
+
|
|
328
|
+
if feature_data:
|
|
329
|
+
parts.append('<h3>Call Features</h3>')
|
|
330
|
+
parts.append('<div class="stat-grid">')
|
|
331
|
+
for ft, display, count in feature_data:
|
|
332
|
+
parts.append(_stat_card(str(count), display))
|
|
333
|
+
parts.append('</div>')
|
|
334
|
+
|
|
335
|
+
# Feature mapping table with badges
|
|
336
|
+
feature_table = _build_feature_mapping_table(store)
|
|
337
|
+
if feature_table:
|
|
338
|
+
parts.append(feature_table)
|
|
339
|
+
|
|
340
|
+
# --- Sites ---
|
|
341
|
+
locations = store.get_objects("location")
|
|
342
|
+
if locations:
|
|
343
|
+
parts.append('<h3>Sites</h3>')
|
|
344
|
+
site_rows = _build_site_breakdown(store, locations)
|
|
345
|
+
parts.append(_build_table(
|
|
346
|
+
headers=["Site", "Users", "Devices", "Complexity"],
|
|
347
|
+
rows=site_rows,
|
|
348
|
+
))
|
|
349
|
+
|
|
350
|
+
# Migration coverage callout — Tier 3 informational types
|
|
351
|
+
info_prefixes = [
|
|
352
|
+
"info_region", "info_srst", "info_media_resource_group",
|
|
353
|
+
"info_media_resource_list", "info_aar_group",
|
|
354
|
+
"info_device_mobility_group", "info_conference_bridge",
|
|
355
|
+
"info_softkey_template", "info_ip_phone_service", "info_intercom",
|
|
356
|
+
"info_common_phone_config", "info_phone_button_template",
|
|
357
|
+
"info_feature_control_policy", "info_credential_policy",
|
|
358
|
+
"info_recording_profile", "info_ldap_directory",
|
|
359
|
+
"info_app_user", "info_h323_gateway",
|
|
360
|
+
"info_enterprise_params", "info_service_params",
|
|
361
|
+
]
|
|
362
|
+
info_total = sum(store.count_by_type(p) for p in info_prefixes)
|
|
363
|
+
if info_total > 0:
|
|
364
|
+
mapped_types = sum(1 for t in _REPORT_OBJECT_TYPES if store.count_by_type(t) > 0)
|
|
365
|
+
info_types = sum(1 for p in info_prefixes if store.count_by_type(p) > 0)
|
|
366
|
+
parts.append(
|
|
367
|
+
'<div class="callout">'
|
|
368
|
+
f'<p><strong>Migration Coverage:</strong> '
|
|
369
|
+
f'{mapped_types} object types fully mapped for migration, '
|
|
370
|
+
f'{info_types} additional CUCM types documented for reference '
|
|
371
|
+
f'(see Technical Appendix sections O\u2013R).</p>'
|
|
372
|
+
'</div>'
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
parts.append('</section>')
|
|
376
|
+
return "\n".join(parts)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# Page 3 — Migration Scope (Effort Bands)
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
def _page_scope(store: MigrationStore) -> str:
|
|
384
|
+
"""Page 3: Three effort bands — auto, planning, manual."""
|
|
385
|
+
decisions = store.get_all_decisions()
|
|
386
|
+
|
|
387
|
+
# Decision summary stat grid
|
|
388
|
+
total_decisions = len(decisions)
|
|
389
|
+
resolved = sum(1 for d in decisions if d.get("chosen_option"))
|
|
390
|
+
unresolved = total_decisions - resolved
|
|
391
|
+
critical = sum(1 for d in decisions if d.get("severity", "").upper() == "CRITICAL")
|
|
392
|
+
|
|
393
|
+
auto, planning, manual = _classify_decisions(decisions)
|
|
394
|
+
|
|
395
|
+
parts = [
|
|
396
|
+
f'<section id="decisions">',
|
|
397
|
+
f'<h2>What Needs Attention</h2>',
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
parts.append('<div class="stat-grid">')
|
|
401
|
+
parts.append(_stat_card(str(resolved), "Auto-resolved"))
|
|
402
|
+
parts.append(_stat_card(str(unresolved), "Decisions Needed"))
|
|
403
|
+
if critical:
|
|
404
|
+
parts.append(_stat_card(str(critical), "Critical"))
|
|
405
|
+
parts.append('</div>')
|
|
406
|
+
|
|
407
|
+
# Effort band: Migrates Automatically
|
|
408
|
+
parts.append('<div class="effort-band auto">')
|
|
409
|
+
parts.append(f'<h4>Migrates Automatically</h4>')
|
|
410
|
+
parts.append(f'<p>{len(auto)} items migrate with no manual intervention.</p>')
|
|
411
|
+
if auto:
|
|
412
|
+
parts.append('<ul>')
|
|
413
|
+
for d in auto[:5]:
|
|
414
|
+
display_type = DECISION_TYPE_DISPLAY_NAMES.get(d["type"], d["type"])
|
|
415
|
+
summary = d.get("summary", "")
|
|
416
|
+
parts.append(f'<li><strong>{html.escape(display_type)}:</strong> {html.escape(summary)}</li>')
|
|
417
|
+
if len(auto) > 5:
|
|
418
|
+
parts.append(f'<li><em>...and {len(auto) - 5} more</em></li>')
|
|
419
|
+
parts.append('</ul>')
|
|
420
|
+
parts.append('</div>')
|
|
421
|
+
|
|
422
|
+
# Effort band: Needs Planning
|
|
423
|
+
parts.append('<div class="effort-band planning">')
|
|
424
|
+
parts.append(f'<h4>Needs Planning</h4>')
|
|
425
|
+
parts.append(f'<p>{len(planning)} items need configuration decisions during planning.</p>')
|
|
426
|
+
if planning:
|
|
427
|
+
parts.append('<ul>')
|
|
428
|
+
for d in planning[:5]:
|
|
429
|
+
display_type = DECISION_TYPE_DISPLAY_NAMES.get(d["type"], d["type"])
|
|
430
|
+
summary = d.get("summary", "")
|
|
431
|
+
parts.append(f'<li><strong>{html.escape(display_type)}:</strong> {html.escape(summary)}</li>')
|
|
432
|
+
if len(planning) > 5:
|
|
433
|
+
parts.append(f'<li><em>...and {len(planning) - 5} more</em></li>')
|
|
434
|
+
parts.append('</ul>')
|
|
435
|
+
parts.append('</div>')
|
|
436
|
+
|
|
437
|
+
# Effort band: Requires Manual Work
|
|
438
|
+
parts.append('<div class="effort-band manual">')
|
|
439
|
+
parts.append(f'<h4>Requires Manual Work</h4>')
|
|
440
|
+
parts.append(f'<p>{len(manual)} items require manual configuration or hardware changes.</p>')
|
|
441
|
+
if manual:
|
|
442
|
+
parts.append('<ul>')
|
|
443
|
+
for d in manual[:5]:
|
|
444
|
+
display_type = DECISION_TYPE_DISPLAY_NAMES.get(d["type"], d["type"])
|
|
445
|
+
summary = d.get("summary", "")
|
|
446
|
+
parts.append(f'<li><strong>{html.escape(display_type)}:</strong> {html.escape(summary)}</li>')
|
|
447
|
+
if len(manual) > 5:
|
|
448
|
+
parts.append(f'<li><em>...and {len(manual) - 5} more</em></li>')
|
|
449
|
+
parts.append('</ul>')
|
|
450
|
+
parts.append('</div>')
|
|
451
|
+
|
|
452
|
+
# Decision resolution bar
|
|
453
|
+
total = len(decisions)
|
|
454
|
+
resolved = sum(1 for d in decisions if d.get("chosen_option"))
|
|
455
|
+
if total > 0:
|
|
456
|
+
pct = round(resolved / total * 100)
|
|
457
|
+
parts.append(
|
|
458
|
+
f'<p class="small muted">'
|
|
459
|
+
f'Decision resolution: <strong>{resolved} of {total}</strong> auto-resolved ({pct}%)</p>'
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
sch_count = _count_selective_call_handling_candidates(store)
|
|
463
|
+
if sch_count:
|
|
464
|
+
parts.append(
|
|
465
|
+
f'<div class="callout">'
|
|
466
|
+
f'<p><strong>Selective Call Handling:</strong> {sch_count} '
|
|
467
|
+
f'candidate(s) for Webex Selective Forward / Accept / Reject. '
|
|
468
|
+
f'See appendix section AB for the per-user breakdown.</p>'
|
|
469
|
+
f'</div>'
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
parts.append('</section>')
|
|
473
|
+
return "\n".join(parts)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _classify_decisions(
|
|
477
|
+
decisions: list[dict[str, Any]],
|
|
478
|
+
) -> tuple[list[dict], list[dict], list[dict]]:
|
|
479
|
+
"""Classify decisions into auto/planning/manual effort bands.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
(auto_resolved, needs_planning, requires_manual) — three lists.
|
|
483
|
+
"""
|
|
484
|
+
auto = []
|
|
485
|
+
planning = []
|
|
486
|
+
manual = []
|
|
487
|
+
|
|
488
|
+
# Types that typically require manual work
|
|
489
|
+
manual_types = {"DEVICE_INCOMPATIBLE", "VOICEMAIL_INCOMPATIBLE", "MISSING_DATA"}
|
|
490
|
+
# Types that need planning decisions
|
|
491
|
+
planning_types = {
|
|
492
|
+
"CSS_ROUTING_MISMATCH", "CALLING_PERMISSION_MISMATCH", "LOCATION_AMBIGUOUS",
|
|
493
|
+
"SHARED_LINE_COMPLEX", "EXTENSION_CONFLICT", "DN_AMBIGUOUS",
|
|
494
|
+
"WORKSPACE_TYPE_UNCERTAIN", "WORKSPACE_LICENSE_TIER",
|
|
495
|
+
"HOTDESK_DN_CONFLICT", "NUMBER_CONFLICT", "DUPLICATE_USER",
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for d in decisions:
|
|
499
|
+
if d.get("chosen_option"):
|
|
500
|
+
auto.append(d)
|
|
501
|
+
elif d.get("type") in manual_types:
|
|
502
|
+
# Webex App transitions are INFO severity and go to planning, not manual
|
|
503
|
+
if d.get("type") == "DEVICE_INCOMPATIBLE":
|
|
504
|
+
ctx = d.get("context", {})
|
|
505
|
+
if isinstance(ctx, str):
|
|
506
|
+
try:
|
|
507
|
+
ctx = json.loads(ctx)
|
|
508
|
+
except (json.JSONDecodeError, TypeError):
|
|
509
|
+
ctx = {}
|
|
510
|
+
if ctx.get("webex_app_transition") is True:
|
|
511
|
+
planning.append(d)
|
|
512
|
+
continue
|
|
513
|
+
manual.append(d)
|
|
514
|
+
elif d.get("type") in planning_types:
|
|
515
|
+
planning.append(d)
|
|
516
|
+
elif d.get("severity", "").upper() in ("CRITICAL", "HIGH"):
|
|
517
|
+
manual.append(d)
|
|
518
|
+
else:
|
|
519
|
+
planning.append(d)
|
|
520
|
+
|
|
521
|
+
return auto, planning, manual
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# ---------------------------------------------------------------------------
|
|
525
|
+
# Page 4 — Next Steps
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
def _page_next_steps(
|
|
529
|
+
store: MigrationStore,
|
|
530
|
+
brand: str,
|
|
531
|
+
prepared_by: str,
|
|
532
|
+
) -> str:
|
|
533
|
+
"""Page 4: Prerequisites, planning phase items, CTA."""
|
|
534
|
+
decisions = store.get_all_decisions()
|
|
535
|
+
unresolved = [d for d in decisions if d.get("chosen_option") is None]
|
|
536
|
+
user_count = store.count_by_type("user")
|
|
537
|
+
device_count = len(store.get_objects("device"))
|
|
538
|
+
|
|
539
|
+
parts = [
|
|
540
|
+
f'<section id="next-steps">',
|
|
541
|
+
f'<h2>Next Steps</h2>',
|
|
542
|
+
]
|
|
543
|
+
|
|
544
|
+
# Before Migration
|
|
545
|
+
parts.append('<h3>Before Migration</h3>')
|
|
546
|
+
parts.append('<table>')
|
|
547
|
+
parts.append('<thead><tr><th>Item</th><th class="num">Count</th><th>Status</th></tr></thead>')
|
|
548
|
+
parts.append('<tbody>')
|
|
549
|
+
parts.append(
|
|
550
|
+
f'<tr><td>Webex Calling licenses needed</td>'
|
|
551
|
+
f'<td class="num">{user_count}</td>'
|
|
552
|
+
f'<td>Verify with Cisco</td></tr>'
|
|
553
|
+
)
|
|
554
|
+
parts.append(
|
|
555
|
+
f'<tr><td>Phone numbers to port</td>'
|
|
556
|
+
f'<td class="num">{user_count}</td>'
|
|
557
|
+
f'<td>Confirm with carrier</td></tr>'
|
|
558
|
+
)
|
|
559
|
+
parts.append(
|
|
560
|
+
f'<tr><td>Decisions to resolve</td>'
|
|
561
|
+
f'<td class="num">{len(unresolved)}</td>'
|
|
562
|
+
f'<td>{"Pending review" if unresolved else "Complete"}</td></tr>'
|
|
563
|
+
)
|
|
564
|
+
parts.append('</tbody></table>')
|
|
565
|
+
|
|
566
|
+
# Voicemail greeting re-recording callout
|
|
567
|
+
greeting_count = _count_custom_greetings(store)
|
|
568
|
+
if greeting_count > 0:
|
|
569
|
+
parts.append(
|
|
570
|
+
f'<div class="callout warning">'
|
|
571
|
+
f'<p><strong>Voicemail Greeting Re-Recording:</strong> '
|
|
572
|
+
f'{greeting_count} user{"s" if greeting_count != 1 else ""} '
|
|
573
|
+
f'must re-record their voicemail greeting{"s" if greeting_count != 1 else ""} after migration. '
|
|
574
|
+
f'Send user communication at least 1 week before cutover. '
|
|
575
|
+
f'See Appendix H for email template.</p>'
|
|
576
|
+
f'</div>'
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Planning Phase
|
|
580
|
+
if unresolved:
|
|
581
|
+
parts.append('<h3>Planning Phase</h3>')
|
|
582
|
+
parts.append(f'<p>{len(unresolved)} decisions need review before migration can proceed:</p>')
|
|
583
|
+
parts.append('<ul>')
|
|
584
|
+
# Group by type
|
|
585
|
+
type_counts: dict[str, int] = {}
|
|
586
|
+
for d in unresolved:
|
|
587
|
+
dt = d.get("type", "UNKNOWN")
|
|
588
|
+
type_counts[dt] = type_counts.get(dt, 0) + 1
|
|
589
|
+
for dt, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
|
590
|
+
display = DECISION_TYPE_DISPLAY_NAMES.get(dt, dt)
|
|
591
|
+
parts.append(f'<li>{html.escape(display)}: {count}</li>')
|
|
592
|
+
parts.append('</ul>')
|
|
593
|
+
|
|
594
|
+
# CTA box
|
|
595
|
+
parts.append(
|
|
596
|
+
f'<div class="cta-box">'
|
|
597
|
+
f'<h3>Ready to Plan</h3>'
|
|
598
|
+
f'<p>For questions about this assessment or to start the migration planning engagement, '
|
|
599
|
+
f'contact <strong>{html.escape(prepared_by)}</strong>.</p>'
|
|
600
|
+
f'</div>'
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
parts.append('</section>')
|
|
604
|
+
return "\n".join(parts)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
# Helpers
|
|
609
|
+
# ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def _stat_card(value: str, label: str) -> str:
|
|
612
|
+
"""Render a stat card HTML fragment."""
|
|
613
|
+
return (
|
|
614
|
+
f'<div class="stat-card">'
|
|
615
|
+
f'<div class="stat-number">{html.escape(value)}</div>'
|
|
616
|
+
f'<div class="stat-label">{html.escape(label)}</div>'
|
|
617
|
+
f'</div>'
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _build_site_breakdown(
|
|
622
|
+
store: MigrationStore,
|
|
623
|
+
locations: list[dict[str, Any]],
|
|
624
|
+
) -> list[tuple[str, ...]]:
|
|
625
|
+
"""Build per-site breakdown rows."""
|
|
626
|
+
decisions = store.get_all_decisions()
|
|
627
|
+
all_users = store.get_objects("user")
|
|
628
|
+
all_devices = store.get_objects("device")
|
|
629
|
+
rows = []
|
|
630
|
+
|
|
631
|
+
for loc in locations:
|
|
632
|
+
loc_id = loc.get("canonical_id", "")
|
|
633
|
+
loc_name = loc.get("name", "")
|
|
634
|
+
if not loc_name:
|
|
635
|
+
loc_name = strip_canonical_id(loc_id)
|
|
636
|
+
friendly = friendly_site_name(loc_name) if loc_name.startswith("DP-") else loc_name
|
|
637
|
+
|
|
638
|
+
user_count = sum(1 for u in all_users if u.get("location_id") == loc_id)
|
|
639
|
+
loc_user_ids = {u.get("canonical_id") for u in all_users if u.get("location_id") == loc_id}
|
|
640
|
+
device_count = sum(1 for d in all_devices if d.get("owner_canonical_id") in loc_user_ids)
|
|
641
|
+
loc_decisions = _count_decisions_for_location(store, decisions, loc_id, loc_user_ids)
|
|
642
|
+
|
|
643
|
+
if loc_decisions == 0:
|
|
644
|
+
complexity = '<span class="badge badge-direct">Straightforward</span>'
|
|
645
|
+
elif loc_decisions <= 2:
|
|
646
|
+
complexity = '<span class="badge badge-approx">Moderate</span>'
|
|
647
|
+
else:
|
|
648
|
+
complexity = '<span class="badge badge-decision">Complex</span>'
|
|
649
|
+
|
|
650
|
+
rows.append((
|
|
651
|
+
html.escape(friendly),
|
|
652
|
+
str(user_count),
|
|
653
|
+
str(device_count),
|
|
654
|
+
complexity,
|
|
655
|
+
))
|
|
656
|
+
|
|
657
|
+
return rows
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _count_custom_greetings(store: MigrationStore) -> int:
|
|
661
|
+
"""Count MISSING_DATA decisions for custom voicemail greetings."""
|
|
662
|
+
count = 0
|
|
663
|
+
for d in store.get_all_decisions():
|
|
664
|
+
if d.get("type") != "MISSING_DATA":
|
|
665
|
+
continue
|
|
666
|
+
ctx = d.get("context", {})
|
|
667
|
+
if ctx.get("reason") == "custom_greeting_not_extractable":
|
|
668
|
+
count += 1
|
|
669
|
+
return count
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _count_decisions_for_location(
|
|
673
|
+
store: MigrationStore,
|
|
674
|
+
decisions: list[dict[str, Any]],
|
|
675
|
+
loc_id: str,
|
|
676
|
+
loc_user_ids: set[str],
|
|
677
|
+
) -> int:
|
|
678
|
+
"""Count decisions that reference a given location or its objects."""
|
|
679
|
+
count = 0
|
|
680
|
+
for d in decisions:
|
|
681
|
+
ctx = d.get("context", {})
|
|
682
|
+
obj_id = ctx.get("object_id", "")
|
|
683
|
+
css_id = ctx.get("css_id", "")
|
|
684
|
+
|
|
685
|
+
if obj_id:
|
|
686
|
+
obj = store.get_object(obj_id)
|
|
687
|
+
if obj and obj.get("location_id") == loc_id:
|
|
688
|
+
count += 1
|
|
689
|
+
elif obj_id in loc_user_ids:
|
|
690
|
+
count += 1
|
|
691
|
+
elif css_id:
|
|
692
|
+
count += 1
|
|
693
|
+
return count
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _build_feature_mapping_table(
|
|
697
|
+
store: MigrationStore,
|
|
698
|
+
) -> str:
|
|
699
|
+
"""Build feature mapping table with Direct/Approximation badges."""
|
|
700
|
+
decisions = store.get_all_decisions()
|
|
701
|
+
approx_by_object: dict[str, dict[str, Any]] = {}
|
|
702
|
+
for d in decisions:
|
|
703
|
+
if d["type"] == "FEATURE_APPROXIMATION":
|
|
704
|
+
obj_id = d.get("context", {}).get("object_id", "")
|
|
705
|
+
if obj_id:
|
|
706
|
+
approx_by_object[obj_id] = d
|
|
707
|
+
|
|
708
|
+
rows = []
|
|
709
|
+
for type_key, display_name in _FEATURE_DISPLAY_NAMES.items():
|
|
710
|
+
count = store.count_by_type(type_key)
|
|
711
|
+
if count == 0:
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
webex_equiv = _WEBEX_EQUIVALENTS.get(type_key, display_name)
|
|
715
|
+
has_approx = False
|
|
716
|
+
has_unresolved = False
|
|
717
|
+
|
|
718
|
+
objects = store.get_objects(type_key)
|
|
719
|
+
for obj in objects:
|
|
720
|
+
obj_id = obj.get("canonical_id", "")
|
|
721
|
+
if obj_id in approx_by_object:
|
|
722
|
+
has_approx = True
|
|
723
|
+
d = approx_by_object[obj_id]
|
|
724
|
+
ctx_webex = d.get("context", {}).get("webex_feature", "")
|
|
725
|
+
if ctx_webex:
|
|
726
|
+
webex_equiv = ctx_webex
|
|
727
|
+
if d.get("chosen_option") is None:
|
|
728
|
+
has_unresolved = True
|
|
729
|
+
|
|
730
|
+
if not has_approx:
|
|
731
|
+
status = '<span class="badge badge-direct">Direct</span>'
|
|
732
|
+
elif has_unresolved:
|
|
733
|
+
status = '<span class="badge badge-decision">Decision needed</span>'
|
|
734
|
+
else:
|
|
735
|
+
status = '<span class="badge badge-approx">Approximation</span>'
|
|
736
|
+
|
|
737
|
+
rows.append((
|
|
738
|
+
display_name,
|
|
739
|
+
str(count),
|
|
740
|
+
html.escape(webex_equiv),
|
|
741
|
+
status,
|
|
742
|
+
))
|
|
743
|
+
|
|
744
|
+
if not rows:
|
|
745
|
+
return ""
|
|
746
|
+
|
|
747
|
+
return _build_table(
|
|
748
|
+
headers=["CUCM Feature", "Count", "Webex Equivalent", "Status"],
|
|
749
|
+
rows=rows,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _build_table(
|
|
754
|
+
headers: list[str],
|
|
755
|
+
rows: list[tuple[str, ...]],
|
|
756
|
+
) -> str:
|
|
757
|
+
"""Build an HTML table from headers and row tuples."""
|
|
758
|
+
parts = ['<table>']
|
|
759
|
+
parts.append('<thead><tr>')
|
|
760
|
+
for h in headers:
|
|
761
|
+
parts.append(f'<th>{html.escape(h)}</th>')
|
|
762
|
+
parts.append('</tr></thead>')
|
|
763
|
+
parts.append('<tbody>')
|
|
764
|
+
for row in rows:
|
|
765
|
+
parts.append('<tr>')
|
|
766
|
+
for cell in row:
|
|
767
|
+
parts.append(f'<td>{cell}</td>')
|
|
768
|
+
parts.append('</tr>')
|
|
769
|
+
parts.append('</tbody>')
|
|
770
|
+
parts.append('</table>')
|
|
771
|
+
return "\n".join(parts)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _total_object_count(store: MigrationStore) -> int:
|
|
775
|
+
"""Count all objects in the store across known report types."""
|
|
776
|
+
return sum(store.count_by_type(t) for t in _REPORT_OBJECT_TYPES)
|