trueppm-api 0.4.0b2__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.
- trueppm_api/__init__.py +14 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +164 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +2837 -0
- trueppm_api/apps/access/serializers.py +946 -0
- trueppm_api/apps/access/services.py +667 -0
- trueppm_api/apps/access/signals.py +103 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +95 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1931 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +108 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +418 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +177 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +837 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +403 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +622 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +519 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +731 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1106 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +277 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +500 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1231 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +436 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +447 -0
- trueppm_api/apps/notifications/email_backend.py +330 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1197 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +961 -0
- trueppm_api/apps/notifications/services.py +1451 -0
- trueppm_api/apps/notifications/tasks.py +711 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +904 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +551 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +969 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +523 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +466 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +449 -0
- trueppm_api/apps/projects/backfill.py +287 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +204 -0
- trueppm_api/apps/projects/config_notice.py +1325 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +93 -0
- trueppm_api/apps/projects/decisions_views.py +94 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +353 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +8946 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +575 -0
- trueppm_api/apps/projects/program_schedule.py +745 -0
- trueppm_api/apps/projects/program_views.py +3220 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/exporter.py +1644 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2390 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +126 -0
- trueppm_api/apps/projects/seed/replay.py +1691 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1149 -0
- trueppm_api/apps/projects/serializers.py +12310 -0
- trueppm_api/apps/projects/services.py +6966 -0
- trueppm_api/apps/projects/share_serializers.py +132 -0
- trueppm_api/apps/projects/share_services.py +325 -0
- trueppm_api/apps/projects/share_views.py +374 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +909 -0
- trueppm_api/apps/projects/signal_privacy_views.py +476 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +266 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1152 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1917 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +258 -0
- trueppm_api/apps/projects/template_views.py +799 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1011 -0
- trueppm_api/apps/projects/utilization.py +885 -0
- trueppm_api/apps/projects/views.py +20572 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1749 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +119 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +516 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +283 -0
- trueppm_api/apps/scheduling/serializers.py +564 -0
- trueppm_api/apps/scheduling/services.py +1182 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +1984 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2564 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +437 -0
- trueppm_api/apps/sso/services.py +1385 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +320 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +471 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +225 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +371 -0
- trueppm_api/apps/sync/serializers.py +687 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +501 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +930 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +434 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +117 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +526 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +144 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +372 -0
- trueppm_api/apps/webhooks/serializers.py +318 -0
- trueppm_api/apps/webhooks/tasks.py +663 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +383 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +368 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +242 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +919 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +555 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1502 -0
- trueppm_api/asgi.py +42 -0
- trueppm_api/celery.py +31 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +916 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +949 -0
- trueppm_api/core/password_reset.py +500 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +884 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +344 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2455 -0
- trueppm_api/settings/dev.py +273 -0
- trueppm_api/settings/prod.py +228 -0
- trueppm_api/urls.py +153 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
- trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
- trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
"""Read-only aggregation for the System Health overview (#692, ADR-0172).
|
|
2
|
+
|
|
3
|
+
Composes existing durable-execution signals into a single payload for the
|
|
4
|
+
workspace-admin overview dashboard. Pure reads — no writes, no dispatch — so
|
|
5
|
+
the whole module is safe to call on a 10 s refresh interval.
|
|
6
|
+
|
|
7
|
+
Each of the five operator-facing components reports one of four statuses:
|
|
8
|
+
|
|
9
|
+
* ``ok`` — healthy, nothing to do.
|
|
10
|
+
* ``warn`` — needs attention (e.g. dead-lettered tasks parked).
|
|
11
|
+
* ``crit`` — broken now (e.g. Beat heartbeat stale).
|
|
12
|
+
* ``unknown`` — no backing telemetry exists yet; rendered muted, **never** as an
|
|
13
|
+
error. Only the Retention-purge component is ``unknown`` today, because OSS
|
|
14
|
+
records no purge-run history (resolved when #693 lands that model).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import threading
|
|
21
|
+
from datetime import timedelta
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from celery.schedules import crontab
|
|
25
|
+
from django.conf import settings
|
|
26
|
+
from django.db.migrations.loader import MigrationLoader
|
|
27
|
+
from django.db.models import Count, Q
|
|
28
|
+
from django.utils import timezone
|
|
29
|
+
|
|
30
|
+
from trueppm_api.apps.notifications.models import Notification
|
|
31
|
+
from trueppm_api.apps.observability.models import BeatHeartbeat, PurgeRun
|
|
32
|
+
from trueppm_api.apps.observability.retention import resolve_retention_map
|
|
33
|
+
from trueppm_api.apps.scheduling.models import (
|
|
34
|
+
FailedTask,
|
|
35
|
+
FailedTaskStatus,
|
|
36
|
+
ScheduleRequest,
|
|
37
|
+
ScheduleRequestStatus,
|
|
38
|
+
)
|
|
39
|
+
from trueppm_api.apps.workflow_engine.models import WorkflowOutboxRow, WorkflowOutboxStatus
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Latch so the DB-ahead override warning is logged once per process rather than
|
|
44
|
+
# on every kubelet readiness call. See ``_warn_db_ahead_override_once``.
|
|
45
|
+
_db_ahead_override_warned = False
|
|
46
|
+
|
|
47
|
+
# Process-wide cache of the on-disk migration scan (#2820). Populated once by
|
|
48
|
+
# ``_cached_load_disk`` (patched onto ``MigrationLoader.load_disk`` at import
|
|
49
|
+
# time by ``_install_cached_load_disk``) and reused by every later ``/readyz``
|
|
50
|
+
# call in this process — see that function for why disk state never needs a
|
|
51
|
+
# second scan. A lock, not a bare check, because a readiness probe can race a
|
|
52
|
+
# request thread under an ASGI worker with more than one concurrent request;
|
|
53
|
+
# the cost of losing the race is redoing one disk scan, never a wrong answer,
|
|
54
|
+
# so the lock only needs to cover the read-modify-write of the cache itself.
|
|
55
|
+
_disk_migration_cache: tuple[dict[Any, Any], set[str], set[str]] | None = None
|
|
56
|
+
_disk_migration_cache_lock = threading.Lock()
|
|
57
|
+
|
|
58
|
+
# First conclusive migration state this process observed, latched at the first
|
|
59
|
+
# readiness probe. It is what tells a rolled-back pod (booted into "ahead") apart
|
|
60
|
+
# from an old pod mid-rollout (booted "in_sync", drifted to "ahead") — a
|
|
61
|
+
# distinction the database cannot make. See ``_remember_first_migration_state``.
|
|
62
|
+
_first_migration_state: str | None = None
|
|
63
|
+
|
|
64
|
+
# Status literals shared with the frontend contract (ADR-0172).
|
|
65
|
+
STATUS_OK = "ok"
|
|
66
|
+
STATUS_WARN = "warn"
|
|
67
|
+
STATUS_CRIT = "crit"
|
|
68
|
+
STATUS_UNKNOWN = "unknown"
|
|
69
|
+
|
|
70
|
+
# Per-dependency readiness literals for the unauthenticated /readyz probe (#1894).
|
|
71
|
+
# Deliberately coarse — "ok"/"fail" only — so the response never leaks connection
|
|
72
|
+
# strings, host names, or driver error text to an unauthenticated caller.
|
|
73
|
+
READY_OK = "ok"
|
|
74
|
+
READY_FAIL = "fail"
|
|
75
|
+
|
|
76
|
+
# Direction-of-drift literals for the readiness migration check (#2806). Reported
|
|
77
|
+
# alongside the coarse ``checks`` map as ``migration_state`` so an operator can tell
|
|
78
|
+
# a rolling-forward pod (``behind``) from a rolled-back one (``ahead``) without
|
|
79
|
+
# shelling into the pod. Still coarse by design — an enum, never a migration name,
|
|
80
|
+
# app label, or count, so the unauthenticated body stays free of internal detail.
|
|
81
|
+
READY_MIGRATIONS_IN_SYNC = "in_sync"
|
|
82
|
+
READY_MIGRATIONS_BEHIND = "behind"
|
|
83
|
+
READY_MIGRATIONS_AHEAD = "ahead"
|
|
84
|
+
READY_MIGRATIONS_UNKNOWN = "unknown"
|
|
85
|
+
|
|
86
|
+
# Upper bound on the readiness DB probe so a wedged/slow database fails the probe
|
|
87
|
+
# fast (503) instead of hanging kubelet's readiness call. Two seconds is well
|
|
88
|
+
# under a typical kubelet ``timeoutSeconds`` while tolerating a brief blip.
|
|
89
|
+
_READY_DB_STATEMENT_TIMEOUT_MS = 2000
|
|
90
|
+
|
|
91
|
+
# Cache key the readiness probe writes-then-reads to prove a live round-trip to
|
|
92
|
+
# Valkey/Redis. Namespaced and short-lived; value is irrelevant.
|
|
93
|
+
_READY_CACHE_KEY = "trueppm:readyz:probe"
|
|
94
|
+
|
|
95
|
+
_SINGLETON_KEY = 1
|
|
96
|
+
|
|
97
|
+
# A dispatched outbox row younger than this may simply be in flight (the row is
|
|
98
|
+
# written before the on_commit dispatch fires), so we don't flag it as stuck.
|
|
99
|
+
# Matches the schedule-request drain orphan window.
|
|
100
|
+
_OUTBOX_STUCK_THRESHOLD = timedelta(minutes=10)
|
|
101
|
+
|
|
102
|
+
# A notification email still queued after this long is not being drained. Measured
|
|
103
|
+
# against the IMMUTABLE ``created_at``, never against ``email_failed_at`` — see
|
|
104
|
+
# ``_notification_email_signals`` for why that distinction is the whole bug in #2886.
|
|
105
|
+
_NOTIFICATION_STUCK_THRESHOLD = timedelta(hours=1)
|
|
106
|
+
|
|
107
|
+
# Rolling window over which permanently-failed and successful sends are counted.
|
|
108
|
+
# Short enough that a resolved outage clears the card without operator action, long
|
|
109
|
+
# enough to survive the gaps between drain ticks and low-traffic periods.
|
|
110
|
+
_NOTIFICATION_FAILURE_WINDOW = timedelta(hours=1)
|
|
111
|
+
|
|
112
|
+
# A dead-letter parked longer than this escalates the component from warn→crit.
|
|
113
|
+
_DEAD_LETTER_CRIT_THRESHOLD = timedelta(hours=24)
|
|
114
|
+
|
|
115
|
+
# Retention settings surfaced read-only on the overview (ADR-0172 §2). Each maps
|
|
116
|
+
# a settings key to a human label and a unit. ``None`` means purge is disabled.
|
|
117
|
+
_RETENTION_KEYS: list[tuple[str, str, str]] = [
|
|
118
|
+
("TRUEPPM_WEBHOOK_RETENTION_DAYS", "Webhook deliveries", "days"),
|
|
119
|
+
("TRUEPPM_IMPORT_RETENTION_DAYS", "Import requests", "days"),
|
|
120
|
+
("HISTORY_RETENTION_DAYS", "Event history", "days"),
|
|
121
|
+
("TASK_RUN_RETENTION_DAYS", "Task runs", "days"),
|
|
122
|
+
("TRUEPPM_SYNC_BATCH_RETENTION_HOURS", "Sync batches", "hours"),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _component(key: str, label: str, status: str, state_label: str, meta: str) -> dict[str, str]:
|
|
127
|
+
return {
|
|
128
|
+
"key": key,
|
|
129
|
+
"label": label,
|
|
130
|
+
"status": status,
|
|
131
|
+
"state_label": state_label,
|
|
132
|
+
"meta": meta,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _humanize_schedule(schedule: object) -> str:
|
|
137
|
+
"""Render a Beat schedule entry as an operator-readable cadence string.
|
|
138
|
+
|
|
139
|
+
Handles the two forms used in ``CELERY_BEAT_SCHEDULE``: numeric/timedelta
|
|
140
|
+
intervals (the 30 s/60 s drains) and ``crontab`` instances (the nightly and
|
|
141
|
+
hourly purges). All cron times are UTC — Beat runs on UTC (ADR-0081).
|
|
142
|
+
"""
|
|
143
|
+
if isinstance(schedule, crontab):
|
|
144
|
+
hours = sorted(schedule.hour)
|
|
145
|
+
minutes = sorted(schedule.minute)
|
|
146
|
+
# An all-hours cron (e.g. crontab(minute=5)) is hourly; a single-hour
|
|
147
|
+
# cron is a once-daily run.
|
|
148
|
+
if len(hours) >= 24 and len(minutes) == 1:
|
|
149
|
+
return f"hourly at :{minutes[0]:02d}"
|
|
150
|
+
if len(hours) == 1 and len(minutes) == 1:
|
|
151
|
+
return f"daily {hours[0]:02d}:{minutes[0]:02d} UTC"
|
|
152
|
+
return "scheduled (cron)"
|
|
153
|
+
|
|
154
|
+
if isinstance(schedule, timedelta):
|
|
155
|
+
seconds = schedule.total_seconds()
|
|
156
|
+
elif isinstance(schedule, (int, float)):
|
|
157
|
+
seconds = float(schedule)
|
|
158
|
+
else:
|
|
159
|
+
return "scheduled"
|
|
160
|
+
|
|
161
|
+
if seconds < 60:
|
|
162
|
+
return f"every {int(seconds)}s"
|
|
163
|
+
if seconds < 3600:
|
|
164
|
+
return f"every {int(seconds // 60)}m"
|
|
165
|
+
return f"every {int(seconds // 3600)}h"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _categorize(task: str) -> str:
|
|
169
|
+
"""Bucket a Beat task into an operator category for the reference list."""
|
|
170
|
+
if "heartbeat" in task:
|
|
171
|
+
return "heartbeat"
|
|
172
|
+
if "drain" in task:
|
|
173
|
+
return "drain"
|
|
174
|
+
if "purge" in task or "archive" in task:
|
|
175
|
+
return "purge"
|
|
176
|
+
if "burndown" in task or "snapshot" in task:
|
|
177
|
+
return "snapshot"
|
|
178
|
+
return "other"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _scheduled_tasks() -> list[dict[str, str]]:
|
|
182
|
+
"""Static view of the configured Beat schedule (name · cadence · category).
|
|
183
|
+
|
|
184
|
+
This is the *configured* schedule, not per-task last-run status — TruePPM
|
|
185
|
+
records only a single global heartbeat, not per-task execution times
|
|
186
|
+
(ADR-0172 §4). Overall Beat liveness is answered by the heartbeat panel.
|
|
187
|
+
"""
|
|
188
|
+
rows: list[dict[str, str]] = []
|
|
189
|
+
for name, entry in settings.CELERY_BEAT_SCHEDULE.items():
|
|
190
|
+
task = str(entry.get("task", ""))
|
|
191
|
+
rows.append(
|
|
192
|
+
{
|
|
193
|
+
"name": name,
|
|
194
|
+
"task": task,
|
|
195
|
+
"cadence": _humanize_schedule(entry.get("schedule")),
|
|
196
|
+
"category": _categorize(task),
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
rows.sort(key=lambda r: (r["category"], r["name"]))
|
|
200
|
+
return rows
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _beat_status() -> tuple[dict[str, Any], dict[str, str]]:
|
|
204
|
+
"""Return (beat panel payload, Celery Beat component card)."""
|
|
205
|
+
row = BeatHeartbeat.objects.filter(singleton_key=_SINGLETON_KEY).first()
|
|
206
|
+
threshold = settings.TRUEPPM_BEAT_STALE_SECONDS
|
|
207
|
+
|
|
208
|
+
if row is None:
|
|
209
|
+
panel: dict[str, Any] = {
|
|
210
|
+
"last_heartbeat": None,
|
|
211
|
+
"seconds_since": None,
|
|
212
|
+
"stale": True,
|
|
213
|
+
"stale_threshold_seconds": threshold,
|
|
214
|
+
}
|
|
215
|
+
card = _component(
|
|
216
|
+
"celery_beat", "Celery Beat", STATUS_CRIT, "No heartbeat", "never recorded"
|
|
217
|
+
)
|
|
218
|
+
return panel, card
|
|
219
|
+
|
|
220
|
+
seconds_since = int((timezone.now() - row.last_heartbeat).total_seconds())
|
|
221
|
+
stale = seconds_since > threshold
|
|
222
|
+
panel = {
|
|
223
|
+
"last_heartbeat": row.last_heartbeat,
|
|
224
|
+
"seconds_since": seconds_since,
|
|
225
|
+
"stale": stale,
|
|
226
|
+
"stale_threshold_seconds": threshold,
|
|
227
|
+
}
|
|
228
|
+
card = _component(
|
|
229
|
+
"celery_beat",
|
|
230
|
+
"Celery Beat",
|
|
231
|
+
STATUS_CRIT if stale else STATUS_OK,
|
|
232
|
+
"Stale" if stale else "Live",
|
|
233
|
+
f"beat {seconds_since}s ago",
|
|
234
|
+
)
|
|
235
|
+
return panel, card
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _outbox_card() -> dict[str, str]:
|
|
239
|
+
"""Health of the transactional outbox dispatchers (CPM + workflow steps)."""
|
|
240
|
+
cutoff = timezone.now() - _OUTBOX_STUCK_THRESHOLD
|
|
241
|
+
|
|
242
|
+
sched = ScheduleRequest.objects.aggregate(
|
|
243
|
+
dead=Count("id", filter=Q(status=ScheduleRequestStatus.DEAD)),
|
|
244
|
+
stuck=Count(
|
|
245
|
+
"id",
|
|
246
|
+
filter=Q(status=ScheduleRequestStatus.DISPATCHED, dispatched_at__lt=cutoff),
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
wf = WorkflowOutboxRow.objects.aggregate(
|
|
250
|
+
dead=Count("id", filter=Q(status=WorkflowOutboxStatus.DEAD)),
|
|
251
|
+
stuck=Count(
|
|
252
|
+
"id",
|
|
253
|
+
filter=Q(status=WorkflowOutboxStatus.DISPATCHED, dispatched_at__lt=cutoff),
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
dead = (sched["dead"] or 0) + (wf["dead"] or 0)
|
|
257
|
+
stuck = (sched["stuck"] or 0) + (wf["stuck"] or 0)
|
|
258
|
+
|
|
259
|
+
if dead:
|
|
260
|
+
status = STATUS_CRIT
|
|
261
|
+
state = f"{dead} dead"
|
|
262
|
+
elif stuck:
|
|
263
|
+
status = STATUS_WARN
|
|
264
|
+
state = f"{stuck} stuck"
|
|
265
|
+
else:
|
|
266
|
+
status = STATUS_OK
|
|
267
|
+
state = "Healthy"
|
|
268
|
+
return _component(
|
|
269
|
+
"outbox_dispatcher", "Outbox dispatcher", status, state, f"{dead} dead, {stuck} stuck >10m"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _dead_letter() -> tuple[dict[str, Any], dict[str, str]]:
|
|
274
|
+
"""Return (dead-letter summary payload, Dead-letter component card)."""
|
|
275
|
+
by_status_rows = FailedTask.objects.values("status").annotate(n=Count("id")).order_by("status")
|
|
276
|
+
by_status = {row["status"]: row["n"] for row in by_status_rows}
|
|
277
|
+
parked = by_status.get(FailedTaskStatus.DEAD, 0)
|
|
278
|
+
|
|
279
|
+
oldest_age_seconds: int | None = None
|
|
280
|
+
top_cause: str | None = None
|
|
281
|
+
if parked:
|
|
282
|
+
dead_qs = FailedTask.objects.filter(status=FailedTaskStatus.DEAD)
|
|
283
|
+
oldest = dead_qs.order_by("last_failed_at").values_list("last_failed_at", flat=True).first()
|
|
284
|
+
if oldest is not None:
|
|
285
|
+
oldest_age_seconds = int((timezone.now() - oldest).total_seconds())
|
|
286
|
+
cause_row = (
|
|
287
|
+
dead_qs.values("exception_type")
|
|
288
|
+
.annotate(n=Count("id"))
|
|
289
|
+
.order_by("-n", "exception_type")
|
|
290
|
+
.first()
|
|
291
|
+
)
|
|
292
|
+
if cause_row is not None:
|
|
293
|
+
top_cause = cause_row["exception_type"]
|
|
294
|
+
|
|
295
|
+
summary = {
|
|
296
|
+
"parked": parked,
|
|
297
|
+
"oldest_age_seconds": oldest_age_seconds,
|
|
298
|
+
"top_cause": top_cause,
|
|
299
|
+
"by_status": {
|
|
300
|
+
FailedTaskStatus.DEAD: by_status.get(FailedTaskStatus.DEAD, 0),
|
|
301
|
+
FailedTaskStatus.PENDING_RETRY: by_status.get(FailedTaskStatus.PENDING_RETRY, 0),
|
|
302
|
+
FailedTaskStatus.DISMISSED: by_status.get(FailedTaskStatus.DISMISSED, 0),
|
|
303
|
+
FailedTaskStatus.RETRIED: by_status.get(FailedTaskStatus.RETRIED, 0),
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
parked
|
|
309
|
+
and oldest_age_seconds is not None
|
|
310
|
+
and (oldest_age_seconds > _DEAD_LETTER_CRIT_THRESHOLD.total_seconds())
|
|
311
|
+
):
|
|
312
|
+
status, state = STATUS_CRIT, f"{parked} parked"
|
|
313
|
+
elif parked:
|
|
314
|
+
status, state = STATUS_WARN, f"{parked} parked"
|
|
315
|
+
else:
|
|
316
|
+
status, state = STATUS_OK, "Clear"
|
|
317
|
+
meta = f"oldest {_format_age(oldest_age_seconds)}" if parked else "no parked tasks"
|
|
318
|
+
card = _component("dead_letter", "Dead-letter alerting", status, state, meta)
|
|
319
|
+
return summary, card
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def notification_email_signals() -> dict[str, Any]:
|
|
323
|
+
"""Compute the three notification-email health signals (#2886).
|
|
324
|
+
|
|
325
|
+
The previous single signal was ``email_pending=True AND email_attempts>0 AND
|
|
326
|
+
email_failed_at < now-1h``, and it was **mutually unsatisfiable exactly when SMTP
|
|
327
|
+
was what had broken**. Two of its conjuncts are written by the same statement that
|
|
328
|
+
ends a row's life: at ``email_attempts >= EMAIL_MAX_RETRIES`` the drain clears
|
|
329
|
+
``email_pending`` — so on a 30 s beat a row exhausts its three attempts in about
|
|
330
|
+
90 seconds and leaves the ``email_pending=True`` half of the predicate roughly
|
|
331
|
+
forty times before the one-hour half could ever be reached. And because
|
|
332
|
+
``email_failed_at`` was rewritten to ``now`` on *every* attempt, it could only age
|
|
333
|
+
past an hour if Beat itself stopped — which ``TruePPMBeatStale`` already covers. A
|
|
334
|
+
completely dead relay reported "Draining / ok" indefinitely, verified over four
|
|
335
|
+
real drain ticks.
|
|
336
|
+
|
|
337
|
+
The replacement is three signals, each of which is *positive evidence* of a
|
|
338
|
+
specific failure rather than an inference from state that the failure path erases:
|
|
339
|
+
|
|
340
|
+
``queued_aging``
|
|
341
|
+
``email_pending=True`` and ``created_at`` older than the threshold. The age is
|
|
342
|
+
measured on ``created_at``, which is ``auto_now_add`` and which **no code
|
|
343
|
+
path ever writes again** — so unlike ``email_failed_at`` it cannot be reset
|
|
344
|
+
out from under the comparison. The drain's only two exits from
|
|
345
|
+
``email_pending=True`` are a success and an attempt-exhaustion, both of which
|
|
346
|
+
occur within the 5-minute orphan window plus three ticks (~6 min 30 s). A row
|
|
347
|
+
still pending at one hour therefore means rows are not being completed at all:
|
|
348
|
+
Beat is dead, the worker is gone, or the transport cannot even be built (the
|
|
349
|
+
#2886 item-2 fail-closed path, which deliberately leaves rows pending rather
|
|
350
|
+
than burning their retries).
|
|
351
|
+
|
|
352
|
+
``failed_recent``
|
|
353
|
+
The terminal state, derived with no new column: ``email_pending=False`` and
|
|
354
|
+
``email_sent_at IS NULL`` and ``email_attempts >= EMAIL_MAX_RETRIES`` and
|
|
355
|
+
``email_failed_at`` within the window. Every one of those four conjuncts is
|
|
356
|
+
asserted by the single ``UPDATE`` the drain issues on the final failed
|
|
357
|
+
attempt, so the terminal write cannot clear the evidence — it *is* the
|
|
358
|
+
evidence. And the window wants ``email_failed_at`` to be **recent** rather
|
|
359
|
+
than old, so the predicate becomes satisfiable at the instant of failure
|
|
360
|
+
instead of requiring an hour of aging the failure path itself prevented.
|
|
361
|
+
|
|
362
|
+
``sent_recent``
|
|
363
|
+
Successful sends in the same window. Paired with ``failed_recent`` this
|
|
364
|
+
separates "some mail bounces" from "no mail is getting out at all", which is
|
|
365
|
+
the dead-relay fingerprint and the only one that warrants ``crit``.
|
|
366
|
+
|
|
367
|
+
``transport``
|
|
368
|
+
Read-derived transport classification (see
|
|
369
|
+
``notifications.email_backend.transport_status``). Names the cause when the
|
|
370
|
+
configured credential cannot be decrypted, so an operator does not have to
|
|
371
|
+
infer it from a backlog.
|
|
372
|
+
|
|
373
|
+
A note on precision, since a health signal that over-claims is its own bug:
|
|
374
|
+
``failed_recent`` counts every notification whose email was permanently
|
|
375
|
+
undeliverable, which includes causes that are not the relay — a recipient with no
|
|
376
|
+
address on file, or a source comment deleted before the drain reached it. The card
|
|
377
|
+
labels the count as failed sends, which is what it is; it does not claim the relay
|
|
378
|
+
specifically. The relay verdict is the ``failed_recent > 0 and sent_recent == 0``
|
|
379
|
+
conjunction.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
``{"queued_aging": int, "failed_recent": int, "sent_recent": int,
|
|
383
|
+
"transport": str}``.
|
|
384
|
+
"""
|
|
385
|
+
from trueppm_api.apps.notifications.email_backend import transport_status
|
|
386
|
+
from trueppm_api.apps.notifications.tasks import EMAIL_MAX_RETRIES
|
|
387
|
+
|
|
388
|
+
now = timezone.now()
|
|
389
|
+
stuck_cutoff = now - _NOTIFICATION_STUCK_THRESHOLD
|
|
390
|
+
window_cutoff = now - _NOTIFICATION_FAILURE_WINDOW
|
|
391
|
+
|
|
392
|
+
queued_aging = Notification.objects.filter(
|
|
393
|
+
email_pending=True,
|
|
394
|
+
email_sent_at__isnull=True,
|
|
395
|
+
created_at__lt=stuck_cutoff,
|
|
396
|
+
).count()
|
|
397
|
+
failed_recent = Notification.objects.filter(
|
|
398
|
+
email_pending=False,
|
|
399
|
+
email_sent_at__isnull=True,
|
|
400
|
+
email_attempts__gte=EMAIL_MAX_RETRIES,
|
|
401
|
+
email_failed_at__gte=window_cutoff,
|
|
402
|
+
).count()
|
|
403
|
+
sent_recent = Notification.objects.filter(email_sent_at__gte=window_cutoff).count()
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
"queued_aging": queued_aging,
|
|
407
|
+
"failed_recent": failed_recent,
|
|
408
|
+
"sent_recent": sent_recent,
|
|
409
|
+
"transport": transport_status(),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _notification_card() -> dict[str, str]:
|
|
414
|
+
"""Health of the notification email dispatcher (#2886).
|
|
415
|
+
|
|
416
|
+
Escalation order, most-specific cause first — see
|
|
417
|
+
:func:`notification_email_signals` for what each signal proves:
|
|
418
|
+
|
|
419
|
+
* ``crit`` — the configured transport's credential cannot be decrypted. No mail
|
|
420
|
+
leaves the workspace until an operator re-enters it.
|
|
421
|
+
* ``crit`` — mail permanently failed in the window and nothing succeeded in it.
|
|
422
|
+
This is the dead-relay fingerprint.
|
|
423
|
+
* ``warn`` — mail permanently failed, but other mail also got through.
|
|
424
|
+
* ``warn`` — the queue is aging: rows have sat pending past the threshold, so
|
|
425
|
+
nothing is completing them.
|
|
426
|
+
* ``ok`` — no failures, no aging backlog, credential usable.
|
|
427
|
+
"""
|
|
428
|
+
from trueppm_api.apps.notifications.email_backend import TRANSPORT_UNDECRYPTABLE
|
|
429
|
+
|
|
430
|
+
signals = notification_email_signals()
|
|
431
|
+
failed = int(signals["failed_recent"])
|
|
432
|
+
sent = int(signals["sent_recent"])
|
|
433
|
+
aging = int(signals["queued_aging"])
|
|
434
|
+
|
|
435
|
+
def card(status: str, state_label: str, meta: str) -> dict[str, str]:
|
|
436
|
+
return _component(
|
|
437
|
+
"notification_dispatcher", "Notification dispatcher", status, state_label, meta
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if signals["transport"] == TRANSPORT_UNDECRYPTABLE:
|
|
441
|
+
return card(
|
|
442
|
+
STATUS_CRIT,
|
|
443
|
+
"Credential unusable",
|
|
444
|
+
"the stored SMTP password cannot be decrypted — re-enter it",
|
|
445
|
+
)
|
|
446
|
+
if failed and not sent:
|
|
447
|
+
return card(
|
|
448
|
+
STATUS_CRIT,
|
|
449
|
+
"Delivery failing",
|
|
450
|
+
f"{failed} permanently failed in the last hour, 0 delivered",
|
|
451
|
+
)
|
|
452
|
+
if failed:
|
|
453
|
+
return card(
|
|
454
|
+
STATUS_WARN,
|
|
455
|
+
f"{failed} failed",
|
|
456
|
+
f"{failed} permanently failed, {sent} delivered in the last hour",
|
|
457
|
+
)
|
|
458
|
+
if aging:
|
|
459
|
+
return card(
|
|
460
|
+
STATUS_WARN,
|
|
461
|
+
f"{aging} queued",
|
|
462
|
+
f"{aging} still queued after >1h — the drain is not completing rows",
|
|
463
|
+
)
|
|
464
|
+
return card(STATUS_OK, "Draining", f"0 failed, {sent} delivered in the last hour")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _retention() -> tuple[list[dict[str, Any]], dict[str, str]]:
|
|
468
|
+
"""Return (retention config rows, Retention-purge component card).
|
|
469
|
+
|
|
470
|
+
Values reflect operator overrides (ADR-0173) layered over the ADR-0081
|
|
471
|
+
settings defaults. The card is derived from the latest non-dry-run ``PurgeRun``:
|
|
472
|
+
``unknown`` until the first run is recorded, then ``ok``/``warn``/``crit`` from
|
|
473
|
+
its outcome — this is what resolves ADR-0172 §3's perpetually-unknown card.
|
|
474
|
+
"""
|
|
475
|
+
resolved = resolve_retention_map()
|
|
476
|
+
rows: list[dict[str, Any]] = []
|
|
477
|
+
for key, label, unit in _RETENTION_KEYS:
|
|
478
|
+
value = resolved.get(key)
|
|
479
|
+
rows.append(
|
|
480
|
+
{
|
|
481
|
+
"key": key,
|
|
482
|
+
"label": label,
|
|
483
|
+
"unit": unit,
|
|
484
|
+
"value": value,
|
|
485
|
+
"disabled": value is None,
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
last = (
|
|
490
|
+
PurgeRun.objects.exclude(trigger=PurgeRun.Trigger.DRY_RUN).order_by("-started_at").first()
|
|
491
|
+
)
|
|
492
|
+
if last is None:
|
|
493
|
+
card = _component(
|
|
494
|
+
"retention_purge",
|
|
495
|
+
"Retention purge",
|
|
496
|
+
STATUS_UNKNOWN,
|
|
497
|
+
"No telemetry",
|
|
498
|
+
"no purge run recorded yet",
|
|
499
|
+
)
|
|
500
|
+
return rows, card
|
|
501
|
+
|
|
502
|
+
age = _format_age(int((timezone.now() - last.started_at).total_seconds()))
|
|
503
|
+
if last.state == PurgeRun.State.FAILED:
|
|
504
|
+
status, state_label = STATUS_CRIT, "Last run failed"
|
|
505
|
+
elif last.state == PurgeRun.State.PARTIAL:
|
|
506
|
+
status, state_label = STATUS_WARN, "Last run partial"
|
|
507
|
+
elif last.state == PurgeRun.State.RUNNING:
|
|
508
|
+
status, state_label = STATUS_OK, "Running"
|
|
509
|
+
else:
|
|
510
|
+
status, state_label = STATUS_OK, "Healthy"
|
|
511
|
+
card = _component(
|
|
512
|
+
"retention_purge",
|
|
513
|
+
"Retention purge",
|
|
514
|
+
status,
|
|
515
|
+
state_label,
|
|
516
|
+
f"last purge {age} ago · {last.rows_deleted:,} rows",
|
|
517
|
+
)
|
|
518
|
+
return rows, card
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _format_age(seconds: int | None) -> str:
|
|
522
|
+
"""Compact relative-age string, e.g. '2h20m' or '45s'."""
|
|
523
|
+
if seconds is None:
|
|
524
|
+
return "—"
|
|
525
|
+
if seconds < 60:
|
|
526
|
+
return f"{seconds}s"
|
|
527
|
+
if seconds < 3600:
|
|
528
|
+
return f"{seconds // 60}m"
|
|
529
|
+
if seconds < 86400:
|
|
530
|
+
return f"{seconds // 3600}h{(seconds % 3600) // 60}m"
|
|
531
|
+
return f"{seconds // 86400}d"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _probe_database() -> bool:
|
|
535
|
+
"""Prove the primary database is reachable with a bounded ``SELECT 1``.
|
|
536
|
+
|
|
537
|
+
Wrapped in a transaction so ``SET LOCAL statement_timeout`` scopes to this
|
|
538
|
+
probe only (it resets at commit/rollback) — a hung or slow database trips the
|
|
539
|
+
timeout and returns ``False`` fast rather than blocking the readiness call.
|
|
540
|
+
Any driver/OperationalError is swallowed into ``False``: the caller only needs
|
|
541
|
+
a boolean, and the exception text must never reach an unauthenticated client.
|
|
542
|
+
"""
|
|
543
|
+
from django.db import connection, transaction
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
with transaction.atomic(), connection.cursor() as cursor:
|
|
547
|
+
# statement_timeout is PostgreSQL-only; guard so a non-PG test
|
|
548
|
+
# backend (e.g. sqlite) still runs the SELECT 1 round-trip.
|
|
549
|
+
if connection.vendor == "postgresql":
|
|
550
|
+
cursor.execute(
|
|
551
|
+
"SET LOCAL statement_timeout = %s",
|
|
552
|
+
[_READY_DB_STATEMENT_TIMEOUT_MS],
|
|
553
|
+
)
|
|
554
|
+
cursor.execute("SELECT 1")
|
|
555
|
+
return bool(cursor.fetchone() == (1,))
|
|
556
|
+
except Exception: # any failure means "not ready", by design
|
|
557
|
+
return False
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _has_migrations_unknown_to_image(loader: MigrationLoader) -> bool:
|
|
561
|
+
"""Report whether the database records migrations this image does not ship.
|
|
562
|
+
|
|
563
|
+
This is the **DB-ahead** half of the readiness migration check, and it cannot
|
|
564
|
+
be expressed as a migration plan: ``migration_plan()`` walks *this image's*
|
|
565
|
+
graph, and a migration the image never shipped is not a node in that graph, so
|
|
566
|
+
it can never appear in the plan no matter how the targets are chosen. Detecting
|
|
567
|
+
a rolled-back image therefore has to compare the recorded rows against what is
|
|
568
|
+
on disk, which is what this does (#2806).
|
|
569
|
+
|
|
570
|
+
Two deliberate exclusions keep it from crying wolf:
|
|
571
|
+
|
|
572
|
+
* **Squash-replaced originals.** The project squashes with ``replaces=`` and
|
|
573
|
+
keeps the original migration files on disk, so their recorded rows normally
|
|
574
|
+
resolve against ``disk_migrations`` directly. A recorded row is still treated
|
|
575
|
+
as known when it is merely *named* in some disk migration's ``replaces=``
|
|
576
|
+
list, so a future squash that does drop the originals cannot flip every pod
|
|
577
|
+
to not-ready.
|
|
578
|
+
* **Apps this image does not install.** Rows are only judged for app labels in
|
|
579
|
+
``loader.migrated_apps`` — the installed apps that have a migrations module.
|
|
580
|
+
A row for an app that is absent from ``INSTALLED_APPS`` is ignored, because
|
|
581
|
+
this image ships no code that reads those tables, so their schema cannot
|
|
582
|
+
disagree with it. Flagging them would make two legitimate, safe operations
|
|
583
|
+
permanently unready: removing an optional app from settings, and running the
|
|
584
|
+
community image against a database an Enterprise image migrated (the
|
|
585
|
+
Enterprise apps' tables sit inert, exactly like the additive-upgrade case).
|
|
586
|
+
The cost of that posture is that a downgrade whose *only* difference is a
|
|
587
|
+
dropped app goes undetected — accepted, since the surviving apps' schema is
|
|
588
|
+
what this image's code actually touches.
|
|
589
|
+
"""
|
|
590
|
+
known: set[tuple[str, str]] = set(loader.disk_migrations)
|
|
591
|
+
for migration in loader.disk_migrations.values():
|
|
592
|
+
# Indexed rather than unpacked: a hand-written ``replaces`` entry is
|
|
593
|
+
# occasionally a list, and Django reads it positionally too.
|
|
594
|
+
known.update((entry[0], entry[1]) for entry in migration.replaces or ())
|
|
595
|
+
return any(
|
|
596
|
+
key not in known for key in loader.applied_migrations if key[0] in loader.migrated_apps
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _cached_load_disk(loader: MigrationLoader, _original: Any) -> None:
|
|
601
|
+
"""Populate ``loader`` from a process-wide cache instead of rescanning disk.
|
|
602
|
+
|
|
603
|
+
The real ``MigrationLoader.load_disk()`` walks every installed app's
|
|
604
|
+
migrations package and ``import_module()``s each migration file — genuine
|
|
605
|
+
disk I/O and code loading that reflects this image's own migration files,
|
|
606
|
+
which cannot change for the life of a running process (the image is
|
|
607
|
+
immutable). Before this, ``_probe_migrations`` rebuilt a full
|
|
608
|
+
``MigrationExecutor`` — and therefore a full disk scan — on every call, so
|
|
609
|
+
kubelet's default 10 s readiness period turned into a complete
|
|
610
|
+
migration-graph rebuild every 10 seconds, for every Ready pod, forever
|
|
611
|
+
(#2820): pure waste on the hot path of a probe that is supposed to be cheap.
|
|
612
|
+
|
|
613
|
+
Only ``disk_migrations``/``unmigrated_apps``/``migrated_apps`` — the three
|
|
614
|
+
attributes ``load_disk()`` sets — are cached. Every caller still gets a
|
|
615
|
+
fresh ``applied_migrations`` read from the database and a freshly rebuilt
|
|
616
|
+
graph (``build_graph()`` runs its full node/edge/replacement logic on every
|
|
617
|
+
call): that is the part that legitimately changes mid-rollout, and it is
|
|
618
|
+
one indexed query plus in-memory graph construction, not a filesystem scan.
|
|
619
|
+
|
|
620
|
+
Every caller gets its own **copy** of the cached containers, never the
|
|
621
|
+
cached objects themselves. ``build_graph()``'s replacement handling only
|
|
622
|
+
reads ``disk_migrations``, but test code that deliberately edits a loader's
|
|
623
|
+
``disk_migrations`` to simulate a squash (see
|
|
624
|
+
``TestUnknownAppliedMigrationDetection`` in ``test_readyz.py``) does not —
|
|
625
|
+
and handing out the same dict/set objects to every loader would let one
|
|
626
|
+
caller's edit permanently corrupt every other loader in the process for
|
|
627
|
+
the rest of its life. The ``Migration`` objects the copies point to are
|
|
628
|
+
still shared (they are immutable once constructed), so this stays cheap.
|
|
629
|
+
|
|
630
|
+
A scan that comes back with **no migrated apps at all** is never cached.
|
|
631
|
+
A real image always has migrated apps (``django.contrib.auth`` alone
|
|
632
|
+
guarantees that), so an empty result means this call raced something else
|
|
633
|
+
still setting up the app registry rather than describing this image's
|
|
634
|
+
actual migrations — caching it would lock the *whole process* onto a
|
|
635
|
+
permanently wrong, empty answer for every later caller, including ones in
|
|
636
|
+
entirely unrelated test modules that build their own ``MigrationLoader``
|
|
637
|
+
(#3346). Leaving the cache unset lets the next caller retry a real scan.
|
|
638
|
+
"""
|
|
639
|
+
global _disk_migration_cache
|
|
640
|
+
with _disk_migration_cache_lock:
|
|
641
|
+
if _disk_migration_cache is None:
|
|
642
|
+
_original(loader)
|
|
643
|
+
if not loader.migrated_apps:
|
|
644
|
+
return
|
|
645
|
+
_disk_migration_cache = (
|
|
646
|
+
dict(loader.disk_migrations),
|
|
647
|
+
set(loader.unmigrated_apps),
|
|
648
|
+
set(loader.migrated_apps),
|
|
649
|
+
)
|
|
650
|
+
# Always hand out fresh copies — including right after populating the
|
|
651
|
+
# cache above, so the loader that did the real scan is not itself the
|
|
652
|
+
# object every later caller shares (see the copy note above).
|
|
653
|
+
cached_disk, cached_unmigrated, cached_migrated = _disk_migration_cache
|
|
654
|
+
loader.disk_migrations = dict(cached_disk)
|
|
655
|
+
loader.unmigrated_apps = set(cached_unmigrated)
|
|
656
|
+
loader.migrated_apps = set(cached_migrated)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _install_cached_load_disk() -> None:
|
|
660
|
+
"""Patch ``MigrationLoader.load_disk`` process-wide, once (#2820).
|
|
661
|
+
|
|
662
|
+
Patched at the ``MigrationLoader`` class itself, rather than by swapping in
|
|
663
|
+
a loader subclass, so every existing call site — including
|
|
664
|
+
``MigrationExecutor.__init__``, which hard-codes ``MigrationLoader(self.
|
|
665
|
+
connection)`` with no constructor hook to inject a different loader class —
|
|
666
|
+
picks up the cache with no other code change. ``_probe_migrations`` below
|
|
667
|
+
is therefore unchanged from before #2820 except for this patch's effect,
|
|
668
|
+
which keeps ``unittest.mock.patch("...migrations.executor.MigrationExecutor",
|
|
669
|
+
...)`` in the test suite patching the same name this module calls.
|
|
670
|
+
"""
|
|
671
|
+
original = MigrationLoader.load_disk
|
|
672
|
+
|
|
673
|
+
def patched(self: MigrationLoader) -> None:
|
|
674
|
+
_cached_load_disk(self, original)
|
|
675
|
+
|
|
676
|
+
MigrationLoader.load_disk = patched # type: ignore[method-assign]
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
_install_cached_load_disk()
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _probe_migrations() -> str:
|
|
683
|
+
"""Classify how this image's migrations relate to the connected database.
|
|
684
|
+
|
|
685
|
+
Returns one of ``in_sync`` / ``behind`` / ``ahead`` / ``unknown``. A pod is
|
|
686
|
+
ready only on ``in_sync``; the other three keep it out of the Service.
|
|
687
|
+
|
|
688
|
+
* ``behind`` — this image ships migrations the database has not applied. This
|
|
689
|
+
is the rolling-*forward* window: the ``migrate`` init container is still
|
|
690
|
+
bringing the schema up while old-image pods serve, and a new pod that took
|
|
691
|
+
traffic now would error or write against a half-migrated schema (#2217).
|
|
692
|
+
Detected with ``MigrationExecutor.migration_plan`` over the full leaf set —
|
|
693
|
+
a non-empty plan means unapplied or in-flight steps.
|
|
694
|
+
* ``ahead`` — the database records migrations this image does not ship. Two
|
|
695
|
+
very different situations produce it: an image rolled back without restoring
|
|
696
|
+
the schema, and an old pod still serving while a newer pod's ``migrate``
|
|
697
|
+
moves the schema under it. The probe reports both identically because the
|
|
698
|
+
database cannot tell them apart; ``get_readiness`` decides which one gates,
|
|
699
|
+
from the state the process booted into. A plan is structurally blind to this
|
|
700
|
+
state either way (see ``_has_migrations_unknown_to_image``), which is why it
|
|
701
|
+
is a separate comparison rather than a wider target set.
|
|
702
|
+
* ``unknown`` — the check itself failed. Any error is swallowed into a
|
|
703
|
+
not-ready state, consistent with the DB and cache probes: no exception text
|
|
704
|
+
may reach an unauthenticated client.
|
|
705
|
+
|
|
706
|
+
Neither direction is a data-compatibility check. ``ahead`` says the recorded
|
|
707
|
+
schema is newer than this code; it says nothing about whether a destructive
|
|
708
|
+
migration already discarded data the old code needs. Rolling back across a
|
|
709
|
+
destructive change still requires restoring the pre-upgrade backup.
|
|
710
|
+
"""
|
|
711
|
+
from django.db import connection, transaction
|
|
712
|
+
from django.db.migrations.executor import MigrationExecutor
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
# Same envelope as ``_probe_database``, for two reasons. (1) ``ATOMIC_REQUESTS``
|
|
716
|
+
# is on, so a failure in the loader's own queries would poison the request's
|
|
717
|
+
# transaction and make the *database* probe that runs after this one report
|
|
718
|
+
# ``fail`` for a healthy database — an operator sent chasing the wrong outage.
|
|
719
|
+
# The savepoint keeps the failure local. (2) The loader reads the migration
|
|
720
|
+
# table and the catalog unbounded otherwise; the timeout keeps a wedged
|
|
721
|
+
# database from hanging kubelet's readiness call here too.
|
|
722
|
+
with transaction.atomic(), connection.cursor() as cursor:
|
|
723
|
+
if connection.vendor == "postgresql":
|
|
724
|
+
cursor.execute(
|
|
725
|
+
"SET LOCAL statement_timeout = %s",
|
|
726
|
+
[_READY_DB_STATEMENT_TIMEOUT_MS],
|
|
727
|
+
)
|
|
728
|
+
executor = MigrationExecutor(connection)
|
|
729
|
+
# A non-empty plan ⇒ steps this code expects are not applied yet.
|
|
730
|
+
if executor.migration_plan(executor.loader.graph.leaf_nodes()):
|
|
731
|
+
return READY_MIGRATIONS_BEHIND
|
|
732
|
+
if _has_migrations_unknown_to_image(executor.loader):
|
|
733
|
+
return READY_MIGRATIONS_AHEAD
|
|
734
|
+
return READY_MIGRATIONS_IN_SYNC
|
|
735
|
+
except Exception: # any failure means "not ready", by design
|
|
736
|
+
return READY_MIGRATIONS_UNKNOWN
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _probe_cache() -> bool:
|
|
740
|
+
"""Prove the Valkey/Redis cache is reachable with a write-then-read round-trip.
|
|
741
|
+
|
|
742
|
+
A bare ``get`` can be served from a local layer or return ``None`` for a dead
|
|
743
|
+
backend without erroring, so we ``set`` then ``get`` to force an actual round
|
|
744
|
+
trip. The channel layer shares the same Valkey instance (different logical db),
|
|
745
|
+
so cache reachability is a sufficient proxy for real-time readiness too. Any
|
|
746
|
+
backend error is swallowed into ``False`` — see ``_probe_database``.
|
|
747
|
+
"""
|
|
748
|
+
from django.core.cache import cache
|
|
749
|
+
|
|
750
|
+
try:
|
|
751
|
+
cache.set(_READY_CACHE_KEY, "1", timeout=5)
|
|
752
|
+
return bool(cache.get(_READY_CACHE_KEY) == "1")
|
|
753
|
+
except Exception: # any failure means "not ready", by design
|
|
754
|
+
return False
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _remember_first_migration_state(state: str) -> str | None:
|
|
758
|
+
"""Latch the first conclusive migration state this process observed.
|
|
759
|
+
|
|
760
|
+
This is what separates a **rolled-back** pod from an **old pod mid-rollout**,
|
|
761
|
+
which the database alone cannot distinguish — both are "image older than the
|
|
762
|
+
recorded schema". The difference is when the pod met that schema:
|
|
763
|
+
|
|
764
|
+
* A pod that boots into ``ahead`` was started against a database already newer
|
|
765
|
+
than its code. That is a rollback (or an old-image pod scaled up after the
|
|
766
|
+
migration), and it must not take traffic.
|
|
767
|
+
* A pod that booted ``in_sync`` and *drifted* to ``ahead`` is the ordinary
|
|
768
|
+
forward rolling upgrade: the new pod's ``migrate`` init container moved the
|
|
769
|
+
schema while this pod kept serving. Gating here would empty the Service of
|
|
770
|
+
endpoints exactly when the new pods are not Ready yet — the outage
|
|
771
|
+
expand-migrate-contract exists to avoid.
|
|
772
|
+
|
|
773
|
+
``unknown`` never latches: a probe that failed says nothing about which side of
|
|
774
|
+
the schema this pod started on, and latching it would freeze the wrong verdict.
|
|
775
|
+
"""
|
|
776
|
+
global _first_migration_state
|
|
777
|
+
if _first_migration_state is None and state != READY_MIGRATIONS_UNKNOWN:
|
|
778
|
+
_first_migration_state = state
|
|
779
|
+
return _first_migration_state
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _warn_db_ahead_override_once() -> None:
|
|
783
|
+
"""Log the open DB-ahead gate once per process.
|
|
784
|
+
|
|
785
|
+
Once, not per probe: kubelet calls readiness every ~10 s, and a line at that
|
|
786
|
+
rate is noise an operator learns to filter — which would defeat the point.
|
|
787
|
+
The module-level latch is deliberately not reset; a pod restart re-arms it,
|
|
788
|
+
which is exactly when the posture is worth restating.
|
|
789
|
+
"""
|
|
790
|
+
global _db_ahead_override_warned
|
|
791
|
+
if _db_ahead_override_warned:
|
|
792
|
+
return
|
|
793
|
+
_db_ahead_override_warned = True
|
|
794
|
+
logger.warning(
|
|
795
|
+
"TRUEPPM_READYZ_ALLOW_DB_AHEAD is enabled and this pod is reporting ready "
|
|
796
|
+
"with a database ahead of its image: it is serving older code against a "
|
|
797
|
+
"newer recorded schema. Intended only for a rollback whose migrations have "
|
|
798
|
+
"been classified as additive-only. Return the setting to false after "
|
|
799
|
+
"rolling forward."
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def get_readiness() -> tuple[bool, dict[str, str], str]:
|
|
804
|
+
"""Probe every hard dependency; return ``(ready, statuses, migration_state)``.
|
|
805
|
+
|
|
806
|
+
Backs the unauthenticated ``/api/v1/readyz`` Kubernetes readiness probe
|
|
807
|
+
(#1894): a pod is Ready only when the database and the Valkey/Redis cache
|
|
808
|
+
answer a live round-trip **and** its migrations agree with the recorded schema
|
|
809
|
+
— a pod whose own migrations are not applied yet (#2217) and a pod that booted
|
|
810
|
+
against a schema newer than its code (#2806) are both kept out of the Service.
|
|
811
|
+
|
|
812
|
+
``statuses`` stays a coarse ``ok``/``fail`` map — that contract is what the
|
|
813
|
+
body's freedom from infrastructure detail rests on, and adding a reason there
|
|
814
|
+
would break it. The migration drift direction is returned separately and
|
|
815
|
+
surfaced as its own ``migration_state`` field, so an operator can tell "still
|
|
816
|
+
migrating" from "this image is older than the schema" without a shell.
|
|
817
|
+
|
|
818
|
+
``ahead`` gates the pod **only when the pod booted into it**. A pod that
|
|
819
|
+
started against a database already newer than its code is a rollback, and it
|
|
820
|
+
must not take traffic; a pod that booted ``in_sync`` and drifted to ``ahead``
|
|
821
|
+
is the ordinary forward rolling upgrade, where the new pod's ``migrate`` init
|
|
822
|
+
container moves the schema while this one keeps serving. Gating the second
|
|
823
|
+
case would empty the Service of endpoints at the exact moment the new pods are
|
|
824
|
+
not Ready yet — a self-inflicted outage on every upgrade, and the reason
|
|
825
|
+
expand-migrate-contract exists. ``_remember_first_migration_state`` holds that
|
|
826
|
+
distinction, which the database alone cannot express.
|
|
827
|
+
|
|
828
|
+
Even a gated ``ahead`` is a guess the probe cannot verify: it knows the schema
|
|
829
|
+
is newer, not whether the old code can serve it. So
|
|
830
|
+
``TRUEPPM_READYZ_ALLOW_DB_AHEAD`` lets an operator who has read the release's
|
|
831
|
+
migrations and classified them additive-only complete the image-only rollback
|
|
832
|
+
the upgrade runbook recommends. Neither the latch nor the flag touches
|
|
833
|
+
``behind`` — nothing makes a half-migrated pod safe to serve.
|
|
834
|
+
|
|
835
|
+
When the flag *is* suppressing an ``ahead`` state, ``checks["migrations"]``
|
|
836
|
+
reads ``ok`` while ``migration_state`` reads ``ahead`` — deliberate, but it
|
|
837
|
+
means every monitor that watches only ``status``/``checks`` (all of the
|
|
838
|
+
existing ones, by the compatibility promise above) sees green on a pod
|
|
839
|
+
knowingly serving older code against a newer schema. So the suppression also
|
|
840
|
+
logs a WARNING, once per process: an override set during one incident must not
|
|
841
|
+
silently cover the *next* rollback, which may be the destructive one.
|
|
842
|
+
"""
|
|
843
|
+
migration_state = _probe_migrations()
|
|
844
|
+
booted_into = _remember_first_migration_state(migration_state)
|
|
845
|
+
ahead_gates = (
|
|
846
|
+
migration_state == READY_MIGRATIONS_AHEAD and booted_into == READY_MIGRATIONS_AHEAD
|
|
847
|
+
)
|
|
848
|
+
override_in_use = ahead_gates and settings.TRUEPPM_READYZ_ALLOW_DB_AHEAD
|
|
849
|
+
if override_in_use:
|
|
850
|
+
_warn_db_ahead_override_once()
|
|
851
|
+
migrations_ok = migration_state in (
|
|
852
|
+
READY_MIGRATIONS_IN_SYNC,
|
|
853
|
+
READY_MIGRATIONS_AHEAD,
|
|
854
|
+
) and (not ahead_gates or override_in_use)
|
|
855
|
+
checks = {
|
|
856
|
+
"database": READY_OK if _probe_database() else READY_FAIL,
|
|
857
|
+
"cache": READY_OK if _probe_cache() else READY_FAIL,
|
|
858
|
+
"migrations": READY_OK if migrations_ok else READY_FAIL,
|
|
859
|
+
}
|
|
860
|
+
ready = all(state == READY_OK for state in checks.values())
|
|
861
|
+
return ready, checks, migration_state
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _telemetry() -> dict[str, Any]:
|
|
865
|
+
"""Read-only OpenTelemetry exporter posture for the System Health page (#2022).
|
|
866
|
+
|
|
867
|
+
Surfaces whether OTLP export is live and the effective transport/sampler so an
|
|
868
|
+
operator can confirm *from the app* why spans/metrics are (or are not) arriving,
|
|
869
|
+
without shelling into the pod to read env. Pure ``settings`` reads — telemetry
|
|
870
|
+
stays env/Helm-configured and is never writable here.
|
|
871
|
+
|
|
872
|
+
``OTEL_EXPORTER_OTLP_HEADERS`` is deliberately never included: it carries the
|
|
873
|
+
export bearer token (``authorization=Bearer …``) and must not be disclosed even
|
|
874
|
+
to an admin. ``enabled`` mirrors the bootstrap gate — export is live only when an
|
|
875
|
+
endpoint is set *and* the master ``TRUEPPM_OTEL_ENABLED`` switch is on.
|
|
876
|
+
"""
|
|
877
|
+
from trueppm_api.apps.observability.otel import provider
|
|
878
|
+
|
|
879
|
+
endpoint = settings.OTEL_EXPORTER_OTLP_ENDPOINT.strip()
|
|
880
|
+
enabled = bool(endpoint) and settings.TRUEPPM_OTEL_ENABLED
|
|
881
|
+
return {
|
|
882
|
+
"enabled": enabled,
|
|
883
|
+
"endpoint": endpoint,
|
|
884
|
+
"endpoint_configured": bool(endpoint),
|
|
885
|
+
"protocol": settings.OTEL_EXPORTER_OTLP_PROTOCOL,
|
|
886
|
+
"service_name": settings.OTEL_SERVICE_NAME,
|
|
887
|
+
# Resource identity — the service.version / edition the canary and every
|
|
888
|
+
# real span carry, so the operator can match this card to what lands in
|
|
889
|
+
# their backend. Cheap: package metadata + a settings read.
|
|
890
|
+
"service_version": provider.service_version(),
|
|
891
|
+
"edition": str(getattr(settings, "TRUEPPM_EDITION", "community")),
|
|
892
|
+
# Raw per-signal toggles — reported as configured, so an operator can see a
|
|
893
|
+
# signal is switched off even while the overall exporter is live.
|
|
894
|
+
"traces_enabled": settings.TRUEPPM_OTEL_TRACES_ENABLED,
|
|
895
|
+
"metrics_enabled": settings.TRUEPPM_OTEL_METRICS_ENABLED,
|
|
896
|
+
"sampler": settings.OTEL_TRACES_SAMPLER,
|
|
897
|
+
"sampler_arg": settings.OTEL_TRACES_SAMPLER_ARG,
|
|
898
|
+
# Live cluster export-health strip (ADR-0601, #2109). Additive block that
|
|
899
|
+
# aggregates the worker/beat pods actually exporting. Skipped (available:
|
|
900
|
+
# false) when export is off — the card shows the config posture there and
|
|
901
|
+
# never renders the strip — so an unconfigured deployment pays no Valkey
|
|
902
|
+
# round-trip on this read.
|
|
903
|
+
"live": _export_health_live(enabled),
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _export_health_live(enabled: bool) -> dict[str, Any]:
|
|
908
|
+
"""Aggregate cross-process OTLP export health for the live Telemetry strip.
|
|
909
|
+
|
|
910
|
+
Returns ``{"available": False}`` when export is disabled or the metrics store is
|
|
911
|
+
unreachable, so ``_telemetry()`` never fabricates a number: the FE keeps the
|
|
912
|
+
config-only posture in that case. The ``state`` verdict is computed server-side
|
|
913
|
+
(in :mod:`otel.export_health`) so the card and any API/agent consumer agree.
|
|
914
|
+
"""
|
|
915
|
+
if not enabled:
|
|
916
|
+
return {"available": False}
|
|
917
|
+
from trueppm_api.apps.observability.otel import export_health
|
|
918
|
+
|
|
919
|
+
return export_health.read_export_health(
|
|
920
|
+
traces_enabled=settings.TRUEPPM_OTEL_TRACES_ENABLED,
|
|
921
|
+
metrics_enabled=settings.TRUEPPM_OTEL_METRICS_ENABLED,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _security() -> dict[str, Any]:
|
|
926
|
+
"""Read-only security posture for the System Health page (ADR-0604).
|
|
927
|
+
|
|
928
|
+
Currently surfaces whether API rate limiting is enabled. ``rate_limiting_enabled``
|
|
929
|
+
is False only when an operator has both set ``TRUEPPM_RATE_LIMIT_ENABLED=false``
|
|
930
|
+
and provided the acknowledgment (see :mod:`trueppm_api.core.ratelimit`) — i.e. all
|
|
931
|
+
DRF throttling is off. Surfaced ONLY on this admin-gated endpoint, never on the
|
|
932
|
+
public ``/health/`` / ``/readyz`` / ``/edition/`` probes, so a disabled protection
|
|
933
|
+
is not advertised to anonymous callers. Pure settings read — deploy-time operator
|
|
934
|
+
config, never writable here.
|
|
935
|
+
"""
|
|
936
|
+
return {
|
|
937
|
+
"rate_limiting_enabled": bool(getattr(settings, "RATE_LIMIT_ENABLED", True)),
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def get_system_health() -> dict[str, Any]:
|
|
942
|
+
"""Aggregate the full System Health overview payload (ADR-0172 §2).
|
|
943
|
+
|
|
944
|
+
Returns the component cards, Beat heartbeat panel + configured schedule,
|
|
945
|
+
dead-letter summary, read-only retention config, and read-only telemetry
|
|
946
|
+
(OTel exporter) posture. All reads are committed state, so the figures reflect
|
|
947
|
+
work done by the Celery worker/Beat processes even though this is served by the
|
|
948
|
+
web process.
|
|
949
|
+
"""
|
|
950
|
+
beat_panel, beat_card = _beat_status()
|
|
951
|
+
dead_letter_summary, dead_letter_card = _dead_letter()
|
|
952
|
+
retention_rows, retention_card = _retention()
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
"generated_at": timezone.now(),
|
|
956
|
+
"components": [
|
|
957
|
+
_outbox_card(),
|
|
958
|
+
beat_card,
|
|
959
|
+
dead_letter_card,
|
|
960
|
+
_notification_card(),
|
|
961
|
+
retention_card,
|
|
962
|
+
],
|
|
963
|
+
"beat": beat_panel,
|
|
964
|
+
"scheduled_tasks": _scheduled_tasks(),
|
|
965
|
+
"dead_letter": dead_letter_summary,
|
|
966
|
+
"retention": retention_rows,
|
|
967
|
+
"telemetry": _telemetry(),
|
|
968
|
+
"security": _security(),
|
|
969
|
+
}
|