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,183 @@
|
|
|
1
|
+
"""Report assembler — combines executive summary + appendix into a self-contained HTML file (v4).
|
|
2
|
+
|
|
3
|
+
One public function: ``assemble_report()``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import html as html_mod
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from wxcli.migration.report.executive import generate_executive_summary
|
|
12
|
+
from wxcli.migration.report.appendix import generate_appendix
|
|
13
|
+
from wxcli.migration.report.styles import REPORT_CSS, GOOGLE_FONTS_LINKS
|
|
14
|
+
from wxcli.migration.store import MigrationStore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def assemble_report(
|
|
18
|
+
store: MigrationStore,
|
|
19
|
+
brand: str,
|
|
20
|
+
prepared_by: str,
|
|
21
|
+
cluster_name: str = "",
|
|
22
|
+
cucm_version: str = "",
|
|
23
|
+
executive_only: bool = False,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Assemble a complete, self-contained HTML assessment report.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
store: Populated MigrationStore (post-analyze state).
|
|
29
|
+
brand: Customer/brand name for the report header.
|
|
30
|
+
prepared_by: Name of the SE/engineer who prepared the report.
|
|
31
|
+
cluster_name: CUCM cluster name (optional).
|
|
32
|
+
cucm_version: CUCM version string (optional).
|
|
33
|
+
executive_only: If True, skip the technical appendix.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Complete HTML document string (starts with ``<!DOCTYPE html>``).
|
|
37
|
+
"""
|
|
38
|
+
safe_brand = html_mod.escape(brand)
|
|
39
|
+
safe_prepared_by = html_mod.escape(prepared_by)
|
|
40
|
+
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
41
|
+
|
|
42
|
+
# Generate executive summary HTML
|
|
43
|
+
executive_html = generate_executive_summary(
|
|
44
|
+
store,
|
|
45
|
+
brand=brand,
|
|
46
|
+
prepared_by=prepared_by,
|
|
47
|
+
cluster_name=cluster_name,
|
|
48
|
+
cucm_version=cucm_version,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Generate appendix HTML (unless executive_only)
|
|
52
|
+
appendix_html = ""
|
|
53
|
+
if not executive_only:
|
|
54
|
+
appendix_html = generate_appendix(store)
|
|
55
|
+
|
|
56
|
+
# Build sidebar nav
|
|
57
|
+
sidebar_nav = _build_sidebar_nav(executive_only)
|
|
58
|
+
|
|
59
|
+
# IntersectionObserver script for scroll tracking
|
|
60
|
+
observer_js = _intersection_observer_js()
|
|
61
|
+
|
|
62
|
+
return f"""\
|
|
63
|
+
<!DOCTYPE html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="UTF-8">
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
68
|
+
<title>CUCM Migration Assessment — {safe_brand}</title>
|
|
69
|
+
{GOOGLE_FONTS_LINKS}
|
|
70
|
+
<style>{REPORT_CSS}</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<!-- Print header (hidden on screen) -->
|
|
74
|
+
<div class="print-header">
|
|
75
|
+
<h1>CUCM Migration Assessment</h1>
|
|
76
|
+
<p class="meta">{safe_brand} | Prepared by {safe_prepared_by} | {date_str}</p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Screen header -->
|
|
80
|
+
<div class="page-header">
|
|
81
|
+
<div class="brand">{safe_brand}</div>
|
|
82
|
+
<h1>CUCM Migration Assessment</h1>
|
|
83
|
+
<div class="subtitle">Prepared by {safe_prepared_by}</div>
|
|
84
|
+
<div class="meta">{date_str}</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="main-layout">
|
|
88
|
+
<nav class="step-list">
|
|
89
|
+
{sidebar_nav}
|
|
90
|
+
</nav>
|
|
91
|
+
<div class="detail-panel">
|
|
92
|
+
{executive_html}
|
|
93
|
+
{appendix_html}
|
|
94
|
+
<footer class="report-footer">
|
|
95
|
+
<p>Generated by cucm-assess | {date_str}</p>
|
|
96
|
+
</footer>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{observer_js}
|
|
101
|
+
</body>
|
|
102
|
+
</html>"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_sidebar_nav(executive_only: bool) -> str:
|
|
106
|
+
"""Build the sidebar navigation HTML with numbered exec + lettered tech items."""
|
|
107
|
+
exec_items = [
|
|
108
|
+
("1", "#score", "Migration Complexity"),
|
|
109
|
+
("2", "#inventory", "What You Have"),
|
|
110
|
+
("3", "#decisions", "What Needs Attention"),
|
|
111
|
+
("4", "#next-steps", "Next Steps"),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
tech_items = [
|
|
115
|
+
("A", "#objects", "Object Inventory"),
|
|
116
|
+
("B", "#decision-detail", "Decision Detail"),
|
|
117
|
+
("C", "#css-partitions", "CSS / Partitions"),
|
|
118
|
+
("D", "#device-detail", "Device Inventory"),
|
|
119
|
+
("E", "#dn-analysis", "DN Analysis"),
|
|
120
|
+
("F", "#user-device-map", "User/Device Map"),
|
|
121
|
+
("G", "#routing", "Routing Topology"),
|
|
122
|
+
("H", "#voicemail", "Voicemail Analysis"),
|
|
123
|
+
("I", "#audio-assets", "Audio Assets"),
|
|
124
|
+
("J", "#coverage", "Data Coverage"),
|
|
125
|
+
("K", "#gateways", "Gateways & Analog Ports"),
|
|
126
|
+
("L", "#call-features", "Call Features"),
|
|
127
|
+
("M", "#button-templates", "Button Templates"),
|
|
128
|
+
("N", "#device-layouts", "Device Layouts"),
|
|
129
|
+
("O", "#softkeys", "Softkey Migration"),
|
|
130
|
+
("P", "#cloud-managed", "Cloud-Managed"),
|
|
131
|
+
("Q", "#feature-gaps", "Feature Gaps"),
|
|
132
|
+
("R", "#manual-reconfig", "Manual Reconfig"),
|
|
133
|
+
("S", "#planning-inputs", "Planning Inputs"),
|
|
134
|
+
("T", "#recording-inventory", "Call Recording"),
|
|
135
|
+
("U", "#snr-inventory", "Single Number Reach"),
|
|
136
|
+
("V", "#caller-id-xforms", "Caller ID Transforms"),
|
|
137
|
+
("W", "#extension-mobility", "Extension Mobility"),
|
|
138
|
+
("X", "#dect-networks", "DECT Networks"),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
parts = [
|
|
142
|
+
'<div class="step-list-section">',
|
|
143
|
+
'<div class="step-list-section-title">Executive Summary</div>',
|
|
144
|
+
]
|
|
145
|
+
for num, href, label in exec_items:
|
|
146
|
+
parts.append(
|
|
147
|
+
f'<a href="{href}" class="step-item">'
|
|
148
|
+
f'<span class="step-icon exec">{num}</span> {html_mod.escape(label)}</a>'
|
|
149
|
+
)
|
|
150
|
+
parts.append('</div>')
|
|
151
|
+
|
|
152
|
+
if not executive_only:
|
|
153
|
+
parts.append('<div class="step-list-section">')
|
|
154
|
+
parts.append('<div class="step-list-section-title">Technical Appendix</div>')
|
|
155
|
+
for letter, href, label in tech_items:
|
|
156
|
+
parts.append(
|
|
157
|
+
f'<a href="{href}" class="step-item">'
|
|
158
|
+
f'<span class="step-icon tech">{letter}</span> {html_mod.escape(label)}</a>'
|
|
159
|
+
)
|
|
160
|
+
parts.append('</div>')
|
|
161
|
+
|
|
162
|
+
return "\n ".join(parts)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _intersection_observer_js() -> str:
|
|
166
|
+
"""Return IntersectionObserver JS for sidebar scroll tracking."""
|
|
167
|
+
return """<script>
|
|
168
|
+
(function() {
|
|
169
|
+
const sections = document.querySelectorAll('.detail-panel section, .detail-panel details');
|
|
170
|
+
const navLinks = document.querySelectorAll('.step-item');
|
|
171
|
+
const observer = new IntersectionObserver(entries => {
|
|
172
|
+
entries.forEach(entry => {
|
|
173
|
+
if (entry.isIntersecting) {
|
|
174
|
+
const id = entry.target.id;
|
|
175
|
+
navLinks.forEach(link => {
|
|
176
|
+
link.classList.toggle('active', link.getAttribute('href') === '#' + id);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}, { root: document.querySelector('.detail-panel'), threshold: 0.2 });
|
|
181
|
+
sections.forEach(s => { if (s.id) observer.observe(s); });
|
|
182
|
+
})();
|
|
183
|
+
</script>"""
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Inline SVG chart generators for the CUCM assessment report.
|
|
2
|
+
|
|
3
|
+
Pure functions that accept data and return SVG strings.
|
|
4
|
+
No store dependency, no side effects.
|
|
5
|
+
All SVGs are inline-safe for HTML embedding (no JavaScript).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import html
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
_FONT = 'font-family="IBM Plex Sans, system-ui, sans-serif"'
|
|
14
|
+
_XMLNS = 'xmlns="http://www.w3.org/2000/svg"'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# 1. Gauge chart — 240-degree arc for complexity score
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def gauge_chart(score: int, color: str, label: str) -> str:
|
|
22
|
+
"""Return an SVG gauge chart showing a score from 0-100.
|
|
23
|
+
|
|
24
|
+
The gauge is a 240-degree arc (from 150 degrees to 390 degrees,
|
|
25
|
+
i.e. 7-o'clock to 5-o'clock). A gray background arc shows the full
|
|
26
|
+
range; a colored foreground arc shows the proportion filled.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
score: Integer 0-100.
|
|
30
|
+
color: Hex color for the foreground arc (e.g. "#2E7D32").
|
|
31
|
+
label: Text label below the score (e.g. "Straightforward").
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
SVG string.
|
|
35
|
+
"""
|
|
36
|
+
score = max(0, min(100, score))
|
|
37
|
+
|
|
38
|
+
cx, cy = 100, 95
|
|
39
|
+
r = 70
|
|
40
|
+
stroke_width = 12
|
|
41
|
+
total_angle = 240 # degrees
|
|
42
|
+
start_angle = 150 # degrees (7-o'clock position)
|
|
43
|
+
|
|
44
|
+
def _arc_endpoint(angle_deg: float) -> tuple[float, float]:
|
|
45
|
+
rad = math.radians(angle_deg)
|
|
46
|
+
return cx + r * math.cos(rad), cy + r * math.sin(rad)
|
|
47
|
+
|
|
48
|
+
def _arc_path(from_angle: float, sweep_deg: float, stroke: str, width: int = stroke_width) -> str:
|
|
49
|
+
if sweep_deg <= 0:
|
|
50
|
+
return ""
|
|
51
|
+
x1, y1 = _arc_endpoint(from_angle)
|
|
52
|
+
x2, y2 = _arc_endpoint(from_angle + sweep_deg)
|
|
53
|
+
large_arc = 1 if sweep_deg > 180 else 0
|
|
54
|
+
return (
|
|
55
|
+
f'<path d="M {x1:.1f} {y1:.1f} A {r} {r} 0 {large_arc} 1 {x2:.1f} {y2:.1f}" '
|
|
56
|
+
f'fill="none" stroke="{stroke}" stroke-width="{width}" stroke-linecap="round"/>'
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
fg_angle = total_angle * score / 100
|
|
60
|
+
|
|
61
|
+
parts = [
|
|
62
|
+
f'<svg {_XMLNS} viewBox="0 0 200 155" width="160" height="125">',
|
|
63
|
+
# Background arc (full 240 degrees)
|
|
64
|
+
_arc_path(start_angle, total_angle, "#e5e7eb"),
|
|
65
|
+
# Foreground arc (proportional to score)
|
|
66
|
+
_arc_path(start_angle, fg_angle, color),
|
|
67
|
+
# Score number centered
|
|
68
|
+
f'<text x="{cx}" y="{cy + 8}" text-anchor="middle" '
|
|
69
|
+
f'{_FONT} font-size="36" font-weight="700" fill="#111827">{score}</text>',
|
|
70
|
+
# Label below
|
|
71
|
+
f'<text x="{cx}" y="{cy + 50}" text-anchor="middle" '
|
|
72
|
+
f'{_FONT} font-size="9" font-weight="500" letter-spacing="0.1em" '
|
|
73
|
+
f'fill="#6b7280">{html.escape(label.upper())}</text>',
|
|
74
|
+
"</svg>",
|
|
75
|
+
]
|
|
76
|
+
return "\n".join(p for p in parts if p)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# 2. Donut chart — segment breakdown (e.g. phone compatibility)
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def donut_chart(segments: list[dict]) -> str:
|
|
84
|
+
"""Return an SVG donut chart with legend.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
segments: List of dicts with keys "label", "value", "color".
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
SVG string.
|
|
91
|
+
"""
|
|
92
|
+
total = sum(s["value"] for s in segments)
|
|
93
|
+
|
|
94
|
+
cx, cy = 100, 100
|
|
95
|
+
r = 70
|
|
96
|
+
stroke_width = 30
|
|
97
|
+
circumference = 2 * math.pi * r
|
|
98
|
+
|
|
99
|
+
parts = [
|
|
100
|
+
f'<svg {_XMLNS} viewBox="0 0 400 250" width="400" height="250">',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
if total == 0:
|
|
104
|
+
# Empty state: just draw a gray circle
|
|
105
|
+
parts.append(
|
|
106
|
+
f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" '
|
|
107
|
+
f'stroke="#E0E0E0" stroke-width="{stroke_width}"/>'
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
# Draw each segment using stroke-dasharray on rotated circles.
|
|
111
|
+
cumulative_offset = 0.0
|
|
112
|
+
for seg in segments:
|
|
113
|
+
if seg["value"] <= 0:
|
|
114
|
+
continue
|
|
115
|
+
seg_fraction = seg["value"] / total
|
|
116
|
+
seg_len = circumference * seg_fraction
|
|
117
|
+
gap_len = circumference - seg_len
|
|
118
|
+
# stroke-dashoffset rotates the start; we offset by cumulative.
|
|
119
|
+
# SVG circles start at 3-o'clock; rotate -90 to start at 12-o'clock.
|
|
120
|
+
dash_offset = circumference * 0.25 - cumulative_offset
|
|
121
|
+
parts.append(
|
|
122
|
+
f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" '
|
|
123
|
+
f'stroke="{seg["color"]}" stroke-width="{stroke_width}" '
|
|
124
|
+
f'stroke-dasharray="{seg_len:.2f} {gap_len:.2f}" '
|
|
125
|
+
f'stroke-dashoffset="{dash_offset:.2f}"/>'
|
|
126
|
+
)
|
|
127
|
+
cumulative_offset += seg_len
|
|
128
|
+
|
|
129
|
+
# Center label: total count
|
|
130
|
+
parts.append(
|
|
131
|
+
f'<text x="{cx}" y="{cy + 6}" text-anchor="middle" '
|
|
132
|
+
f'{_FONT} font-size="28" font-weight="700" fill="#212121">{total}</text>'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Legend — to the right and below
|
|
136
|
+
legend_x = 220
|
|
137
|
+
legend_y = 30
|
|
138
|
+
row_height = 28
|
|
139
|
+
for seg in segments:
|
|
140
|
+
pct = round(seg["value"] / total * 100) if total > 0 else 0
|
|
141
|
+
# Color swatch
|
|
142
|
+
parts.append(
|
|
143
|
+
f'<rect x="{legend_x}" y="{legend_y - 10}" width="12" height="12" '
|
|
144
|
+
f'rx="2" fill="{seg["color"]}"/>'
|
|
145
|
+
)
|
|
146
|
+
# Label + value + percentage
|
|
147
|
+
parts.append(
|
|
148
|
+
f'<text x="{legend_x + 18}" y="{legend_y}" '
|
|
149
|
+
f'{_FONT} font-size="11" fill="#424242">'
|
|
150
|
+
f'{html.escape(seg["label"])} {seg["value"]} ({pct}%)</text>'
|
|
151
|
+
)
|
|
152
|
+
legend_y += row_height
|
|
153
|
+
|
|
154
|
+
parts.append("</svg>")
|
|
155
|
+
return "\n".join(parts)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# 3. Horizontal bar chart — object inventory
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def horizontal_bar_chart(items: list[dict]) -> str:
|
|
163
|
+
"""Return an SVG horizontal bar chart, sorted descending by value.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
items: List of dicts with keys "label", "value", "color".
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
SVG string.
|
|
170
|
+
"""
|
|
171
|
+
sorted_items = sorted(items, key=lambda x: x["value"], reverse=True)
|
|
172
|
+
|
|
173
|
+
row_height = 30
|
|
174
|
+
padding_top = 10
|
|
175
|
+
padding_bottom = 30
|
|
176
|
+
label_width = 120
|
|
177
|
+
value_width = 50
|
|
178
|
+
bar_area_width = 300
|
|
179
|
+
total_width = 500
|
|
180
|
+
total_height = len(sorted_items) * row_height + padding_top + padding_bottom
|
|
181
|
+
|
|
182
|
+
max_value = max((it["value"] for it in sorted_items), default=1)
|
|
183
|
+
if max_value == 0:
|
|
184
|
+
max_value = 1
|
|
185
|
+
|
|
186
|
+
parts = [
|
|
187
|
+
f'<svg {_XMLNS} viewBox="0 0 {total_width} {total_height}" '
|
|
188
|
+
f'width="{total_width}" height="{total_height}">',
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
for i, item in enumerate(sorted_items):
|
|
192
|
+
y = padding_top + i * row_height
|
|
193
|
+
bar_width = (item["value"] / max_value) * bar_area_width if max_value else 0
|
|
194
|
+
text_y = y + row_height * 0.65
|
|
195
|
+
|
|
196
|
+
# Label (right-aligned within label area)
|
|
197
|
+
parts.append(
|
|
198
|
+
f'<text x="{label_width - 8}" y="{text_y}" text-anchor="end" '
|
|
199
|
+
f'{_FONT} font-size="12" fill="#424242">{html.escape(item["label"])}</text>'
|
|
200
|
+
)
|
|
201
|
+
# Bar
|
|
202
|
+
parts.append(
|
|
203
|
+
f'<rect x="{label_width}" y="{y + 4}" '
|
|
204
|
+
f'width="{bar_width:.1f}" height="{row_height - 10}" '
|
|
205
|
+
f'rx="3" fill="{item["color"]}"/>'
|
|
206
|
+
)
|
|
207
|
+
# Value (after bar)
|
|
208
|
+
parts.append(
|
|
209
|
+
f'<text x="{label_width + bar_area_width + 8}" y="{text_y}" '
|
|
210
|
+
f'{_FONT} font-size="12" font-weight="600" fill="#424242">{item["value"]}</text>'
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
parts.append("</svg>")
|
|
214
|
+
return "\n".join(parts)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# 4. Stacked bar chart — horizontal segments with legend
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def stacked_bar_chart(segments: list[dict]) -> str:
|
|
222
|
+
"""Render a horizontal stacked bar with legend.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
segments: List of {"label": str, "value": int, "color": hex}.
|
|
226
|
+
Zero-value segments are omitted.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
HTML string (div with inline stacked bar + legend), or "" if no data.
|
|
230
|
+
"""
|
|
231
|
+
segments = [s for s in segments if s.get("value", 0) > 0]
|
|
232
|
+
if not segments:
|
|
233
|
+
return ""
|
|
234
|
+
|
|
235
|
+
total = sum(s["value"] for s in segments)
|
|
236
|
+
if total == 0:
|
|
237
|
+
return ""
|
|
238
|
+
|
|
239
|
+
bar_parts = []
|
|
240
|
+
for seg in segments:
|
|
241
|
+
pct = seg["value"] / total * 100
|
|
242
|
+
bar_parts.append(
|
|
243
|
+
f'<div style="background:{seg["color"]};width:{pct:.1f}%;" '
|
|
244
|
+
f'title="{html.escape(seg["label"])}: {seg["value"]}"></div>'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
legend_parts = []
|
|
248
|
+
for seg in segments:
|
|
249
|
+
pct = seg["value"] / total * 100
|
|
250
|
+
legend_parts.append(
|
|
251
|
+
f'<span>'
|
|
252
|
+
f'<span style="display:inline-block;width:8px;height:8px;border-radius:2px;'
|
|
253
|
+
f'background:{seg["color"]};margin-right:4px;vertical-align:middle;"></span>'
|
|
254
|
+
f'{html.escape(seg["label"])}: {seg["value"]} ({pct:.0f}%)</span>'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
f'<div style="display:flex;height:20px;border-radius:4px;overflow:hidden;">'
|
|
259
|
+
f'{"".join(bar_parts)}</div>'
|
|
260
|
+
f'<div style="display:flex;justify-content:space-between;margin-top:0.35rem;'
|
|
261
|
+
f'font-size:0.7rem;font-family:var(--font-body);color:var(--slate-500);">'
|
|
262
|
+
f'{"".join(legend_parts)}</div>'
|
|
263
|
+
)
|