trueppm-api 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trueppm_api/__init__.py +27 -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 +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -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_squashed_0019_release_0_4.py +302 -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 +143 -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/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -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/0001_squashed_0003_release_0_4.py +344 -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 +109 -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/0001_squashed_0003_release_0_4.py +90 -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 +459 -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 +178 -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 +863 -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 +597 -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_squashed_0014_release_0_4.py +225 -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/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -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 +528 -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 +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -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 +280 -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 +551 -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 +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -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 +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -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 +299 -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_squashed_0011_release_0_4.py +204 -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 +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -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 +566 -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 +993 -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 +537 -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_squashed_0009_release_0_4.py +128 -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 +541 -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 +458 -0
- trueppm_api/apps/projects/backfill.py +459 -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 +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -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/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -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 +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -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/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -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 +59 -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 +75 -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 +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -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/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -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 +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -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 +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -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 +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -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 +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -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 +1244 -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 +1979 -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 +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -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/0014_squashed_0015_release_0_4.py +47 -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 +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -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/mc_admission.py +181 -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/0010_squashed_0016_release_0_4.py +106 -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/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -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 +2908 -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 +446 -0
- trueppm_api/apps/sso/services.py +1473 -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 +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -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/0003_squashed_0005_release_0_4.py +77 -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/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -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 +159 -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/0004_backfill_default_teams_on_upgrade.py +49 -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 +440 -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/0001_squashed_0004_release_0_4.py +167 -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 +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -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 +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -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 +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -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 +371 -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 +254 -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_squashed_0027_release_0_4.py +199 -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 +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -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/demo_read_only.py +314 -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 +1221 -0
- trueppm_api/core/password_reset.py +513 -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 +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -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/core/worker_selfheal.py +205 -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 +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -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.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
|
@@ -0,0 +1,2605 @@
|
|
|
1
|
+
"""Base Django settings shared across all environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import environ
|
|
11
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
12
|
+
|
|
13
|
+
from trueppm_api.core.demo_read_only import (
|
|
14
|
+
parse_demo_access_gate,
|
|
15
|
+
parse_demo_login_hint,
|
|
16
|
+
parse_demo_read_only,
|
|
17
|
+
parse_demo_reset_schedule,
|
|
18
|
+
)
|
|
19
|
+
from trueppm_api.core.ratelimit import apply_rate_limit_disable, resolve_rate_limit_enabled
|
|
20
|
+
from trueppm_api.core.refresh_cookie_policy import resolve_refresh_cookie_samesite
|
|
21
|
+
from trueppm_api.core.storage_config import S3_STORAGE_BACKENDS, build_s3_storage_options
|
|
22
|
+
from trueppm_api.core.valkey_config import parse_sentinels
|
|
23
|
+
|
|
24
|
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
25
|
+
|
|
26
|
+
env = environ.Env()
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Core
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
SECRET_KEY = env("SECRET_KEY", default="django-insecure-change-me-in-prod")
|
|
33
|
+
|
|
34
|
+
# Dedicated JWT signing key (#2247). Defaults to SECRET_KEY so a default install
|
|
35
|
+
# stays single-knob, but can be set independently via JWT_SIGNING_KEY to:
|
|
36
|
+
# (a) limit the blast radius of a SECRET_KEY leak — a leaked SECRET_KEY alone
|
|
37
|
+
# can no longer forge access/refresh tokens for any user; and
|
|
38
|
+
# (b) give operators a deliberate "log everyone out now" lever: rotating
|
|
39
|
+
# JWT_SIGNING_KEY invalidates every outstanding token without also
|
|
40
|
+
# rotating the general Django secret (which would churn CSRF/session
|
|
41
|
+
# signing too).
|
|
42
|
+
# When set explicitly it is strength-validated in prod exactly like SECRET_KEY
|
|
43
|
+
# (see core.security_checks.validate_signing_key); when it defaults to SECRET_KEY
|
|
44
|
+
# the SECRET_KEY validation already covers it. Rotation runbook:
|
|
45
|
+
# packages/website/src/content/docs/administration/secret-rotation.md.
|
|
46
|
+
JWT_SIGNING_KEY = env("JWT_SIGNING_KEY", default=SECRET_KEY)
|
|
47
|
+
|
|
48
|
+
DEBUG = False
|
|
49
|
+
|
|
50
|
+
ALLOWED_HOSTS: list[str] = []
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Applications
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
DJANGO_APPS = [
|
|
57
|
+
# django.contrib.admin, but with HardenedAdminSite as the default site
|
|
58
|
+
# (#3557) -- off unless TRUEPPM_DJANGO_ADMIN_ENABLED, and when on, its login
|
|
59
|
+
# carries the API login's throttles, audit lines and policy seam. Django's
|
|
60
|
+
# documented hook for swapping the default site is an AdminConfig subclass in
|
|
61
|
+
# INSTALLED_APPS; the app itself (label "admin", its migrations) is unchanged.
|
|
62
|
+
"trueppm_api.core.admin_site.HardenedAdminConfig",
|
|
63
|
+
"django.contrib.auth",
|
|
64
|
+
"django.contrib.contenttypes",
|
|
65
|
+
"django.contrib.sessions",
|
|
66
|
+
"django.contrib.messages",
|
|
67
|
+
"django.contrib.staticfiles",
|
|
68
|
+
"django.contrib.postgres",
|
|
69
|
+
# SITE_ID = 1 (below) is already set; allauth.socialaccount's SocialApp is an
|
|
70
|
+
# M2M to Site, so the sites framework must be installed (ADR-0517 §1).
|
|
71
|
+
"django.contrib.sites",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
THIRD_PARTY_APPS = [
|
|
75
|
+
"rest_framework",
|
|
76
|
+
"rest_framework_simplejwt",
|
|
77
|
+
# Refresh-token revocation (#910): provides OutstandingToken + BlacklistedToken
|
|
78
|
+
# so refresh-token rotation (BLACKLIST_AFTER_ROTATION) and logout actually
|
|
79
|
+
# revoke the old token instead of letting it live out its full TTL. The
|
|
80
|
+
# auth_views blacklist() calls are no-ops without this app installed.
|
|
81
|
+
"rest_framework_simplejwt.token_blacklist",
|
|
82
|
+
"allauth",
|
|
83
|
+
"allauth.account",
|
|
84
|
+
# ADR-0517: adopt allauth.socialaccount as the provider *registry* only — our
|
|
85
|
+
# own hardened views in apps/sso drive the flow (egress, alg allow-list, Fernet
|
|
86
|
+
# secret, JWT bridge). We do NOT mount allauth.urls. openid_connect covers every
|
|
87
|
+
# OIDC IdP (as named APPS keyed by our registry slug); github is the one non-OIDC
|
|
88
|
+
# OAuth2 IdP that motivated the re-platform (#2108).
|
|
89
|
+
"allauth.socialaccount",
|
|
90
|
+
"allauth.socialaccount.providers.openid_connect",
|
|
91
|
+
"allauth.socialaccount.providers.github",
|
|
92
|
+
"channels",
|
|
93
|
+
"drf_spectacular",
|
|
94
|
+
# Serves the Swagger UI / ReDoc static bundles from Django static ('self')
|
|
95
|
+
# so the /api/docs/ and /api/schema/swagger-ui/ pages render under the strict
|
|
96
|
+
# CSP without a CDN. Must precede any app that would shadow its static files.
|
|
97
|
+
"drf_spectacular_sidecar",
|
|
98
|
+
"simple_history",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
LOCAL_APPS = [
|
|
102
|
+
"trueppm_api.apps.access",
|
|
103
|
+
"trueppm_api.apps.projects",
|
|
104
|
+
"trueppm_api.apps.resources",
|
|
105
|
+
"trueppm_api.apps.scheduling",
|
|
106
|
+
"trueppm_api.apps.sync",
|
|
107
|
+
"trueppm_api.apps.history",
|
|
108
|
+
"trueppm_api.apps.msproject",
|
|
109
|
+
"trueppm_api.apps.jiraimport",
|
|
110
|
+
"trueppm_api.apps.csvimport",
|
|
111
|
+
"trueppm_api.apps.webhooks",
|
|
112
|
+
"trueppm_api.apps.taskruns",
|
|
113
|
+
"trueppm_api.apps.notifications",
|
|
114
|
+
"trueppm_api.apps.integrations",
|
|
115
|
+
"trueppm_api.apps.observability",
|
|
116
|
+
"trueppm_api.apps.workflow_engine",
|
|
117
|
+
"trueppm_api.apps.idempotency",
|
|
118
|
+
"trueppm_api.apps.workspace",
|
|
119
|
+
"trueppm_api.apps.teams",
|
|
120
|
+
"trueppm_api.apps.profiles",
|
|
121
|
+
"trueppm_api.apps.timetracking",
|
|
122
|
+
"trueppm_api.apps.sso",
|
|
123
|
+
"trueppm_api.apps.agents",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Middleware
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
MIDDLEWARE = [
|
|
133
|
+
"django.middleware.security.SecurityMiddleware",
|
|
134
|
+
# Serve collected static files from the ASGI app itself (#1603). We run under
|
|
135
|
+
# `uvicorn ...asgi:application` in dev and prod — not `manage.py runserver` —
|
|
136
|
+
# so Django's dev static handler never engages and there is no nginx/CDN in the
|
|
137
|
+
# request path. Without this, every /static/ request 404s: Django admin CSS and
|
|
138
|
+
# the Swagger UI / ReDoc bundles (served same-origin under our strict CSP) both
|
|
139
|
+
# fail. WhiteNoise must sit immediately after SecurityMiddleware (its documented
|
|
140
|
+
# position) so it can short-circuit static requests before the rest of the stack.
|
|
141
|
+
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
142
|
+
# Assign each request a correlation id and bind it to the logging contextvar
|
|
143
|
+
# (#1899) so every log record emitted while handling the request carries it,
|
|
144
|
+
# and echo it back on the response. Placed high — but after WhiteNoise, which
|
|
145
|
+
# short-circuits static requests that need no id — so the id is set before any
|
|
146
|
+
# downstream middleware or view can log.
|
|
147
|
+
"trueppm_api.apps.observability.logging.RequestIDMiddleware",
|
|
148
|
+
# CSP header on every response (#897). Placed high so the header is attached
|
|
149
|
+
# even for early short-circuit responses (redirects, errors).
|
|
150
|
+
"trueppm_api.core.csp.ContentSecurityPolicyMiddleware",
|
|
151
|
+
# Reject NUL bytes in /api/ query strings before the view opens its
|
|
152
|
+
# ATOMIC_REQUESTS transaction (#2229) — a NUL in a text DB filter otherwise
|
|
153
|
+
# raises an uncaught psycopg DataError (500). Placed inside RequestID/CSP so
|
|
154
|
+
# the 400 still carries the correlation id and CSP header, but before Session
|
|
155
|
+
# so a garbage request skips session/auth work.
|
|
156
|
+
"trueppm_api.core.middleware.RejectNullBytesMiddleware",
|
|
157
|
+
# Demo read-only mode (interactive-demo design #3912, #3924): refuse every unsafe /api/ request
|
|
158
|
+
# unless it is one of the three auth writes. Sits BEFORE Session/Auth so anonymous
|
|
159
|
+
# POSTs (password-reset request, SSO, inbound integrations) are refused too, and
|
|
160
|
+
# before the view's ATOMIC_REQUESTS transaction so a refusal costs no DB work. It
|
|
161
|
+
# must not move below AuthenticationMiddleware: a role-aware position is exactly
|
|
162
|
+
# the "authority derived from the account" design this mode exists to avoid. A
|
|
163
|
+
# no-op unless DEMO_READ_ONLY is set.
|
|
164
|
+
"trueppm_api.core.demo_read_only.DemoReadOnlyMiddleware",
|
|
165
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
166
|
+
"django.middleware.common.CommonMiddleware",
|
|
167
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
168
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
169
|
+
"allauth.account.middleware.AccountMiddleware",
|
|
170
|
+
"simple_history.middleware.HistoryRequestMiddleware",
|
|
171
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
172
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
173
|
+
# Write queued agent-action audit rows after the view's ATOMIC_REQUESTS
|
|
174
|
+
# transaction has closed (#3017, ADR-0902). A refusal audit written inside that
|
|
175
|
+
# transaction is discarded by the set_rollback() DRF performs for every
|
|
176
|
+
# APIException, which is why the log contained only successes. Placed LAST so its
|
|
177
|
+
# response half runs FIRST on the way out — as close to the view as a middleware
|
|
178
|
+
# can be, while still being outside the transaction.
|
|
179
|
+
"trueppm_api.apps.agents.middleware.AgentActionAuditMiddleware",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
ROOT_URLCONF = "trueppm_api.urls"
|
|
183
|
+
|
|
184
|
+
TEMPLATES = [
|
|
185
|
+
{
|
|
186
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
187
|
+
"DIRS": [],
|
|
188
|
+
"APP_DIRS": True,
|
|
189
|
+
"OPTIONS": {
|
|
190
|
+
"context_processors": [
|
|
191
|
+
"django.template.context_processors.debug",
|
|
192
|
+
"django.template.context_processors.request",
|
|
193
|
+
"django.contrib.auth.context_processors.auth",
|
|
194
|
+
"django.contrib.messages.context_processors.messages",
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
WSGI_APPLICATION = "trueppm_api.wsgi.application"
|
|
201
|
+
ASGI_APPLICATION = "trueppm_api.asgi.application"
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Database
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
DATABASES = {
|
|
208
|
+
"default": env.db("DATABASE_URL", default="postgres://trueppm:trueppm@db:5432/trueppm")
|
|
209
|
+
}
|
|
210
|
+
DATABASES["default"]["CONN_MAX_AGE"] = 60
|
|
211
|
+
# Wrap every request in a transaction so transaction.on_commit() defers
|
|
212
|
+
# callbacks to after commit rather than firing immediately in autocommit mode.
|
|
213
|
+
DATABASES["default"]["ATOMIC_REQUESTS"] = True
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# File / object storage (#775)
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
# Default file-storage backend. TaskAttachment.file is stored here. The
|
|
220
|
+
# FileSystemStorage default is fine for local dev, but a containerized prod
|
|
221
|
+
# deploy must point this at a remote object-storage backend (S3/MinIO via
|
|
222
|
+
# django-storages) or attachment uploads are lost on every pod restart. prod.py
|
|
223
|
+
# refuses to boot on the local default unless the operator explicitly opts in.
|
|
224
|
+
STORAGES = {
|
|
225
|
+
"default": {
|
|
226
|
+
"BACKEND": env(
|
|
227
|
+
"TRUEPPM_DEFAULT_FILE_STORAGE",
|
|
228
|
+
default="django.core.files.storage.FileSystemStorage",
|
|
229
|
+
),
|
|
230
|
+
},
|
|
231
|
+
"staticfiles": {
|
|
232
|
+
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# S3 / MinIO options, attached ONLY for an S3-family backend — a FileSystemStorage
|
|
237
|
+
# deploy must not be handed S3 kwargs it would reject. See core.storage_config for
|
|
238
|
+
# why this plumbing is required rather than cosmetic (#2559): the backend the docs
|
|
239
|
+
# recommend reads its bucket from Django settings, not the environment, so setting
|
|
240
|
+
# TRUEPPM_DEFAULT_FILE_STORAGE alone booted and then failed on the first upload.
|
|
241
|
+
if STORAGES["default"]["BACKEND"] in S3_STORAGE_BACKENDS:
|
|
242
|
+
STORAGES["default"]["OPTIONS"] = build_s3_storage_options(os.environ)
|
|
243
|
+
|
|
244
|
+
# Where FileSystemStorage writes task attachments. Django's own default is "",
|
|
245
|
+
# which resolves upload paths against the process CWD — /app in the container
|
|
246
|
+
# image, whose root filesystem is read-only on both supported production paths
|
|
247
|
+
# (Helm readOnlyRootFilesystem, compose read_only). Every upload therefore failed
|
|
248
|
+
# at write time with EROFS while the boot guard reported the deployment healthy
|
|
249
|
+
# (#3184). TRUEPPM_MEDIA_ROOT was already read by scripts/backup.sh and
|
|
250
|
+
# scripts/restore.sh but by nothing in Django, so the variable the backup tooling
|
|
251
|
+
# documents now actually decides where the files it archives live.
|
|
252
|
+
#
|
|
253
|
+
# The default is the path the chart's persistence.media PVC and the compose
|
|
254
|
+
# `media` volume both mount. It is deliberately outside BASE_DIR: BASE_DIR
|
|
255
|
+
# resolves inside the read-only venv when the app is pip-installed, exactly as
|
|
256
|
+
# STATIC_ROOT's comment below notes. settings/dev.py overrides it to a
|
|
257
|
+
# repo-local directory so a workstation and pytest never need /var/lib.
|
|
258
|
+
MEDIA_ROOT = Path(env("TRUEPPM_MEDIA_ROOT", default="/var/lib/trueppm/media"))
|
|
259
|
+
|
|
260
|
+
# Operator opt-in to run prod on local-disk attachment storage (e.g. when it is
|
|
261
|
+
# backed by a persistent volume). Consumed by validate_attachment_storage.
|
|
262
|
+
ALLOW_LOCAL_ATTACHMENT_STORAGE = env.bool("TRUEPPM_ALLOW_LOCAL_ATTACHMENT_STORAGE", default=False)
|
|
263
|
+
|
|
264
|
+
# Operator opt-in confirming the configured STORAGES['default']['BACKEND'] signs
|
|
265
|
+
# its .url() output with a real time-limited URL, for a backend not on
|
|
266
|
+
# security_checks.storage_backend_supports_signed_urls's allow-list (#573,
|
|
267
|
+
# MED-2). Default False: the TaskAttachmentViewSet.signed_url action refuses
|
|
268
|
+
# with 501 rather than hand out a stable indefinite-lifetime URL labeled as
|
|
269
|
+
# time-limited for any backend it can't positively identify as signing-capable.
|
|
270
|
+
ATTACHMENT_STORAGE_SIGNS_URLS = env.bool("TRUEPPM_ATTACHMENT_STORAGE_SIGNS_URLS", default=False)
|
|
271
|
+
|
|
272
|
+
# Operator opt-in to run prod against a DATABASE_URL without sslmode=require (e.g.
|
|
273
|
+
# when TLS to the database is enforced at the network layer). Consumed by the
|
|
274
|
+
# unencrypted-DB boot guard in settings/prod.py.
|
|
275
|
+
ALLOW_UNENCRYPTED_DB = env.bool("TRUEPPM_ALLOW_UNENCRYPTED_DB", default=False)
|
|
276
|
+
|
|
277
|
+
# Operator acknowledgment that ALLOWED_HOSTS is a bare "*" in prod (#3515).
|
|
278
|
+
# Consumed by the wildcard-host boot guard in settings/prod.py, which otherwise
|
|
279
|
+
# refuses to start: "*" is the one value that disables host validation outright,
|
|
280
|
+
# and host validation is the only bound on the absolute URLs the API builds.
|
|
281
|
+
ALLOW_WILDCARD_ALLOWED_HOSTS = env.bool("TRUEPPM_ALLOW_WILDCARD_HOSTS", default=False)
|
|
282
|
+
|
|
283
|
+
# Whether /admin/ answers at all (#3557). Default OFF everywhere except the dev
|
|
284
|
+
# settings module, which flips it back on for a developer workstation.
|
|
285
|
+
#
|
|
286
|
+
# Django admin is a plain Django view, so none of the API's login defenses reach
|
|
287
|
+
# it by inheritance: DRF throttles apply to DRF views only, the auth audit lines
|
|
288
|
+
# are emitted by our own login view, and the enterprise local_login_allowed seam
|
|
289
|
+
# is consulted there too. Left unhardened it is a second password door onto a
|
|
290
|
+
# known-present superuser (the create_admin bootstrap makes one on first deploy)
|
|
291
|
+
# with none of the first door's controls. The chart's nginx denies /admin/ by
|
|
292
|
+
# default, but Docker Compose and bare-metal deploys route to the API directly
|
|
293
|
+
# and have no such edge.
|
|
294
|
+
#
|
|
295
|
+
# Off-by-default is the primary control, because the strongest statement about a
|
|
296
|
+
# surface almost nobody uses is that it is not there. HardenedAdminSite raises
|
|
297
|
+
# Http404 for every /admin/ path when this is False -- a 404, not a 403, so the
|
|
298
|
+
# surface is not even advertised. An operator who genuinely needs the admin sets
|
|
299
|
+
# this to true and gets the hardened door rather than the bare one: the same
|
|
300
|
+
# "login"/"login_account" throttle buckets as the API login, the same
|
|
301
|
+
# auth.login_failed / auth.login_succeeded lines, and the same policy seam.
|
|
302
|
+
DJANGO_ADMIN_ENABLED = env.bool("TRUEPPM_DJANGO_ADMIN_ENABLED", default=False)
|
|
303
|
+
|
|
304
|
+
# Interactive-demo read-only mode (#3912, #3924). When true,
|
|
305
|
+
# DemoReadOnlyMiddleware refuses every unsafe HTTP method under /api/ except login,
|
|
306
|
+
# token refresh and logout, regardless of role or permission class. Off by default; a
|
|
307
|
+
# normal install is unaffected. Parsed strictly rather than with env.bool(): env.bool
|
|
308
|
+
# reads any unrecognized string (a typo like "ture") as False, which would silently
|
|
309
|
+
# disable the one control the demo's safety rests on. An unrecognized value here
|
|
310
|
+
# refuses to boot instead.
|
|
311
|
+
DEMO_READ_ONLY: bool = parse_demo_read_only(os.environ.get("TRUEPPM_DEMO_READ_ONLY"))
|
|
312
|
+
|
|
313
|
+
# External access gate declared in front of a demo host (#3969, ADR-1197 D8 resolution
|
|
314
|
+
# of 2026-09-21). A gate such as Cloudflare Access collects a visitor's email address
|
|
315
|
+
# before any TruePPM code runs; this is how the deployment says so, and it is surfaced
|
|
316
|
+
# only while DEMO_READ_ONLY is on (see the edition view). IT IS AN OPERATOR ASSERTION
|
|
317
|
+
# THAT NOTHING VERIFIES -- the gate is configured in a third-party dashboard outside
|
|
318
|
+
# the cluster, so neither setting it nor clearing it proves anything about what is
|
|
319
|
+
# actually in front of this pod. Unset on every normal install, which is the point:
|
|
320
|
+
# the disclosure is conditional, never a blanket claim.
|
|
321
|
+
DEMO_ACCESS_GATE: dict[str, str | None] | None = parse_demo_access_gate(
|
|
322
|
+
os.environ.get("TRUEPPM_DEMO_ACCESS_GATE_PROVIDER"),
|
|
323
|
+
os.environ.get("TRUEPPM_DEMO_ACCESS_GATE_PRIVACY_URL"),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Global rate-limiting kill switch (ADR-0604, extends ADR-0208). Operator-only
|
|
327
|
+
# escape hatch to disable ALL DRF throttling — used by the k6 perf:load job to
|
|
328
|
+
# measure raw throughput, and by an operator load-testing / debugging a real
|
|
329
|
+
# deployment. Disabling requires BOTH TRUEPPM_RATE_LIMIT_ENABLED=false AND the
|
|
330
|
+
# acknowledgment sentinel, in every environment; an unacknowledged disable is
|
|
331
|
+
# refused (limits stay ON) and logged at CRITICAL, so a stray env var can never
|
|
332
|
+
# silently open a DoS path. See trueppm_api.core.ratelimit for the full rationale.
|
|
333
|
+
# The effective throttle neutralization runs after REST_FRAMEWORK is defined
|
|
334
|
+
# (search "apply_rate_limit_disable" below). Surfaced read-only to admins via
|
|
335
|
+
# /health/system/ and a banner; never a UI/API toggle (ADR-0497).
|
|
336
|
+
RATE_LIMIT_ENABLED, _rate_limit_switch_message = resolve_rate_limit_enabled(
|
|
337
|
+
requested_enabled=env.bool("TRUEPPM_RATE_LIMIT_ENABLED", default=True),
|
|
338
|
+
ack=env("TRUEPPM_RATE_LIMIT_DISABLE_ACK", default=""),
|
|
339
|
+
)
|
|
340
|
+
_settings_log = logging.getLogger("trueppm.settings")
|
|
341
|
+
if _rate_limit_switch_message:
|
|
342
|
+
_settings_log.critical(_rate_limit_switch_message)
|
|
343
|
+
|
|
344
|
+
# The shared credential the interactive demo's login page prints (#3926, ADR-1197 D3).
|
|
345
|
+
# "username:password"; unset on every normal install. Read here rather than derived from
|
|
346
|
+
# any real password variable so nothing can promote a live secret to published copy by
|
|
347
|
+
# accident. /api/v1/edition/ emits it ONLY while DEMO_READ_ONLY is true.
|
|
348
|
+
#
|
|
349
|
+
# Parsed strictly only where it MATTERS. Refusing to boot on a malformed value is right
|
|
350
|
+
# for a demo — the one credential it advertises being a typo makes the demo unusable —
|
|
351
|
+
# and wrong for a production install, where a leftover variable is inert and crashing
|
|
352
|
+
# the process over it is an outage caused by something that does nothing. What the
|
|
353
|
+
# `else` branch must not be is silent: hint-set-while-mode-off is exactly the state
|
|
354
|
+
# "a real account's password is sitting in a writable install's environment", and
|
|
355
|
+
# nothing else anywhere would say so.
|
|
356
|
+
_demo_login_hint_raw = os.environ.get("TRUEPPM_DEMO_LOGIN_HINT")
|
|
357
|
+
DEMO_LOGIN_HINT: dict[str, str] | None = (
|
|
358
|
+
parse_demo_login_hint(_demo_login_hint_raw) if DEMO_READ_ONLY else None
|
|
359
|
+
)
|
|
360
|
+
if not DEMO_READ_ONLY and _demo_login_hint_raw and _demo_login_hint_raw.strip():
|
|
361
|
+
_settings_log.warning(
|
|
362
|
+
"TRUEPPM_DEMO_LOGIN_HINT is set but TRUEPPM_DEMO_READ_ONLY is not true. The "
|
|
363
|
+
"value is IGNORED and never served, but a published credential is sitting in "
|
|
364
|
+
"this install's environment. Unset it unless this really is a demo."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# The reset CronJob's own cadence, restated so the demo bar and login page can say
|
|
368
|
+
# "resets daily at HH:MM" from the value that actually drives the CronJob rather than
|
|
369
|
+
# a hardcoded number (#4152, ADR-1197 D9). The chart renders this empty whenever
|
|
370
|
+
# demo.reset is disabled, so `None` here already means "no schedule to state" without
|
|
371
|
+
# a second demo-mode gate — but /api/v1/edition/ still gates emission on DEMO_READ_ONLY,
|
|
372
|
+
# like the hint and the access gate, so the field says nothing on a normal install even
|
|
373
|
+
# if the variable were somehow left set.
|
|
374
|
+
DEMO_RESET_SCHEDULE: str | None = parse_demo_reset_schedule(
|
|
375
|
+
os.environ.get("TRUEPPM_DEMO_RESET_SCHEDULE")
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Origins trusted for cross-origin POST / CSRF — required when the web app is
|
|
379
|
+
# served from a different origin than the API (split dev setup or subdomain
|
|
380
|
+
# split). Empty by default (same-origin reverse-proxy deploy).
|
|
381
|
+
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
|
|
382
|
+
|
|
383
|
+
# Public origin the web app is served from, e.g. "https://app.example.com". Used
|
|
384
|
+
# to build absolute deep-links into emails/notifications (ADR-0165, #1158) — the
|
|
385
|
+
# API otherwise has no notion of where the frontend lives. Empty by default
|
|
386
|
+
# (zero-config): when unset, email bodies render without a link rather than emit a
|
|
387
|
+
# broken relative URL. Trailing slash is stripped at use; do not include a path.
|
|
388
|
+
# Env var standardized on the TRUEPPM_ prefix pre-0.3 (#1325, #1355); the legacy
|
|
389
|
+
# bare name remains a fallback so existing deploys keep working. ``or`` (not a
|
|
390
|
+
# nested default) so an *empty* TRUEPPM_ value — e.g. the chart's documented
|
|
391
|
+
# ``TRUEPPM_FRONTEND_BASE_URL: ""`` default — falls through to a legacy override
|
|
392
|
+
# rather than shadowing it. Empty means "no deep-links", so the fallthrough is
|
|
393
|
+
# also semantically correct.
|
|
394
|
+
FRONTEND_BASE_URL = env("TRUEPPM_FRONTEND_BASE_URL", default="") or env(
|
|
395
|
+
"FRONTEND_BASE_URL", default=""
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Public origin of the API itself, used only to derive the OIDC ``redirect_uri``
|
|
399
|
+
# (ADR-0187): ``{TRUEPPM_PUBLIC_API_BASE_URL}/api/v1/auth/oidc/callback/``. The
|
|
400
|
+
# operator copies this exact value into their IdP's allowed-redirect list. Empty
|
|
401
|
+
# by default (zero-config): the SSO views fall back to the request's absolute URI,
|
|
402
|
+
# correct for a single-origin dev setup; set it explicitly behind a reverse proxy
|
|
403
|
+
# so the allow-listed value is deterministic. Trailing slash is stripped at use.
|
|
404
|
+
TRUEPPM_PUBLIC_API_BASE_URL = env("TRUEPPM_PUBLIC_API_BASE_URL", default="")
|
|
405
|
+
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
# Cache / Channels / Celery (all backed by Redis)
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
REDIS_URL = env("REDIS_URL", default="redis://redis:6379")
|
|
411
|
+
|
|
412
|
+
# --- Valkey topology (ADR-0716, #2554) ------------------------------------
|
|
413
|
+
# Sentinel mode is EXPERIMENTAL as of 0.4: the wiring below is unit-tested, but no
|
|
414
|
+
# live-quorum failover has been exercised (the bundled dev stack is single-node).
|
|
415
|
+
# Setting TRUEPPM_VALKEY_SENTINELS switches every consumer to Sentinel mode; left
|
|
416
|
+
# empty (the default) everything below derives from REDIS_URL exactly as before,
|
|
417
|
+
# so an existing deploy needs no config change. Sentinel is a *block* rather than
|
|
418
|
+
# per-role URL overrides because the blocking constraint is that neither
|
|
419
|
+
# channels-redis nor the Django cache can express Sentinel as a URL at all — see
|
|
420
|
+
# ADR-0716 for the rejected alternatives. Cluster mode is unsupported: a clustered
|
|
421
|
+
# endpoint exposes only db 0 and we use four (0 Celery, 1 Channels, 2 cache, 3
|
|
422
|
+
# notification throttles).
|
|
423
|
+
VALKEY_SENTINELS_RAW = env("TRUEPPM_VALKEY_SENTINELS", default="")
|
|
424
|
+
VALKEY_MASTER_NAME = env("TRUEPPM_VALKEY_MASTER_NAME", default="")
|
|
425
|
+
VALKEY_PASSWORD = env("TRUEPPM_VALKEY_PASSWORD", default="")
|
|
426
|
+
# Sentinel nodes commonly carry a different password from the data nodes (often
|
|
427
|
+
# none at all), so this is deliberately separate rather than reusing the above.
|
|
428
|
+
VALKEY_SENTINEL_PASSWORD = env("TRUEPPM_VALKEY_SENTINEL_PASSWORD", default="")
|
|
429
|
+
VALKEY_USE_TLS = env.bool("TRUEPPM_VALKEY_USE_TLS", default=False)
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
VALKEY_SENTINELS: list[tuple[str, int]] = parse_sentinels(VALKEY_SENTINELS_RAW)
|
|
433
|
+
except ValueError:
|
|
434
|
+
# A malformed entry must not raise out of settings import — that surfaces as an
|
|
435
|
+
# unreadable traceback before Django can report anything. Fall back to
|
|
436
|
+
# single-endpoint mode and let core.valkey_checks emit trueppm.valkey.E002,
|
|
437
|
+
# which names the bad entry and refuses the boot cleanly.
|
|
438
|
+
VALKEY_SENTINELS = []
|
|
439
|
+
|
|
440
|
+
VALKEY_SENTINEL_MODE = bool(VALKEY_SENTINELS)
|
|
441
|
+
|
|
442
|
+
# Shared cache backend (Valkey/Redis db 2 — db 1 is the channel layer, db 0 is
|
|
443
|
+
# Celery). Backs the short-lived, single-use OIDC login state / PKCE verifier /
|
|
444
|
+
# nonce (ADR-0187 §Durable Execution) and the DRF scoped throttles, both of which
|
|
445
|
+
# must be consistent across worker processes in a multi-worker deploy. ``dev.py``
|
|
446
|
+
# overrides this to LocMemCache so a local run / pytest needs no separate cache
|
|
447
|
+
# service (single process, so per-process memory is sufficient there).
|
|
448
|
+
# In Sentinel mode the backend swaps to our own RedisCache subclass (ADR-0716) —
|
|
449
|
+
# Django's built-in has no Sentinel support, and subclassing it is ~30 lines
|
|
450
|
+
# against adding django-redis, which ADR-0065/0068 declined.
|
|
451
|
+
CACHES = {
|
|
452
|
+
"default": (
|
|
453
|
+
{
|
|
454
|
+
"BACKEND": "trueppm_api.core.valkey_cache.SentinelRedisCache",
|
|
455
|
+
# LOCATION is ignored by that backend (topology comes from the
|
|
456
|
+
# TRUEPPM_VALKEY_* settings) but Django requires the key present.
|
|
457
|
+
"LOCATION": "",
|
|
458
|
+
}
|
|
459
|
+
if VALKEY_SENTINEL_MODE
|
|
460
|
+
else {
|
|
461
|
+
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
|
462
|
+
"LOCATION": f"{REDIS_URL}/2",
|
|
463
|
+
}
|
|
464
|
+
),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# channels-redis accepts Sentinel ONLY as a dict host — it can never be carried in
|
|
468
|
+
# a URL, which is the single fact that made Sentinel unreachable before ADR-0716.
|
|
469
|
+
_CHANNEL_HOST: dict[str, object] | str = (
|
|
470
|
+
{
|
|
471
|
+
"sentinels": VALKEY_SENTINELS,
|
|
472
|
+
"master_name": VALKEY_MASTER_NAME,
|
|
473
|
+
"db": 1,
|
|
474
|
+
**({"password": VALKEY_PASSWORD} if VALKEY_PASSWORD else {}),
|
|
475
|
+
**(
|
|
476
|
+
{"sentinel_kwargs": {"password": VALKEY_SENTINEL_PASSWORD}}
|
|
477
|
+
if VALKEY_SENTINEL_PASSWORD
|
|
478
|
+
else {}
|
|
479
|
+
),
|
|
480
|
+
}
|
|
481
|
+
if VALKEY_SENTINEL_MODE
|
|
482
|
+
else f"{REDIS_URL}/1"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
CHANNEL_LAYERS = {
|
|
486
|
+
"default": {
|
|
487
|
+
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
|
488
|
+
"CONFIG": {
|
|
489
|
+
"hosts": [_CHANNEL_HOST],
|
|
490
|
+
"capacity": 1500,
|
|
491
|
+
"expiry": 10,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
# Legacy WebSocket ?token=<jwt> handshake (ADR-0141, #1723). The single-use
|
|
497
|
+
# ?ticket= scheme replaced it: a raw JWT in the URL leaks verbatim into
|
|
498
|
+
# reverse-proxy / ingress / Daphne access logs (the credential-in-URL class of
|
|
499
|
+
# #818). The fallback shipped deprecated-but-live for one release; it is now
|
|
500
|
+
# OFF by default and must be explicitly opted into by an operator who still has
|
|
501
|
+
# clients on the old path. It is removed entirely next release.
|
|
502
|
+
TRUEPPM_WS_LEGACY_TOKEN_AUTH_ENABLED: bool = env.bool(
|
|
503
|
+
"TRUEPPM_WS_LEGACY_TOKEN_AUTH_ENABLED", default=False
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Celery accepts a sentinel:// URL, but ONLY together with a master_name in the
|
|
507
|
+
# transport options — the URL alone silently connects to whichever sentinel
|
|
508
|
+
# answers rather than to the primary. Broker and result backend take separate
|
|
509
|
+
# option dicts; setting only broker_transport_options is the common half-fix and
|
|
510
|
+
# leaves the result backend pointed at a sentinel port (ADR-0716).
|
|
511
|
+
if VALKEY_SENTINEL_MODE:
|
|
512
|
+
_sentinel_authority = ";".join(
|
|
513
|
+
f"sentinel://{f':{VALKEY_PASSWORD}@' if VALKEY_PASSWORD else ''}{host}:{port}"
|
|
514
|
+
for host, port in VALKEY_SENTINELS
|
|
515
|
+
)
|
|
516
|
+
CELERY_BROKER_URL = _sentinel_authority
|
|
517
|
+
CELERY_RESULT_BACKEND = _sentinel_authority
|
|
518
|
+
_celery_transport_options: dict[str, object] = {
|
|
519
|
+
"master_name": VALKEY_MASTER_NAME,
|
|
520
|
+
"db": 0,
|
|
521
|
+
}
|
|
522
|
+
if VALKEY_SENTINEL_PASSWORD:
|
|
523
|
+
_celery_transport_options["sentinel_kwargs"] = {"password": VALKEY_SENTINEL_PASSWORD}
|
|
524
|
+
CELERY_BROKER_TRANSPORT_OPTIONS = _celery_transport_options
|
|
525
|
+
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = _celery_transport_options
|
|
526
|
+
else:
|
|
527
|
+
CELERY_BROKER_URL = f"{REDIS_URL}/0"
|
|
528
|
+
CELERY_RESULT_BACKEND = f"{REDIS_URL}/0"
|
|
529
|
+
# Bound the redelivery window on the non-sentinel path (#3188). Tasks carry
|
|
530
|
+
# acks_late=True + reject_on_worker_lost=True in 60+ places, so a worker
|
|
531
|
+
# killed mid-task does not LOSE the work — the broker redelivers it. But
|
|
532
|
+
# Kombu's Redis transport hides an unacked message for `visibility_timeout`
|
|
533
|
+
# seconds first, and its default is 3600. A worker SIGKILLed during a
|
|
534
|
+
# rolling upgrade therefore left that task invisible for up to an hour, with
|
|
535
|
+
# nothing in the queue depth or the task list to say why. This block set
|
|
536
|
+
# transport options only on the sentinel path, so the default applied to
|
|
537
|
+
# every ordinary deploy.
|
|
538
|
+
#
|
|
539
|
+
# 900s is chosen against the longest task we actually run rather than a
|
|
540
|
+
# round number: MS Project import and workspace export are the multi-minute
|
|
541
|
+
# ones. It MUST exceed the longest task's runtime — a visibility timeout
|
|
542
|
+
# shorter than the work redelivers a task that is still running, which turns
|
|
543
|
+
# one slow import into two concurrent ones. Raise it if you raise
|
|
544
|
+
# terminationGracePeriodSeconds on the worker past this.
|
|
545
|
+
CELERY_BROKER_TRANSPORT_OPTIONS = {"visibility_timeout": 900}
|
|
546
|
+
|
|
547
|
+
CELERY_ACCEPT_CONTENT = ["json"]
|
|
548
|
+
CELERY_TASK_SERIALIZER = "json"
|
|
549
|
+
CELERY_RESULT_SERIALIZER = "json"
|
|
550
|
+
CELERY_TIMEZONE = "UTC"
|
|
551
|
+
from celery.schedules import crontab # noqa: E402 — must follow REDIS_URL
|
|
552
|
+
|
|
553
|
+
CELERY_BEAT_SCHEDULE = {
|
|
554
|
+
# Retention purge coordinator (ADR-0173): one self-gating task that purges the
|
|
555
|
+
# six operational tables (event history, task runs, webhook deliveries, import
|
|
556
|
+
# requests, sync batches, soft-deleted projects) as a single unified run,
|
|
557
|
+
# recorded in PurgeRun. Fires every 30 min and no-ops outside the
|
|
558
|
+
# operator-configured window (Settings → System health → Retention & purge).
|
|
559
|
+
# Replaces the former per-table nightly purge entries — the per-table tasks
|
|
560
|
+
# remain dispatchable but are no longer independently scheduled.
|
|
561
|
+
"retention-purge-coordinator": {
|
|
562
|
+
"task": "retention.run_purge",
|
|
563
|
+
"schedule": crontab(minute="*/30"),
|
|
564
|
+
},
|
|
565
|
+
# Outbox drain: dispatches pending ScheduleRequest rows every 30 seconds.
|
|
566
|
+
# Also recovers orphaned dispatched rows (worker died before completing).
|
|
567
|
+
"drain-schedule-queue": {
|
|
568
|
+
"task": "scheduling.drain_schedule_queue",
|
|
569
|
+
"schedule": 30.0,
|
|
570
|
+
},
|
|
571
|
+
# Nightly cleanup: deletes done/dead ScheduleRequest rows older than 7 days.
|
|
572
|
+
"schedule-requests-purge-nightly": {
|
|
573
|
+
"task": "scheduling.purge_old_schedule_requests",
|
|
574
|
+
# 02:15 UTC — between the two existing purge jobs.
|
|
575
|
+
"schedule": crontab(hour=2, minute=15),
|
|
576
|
+
},
|
|
577
|
+
# Config-change notice outbox drain (#3009, ADR-1174): recovers rows whose
|
|
578
|
+
# worker was lost and dispatches notices whose on-commit dispatch stranded.
|
|
579
|
+
"drain-config-notice-requests": {
|
|
580
|
+
"task": "projects.drain_config_notice_requests",
|
|
581
|
+
"schedule": 30.0,
|
|
582
|
+
},
|
|
583
|
+
# Nightly cleanup: deletes done/dead ConfigNoticeRequest rows older than 7 days.
|
|
584
|
+
"config-notice-requests-purge-nightly": {
|
|
585
|
+
"task": "projects.purge_old_config_notice_requests",
|
|
586
|
+
"schedule": crontab(hour=2, minute=55),
|
|
587
|
+
},
|
|
588
|
+
# Nightly cleanup: deletes acknowledged / auto-resolved CrossProjectSlipConflict
|
|
589
|
+
# rows 90 days past their resolution (ADR-0120 D4). Unresolved + unacknowledged
|
|
590
|
+
# conflicts are kept indefinitely — they are still live.
|
|
591
|
+
"purge-resolved-slip-conflicts-nightly": {
|
|
592
|
+
"task": "scheduling.purge_resolved_slip_conflicts",
|
|
593
|
+
# 02:25 UTC — after the Monte Carlo purge.
|
|
594
|
+
"schedule": crontab(hour=2, minute=25),
|
|
595
|
+
},
|
|
596
|
+
# Nightly cleanup: reaps WebSocket replay-buffer rows (BoardEvent) older than
|
|
597
|
+
# TRUEPPM_BOARD_EVENT_RETENTION_HOURS so the buffer stays bounded (ADR-0236,
|
|
598
|
+
# #321). Standalone rather than in the ADR-0173 operator coordinator: the
|
|
599
|
+
# buffer is internal transport plumbing, deliberately not surfaced as an
|
|
600
|
+
# operator-tunable retention row.
|
|
601
|
+
"purge-board-events-nightly": {
|
|
602
|
+
"task": "sync.purge_board_events",
|
|
603
|
+
# 02:35 UTC — after the slip-conflict purge.
|
|
604
|
+
"schedule": crontab(hour=2, minute=35),
|
|
605
|
+
},
|
|
606
|
+
# Nightly cleanup: trims project Monte Carlo run history to the newest
|
|
607
|
+
# MC_HISTORY_CAP rows per project (ADR-0175, #961). No-ops when the cap is
|
|
608
|
+
# None (Enterprise unlimited).
|
|
609
|
+
"purge-monte-carlo-runs-nightly": {
|
|
610
|
+
"task": "scheduling.purge_old_monte_carlo_runs",
|
|
611
|
+
# 02:20 UTC — right after the schedule-requests purge.
|
|
612
|
+
"schedule": crontab(hour=2, minute=20),
|
|
613
|
+
},
|
|
614
|
+
# Daily forecast-snapshot floor: guarantees ≥1 ProjectForecastSnapshot per
|
|
615
|
+
# active project per day and backfills any recompute capture missed by a broker
|
|
616
|
+
# blip / worker death (ADR-0154, #388). The durability backstop for capture.
|
|
617
|
+
"capture-daily-forecast-floor": {
|
|
618
|
+
"task": "scheduling.capture_daily_forecast_floor",
|
|
619
|
+
# 00:30 UTC — early, before the nightly purges, so every project has a row.
|
|
620
|
+
"schedule": crontab(hour=0, minute=30),
|
|
621
|
+
},
|
|
622
|
+
# Nightly cleanup: applies the tiered retention curve to project forecast
|
|
623
|
+
# snapshots — all <90 d, weekly to 1 y, monthly forever (ADR-0154, #388).
|
|
624
|
+
"prune-forecast-snapshots-nightly": {
|
|
625
|
+
"task": "scheduling.prune_forecast_snapshots",
|
|
626
|
+
# 04:15 UTC — after the other nightly purge jobs.
|
|
627
|
+
"schedule": crontab(hour=4, minute=15),
|
|
628
|
+
},
|
|
629
|
+
# Webhook delivery drain: re-enqueues stranded PENDING deliveries whose
|
|
630
|
+
# initial .delay() call was lost (e.g. broker down at creation time).
|
|
631
|
+
"drain-webhook-queue": {
|
|
632
|
+
"task": "webhooks.drain_webhook_queue",
|
|
633
|
+
"schedule": 30.0,
|
|
634
|
+
},
|
|
635
|
+
# MS Project import drain: dispatches pending ImportRequest rows every 30 s.
|
|
636
|
+
# Also recovers orphaned dispatched rows (worker died mid-import).
|
|
637
|
+
"drain-import-queue": {
|
|
638
|
+
"task": "msproject.drain_import_queue",
|
|
639
|
+
"schedule": 30.0,
|
|
640
|
+
},
|
|
641
|
+
# Jira import drain: dispatches pending JiraImportRequest rows every 30 s and
|
|
642
|
+
# recovers orphaned dispatched rows (worker died mid-import).
|
|
643
|
+
"drain-jira-import-queue": {
|
|
644
|
+
"task": "jira.drain_import_queue",
|
|
645
|
+
"schedule": 30.0,
|
|
646
|
+
},
|
|
647
|
+
# CSV/Excel import drain: dispatches pending CsvImportRequest rows every 30 s
|
|
648
|
+
# and recovers orphaned dispatched rows (worker died mid-import). Kept
|
|
649
|
+
# separate from the MSP and Jira drains per ADR-0259/ADR-0632 so no drain has
|
|
650
|
+
# to branch on file type.
|
|
651
|
+
"drain-csv-import-queue": {
|
|
652
|
+
"task": "csv.drain_import_queue",
|
|
653
|
+
"schedule": 30.0,
|
|
654
|
+
},
|
|
655
|
+
# Template-apply drain (ADR-0789, #2729): dispatches pending
|
|
656
|
+
# TemplateApplication rows every 30 s and recovers ones whose worker died
|
|
657
|
+
# mid-apply. This is what makes enqueue_template_apply's best-effort
|
|
658
|
+
# .delay() durable — without it, a broker outage at dispatch time would
|
|
659
|
+
# leave an application pending forever and the Start sheet polling nothing.
|
|
660
|
+
"drain-template-apply-queue": {
|
|
661
|
+
"task": "templates.drain_apply_queue",
|
|
662
|
+
"schedule": 30.0,
|
|
663
|
+
},
|
|
664
|
+
# Sprint close drain: dispatches pending SprintCloseRequest rows every 30 s.
|
|
665
|
+
# Also recovers IN_FLIGHT rows orphaned past the 5-minute window.
|
|
666
|
+
"drain-sprint-close-requests": {
|
|
667
|
+
"task": "projects.drain_sprint_close_requests",
|
|
668
|
+
"schedule": 30.0,
|
|
669
|
+
},
|
|
670
|
+
# Daily burndown snapshot: writes yesterday's row for every ACTIVE sprint
|
|
671
|
+
# — covers days with no task status changes (signal handler covers today).
|
|
672
|
+
"update-sprint-burndown-snapshots": {
|
|
673
|
+
"task": "projects.update_sprint_burndown_snapshots",
|
|
674
|
+
"schedule": crontab(hour=1, minute=0),
|
|
675
|
+
},
|
|
676
|
+
# Nightly cleanup: deletes COMPLETED/FAILED SprintCloseRequest rows >7 days.
|
|
677
|
+
"purge-sprint-close-requests": {
|
|
678
|
+
"task": "projects.purge_sprint_close_requests",
|
|
679
|
+
# 03:00 UTC — after other nightly purge jobs.
|
|
680
|
+
"schedule": crontab(hour=3, minute=0),
|
|
681
|
+
},
|
|
682
|
+
# Notification email outbox drain: send queued notification emails every 30 s.
|
|
683
|
+
# Fan-outs also nudge it on commit, so this tick is the durability backstop, not
|
|
684
|
+
# the latency path (ADR-0087 §Amendment #4191).
|
|
685
|
+
"drain-notification-emails": {
|
|
686
|
+
"task": "notifications.drain_notification_emails",
|
|
687
|
+
"schedule": 30.0,
|
|
688
|
+
},
|
|
689
|
+
# Nightly archive: notifications older than 90 days with is_read=True become
|
|
690
|
+
# is_archived=True. Keeps the unread-bell query path on a shallow index.
|
|
691
|
+
"archive-old-notifications": {
|
|
692
|
+
"task": "notifications.archive_old_notifications",
|
|
693
|
+
# 03:15 UTC — after other nightly purge/archive jobs.
|
|
694
|
+
"schedule": crontab(hour=3, minute=15),
|
|
695
|
+
},
|
|
696
|
+
# Daily stale-task detection (ADR-0200): nudge assignees of non-terminal tasks
|
|
697
|
+
# that have sat in their status past the project's stale_task_threshold_days
|
|
698
|
+
# (default 7). Dedupes against existing unread task.stale notifications.
|
|
699
|
+
"detect-stale-tasks": {
|
|
700
|
+
"task": "notifications.detect_stale_tasks",
|
|
701
|
+
# 05:30 UTC — after the nightly purges/archive, so the scan reads settled
|
|
702
|
+
# state and lands a fresh nudge before the working day starts.
|
|
703
|
+
"schedule": crontab(hour=5, minute=30),
|
|
704
|
+
},
|
|
705
|
+
# Scheduled digests (ADR-0663, #2407): hourly sweep that sends each opted-in
|
|
706
|
+
# user's weekly program-health / overallocation digest when the current hour
|
|
707
|
+
# matches their own slot in their own timezone. Hourly (not weekly) because
|
|
708
|
+
# the schedule is a per-user property — one cluster-wide weekly entry would be
|
|
709
|
+
# 3 a.m. for half the recipients. Runs at minute 10 so it does not contend
|
|
710
|
+
# with generate-recurring-occurrences at minute 0.
|
|
711
|
+
"send-scheduled-digests": {
|
|
712
|
+
"task": "notifications.send_scheduled_digests",
|
|
713
|
+
"schedule": crontab(minute=10),
|
|
714
|
+
},
|
|
715
|
+
# Nightly cleanup: drop digest ledger rows past the 90-day retention window
|
|
716
|
+
# (ADR-0663 §Durable Execution item 6). Longer than the 7-day convention
|
|
717
|
+
# because the ledger's job is to remember a weekly send.
|
|
718
|
+
"purge-old-digest-runs": {
|
|
719
|
+
"task": "notifications.purge_old_digest_runs",
|
|
720
|
+
# 03:20 UTC — between archive-old-notifications (03:15) and the 04:00
|
|
721
|
+
# workflow purge.
|
|
722
|
+
"schedule": crontab(hour=3, minute=20),
|
|
723
|
+
},
|
|
724
|
+
# Beat liveness heartbeat: a single worker writes BeatHeartbeat.last_heartbeat
|
|
725
|
+
# every 30 s. GET /api/v1/health/beat/ reads it to detect a dead Beat (ADR-0081).
|
|
726
|
+
"beat-heartbeat": {
|
|
727
|
+
"task": "beat.heartbeat",
|
|
728
|
+
"schedule": 30.0,
|
|
729
|
+
},
|
|
730
|
+
# Secondary in-cluster stale-heartbeat detector: logs WARNING when the heartbeat
|
|
731
|
+
# is older than TRUEPPM_BEAT_STALE_SECONDS. The /health/beat/ endpoint is the
|
|
732
|
+
# primary (external) detector; this serves adopters with no external monitoring.
|
|
733
|
+
"beat-check-stale-heartbeat": {
|
|
734
|
+
"task": "beat.check_stale_heartbeat",
|
|
735
|
+
"schedule": 60.0,
|
|
736
|
+
},
|
|
737
|
+
# Workflow step-outbox drain: re-dispatch stranded WorkflowOutboxRow rows
|
|
738
|
+
# every 30 s and recover rows orphaned by a dead worker (ADR-0080 §D).
|
|
739
|
+
"drain-workflow-outbox": {
|
|
740
|
+
"task": "workflows.outbox_drain",
|
|
741
|
+
"schedule": 30.0,
|
|
742
|
+
},
|
|
743
|
+
# Workflow sleep-timer drain: fire due WorkflowTimer rows and wake their
|
|
744
|
+
# sleeping workflows. 60 s cadence — sleep durations are minute-grained.
|
|
745
|
+
"drain-workflow-timers": {
|
|
746
|
+
"task": "workflows.timer_drain",
|
|
747
|
+
"schedule": 60.0,
|
|
748
|
+
},
|
|
749
|
+
# Nightly cleanup: terminal workflow outbox rows >7 days and history past
|
|
750
|
+
# the configurable retention window.
|
|
751
|
+
"purge-workflow-records": {
|
|
752
|
+
"task": "workflows.purge_old_records",
|
|
753
|
+
# 04:00 UTC — after the other nightly purge jobs.
|
|
754
|
+
"schedule": crontab(hour=4, minute=0),
|
|
755
|
+
},
|
|
756
|
+
# Hourly cleanup: deletes stored Idempotency-Key rows older than
|
|
757
|
+
# IDEMPOTENCY_RETENTION_HOURS. Hourly (not nightly) so the 24h contract holds —
|
|
758
|
+
# a nightly job would let rows live up to ~48h (ADR-0170).
|
|
759
|
+
"idempotency-keys-purge-hourly": {
|
|
760
|
+
"task": "idempotency.purge_old_keys",
|
|
761
|
+
"schedule": crontab(minute=5),
|
|
762
|
+
},
|
|
763
|
+
# Workspace invite email outbox drain: send queued invite emails every 30 s.
|
|
764
|
+
# create_invite also nudges it on commit, so this tick is the durability
|
|
765
|
+
# backstop, not the latency path (ADR-0087 §Amendment #4191).
|
|
766
|
+
"drain-invite-emails": {
|
|
767
|
+
"task": "workspace.drain_invite_emails",
|
|
768
|
+
"schedule": 30.0,
|
|
769
|
+
},
|
|
770
|
+
# Nightly cleanup: expire overdue pending invites and delete terminal invites
|
|
771
|
+
# older than INVITE_RETENTION_DAYS (ADR-0087 §Durable Execution item 6).
|
|
772
|
+
"purge-stale-invites": {
|
|
773
|
+
"task": "workspace.purge_stale_invites",
|
|
774
|
+
# 04:15 UTC — after the other nightly purge jobs.
|
|
775
|
+
"schedule": crontab(hour=4, minute=15),
|
|
776
|
+
},
|
|
777
|
+
# Re-dispatch workspace export jobs orphaned by a broker outage at on_commit
|
|
778
|
+
# (ADR-0174 §Durable Execution item 2; 5-min orphan window inside the task).
|
|
779
|
+
"drain-workspace-exports": {
|
|
780
|
+
"task": "workspace.drain_workspace_exports",
|
|
781
|
+
"schedule": 30.0,
|
|
782
|
+
},
|
|
783
|
+
# Nightly: delete export jobs past their download-link expiry and their files
|
|
784
|
+
# (ADR-0174 §Durable Execution item 6).
|
|
785
|
+
"purge-expired-exports": {
|
|
786
|
+
"task": "workspace.purge_expired_exports",
|
|
787
|
+
# 04:20 UTC — after purge-stale-invites.
|
|
788
|
+
"schedule": crontab(hour=4, minute=20),
|
|
789
|
+
},
|
|
790
|
+
# Re-dispatch project export bundle jobs orphaned by a broker outage at on_commit
|
|
791
|
+
# (ADR-0219 §Durable Execution item 2; 5-min orphan window inside the task).
|
|
792
|
+
"drain-project-exports": {
|
|
793
|
+
"task": "projects.drain_project_exports",
|
|
794
|
+
"schedule": 30.0,
|
|
795
|
+
},
|
|
796
|
+
# Nightly: delete project export jobs past their download-link expiry and their
|
|
797
|
+
# files (ADR-0219 §Durable Execution item 6; shares TRUEPPM_EXPORT_RETENTION_DAYS).
|
|
798
|
+
"purge-expired-project-exports": {
|
|
799
|
+
"task": "projects.purge_expired_project_exports",
|
|
800
|
+
# 04:25 UTC — after the workspace export purge.
|
|
801
|
+
"schedule": crontab(hour=4, minute=25),
|
|
802
|
+
},
|
|
803
|
+
# Re-dispatch program export bundle jobs orphaned by a broker outage at on_commit
|
|
804
|
+
# (ADR-0219 §Durable Execution item 2, program grain #1958).
|
|
805
|
+
"drain-program-exports": {
|
|
806
|
+
"task": "projects.drain_program_exports",
|
|
807
|
+
"schedule": 30.0,
|
|
808
|
+
},
|
|
809
|
+
# Nightly: delete program export jobs past their download-link expiry and their
|
|
810
|
+
# files (ADR-0219 §Durable Execution item 6; shares TRUEPPM_EXPORT_RETENTION_DAYS).
|
|
811
|
+
"purge-expired-program-exports": {
|
|
812
|
+
"task": "projects.purge_expired_program_exports",
|
|
813
|
+
# 04:30 UTC — after the project export purge.
|
|
814
|
+
"schedule": crontab(hour=4, minute=30),
|
|
815
|
+
},
|
|
816
|
+
# Re-dispatch program seed IMPORT jobs orphaned by a broker outage at on_commit
|
|
817
|
+
# (ADR-0726 §Durable Execution item 2). Deliberately a separate drain from the
|
|
818
|
+
# export one: same shape, different table and different terminal semantics.
|
|
819
|
+
"drain-program-imports": {
|
|
820
|
+
"task": "projects.drain_program_imports",
|
|
821
|
+
"schedule": 30.0,
|
|
822
|
+
},
|
|
823
|
+
# Nightly: delete terminal program seed import jobs past retention and their
|
|
824
|
+
# stored payloads (ADR-0726 §Durable Execution item 6; shares
|
|
825
|
+
# TRUEPPM_IMPORT_RETENTION_DAYS with the MS Project import outbox).
|
|
826
|
+
"purge-expired-program-imports": {
|
|
827
|
+
"task": "projects.purge_expired_program_imports",
|
|
828
|
+
# 04:35 UTC — after the program export purge.
|
|
829
|
+
"schedule": crontab(hour=4, minute=35),
|
|
830
|
+
},
|
|
831
|
+
# Nightly: delete paste-many/cascade ⌘Z undo ledger rows past
|
|
832
|
+
# TRUEPPM_BATCH_OPERATION_RETENTION_DAYS, undone or not (ADR-0810, #2756).
|
|
833
|
+
"purge-expired-batch-operations": {
|
|
834
|
+
"task": "projects.purge_expired_batch_operations",
|
|
835
|
+
# 04:40 UTC — after the program import purge.
|
|
836
|
+
"schedule": crontab(hour=4, minute=40),
|
|
837
|
+
},
|
|
838
|
+
# Lazily materialize upcoming recurring-task occurrences within the
|
|
839
|
+
# TRUEPPM_RECURRENCE_HORIZON_DAYS look-ahead. Hourly: occurrences are date-grained,
|
|
840
|
+
# and a missed tick self-heals on the next one (idempotent). See ADR-0090 / #736.
|
|
841
|
+
"generate-recurring-occurrences": {
|
|
842
|
+
"task": "projects.generate_recurring_occurrences",
|
|
843
|
+
"schedule": crontab(minute=0),
|
|
844
|
+
},
|
|
845
|
+
# Nightly: flush expired OutstandingToken/BlacklistedToken rows so the JWT
|
|
846
|
+
# blacklist tables stay bounded to roughly the active-session window (#910).
|
|
847
|
+
# No-ops when the token_blacklist app is not installed.
|
|
848
|
+
"flush-expired-blacklisted-tokens": {
|
|
849
|
+
"task": "access.flush_expired_blacklisted_tokens",
|
|
850
|
+
# 04:30 UTC — after the other nightly purge jobs.
|
|
851
|
+
"schedule": crontab(hour=4, minute=30),
|
|
852
|
+
},
|
|
853
|
+
# Nightly: hard-delete per-row soft-deleted tombstones (is_deleted=True) from
|
|
854
|
+
# live projects older than TRUEPPM_TOMBSTONE_RETENTION_DAYS (default 90 days).
|
|
855
|
+
# Tombstones are kept so mobile sync clients receive deletion signals; once the
|
|
856
|
+
# retention window passes there is no further sync value in the row (#1321).
|
|
857
|
+
"reap-domain-tombstones-nightly": {
|
|
858
|
+
"task": "sync.reap_domain_tombstones",
|
|
859
|
+
# 03:30 UTC — between the sprint-close and workflow purge windows.
|
|
860
|
+
"schedule": crontab(hour=3, minute=30),
|
|
861
|
+
},
|
|
862
|
+
# External task sync (ADR-0097 §4, #1419). Outbox drain for user-scoped
|
|
863
|
+
# read-only Jira pulls: dispatches PENDING ExternalSyncRequest rows and
|
|
864
|
+
# recovers orphaned DISPATCHED ones. 300 s cadence — a personal read-only
|
|
865
|
+
# pull is not latency-critical (the POST .../sync/ trigger dispatches
|
|
866
|
+
# immediately via on_commit; this is the broker-outage backstop).
|
|
867
|
+
"drain-external-sync": {
|
|
868
|
+
"task": "integrations.drain_external_sync",
|
|
869
|
+
"schedule": 300.0,
|
|
870
|
+
},
|
|
871
|
+
# Opt-in low-frequency poll (ADR-0097 §4). Default-off per connection
|
|
872
|
+
# (config["poll_enabled"], set from the Connected Accounts toggle); fans out
|
|
873
|
+
# zero pulls until a user turns it on. 15 minutes is also the per-connection
|
|
874
|
+
# rate ceiling once one does: the outbox's one-PENDING-row-per-(user, source)
|
|
875
|
+
# constraint coalesces, so an opted-in connection can never exceed one pull
|
|
876
|
+
# per tick no matter how many sources or ticks are in flight.
|
|
877
|
+
"poll-external-sources": {
|
|
878
|
+
"task": "integrations.poll_external_sources",
|
|
879
|
+
"schedule": crontab(minute="*/15"),
|
|
880
|
+
},
|
|
881
|
+
# Nightly: hard-delete terminal (done/dead) ExternalSyncRequest outbox rows
|
|
882
|
+
# older than 7 days and long-stale ExternalWorkItem cache rows (ADR-0097
|
|
883
|
+
# §Durable Execution #6).
|
|
884
|
+
"purge-external-sync-nightly": {
|
|
885
|
+
"task": "integrations.purge_external_sync",
|
|
886
|
+
# 03:45 UTC — after the domain-tombstone reap.
|
|
887
|
+
"schedule": crontab(hour=3, minute=45),
|
|
888
|
+
},
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
# ---------------------------------------------------------------------------
|
|
892
|
+
# Auth / Passwords
|
|
893
|
+
# ---------------------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
896
|
+
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
|
897
|
+
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
|
898
|
+
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
899
|
+
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
|
900
|
+
]
|
|
901
|
+
|
|
902
|
+
# Self-service password reset (ADR-0209). Django's stateless
|
|
903
|
+
# ``PasswordResetTokenGenerator`` (default_token_generator) rejects tokens older
|
|
904
|
+
# than this window in ``check_token``. 30 minutes is the value surfaced in the
|
|
905
|
+
# reset-email copy and on the "sent" / "expired" screens — short enough to bound a
|
|
906
|
+
# leaked-link window, long enough for a user to act on the email.
|
|
907
|
+
PASSWORD_RESET_TIMEOUT = env.int("TRUEPPM_PASSWORD_RESET_TIMEOUT", default=1800)
|
|
908
|
+
|
|
909
|
+
# ---------------------------------------------------------------------------
|
|
910
|
+
# SimpleJWT token lifetimes + httpOnly refresh-cookie migration (#897)
|
|
911
|
+
# ---------------------------------------------------------------------------
|
|
912
|
+
|
|
913
|
+
# Short access-token TTL bounds the blast radius of a leaked in-memory access
|
|
914
|
+
# token; the SPA transparently refreshes via the httpOnly cookie on 401. A
|
|
915
|
+
# 7-day refresh TTL is the session length — long enough to avoid daily re-login,
|
|
916
|
+
# short enough that a stolen refresh token (cookie) self-expires within a week.
|
|
917
|
+
# Rotation + (optional) blacklist further limit replay of a leaked refresh token.
|
|
918
|
+
SIMPLE_JWT = {
|
|
919
|
+
# Sign tokens with the dedicated JWT key (#2247). Defaults to SECRET_KEY, so
|
|
920
|
+
# existing deploys are unaffected; set JWT_SIGNING_KEY to separate the two.
|
|
921
|
+
"SIGNING_KEY": JWT_SIGNING_KEY,
|
|
922
|
+
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
|
|
923
|
+
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
|
924
|
+
"ROTATE_REFRESH_TOKENS": True,
|
|
925
|
+
# Blacklisting requires the token_blacklist app + its migrations, now shipped
|
|
926
|
+
# in INSTALLED_APPS by default (#910), so rotation actually revokes the prior
|
|
927
|
+
# refresh token. The refresh/logout views still tolerate the app's absence so
|
|
928
|
+
# an operator who removes it (lean OSS deploy) degrades gracefully to
|
|
929
|
+
# TTL-only expiry rather than erroring.
|
|
930
|
+
"BLACKLIST_AFTER_ROTATION": True,
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
# httpOnly refresh-cookie attributes (#897). The refresh token rides in this
|
|
934
|
+
# cookie instead of the JSON body so JavaScript (and therefore XSS) cannot read
|
|
935
|
+
# it. Attributes are env-overridable so a non-HTTPS local dev server can still
|
|
936
|
+
# complete the flow.
|
|
937
|
+
#
|
|
938
|
+
# SameSite=Strict is safe here: the refresh request is a same-origin XHR from the
|
|
939
|
+
# SPA, which Strict permits, while it blocks the cookie on any cross-site request
|
|
940
|
+
# — the CSRF mitigation for this path (see core/auth_views.py).
|
|
941
|
+
#
|
|
942
|
+
# Env vars standardized on the TRUEPPM_ prefix pre-0.3 (#1325, #1355); the legacy
|
|
943
|
+
# bare names remain fallbacks so existing deploys keep working.
|
|
944
|
+
AUTH_REFRESH_COOKIE_NAME = env(
|
|
945
|
+
"TRUEPPM_AUTH_REFRESH_COOKIE_NAME",
|
|
946
|
+
default=env("AUTH_REFRESH_COOKIE_NAME", default="trueppm_refresh"),
|
|
947
|
+
)
|
|
948
|
+
# Path-scoped so the cookie is never sent on ordinary API calls. It must cover
|
|
949
|
+
# *every* endpoint that needs to read it, which is both the refresh endpoint and
|
|
950
|
+
# ``/api/v1/auth/logout/`` — scoping it to the refresh endpoint alone (the
|
|
951
|
+
# pre-#2999 default) meant the browser never sent it to logout, so
|
|
952
|
+
# ``CookieTokenLogoutView`` read ``None`` and revoked nothing (RFC 6265 §5.1.4).
|
|
953
|
+
# ``/api/v1/auth/`` is the narrowest prefix covering both.
|
|
954
|
+
AUTH_REFRESH_COOKIE_PATH = env(
|
|
955
|
+
"TRUEPPM_AUTH_REFRESH_COOKIE_PATH",
|
|
956
|
+
default=env("AUTH_REFRESH_COOKIE_PATH", default="/api/v1/auth/"),
|
|
957
|
+
)
|
|
958
|
+
#
|
|
959
|
+
# The resolved value is validated, not passed through verbatim (#3556): refresh
|
|
960
|
+
# and logout are authenticated by this cookie alone (no CSRF token, no
|
|
961
|
+
# double-submit), so SameSite is the only unconditional CSRF control on them.
|
|
962
|
+
# ``None`` requires the acknowledgment sentinel below or it is refused and
|
|
963
|
+
# forced back to ``Strict``; ``Lax`` is accepted but logs a WARNING naming what
|
|
964
|
+
# it gives up. See ``core/refresh_cookie_policy.py`` for the full resolution.
|
|
965
|
+
_refresh_cookie_samesite_resolution = resolve_refresh_cookie_samesite(
|
|
966
|
+
requested=env(
|
|
967
|
+
"TRUEPPM_AUTH_REFRESH_COOKIE_SAMESITE",
|
|
968
|
+
default=env("AUTH_REFRESH_COOKIE_SAMESITE", default="Strict"),
|
|
969
|
+
),
|
|
970
|
+
ack=env("TRUEPPM_AUTH_REFRESH_COOKIE_SAMESITE_NONE_ACK", default=""),
|
|
971
|
+
)
|
|
972
|
+
AUTH_REFRESH_COOKIE_SAMESITE = _refresh_cookie_samesite_resolution.value
|
|
973
|
+
if _refresh_cookie_samesite_resolution.warning:
|
|
974
|
+
_settings_log.warning(_refresh_cookie_samesite_resolution.warning)
|
|
975
|
+
if _refresh_cookie_samesite_resolution.critical:
|
|
976
|
+
_settings_log.critical(_refresh_cookie_samesite_resolution.critical)
|
|
977
|
+
# Default Secure=True; dev settings flip this to False for plain-HTTP localhost.
|
|
978
|
+
AUTH_REFRESH_COOKIE_SECURE = env.bool(
|
|
979
|
+
"TRUEPPM_AUTH_REFRESH_COOKIE_SECURE",
|
|
980
|
+
default=env.bool("AUTH_REFRESH_COOKIE_SECURE", default=True),
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# Refresh-session lifetimes for the "remember me" choice (#2246, ADR-0544).
|
|
984
|
+
# The login checkbox now drives which of these two the refresh token gets:
|
|
985
|
+
# - checked → REFRESH_TOKEN_REMEMBER_LIFETIME: a long-lived, *persistent* cookie
|
|
986
|
+
# (survives browser close) for a trusted device.
|
|
987
|
+
# - unchecked (default) → REFRESH_TOKEN_SESSION_LIFETIME: a *session* cookie that
|
|
988
|
+
# dies on browser close, with a short sliding idle bound for a machine left open.
|
|
989
|
+
# SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"] (7d) stays as the LEGACY default: it is used
|
|
990
|
+
# only when rotating a pre-#2246 token that carries no "remember" claim, so existing
|
|
991
|
+
# sessions are never forcibly logged out (they adopt the new model at next login).
|
|
992
|
+
REFRESH_TOKEN_REMEMBER_LIFETIME = timedelta(
|
|
993
|
+
days=env.int("TRUEPPM_REFRESH_TOKEN_REMEMBER_DAYS", default=30)
|
|
994
|
+
)
|
|
995
|
+
REFRESH_TOKEN_SESSION_LIFETIME = timedelta(
|
|
996
|
+
hours=env.int("TRUEPPM_REFRESH_TOKEN_SESSION_HOURS", default=12)
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
# ---------------------------------------------------------------------------
|
|
1000
|
+
# Django session + CSRF cookie hardening (#2248)
|
|
1001
|
+
# ---------------------------------------------------------------------------
|
|
1002
|
+
# These cookies are effectively exercised only by the Django admin site — the SPA
|
|
1003
|
+
# authenticates with the JWT bearer/refresh-cookie scheme above and the DRF
|
|
1004
|
+
# browsable API is disabled (JSON-only renderer). Django's implicit defaults for
|
|
1005
|
+
# these three already match what we want, but pin them explicitly so the posture
|
|
1006
|
+
# is auditable and cannot silently change across a Django upgrade. `*_SECURE` is
|
|
1007
|
+
# intentionally NOT pinned here: it stays False in dev (plain-HTTP localhost) and
|
|
1008
|
+
# prod.py sets both to True.
|
|
1009
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
1010
|
+
SESSION_COOKIE_SAMESITE = "Lax"
|
|
1011
|
+
CSRF_COOKIE_SAMESITE = "Lax"
|
|
1012
|
+
# CSRF_COOKIE_HTTPONLY stays False (Django default): the token must be readable by
|
|
1013
|
+
# JavaScript to be echoed in the X-CSRFToken header. This is the standard,
|
|
1014
|
+
# correct posture and is not a leak — the CSRF cookie is not a credential.
|
|
1015
|
+
|
|
1016
|
+
# ---------------------------------------------------------------------------
|
|
1017
|
+
# Content-Security-Policy (#897)
|
|
1018
|
+
# ---------------------------------------------------------------------------
|
|
1019
|
+
|
|
1020
|
+
# Strict CSP, assembled into a header by core.csp.ContentSecurityPolicyMiddleware.
|
|
1021
|
+
# script-src 'self' (no inline / no hash) is possible because the theme-init
|
|
1022
|
+
# script was moved to an external file (web/public/theme-init.js). connect-src
|
|
1023
|
+
# carries wss: for the WebSocket collaboration channel; the host is overridable
|
|
1024
|
+
# per deploy. style-src keeps 'unsafe-inline' because the SPA emits inline style
|
|
1025
|
+
# attributes (Tailwind/JS-driven) — tightening this is tracked separately.
|
|
1026
|
+
_CSP_SELF = "'self'"
|
|
1027
|
+
CSP_CONNECT_SRC = env.list("CSP_CONNECT_SRC", default=[_CSP_SELF, "wss:"])
|
|
1028
|
+
CSP_DIRECTIVES: dict[str, list[str]] = {
|
|
1029
|
+
"default-src": [_CSP_SELF],
|
|
1030
|
+
"script-src": [_CSP_SELF],
|
|
1031
|
+
"style-src": [_CSP_SELF, "'unsafe-inline'", "fonts.googleapis.com"],
|
|
1032
|
+
"font-src": ["fonts.gstatic.com"],
|
|
1033
|
+
"img-src": [_CSP_SELF, "data:"],
|
|
1034
|
+
"connect-src": CSP_CONNECT_SRC,
|
|
1035
|
+
"frame-ancestors": ["'none'"],
|
|
1036
|
+
"base-uri": ["'none'"],
|
|
1037
|
+
"form-action": [_CSP_SELF],
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
# ---------------------------------------------------------------------------
|
|
1041
|
+
# Internationalization
|
|
1042
|
+
# ---------------------------------------------------------------------------
|
|
1043
|
+
|
|
1044
|
+
LANGUAGE_CODE = "en-us"
|
|
1045
|
+
TIME_ZONE = "UTC"
|
|
1046
|
+
USE_I18N = True
|
|
1047
|
+
USE_TZ = True
|
|
1048
|
+
|
|
1049
|
+
# ---------------------------------------------------------------------------
|
|
1050
|
+
# Static files
|
|
1051
|
+
# ---------------------------------------------------------------------------
|
|
1052
|
+
|
|
1053
|
+
STATIC_URL = "static/"
|
|
1054
|
+
# Allow an explicit env override so the app can write static files to a
|
|
1055
|
+
# writable path when installed as a pip package (where BASE_DIR resolves
|
|
1056
|
+
# inside the read-only venv).
|
|
1057
|
+
STATIC_ROOT = Path(env("STATIC_ROOT", default=str(BASE_DIR / "staticfiles")))
|
|
1058
|
+
|
|
1059
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _package_version() -> str:
|
|
1063
|
+
"""The installed distribution version, or a dev placeholder off-package.
|
|
1064
|
+
|
|
1065
|
+
Read from installed metadata rather than hard-coded so it cannot drift from
|
|
1066
|
+
`pyproject.toml`; a source checkout that was never `pip install -e`'d has no
|
|
1067
|
+
metadata, and "dev" is a more honest answer there than a stale literal.
|
|
1068
|
+
|
|
1069
|
+
Normalized to PEP 440 here because the metadata spelling belongs to the build
|
|
1070
|
+
backend: hatchling <=1.32.0 wrote "0.4.0b1", 1.32.1 writes pyproject's
|
|
1071
|
+
"0.4.0-beta.1" verbatim, and `pip install -e` builds with whichever is newest.
|
|
1072
|
+
Without this the published schema version flips on an upstream release.
|
|
1073
|
+
"""
|
|
1074
|
+
try:
|
|
1075
|
+
from importlib.metadata import version
|
|
1076
|
+
|
|
1077
|
+
from packaging.version import Version
|
|
1078
|
+
|
|
1079
|
+
return str(Version(version("trueppm-api")))
|
|
1080
|
+
except Exception:
|
|
1081
|
+
return "dev"
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
# ---------------------------------------------------------------------------
|
|
1085
|
+
# Edition detection
|
|
1086
|
+
# Set TRUEPPM_EDITION=enterprise in the enterprise Helm chart to activate
|
|
1087
|
+
# enterprise landing logic in the frontend. The /api/v1/edition/ endpoint
|
|
1088
|
+
# reads this setting and returns it to unauthenticated clients so the React
|
|
1089
|
+
# shell can make the post-login redirect decision without importing enterprise
|
|
1090
|
+
# code (ADR-0029).
|
|
1091
|
+
# ---------------------------------------------------------------------------
|
|
1092
|
+
|
|
1093
|
+
TRUEPPM_EDITION: str = env("TRUEPPM_EDITION", default="community")
|
|
1094
|
+
|
|
1095
|
+
# Build identity surfaced on /api/v1/edition/ so a bug report can name the exact
|
|
1096
|
+
# build it came from (#2392). The version tracks the package version; the SHA is
|
|
1097
|
+
# injected by the image build (Helm sets TRUEPPM_BUILD_SHA from the CI commit)
|
|
1098
|
+
# and is empty on a source checkout, where the version alone is enough.
|
|
1099
|
+
TRUEPPM_VERSION: str = env("TRUEPPM_VERSION", default=_package_version())
|
|
1100
|
+
TRUEPPM_BUILD_SHA: str = env("TRUEPPM_BUILD_SHA", default="")
|
|
1101
|
+
|
|
1102
|
+
# ---------------------------------------------------------------------------
|
|
1103
|
+
# OpenTelemetry observability foundation (ADR-0223, #708)
|
|
1104
|
+
#
|
|
1105
|
+
# OPT-IN, no default endpoint. Telemetry is enabled only when an OTLP endpoint
|
|
1106
|
+
# is configured AND the master switch is on; with no endpoint the provider is a
|
|
1107
|
+
# strict no-op (no SDK objects, no export threads, zero per-request cost). We
|
|
1108
|
+
# read the well-known upstream OTEL_* env vars so operators reuse the names they
|
|
1109
|
+
# already know from every other OTel-emitting service in their cluster, and add
|
|
1110
|
+
# a few TRUEPPM_OTEL_* switches for behaviour the standard vars don't express.
|
|
1111
|
+
# The bootstrap in trueppm_api.apps.observability.otel consumes these settings.
|
|
1112
|
+
# ---------------------------------------------------------------------------
|
|
1113
|
+
|
|
1114
|
+
# The gate: an empty endpoint means telemetry is off. No default is provided on
|
|
1115
|
+
# purpose — export must be a deliberate operator choice, never a silent egress.
|
|
1116
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: str = env("OTEL_EXPORTER_OTLP_ENDPOINT", default="")
|
|
1117
|
+
# "grpc" (default, port 4317) or "http/protobuf" (port 4318).
|
|
1118
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: str = env("OTEL_EXPORTER_OTLP_PROTOCOL", default="grpc")
|
|
1119
|
+
# Passed through verbatim to the exporter, e.g. "authorization=Bearer <token>"
|
|
1120
|
+
# for a SaaS OTLP backend. Comma-separated key=value pairs per the OTel spec.
|
|
1121
|
+
OTEL_EXPORTER_OTLP_HEADERS: str = env("OTEL_EXPORTER_OTLP_HEADERS", default="")
|
|
1122
|
+
# Resource service.name. Defaults to the service, not the pod.
|
|
1123
|
+
OTEL_SERVICE_NAME: str = env("OTEL_SERVICE_NAME", default="trueppm-api")
|
|
1124
|
+
# Master kill switch, AND-ed with endpoint presence. Lets an operator leave the
|
|
1125
|
+
# endpoint configured (e.g. in a shared ConfigMap) while disabling export here.
|
|
1126
|
+
TRUEPPM_OTEL_ENABLED: bool = env.bool("TRUEPPM_OTEL_ENABLED", default=True)
|
|
1127
|
+
# Independent signal toggles (only consulted when telemetry is enabled overall).
|
|
1128
|
+
TRUEPPM_OTEL_TRACES_ENABLED: bool = env.bool("TRUEPPM_OTEL_TRACES_ENABLED", default=True)
|
|
1129
|
+
TRUEPPM_OTEL_METRICS_ENABLED: bool = env.bool("TRUEPPM_OTEL_METRICS_ENABLED", default=True)
|
|
1130
|
+
# Trace sampler selection, using the standard upstream env var names. Because the
|
|
1131
|
+
# TracerProvider is built by hand (not via the SDK auto-config path), these would
|
|
1132
|
+
# otherwise be silently ignored; the bootstrap reads them to build a Sampler so
|
|
1133
|
+
# operators can throttle DB-span-dominated trace volume without a code change.
|
|
1134
|
+
# Recognized samplers: always_on (default), always_off, traceidratio,
|
|
1135
|
+
# parentbased_always_on, parentbased_always_off, parentbased_traceidratio.
|
|
1136
|
+
# The arg carries the ratio (0.0–1.0) for the ratio-based samplers.
|
|
1137
|
+
OTEL_TRACES_SAMPLER: str = env("OTEL_TRACES_SAMPLER", default="parentbased_always_on")
|
|
1138
|
+
OTEL_TRACES_SAMPLER_ARG: str = env("OTEL_TRACES_SAMPLER_ARG", default="")
|
|
1139
|
+
# Wall-clock bound (seconds) for the admin telemetry test-export probe (#2110):
|
|
1140
|
+
# caps the one-off canary export and the TCP reachability probe so a dead collector
|
|
1141
|
+
# can never hang the request thread. Well under the gunicorn worker timeout.
|
|
1142
|
+
TELEMETRY_TEST_EXPORT_TIMEOUT_SECONDS: int = env.int(
|
|
1143
|
+
"TRUEPPM_TELEMETRY_TEST_EXPORT_TIMEOUT_SECONDS", default=5
|
|
1144
|
+
)
|
|
1145
|
+
# Live export-health recording (ADR-0601, #2109). When on (default), each pod that
|
|
1146
|
+
# runs an OSS OTLP exporter wraps it in a best-effort recorder that publishes
|
|
1147
|
+
# per-signal success/error/count into Valkey /2, so the System Health Telemetry
|
|
1148
|
+
# card can show a cluster-wide live strip ("last export 8s ago · 1,204 spans /
|
|
1149
|
+
# 60s") aggregated across the worker/beat pods that actually export. Purely
|
|
1150
|
+
# additive and non-fatal: a recorder or Valkey failure never affects export. Set
|
|
1151
|
+
# false to skip installing the wrappers entirely (the card falls back to the
|
|
1152
|
+
# config-only posture). Requires the Valkey /2 DB to run maxmemory-policy
|
|
1153
|
+
# noeviction — the same requirement the throttle counters already impose.
|
|
1154
|
+
TRUEPPM_OTEL_EXPORT_HEALTH_ENABLED: bool = env.bool(
|
|
1155
|
+
"TRUEPPM_OTEL_EXPORT_HEALTH_ENABLED", default=True
|
|
1156
|
+
)
|
|
1157
|
+
# Optional stable per-pod identity for the export-health record. On Kubernetes the
|
|
1158
|
+
# hostname already IS the pod name, so the default (socket.gethostname():pid) is a
|
|
1159
|
+
# no-config stable identity; the Helm chart injects this via the downward API
|
|
1160
|
+
# (metadata.name) for a stable, human-readable label.
|
|
1161
|
+
TRUEPPM_POD_NAME: str = env("TRUEPPM_POD_NAME", default="")
|
|
1162
|
+
# Live export-health thresholds, in seconds. Defaults suit the standard 60s metric
|
|
1163
|
+
# export interval; tune only for unusual cadences. INVARIANT: STALENESS must exceed
|
|
1164
|
+
# HEALTHY_WITHIN — a record must stay live long enough to be seen as idle/stalled
|
|
1165
|
+
# before it expires, or an overdue signal is masked as a premature `never` (the app
|
|
1166
|
+
# logs a warning if this ordering is violated; it does not clamp).
|
|
1167
|
+
# STALENESS: per-pod record TTL / how long a pod counts as live after its last
|
|
1168
|
+
# export attempt. Beyond it (no export at all) the signal reads `never`.
|
|
1169
|
+
# HEALTHY_WITHIN: a success within this window ⇒ healthy; beyond it (still live) ⇒
|
|
1170
|
+
# stalled (metrics) / idle (traces).
|
|
1171
|
+
# WINDOW: the rolling window the exported-item counts cover; the card labels the
|
|
1172
|
+
# strip from this value ("last 60s"), so changing it re-labels the UI.
|
|
1173
|
+
TRUEPPM_OTEL_EXPORT_HEALTH_STALENESS_SECONDS: int = env.int(
|
|
1174
|
+
"TRUEPPM_OTEL_EXPORT_HEALTH_STALENESS_SECONDS", default=600
|
|
1175
|
+
)
|
|
1176
|
+
TRUEPPM_OTEL_EXPORT_HEALTH_HEALTHY_WITHIN_SECONDS: int = env.int(
|
|
1177
|
+
"TRUEPPM_OTEL_EXPORT_HEALTH_HEALTHY_WITHIN_SECONDS", default=150
|
|
1178
|
+
)
|
|
1179
|
+
TRUEPPM_OTEL_EXPORT_HEALTH_WINDOW_SECONDS: int = env.int(
|
|
1180
|
+
"TRUEPPM_OTEL_EXPORT_HEALTH_WINDOW_SECONDS", default=60
|
|
1181
|
+
)
|
|
1182
|
+
# Actor attributes on the HTTP server span (#2880). When on (default), each request
|
|
1183
|
+
# span carries trueppm.user.id (the acting account's opaque UUID) and
|
|
1184
|
+
# trueppm.user.role (the symbolic project role the request was authorized under), so
|
|
1185
|
+
# a trace can be attributed to a who and a permission level without opening the
|
|
1186
|
+
# payload. NOTHING else about the person is ever exported under trueppm.user.* — no
|
|
1187
|
+
# email, username, display name, or client IP. Set false where a per-user identifier
|
|
1188
|
+
# must not leave the instance even to a self-hosted collector; resource identity
|
|
1189
|
+
# (trueppm.project.id / .program.id / .task.id) is unaffected either way.
|
|
1190
|
+
TRUEPPM_OTEL_ACTOR_ATTRIBUTES_ENABLED: bool = env.bool(
|
|
1191
|
+
"TRUEPPM_OTEL_ACTOR_ATTRIBUTES_ENABLED", default=True
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
# ---------------------------------------------------------------------------
|
|
1195
|
+
# Structured logging + trace correlation (ADR-0223, #1899)
|
|
1196
|
+
#
|
|
1197
|
+
# The base config emits human-readable console lines that still carry the OTel
|
|
1198
|
+
# trace_id/span_id/request_id inline, so a developer sees the same correlation
|
|
1199
|
+
# context locally. prod.py flips this to single-line JSON (TRUEPPM_LOG_JSON) for a
|
|
1200
|
+
# collector to index. The level honours DJANGO_LOG_LEVEL (default INFO) — the same
|
|
1201
|
+
# well-known name operators set on every other Django service. Trace correlation
|
|
1202
|
+
# (not OTLP log export, which is #711) means a log line can be pivoted straight to
|
|
1203
|
+
# the trace exported for the same request.
|
|
1204
|
+
# ---------------------------------------------------------------------------
|
|
1205
|
+
|
|
1206
|
+
DJANGO_LOG_LEVEL: str = env("DJANGO_LOG_LEVEL", default="INFO")
|
|
1207
|
+
# JSON is opt-in in the base config so dev stays readable; prod.py sets this True.
|
|
1208
|
+
TRUEPPM_LOG_JSON: bool = env.bool("TRUEPPM_LOG_JSON", default=False)
|
|
1209
|
+
|
|
1210
|
+
from trueppm_api.apps.observability.logging import ( # noqa: E402
|
|
1211
|
+
build_logging_config,
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
LOGGING = build_logging_config(level=DJANGO_LOG_LEVEL, json_output=TRUEPPM_LOG_JSON)
|
|
1215
|
+
|
|
1216
|
+
# ---------------------------------------------------------------------------
|
|
1217
|
+
# Integration credential encryption key (ADR-0049 §3)
|
|
1218
|
+
#
|
|
1219
|
+
# Fernet key used to encrypt the PAT secrets stored in
|
|
1220
|
+
# trueppm_api.apps.integrations.IntegrationCredential.secret_ciphertext.
|
|
1221
|
+
# Production deployments source this from a Kubernetes Secret via the Helm
|
|
1222
|
+
# chart; the empty default fails loud (ImproperlyConfigured) the first time
|
|
1223
|
+
# the helper is called, which is what we want — better than silently storing
|
|
1224
|
+
# unencrypted PATs in dev because someone forgot to set the env var.
|
|
1225
|
+
#
|
|
1226
|
+
# Generate one with:
|
|
1227
|
+
# python -c "from cryptography.fernet import Fernet; \
|
|
1228
|
+
# print(Fernet.generate_key().decode())"
|
|
1229
|
+
# ---------------------------------------------------------------------------
|
|
1230
|
+
|
|
1231
|
+
INTEGRATION_ENCRYPTION_KEY: str = env(
|
|
1232
|
+
"INTEGRATION_ENCRYPTION_KEY",
|
|
1233
|
+
default="",
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
# Hostnames exempted from the SSRF egress deny-list (apps/integrations/http). An
|
|
1237
|
+
# operator escape hatch for a trusted internal host the operator runs themselves —
|
|
1238
|
+
# chiefly an in-cluster identity provider whose OIDC issuer resolves to a private
|
|
1239
|
+
# ClusterIP (e.g. Keycloak at ``keycloak.sso.svc``), which the deny-list would
|
|
1240
|
+
# otherwise block, making in-cluster SSO impossible. Empty by default keeps the
|
|
1241
|
+
# standard "every resolved address must be globally routable" posture. Exact,
|
|
1242
|
+
# case-insensitive hostname match — no wildcards. Config-only, never
|
|
1243
|
+
# request-derived. NOTE: the allow-list is consulted by *every* egress caller (PAT
|
|
1244
|
+
# verification, git-link refresh, webhooks, SMTP relay, SSO), so an allow-listed
|
|
1245
|
+
# host is reachable through the user-influenceable URL surfaces too — only list a
|
|
1246
|
+
# host at least as trusted as the SSO issuer. See ADR-0590 and the "Running the
|
|
1247
|
+
# identity provider inside your cluster" section of
|
|
1248
|
+
# packages/website/src/content/docs/administration/single-sign-on.md.
|
|
1249
|
+
EGRESS_ALLOWLISTED_HOSTS: list[str] = env.list("TRUEPPM_EGRESS_ALLOWLISTED_HOSTS", default=[])
|
|
1250
|
+
|
|
1251
|
+
# Per-provider base-URL allow-list for user-connected external task sources
|
|
1252
|
+
# (ADR-0097 / ADR-0589). Read at ``apps/integrations/providers.py:85`` and named in
|
|
1253
|
+
# the rejection message an operator sees when their self-hosted Jira/GitLab host is
|
|
1254
|
+
# refused — but bound by NO settings module until #2860, so setting the documented
|
|
1255
|
+
# env var did nothing and the escape hatch the message points at did not exist.
|
|
1256
|
+
#
|
|
1257
|
+
# The allow-list fails CLOSED, so the bug was the inverse of a security hole: an
|
|
1258
|
+
# operator with a legitimately self-hosted instance could not connect it at all, and
|
|
1259
|
+
# the error told them to set a variable that was never read. Documented in four
|
|
1260
|
+
# places (configuration.md, connected-accounts.md, ADR-0589, changelog.d/2270)
|
|
1261
|
+
# throughout.
|
|
1262
|
+
#
|
|
1263
|
+
# Distinct from EGRESS_ALLOWLISTED_HOSTS above, which is the SSRF deny-list escape
|
|
1264
|
+
# hatch consulted by *every* egress caller. This one only widens which base URLs a
|
|
1265
|
+
# user may attach a personal credential to; a host listed here still passes through
|
|
1266
|
+
# the SSRF-guarded http helper on every fetch.
|
|
1267
|
+
INTEGRATION_ALLOWED_HOSTS: list[str] = env.list("TRUEPPM_INTEGRATION_ALLOWED_HOSTS", default=[])
|
|
1268
|
+
|
|
1269
|
+
# Age in seconds past which a connected external-source connection's cache is
|
|
1270
|
+
# "stale" for the My Work on-open refresh (ADR-0097 §4 "on-open refresh-if-
|
|
1271
|
+
# stale"; #1921). Reuses the same order of magnitude as the manual-refresh
|
|
1272
|
+
# cooldown (ADR-0097 §Resolution #5, `services.MANUAL_SYNC_COOLDOWN_SECONDS`)
|
|
1273
|
+
# rather than inventing an unrelated number — a connection synced within this
|
|
1274
|
+
# window is fresh enough that another My Work load should not re-trigger a
|
|
1275
|
+
# pull. Configurable per-operator: a slower Jira instance or a rate-sensitive
|
|
1276
|
+
# token may want a wider floor.
|
|
1277
|
+
TRUEPPM_EXTERNAL_SYNC_ON_OPEN_STALE_SECONDS: int = env.int(
|
|
1278
|
+
"TRUEPPM_EXTERNAL_SYNC_ON_OPEN_STALE_SECONDS", default=300
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
# ---------------------------------------------------------------------------
|
|
1282
|
+
# Monte Carlo simulation caps (OSS tier)
|
|
1283
|
+
# Set to None for unlimited (Enterprise overrides these in enterprise settings).
|
|
1284
|
+
# ---------------------------------------------------------------------------
|
|
1285
|
+
|
|
1286
|
+
MC_SIMULATION_CAP: int | None = 1_000
|
|
1287
|
+
# Raised 500 -> 5000 (#823): the OSS scaling story is "10k tasks", but the MC
|
|
1288
|
+
# endpoint returned HTTP 402 for any project over 500 tasks, so Monte Carlo was
|
|
1289
|
+
# silently unavailable for realistic large projects. The vectorised numpy path
|
|
1290
|
+
# (scheduler/engine.py) is O(runs x tasks x edges) and handles a 5000-task x
|
|
1291
|
+
# 1000-run simulation in a few seconds. Operators on constrained hardware can
|
|
1292
|
+
# lower this; Enterprise overrides it in enterprise settings.
|
|
1293
|
+
MC_TASK_CAP: int | None = 5_000
|
|
1294
|
+
|
|
1295
|
+
# Request-path ceiling on the Monte Carlo lag-delta table (#4129):
|
|
1296
|
+
# (distinct dependency type/lag/calendar-pair keys) x (working-day span) cells, each a
|
|
1297
|
+
# 4-byte int32, so 5M cells is ~20 MB per simulation (the what-if endpoint runs two
|
|
1298
|
+
# in sequence, never concurrently). The scheduler library default is 50M cells
|
|
1299
|
+
# (~200 MB) because a batch caller owns the machine; a synchronous endpoint shares a
|
|
1300
|
+
# pod with concurrent requests and MC_TASK_CAP / MC_SIMULATION_CAP do not bound this
|
|
1301
|
+
# product, so a project that passed every other cap could peak near 630 MB. Above the
|
|
1302
|
+
# cap the run is refused as a 400 naming the lag variety. None = the library default.
|
|
1303
|
+
MC_LAG_DELTA_CELL_CAP: int | None = 5_000_000
|
|
1304
|
+
|
|
1305
|
+
# Request-path ceiling on the PRODUCT runs x n_tasks (#2839). MC_SIMULATION_CAP and
|
|
1306
|
+
# MC_TASK_CAP bound each dimension, but the simulation keeps three (runs, n_tasks)
|
|
1307
|
+
# float64 matrices live at once plus temporaries, so its memory and its (vectorized,
|
|
1308
|
+
# ~linear) wall time follow the product. At 5,000 x 1,000 = 5M cells one simulation
|
|
1309
|
+
# peaked at ~234 MB traced / ~336 MB RSS, on the single default API worker
|
|
1310
|
+
# (processes.api.workers: 1, 2 GiB limit) that also serves every WebSocket, and the
|
|
1311
|
+
# what-if endpoint runs two back to back. 2.5M cells halves that (~117 MB traced,
|
|
1312
|
+
# ~168 MB peak RSS at the measured ~67 B/cell — size limits against RSS),
|
|
1313
|
+
# still runs the full 1,000 iterations up to 2,500 tasks, and gives a project at
|
|
1314
|
+
# MC_TASK_CAP 500 — the percentile resolution of a 500-sample run is 0.2%, well
|
|
1315
|
+
# inside the day granularity the forecast reports. An omitted n_simulations scales
|
|
1316
|
+
# down to fit (the response's `runs` reports it); an explicit one above the
|
|
1317
|
+
# size-derived maximum is refused 400 rather than silently shrunk. Not an edition
|
|
1318
|
+
# gate: it protects the worker, so Enterprise keeps it unless its hardware differs.
|
|
1319
|
+
# None = no product cap.
|
|
1320
|
+
MC_CELL_CAP: int | None = 2_500_000
|
|
1321
|
+
|
|
1322
|
+
# Simulations one API process runs at once (#2839). Sync views run one thread per
|
|
1323
|
+
# request under ASGI, so concurrent runs in one worker are real; MC_CELL_CAP bounds
|
|
1324
|
+
# each, this bounds how many stack. 2 x ~117 MB keeps a full-size burst far inside
|
|
1325
|
+
# the 2 GiB default worker limit. A request waits up to 10 s for a slot, then gets
|
|
1326
|
+
# 503 + Retry-After. Per process, so N workers admit N x this. None = unbounded.
|
|
1327
|
+
MC_MAX_CONCURRENT: int | None = 2
|
|
1328
|
+
|
|
1329
|
+
# Project Monte Carlo run-history retention (ADR-0175, #961): the nightly purge
|
|
1330
|
+
# keeps the newest MC_HISTORY_CAP MonteCarloRun rows per project so a PM can read
|
|
1331
|
+
# finish-date forecast drift over time. None = unlimited (Enterprise overrides it
|
|
1332
|
+
# — unbounded history + cross-program rollup is the portfolio upsell).
|
|
1333
|
+
MC_HISTORY_CAP: int | None = 100
|
|
1334
|
+
|
|
1335
|
+
# Per-run distribution persistence (#1231, ADR-0144). The full
|
|
1336
|
+
# {histogram_buckets, confidence_curve, sensitivity} payload is stored on
|
|
1337
|
+
# MonteCarloRun.distribution so the histogram survives cache expiry. Bounded at
|
|
1338
|
+
# 32 KB: a payload over this is down-sampled (every Nth histogram bucket) before
|
|
1339
|
+
# persist so a pathological high-bucket run cannot bloat the row. The cache copy
|
|
1340
|
+
# is unaffected by the down-sample.
|
|
1341
|
+
MC_DISTRIBUTION_MAX_BYTES: int = 32_768
|
|
1342
|
+
|
|
1343
|
+
# Absolute upper bound on the per-workspace forecast-history retention cap
|
|
1344
|
+
# (ADR-0144). The per-workspace mc_history_retention_cap is clamped to this on
|
|
1345
|
+
# read so an operator (or future Enterprise override) cannot configure an
|
|
1346
|
+
# unbounded per-project history that the nightly purge would never trim.
|
|
1347
|
+
MC_HISTORY_HARD_CAP: int = 500
|
|
1348
|
+
|
|
1349
|
+
# Tiered retention curve for project-grain forecast snapshots (ADR-0154, #388).
|
|
1350
|
+
# The nightly prune keeps every snapshot younger than ``daily_days``, then thins to
|
|
1351
|
+
# one-per-ISO-week up to ``weekly_days``, then one-per-calendar-month beyond that
|
|
1352
|
+
# (kept forever — the cold tail is ~12 rows/project/year). Operators can tune the
|
|
1353
|
+
# two windows; the per-period keeper is always the newest row in that bucket.
|
|
1354
|
+
FORECAST_SNAPSHOT_RETENTION: dict[str, int] = {
|
|
1355
|
+
"daily_days": 90,
|
|
1356
|
+
"weekly_days": 365,
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
# ---------------------------------------------------------------------------
|
|
1360
|
+
# Upload caps (ADR-0075, task attachments)
|
|
1361
|
+
# ---------------------------------------------------------------------------
|
|
1362
|
+
|
|
1363
|
+
# Hard 100 MB ceiling matches the TaskAttachment cap (ADR-0075 locked
|
|
1364
|
+
# constraint #4). The serializer also enforces this, but the setting must
|
|
1365
|
+
# fire FIRST — without it, Django buffers the entire body to /tmp before
|
|
1366
|
+
# the serializer runs, which lets an authenticated Member spam 100 MB+
|
|
1367
|
+
# multipart bodies and exhaust worker time + disk. Operators should also
|
|
1368
|
+
# set nginx `client_max_body_size: 100m` so over-sized requests are
|
|
1369
|
+
# rejected at the edge, not after Django finishes parsing them.
|
|
1370
|
+
DATA_UPLOAD_MAX_MEMORY_SIZE = 100 * 1024 * 1024 # 100 MB
|
|
1371
|
+
FILE_UPLOAD_MAX_MEMORY_SIZE = 2_621_440 # 2.5 MB (Django default; explicit)
|
|
1372
|
+
|
|
1373
|
+
# Days a public share link lasts when the mint request omits ``expires_at`` (#3177).
|
|
1374
|
+
# An explicit null still means "never expires" — this is a default, not a ceiling, so
|
|
1375
|
+
# standing embeds and wallboards stay possible. Set to 0 to restore the pre-#3177
|
|
1376
|
+
# behavior in which an omitted expiry meant never.
|
|
1377
|
+
TRUEPPM_SHARE_LINK_DEFAULT_EXPIRY_DAYS = env.int(
|
|
1378
|
+
"TRUEPPM_SHARE_LINK_DEFAULT_EXPIRY_DAYS", default=90
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
# Per-file cap for MS Project (.mpp/.xml) imports. Lower than the 100 MB
|
|
1382
|
+
# attachment ceiling on purpose: unlike attachments (FileField → disk), an
|
|
1383
|
+
# import is read fully into memory and stored base64-encoded in a single
|
|
1384
|
+
# ImportRequest DB row (~+33%), so a 50 MB upload already costs ~67 MB of
|
|
1385
|
+
# memory and row size. 50 MB is the practical MS Project file ceiling
|
|
1386
|
+
# (larger schedules degrade in Project itself). The global
|
|
1387
|
+
# DATA_UPLOAD_MAX_MEMORY_SIZE and nginx client_max_body_size (both 100 MB)
|
|
1388
|
+
# remain the hard edge cap — do not configure this above them.
|
|
1389
|
+
MSPROJECT_MAX_UPLOAD_MB: int = env.int("MSPROJECT_MAX_UPLOAD_MB", default=50)
|
|
1390
|
+
|
|
1391
|
+
# Max number of tasks (also applied to resources and dependency links) a single
|
|
1392
|
+
# MS Project import may contain (#1721). The upload SIZE cap alone is not enough:
|
|
1393
|
+
# a 50 MB MSPDI XML can encode ~1M tasks, and the importer builds one Task object
|
|
1394
|
+
# per task, computes WBS over all of them, then bulk-creates the lot — a
|
|
1395
|
+
# worker-memory / transaction-time DoS well within the byte ceiling. Rejecting
|
|
1396
|
+
# outright (like the risk-CSV importer's MAX_ROWS) is safer than partially
|
|
1397
|
+
# processing. 20k tasks is far above any realistic hand-authored schedule while
|
|
1398
|
+
# bounding the object graph a single request can materialize.
|
|
1399
|
+
MSPROJECT_MAX_ROWS: int = env.int("MSPROJECT_MAX_ROWS", default=20_000)
|
|
1400
|
+
|
|
1401
|
+
# batch_size for the import bulk_create() calls (#1721). Without it, Django emits
|
|
1402
|
+
# one giant multi-row INSERT that pins the whole task/dependency set in a single
|
|
1403
|
+
# statement (and can exceed PostgreSQL parameter limits); chunking bounds the
|
|
1404
|
+
# per-statement memory and parameter count. Shared by the MS Project and Jira
|
|
1405
|
+
# import paths (both persist through msproject.importer.import_project).
|
|
1406
|
+
IMPORT_BULK_BATCH_SIZE: int = env.int("IMPORT_BULK_BATCH_SIZE", default=500)
|
|
1407
|
+
|
|
1408
|
+
# Hard cap on the XML MPXJ streams to stdout when converting a .mpp (#1722). A
|
|
1409
|
+
# decompression-bomb .mpp within the 50 MB upload cap can expand to multi-GB XML;
|
|
1410
|
+
# buffering it unbounded (the old capture_output=True) OOMs the worker. We stream
|
|
1411
|
+
# stdout and abort past this many bytes. 512 MB is generous headroom for a
|
|
1412
|
+
# legitimate 50 MB .mpp (MSPDI XML is ~5-10x the binary) while still bounded.
|
|
1413
|
+
MPXJ_MAX_OUTPUT_MB: int = env.int("MPXJ_MAX_OUTPUT_MB", default=512)
|
|
1414
|
+
|
|
1415
|
+
# Per-file cap for CSV/Excel imports (#743). Much lower than the MS Project
|
|
1416
|
+
# ceiling on purpose: a spreadsheet migration is a hand-maintained sheet, not a
|
|
1417
|
+
# generated plan, and 10 MB is already far past what any real one weighs. The
|
|
1418
|
+
# same base64-in-a-DB-row cost applies (~+33%).
|
|
1419
|
+
CSV_IMPORT_MAX_UPLOAD_MB: int = env.int("CSV_IMPORT_MAX_UPLOAD_MB", default=10)
|
|
1420
|
+
|
|
1421
|
+
# Max data rows a single CSV/Excel import may contain (#743). The byte cap alone
|
|
1422
|
+
# is not enough — a 10 MB CSV can encode well over a million short rows, and the
|
|
1423
|
+
# parser builds one TaskData per row before anything is written. Rows past the
|
|
1424
|
+
# cap are reported back as `truncated_rows` rather than silently dropped.
|
|
1425
|
+
CSV_IMPORT_MAX_ROWS: int = env.int("CSV_IMPORT_MAX_ROWS", default=5_000)
|
|
1426
|
+
|
|
1427
|
+
# The tested-comfortable Schedule size (#3388, administration/sizing.md
|
|
1428
|
+
# "Tested envelope"): whole-project load is measured at 1.85s at 1,000 tasks and
|
|
1429
|
+
# breaches at 2,000 (60s) — a cliff, not a slope. CSV_IMPORT_MAX_ROWS (5,000) is
|
|
1430
|
+
# far above this, so nothing stops an import from landing a project past the
|
|
1431
|
+
# ceiling it cannot comfortably open. This is a *warn, do not block* line, not a
|
|
1432
|
+
# correctness limit — someone on better hardware may legitimately want more —
|
|
1433
|
+
# so it is only ever surfaced as advisory fields on the import preview response
|
|
1434
|
+
# (`exceeds_recommended_ceiling`) and read client-side against the already-loaded
|
|
1435
|
+
# Schedule task count. Configurable because the comfortable figure is
|
|
1436
|
+
# hardware-dependent and is expected to move as #3383 lands.
|
|
1437
|
+
SCHEDULE_TASK_CEILING: int = env.int("TRUEPPM_SCHEDULE_TASK_CEILING", default=1_000)
|
|
1438
|
+
|
|
1439
|
+
# Decompression-bomb ceiling for .xlsx uploads (#743). An .xlsx is a zip of XML:
|
|
1440
|
+
# openpyxl inherits XXE hardening from defusedxml automatically, but nothing in
|
|
1441
|
+
# that stack bounds the *inflated* size, so a 1 MB upload can expand to
|
|
1442
|
+
# gigabytes and OOM the worker. The parser sums the zip central directory's
|
|
1443
|
+
# declared sizes — cheap, and before any XML is touched — and rejects above this.
|
|
1444
|
+
CSV_IMPORT_MAX_UNCOMPRESSED_MB: int = env.int("CSV_IMPORT_MAX_UNCOMPRESSED_MB", default=100)
|
|
1445
|
+
|
|
1446
|
+
# JVM max-heap (-Xmx) for the MPXJ subprocess (#1722). Bounding the JVM heap is a
|
|
1447
|
+
# second line of defense: even if MPXJ tries to build a giant in-memory model
|
|
1448
|
+
# from a bomb file, the JVM dies with an OutOfMemoryError (caught as a non-zero
|
|
1449
|
+
# exit) instead of driving the host into swap.
|
|
1450
|
+
MPXJ_MAX_HEAP_MB: int = env.int("MPXJ_MAX_HEAP_MB", default=512)
|
|
1451
|
+
|
|
1452
|
+
# Max size of an uploaded Jira XML export (#1664). Jira issue-navigator exports
|
|
1453
|
+
# are small relative to .mpp files; the 25 MB default is generous for the minimal
|
|
1454
|
+
# import slice and stays well under the DATA_UPLOAD_MAX_MEMORY_SIZE hard cap.
|
|
1455
|
+
JIRA_IMPORT_MAX_UPLOAD_MB: int = env.int("JIRA_IMPORT_MAX_UPLOAD_MB", default=25)
|
|
1456
|
+
|
|
1457
|
+
# Max number of issues a single Jira XML import may contain (#1721). Same
|
|
1458
|
+
# rationale as MSPROJECT_MAX_ROWS: the byte cap does not bound the derived task
|
|
1459
|
+
# count, and every issue becomes a Task object built and bulk-created through the
|
|
1460
|
+
# shared importer. Rejected outright over the limit.
|
|
1461
|
+
JIRA_IMPORT_MAX_ROWS: int = env.int("JIRA_IMPORT_MAX_ROWS", default=20_000)
|
|
1462
|
+
|
|
1463
|
+
# Max size of an uploaded JSON program seed (ADR-0109, #615). Seeds are bounded
|
|
1464
|
+
# (the largest bundled sample is a few hundred KB); 5 MB is generous headroom
|
|
1465
|
+
# while bounding the memory a single authenticated import request can consume.
|
|
1466
|
+
SEED_MAX_UPLOAD_MB: int = env.int("SEED_MAX_UPLOAD_MB", default=5)
|
|
1467
|
+
|
|
1468
|
+
# Max seed imports one account may have queued or running at once (#2615). The
|
|
1469
|
+
# ``seed_import`` throttle bounds the *rate* of job creation; it does not bound
|
|
1470
|
+
# how many of those jobs are outstanding, because the per-program in-flight
|
|
1471
|
+
# de-dupe in ``enqueue_program_import`` can never match on this path — the
|
|
1472
|
+
# request creates a fresh Program three lines earlier, so a brand-new program
|
|
1473
|
+
# has no prior job to de-dupe against. Without a per-user ceiling, six requests
|
|
1474
|
+
# a minute sustained becomes an unbounded worker backlog of full subtree builds.
|
|
1475
|
+
SEED_IMPORT_MAX_CONCURRENT_JOBS: int = env.int("SEED_IMPORT_MAX_CONCURRENT_JOBS", default=3)
|
|
1476
|
+
|
|
1477
|
+
# ---------------------------------------------------------------------------
|
|
1478
|
+
# Django REST Framework
|
|
1479
|
+
# ---------------------------------------------------------------------------
|
|
1480
|
+
|
|
1481
|
+
# The tightest scoped-throttle tier, shared by the handful of single-shot
|
|
1482
|
+
# endpoints whose per-call cost — one outbound egress probe or one double
|
|
1483
|
+
# CPM + Monte Carlo pass — makes even a modest loop abusive. Kept as one named
|
|
1484
|
+
# constant so the rate lives in a single place rather than a repeated literal.
|
|
1485
|
+
_STRICT_ABUSE_RATE = "6/min"
|
|
1486
|
+
|
|
1487
|
+
# The remaining scoped-throttle tiers, from the general per-user default down
|
|
1488
|
+
# to the credential-shaped endpoints (login, password reset, invite resend).
|
|
1489
|
+
# Named once for the same reason as _STRICT_ABUSE_RATE above.
|
|
1490
|
+
# Trusted-proxy depth, validated here rather than left to DRF (#4020). A negative
|
|
1491
|
+
# value makes DRF's ``addrs[-min(NUM_PROXIES, len(addrs))]`` index from the LEFT —
|
|
1492
|
+
# the client-written end of X-Forwarded-For — or raise IndexError on a one-entry
|
|
1493
|
+
# header, so it is never a working configuration; refuse to boot on it.
|
|
1494
|
+
_NUM_PROXIES = env.int("TRUEPPM_NUM_PROXIES", default=1)
|
|
1495
|
+
if _NUM_PROXIES < 0:
|
|
1496
|
+
raise ImproperlyConfigured(
|
|
1497
|
+
f"TRUEPPM_NUM_PROXIES must be 0 or a positive integer (got {_NUM_PROXIES}). "
|
|
1498
|
+
"It is the number of reverse proxies in front of Django; see "
|
|
1499
|
+
"administration/networking.md."
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
_STANDARD_RATE = "60/min"
|
|
1503
|
+
_MODERATE_RATE = "20/min"
|
|
1504
|
+
_STRICT_RATE = "10/min"
|
|
1505
|
+
_RELAXED_RATE = "120/min"
|
|
1506
|
+
_STRICTEST_RATE = "5/min"
|
|
1507
|
+
|
|
1508
|
+
REST_FRAMEWORK = {
|
|
1509
|
+
# JWT + owner-scoped API token in the base/prod posture (#2248, #2547). The SPA
|
|
1510
|
+
# authenticates with the bearer access token; there is no Django-session login
|
|
1511
|
+
# path for the API and the browsable API is disabled (JSON-only renderer below),
|
|
1512
|
+
# so SessionAuthentication would be an unused second auth surface (and its CSRF
|
|
1513
|
+
# coupling) in production. Dev settings re-add SessionAuthentication so
|
|
1514
|
+
# `client.force_login()` in the test suite can populate request.user.
|
|
1515
|
+
#
|
|
1516
|
+
# OwnerScopedApiTokenAuthentication is listed first (mirroring
|
|
1517
|
+
# McpReadableViewMixin.get_authenticators' ordering): it returns None for any
|
|
1518
|
+
# bearer that isn't one of our tppm_-prefixed tokens, so JWT still gets a turn
|
|
1519
|
+
# for every human/SPA request. It only accepts an owner-scoped Personal Access
|
|
1520
|
+
# Token carrying the legacy:full scope — project/program-scoped integration
|
|
1521
|
+
# tokens and mcp:read-only tokens are rejected here and keep authenticating only
|
|
1522
|
+
# via the narrow surfaces that reference ProjectApiTokenAuthentication directly
|
|
1523
|
+
# (TaskSyncView, the acceptance-result view, the MCP-readable viewsets).
|
|
1524
|
+
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
1525
|
+
"trueppm_api.apps.projects.authentication.OwnerScopedApiTokenAuthentication",
|
|
1526
|
+
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
|
1527
|
+
],
|
|
1528
|
+
"DEFAULT_PERMISSION_CLASSES": [
|
|
1529
|
+
"rest_framework.permissions.IsAuthenticated",
|
|
1530
|
+
],
|
|
1531
|
+
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
|
1532
|
+
"PAGE_SIZE": 50,
|
|
1533
|
+
# API-only service: render JSON, nothing else. DRF's default renderer set
|
|
1534
|
+
# includes BrowsableAPIRenderer, which content-negotiates on the request
|
|
1535
|
+
# Accept header and, for a serializer-less GenericViewSet, calls
|
|
1536
|
+
# get_serializer() to build its HTML form — firing DRF's
|
|
1537
|
+
# `assert self.serializer_class is not None` (an unhandled 500) whenever a
|
|
1538
|
+
# caller sends `Accept: text/html`. A fuzzer (or any browser) trivially
|
|
1539
|
+
# trips this on every serializer-less action. Restricting to JSONRenderer
|
|
1540
|
+
# turns those requests into a clean 406 and removes the whole class of bug
|
|
1541
|
+
# (#2213). Set unconditionally (not DEBUG-gated) because the nightly fuzz
|
|
1542
|
+
# and the test suite run under dev settings with DEBUG=True — the browsable
|
|
1543
|
+
# API is redundant with the drf-spectacular Swagger UI anyway.
|
|
1544
|
+
"DEFAULT_RENDERER_CLASSES": [
|
|
1545
|
+
"rest_framework.renderers.JSONRenderer",
|
|
1546
|
+
],
|
|
1547
|
+
# Custom handler translates a malformed UUID (bad path segment or query param)
|
|
1548
|
+
# into 404/400 instead of the 500 DRF's default returns for the Django
|
|
1549
|
+
# ValidationError / uuid.UUID ValueError those raise (#2125).
|
|
1550
|
+
"EXCEPTION_HANDLER": "trueppm_api.core.exception_handlers.trueppm_exception_handler",
|
|
1551
|
+
# Tolerant OPTIONS metadata: DRF's default calls get_serializer() while
|
|
1552
|
+
# building action metadata, which asserts (→ 500) on serializer-less
|
|
1553
|
+
# GenericViewSets. This subclass degrades that to empty action metadata (#2229).
|
|
1554
|
+
"DEFAULT_METADATA_CLASS": "trueppm_api.core.metadata.TolerantMetadata",
|
|
1555
|
+
"DEFAULT_FILTER_BACKENDS": [
|
|
1556
|
+
"rest_framework.filters.SearchFilter",
|
|
1557
|
+
"rest_framework.filters.OrderingFilter",
|
|
1558
|
+
],
|
|
1559
|
+
# Custom AutoSchema documents a 429 response on rate-limited operations (#1333);
|
|
1560
|
+
# throttling is a view attribute invisible to the schema post-processing hooks.
|
|
1561
|
+
"DEFAULT_SCHEMA_CLASS": "trueppm_api.core.openapi.TruePPMAutoSchema",
|
|
1562
|
+
# General default throttle (#1080). Every endpoint that does NOT declare its
|
|
1563
|
+
# own throttle_classes gets the baseline "anon"/"user" rate below — the
|
|
1564
|
+
# general DoS / resource-starvation protection a self-hoster expects. The
|
|
1565
|
+
# ProbeExempt* classes are the stock DRF anon/user throttles with one
|
|
1566
|
+
# override: they never count the unauthenticated /api/v1/health/ and
|
|
1567
|
+
# /api/v1/edition/ probe endpoints, which Kubernetes hits on a tight
|
|
1568
|
+
# liveness/readiness loop (the reason a bare AnonRateThrottle was previously
|
|
1569
|
+
# avoided — now solved by the exemption). A view that sets its own
|
|
1570
|
+
# throttle_classes (login, refresh, monte_carlo, etc. below) REPLACES this
|
|
1571
|
+
# default rather than stacking on top of it, so those endpoints keep only
|
|
1572
|
+
# their specific, stricter scope.
|
|
1573
|
+
"DEFAULT_THROTTLE_CLASSES": [
|
|
1574
|
+
"trueppm_api.core.throttling.ProbeExemptAnonRateThrottle",
|
|
1575
|
+
"trueppm_api.core.throttling.ProbeExemptUserRateThrottle",
|
|
1576
|
+
],
|
|
1577
|
+
# Trusted-proxy depth for client-IP extraction (#1080). Without this DRF keys
|
|
1578
|
+
# the anon throttle on the client-supplied X-Forwarded-For header verbatim, so
|
|
1579
|
+
# an attacker could rotate XFF to mint a fresh anon identity per request and
|
|
1580
|
+
# bypass the anon limit entirely. NUM_PROXIES tells DRF how many trusted proxies
|
|
1581
|
+
# sit in front of the app (the standard Helm chart has a single ingress → 1), so
|
|
1582
|
+
# it reads the real client IP from a trusted position and ignores attacker-
|
|
1583
|
+
# prepended entries. Set to the deployment's actual proxy depth via env; 0 means
|
|
1584
|
+
# "no proxy, use REMOTE_ADDR". The authenticated "user" scope keys on the account
|
|
1585
|
+
# id and is unaffected.
|
|
1586
|
+
#
|
|
1587
|
+
# Wrong in EITHER direction is silent (#4020): too low collapses every per-IP
|
|
1588
|
+
# throttle into one shared bucket, too high lets a client pick its own bucket
|
|
1589
|
+
# via X-Forwarded-For. Nothing here can know the real topology, so the check is
|
|
1590
|
+
# a readback — GET /api/v1/health/system/ → security.client_address shows the
|
|
1591
|
+
# address THIS request was keyed on. See administration/networking.md.
|
|
1592
|
+
"NUM_PROXIES": _NUM_PROXIES,
|
|
1593
|
+
"DEFAULT_THROTTLE_RATES": {
|
|
1594
|
+
# General default rates for the ProbeExempt* classes above. Env-tunable
|
|
1595
|
+
# so operators can tighten or loosen the baseline without a rebuild.
|
|
1596
|
+
# "anon" bounds unauthenticated traffic per client IP; "user" bounds an
|
|
1597
|
+
# authenticated account. Both are generous enough that a normal
|
|
1598
|
+
# interactive client never trips them.
|
|
1599
|
+
"anon": env("TRUEPPM_THROTTLE_ANON_RATE", default=_STANDARD_RATE),
|
|
1600
|
+
"user": env("TRUEPPM_THROTTLE_USER_RATE", default="1000/min"),
|
|
1601
|
+
# /api/v1/readyz (#2820). Unlike /health/ and /edition/, readyz is not
|
|
1602
|
+
# free — every call does a real database round-trip and a real cache
|
|
1603
|
+
# round-trip — and it used to be FULLY exempt from the shared throttle
|
|
1604
|
+
# (the same exemption /health/ and /edition/ still get), which meant an
|
|
1605
|
+
# unauthenticated caller who found the pod IP directly (the probe path
|
|
1606
|
+
# bypasses the Ingress) could drive unbounded database load with no rate
|
|
1607
|
+
# limit at all. A dedicated scope, not the shared "anon" bucket: readyz
|
|
1608
|
+
# traffic comes from kubelet hitting the pod IP directly rather than
|
|
1609
|
+
# through the Ingress, so its source IP is the node, not an end user's,
|
|
1610
|
+
# and sharing "anon" would let a legitimate probe loop compete with real
|
|
1611
|
+
# anonymous traffic in both directions. The default is generous enough
|
|
1612
|
+
# that no documented probe configuration comes close to tripping it —
|
|
1613
|
+
# see administration/probes.md for the numbers this assumes — while
|
|
1614
|
+
# still bounding an outright flood from an unauthenticated source.
|
|
1615
|
+
"readyz": env("TRUEPPM_THROTTLE_READYZ_RATE", default="2000/min"),
|
|
1616
|
+
"login": _STRICT_RATE,
|
|
1617
|
+
# Per-account login lockout (#1717). The "login" scope above keys on the
|
|
1618
|
+
# client IP, so it only bounds guesses per source address; a distributed
|
|
1619
|
+
# credential-stuffing attack from a rotating IP pool gets the full per-IP
|
|
1620
|
+
# allowance from every fresh IP and is unbounded in aggregate against one
|
|
1621
|
+
# account. This scope keys on the (hashed, normalized) submitted username
|
|
1622
|
+
# instead, so attempts against a single account are capped no matter how
|
|
1623
|
+
# many IPs participate. It is STACKED with "login" — both throttles apply
|
|
1624
|
+
# to the login view. Env-tunable so an operator can tighten or loosen it.
|
|
1625
|
+
# 5/min still leaves room for a human who fat-fingers a password a few
|
|
1626
|
+
# times, while making cross-IP account targeting expensive.
|
|
1627
|
+
"login_account": env("TRUEPPM_THROTTLE_LOGIN_ACCOUNT_RATE", default=_STRICTEST_RATE),
|
|
1628
|
+
# Self-service password reset (#765, ADR-0209). Covers BOTH the request
|
|
1629
|
+
# endpoint (which emails a reset link) and the confirm endpoint (which sets
|
|
1630
|
+
# the new password). 5/min per IP bounds two abuses at once: email-bombing a
|
|
1631
|
+
# victim by looping the request endpoint, and timing/enumeration probing of
|
|
1632
|
+
# which addresses have accounts (the request endpoint returns an identical
|
|
1633
|
+
# 200 either way, so the throttle is the primary enumeration defense — see
|
|
1634
|
+
# ADR-0209). 5/min still leaves ample room for a human who mistypes, resends,
|
|
1635
|
+
# and retries a confirm a couple of times.
|
|
1636
|
+
"password_reset": _STRICTEST_RATE,
|
|
1637
|
+
# JWT refresh (#814). 60/min is loose enough that any realistic
|
|
1638
|
+
# web/mobile client (5-minute access-token TTL → ~12 refreshes/hour)
|
|
1639
|
+
# never trips it, but tight enough that a stolen/leaked refresh token
|
|
1640
|
+
# cannot be exchanged for access tokens at unbounded rate.
|
|
1641
|
+
"refresh": _STANDARD_RATE,
|
|
1642
|
+
# User typeahead for member invite (#815). Tight enough that a single
|
|
1643
|
+
# account cannot bulk-scrape the workspace user list, loose enough that
|
|
1644
|
+
# interactive typeahead (debounced, >=2 chars) never trips it.
|
|
1645
|
+
"user_search": _STANDARD_RATE,
|
|
1646
|
+
# ⌘K global Epic/Story omni-search (ADR-0508 D4, #2103). A debounced,
|
|
1647
|
+
# >=2-char typeahead over the caller's own membership; the 60/min bound
|
|
1648
|
+
# matches user_search — snug enough that a scripted loop cannot bulk-map
|
|
1649
|
+
# every epic/story title the account can see, loose enough that live typing
|
|
1650
|
+
# never trips it.
|
|
1651
|
+
"omni_search": env("TRUEPPM_THROTTLE_OMNI_SEARCH_RATE", default=_STANDARD_RATE),
|
|
1652
|
+
# Project/program key resolver + key suggestion (ADR-1237 §5-§6). One
|
|
1653
|
+
# per-user bucket for both: the resolver runs once per cold key-URL load
|
|
1654
|
+
# and the suggestion once per debounced keystroke on a create form, so
|
|
1655
|
+
# 120/min never trips a person — and it bounds using either endpoint as a
|
|
1656
|
+
# scan engine over the key namespace (the accepted existence oracle).
|
|
1657
|
+
"resolve": env("TRUEPPM_THROTTLE_RESOLVE_RATE", default=_RELAXED_RATE),
|
|
1658
|
+
# WebSocket connection tickets (#818, ADR-0141). One ticket is minted per
|
|
1659
|
+
# socket open (and per reconnect, since tickets are single-use); 120/min
|
|
1660
|
+
# covers aggressive reconnect storms without letting one account flood
|
|
1661
|
+
# Redis with 30-second ticket keys.
|
|
1662
|
+
"ws_ticket": _RELAXED_RATE,
|
|
1663
|
+
# Invite-email resend (#969, ADR-0149). Each request re-issues a token and
|
|
1664
|
+
# re-queues an email; 5/min per admin bounds email-bomb abuse while leaving
|
|
1665
|
+
# ample room for a human clicking "Resend" on a handful of stuck invites.
|
|
1666
|
+
# "Resend all" is one request → one bucket hit, so it cannot be looped past
|
|
1667
|
+
# the cap.
|
|
1668
|
+
"invite_resend": _STRICTEST_RATE,
|
|
1669
|
+
# Workspace SMTP config (#712, ADR-0213). Writes re-open a candidate SMTP
|
|
1670
|
+
# connection (validate-before-persist); "probe" covers the two outbound
|
|
1671
|
+
# amplifiers — send-test (opens a real SMTP socket) and the deliverability
|
|
1672
|
+
# health check (fires live DNS TXT lookups). Both are blind-SSRF / egress
|
|
1673
|
+
# oracles if unbounded, so cap them tightly (security review H3). An admin
|
|
1674
|
+
# configuring mail never trips these; a script probing hosts does.
|
|
1675
|
+
"email_settings": "12/min",
|
|
1676
|
+
"email_settings_probe": _STRICT_ABUSE_RATE,
|
|
1677
|
+
# SSO domain discovery (ADR-0187). Unauthenticated; reveals only whether a
|
|
1678
|
+
# *domain* uses SSO (never whether an account exists), but the rate still
|
|
1679
|
+
# bounds bulk domain-probing. Loose enough for the interactive login page.
|
|
1680
|
+
"oidc_discover": "30/min",
|
|
1681
|
+
# SSO login start (ADR-0187). Each hit mints a single-use state/PKCE/nonce
|
|
1682
|
+
# cache entry and 302s to the IdP; this also implicitly bounds the callback
|
|
1683
|
+
# (every callback requires a state minted here). Snug but ample for humans.
|
|
1684
|
+
"oidc_login": _MODERATE_RATE,
|
|
1685
|
+
# SSO callback (ADR-0187). A token-issuing endpoint; the implicit bound via
|
|
1686
|
+
# oidc_login already caps legitimate use, but an explicit cap blunts a flood
|
|
1687
|
+
# of forged callbacks (each fails fast at the browser-state-cookie check).
|
|
1688
|
+
"oidc_callback": "30/min",
|
|
1689
|
+
# Admin "Test connection" probe (ADR-0517 §3.4). Each call triggers
|
|
1690
|
+
# server-side egress (OIDC discovery + JWKS, or the GitHub API), so it needs
|
|
1691
|
+
# an abuse bound so an admin cannot drive unbounded outbound requests.
|
|
1692
|
+
# 20/min is ample for a human wiring up a provider.
|
|
1693
|
+
"sso_test_connection": _MODERATE_RATE,
|
|
1694
|
+
# SSO provider create/update/delete (#3552, ADR-1120). These views previously
|
|
1695
|
+
# declared no throttle and fell back to the global "user" rate of 1000/min. Each
|
|
1696
|
+
# accepted write now also appends an AuditEvent row, and the OSS audit table has
|
|
1697
|
+
# no retention or pruning — so a scripted `enabled` toggle would append at the
|
|
1698
|
+
# request ceiling, every row a *genuine* change that the no-op suppression cannot
|
|
1699
|
+
# filter. Note who pays: in a self-hosted install the workspace Admin driving the
|
|
1700
|
+
# loop and the operator whose disk fills are frequently different people. Applied
|
|
1701
|
+
# to the mutating methods only (see ``_SsoProviderWriteThrottle``) — the
|
|
1702
|
+
# collection GET backs the admin page and must not be capped at a write rate.
|
|
1703
|
+
"sso_provider_write": _MODERATE_RATE,
|
|
1704
|
+
# Integration-credential and Git webhook-secret endpoints (#1551). Covers the
|
|
1705
|
+
# per-user credential store (connect/rotate/revoke/read) and the project-admin
|
|
1706
|
+
# Git-automation config + secret-rotation views. Each of these mints, returns,
|
|
1707
|
+
# or reveals the presence of a plaintext credential/secret, so they need the
|
|
1708
|
+
# same brute-force/abuse bound the other credential-adjacent endpoints already
|
|
1709
|
+
# carry. 10/min is snug for a human wiring up integrations yet far below any
|
|
1710
|
+
# automated rotation-scraping rate.
|
|
1711
|
+
"credential_rotate": _STRICT_RATE,
|
|
1712
|
+
# Manual external-source pull trigger (POST /me/connections/{source}/sync/,
|
|
1713
|
+
# ADR-0097 §4, #1419). The service-layer 60 s per-connection cooldown is the
|
|
1714
|
+
# primary spacing control; this scope is the coarse abuse bound so a scripted
|
|
1715
|
+
# loop cannot spam the outbox faster than a human ever would. 20/min sits
|
|
1716
|
+
# above the cooldown's ~1/min steady state with headroom for multiple
|
|
1717
|
+
# connected sources.
|
|
1718
|
+
"external_sync": _MODERATE_RATE,
|
|
1719
|
+
# Synchronous Monte Carlo simulation (#1552). run_monte_carlo executes an
|
|
1720
|
+
# expensive (up to MC_SIMULATION_CAP iterations, caller-controlled
|
|
1721
|
+
# n_simulations) simulation inline in the request/response cycle, gated only
|
|
1722
|
+
# by project membership. Without a rate cap any single member can loop the
|
|
1723
|
+
# endpoint to exhaust CPU. 10/min leaves ample headroom for a human tuning
|
|
1724
|
+
# estimates and re-running the forecast a handful of times, while blunting a
|
|
1725
|
+
# scripted resource-exhaustion flood.
|
|
1726
|
+
"monte_carlo": _STRICT_RATE,
|
|
1727
|
+
# Non-mutating Monte Carlo what-if (#993). The what-if endpoint runs *two*
|
|
1728
|
+
# CPM passes and *two* Monte Carlo simulations per call (baseline vs the
|
|
1729
|
+
# perturbed schedule, so the delta isolates the perturbation) — roughly
|
|
1730
|
+
# double the CPU of a single run_monte_carlo — so it carries a tighter cap
|
|
1731
|
+
# than the plain "monte_carlo" scope. 6/min still leaves ample room for a
|
|
1732
|
+
# human (or an MCP agent) exploring a handful of "what if I slip this task"
|
|
1733
|
+
# scenarios while blunting a scripted resource-exhaustion flood.
|
|
1734
|
+
"monte_carlo_whatif": _STRICT_ABUSE_RATE,
|
|
1735
|
+
# Burn chart / flow metrics reads (#3581). ProjectBurnView.get and
|
|
1736
|
+
# FlowMetricsView.get both reconstruct their series from HistoricalTask
|
|
1737
|
+
# snapshots — the floor cost scales with project history (#3579) even
|
|
1738
|
+
# though #3566 bounds the window per request. Either endpoint is reachable
|
|
1739
|
+
# by any project member (down to Viewer), and both were left on the
|
|
1740
|
+
# general "user" default of 1000/min before this scope existed, which is
|
|
1741
|
+
# meaningful amplification against a shared database. Shared bucket
|
|
1742
|
+
# between the two views (same cost class, same table) rather than a
|
|
1743
|
+
# scope each. 60/min is far above what the Reports page generates —
|
|
1744
|
+
# useBurnChart has a 5-minute staleTime.
|
|
1745
|
+
"burn": env("TRUEPPM_THROTTLE_BURN_RATE", default=_STANDARD_RATE),
|
|
1746
|
+
# On-read CPM derivation (#2839). GET /projects/<pk>/schedule/derivation/ runs
|
|
1747
|
+
# a full schedule() pass inside derive_value on every CPM-quantity call — the
|
|
1748
|
+
# compute class #1808 bounded on its Monte Carlo siblings but missed here, so
|
|
1749
|
+
# it fell through to the general 1000/min account default. One CPM pass is
|
|
1750
|
+
# far cheaper than a simulation, so this sits at the general read tier rather
|
|
1751
|
+
# than the 10/min monte_carlo tier: a person clicking through the "why" of a
|
|
1752
|
+
# dozen values never trips it, a scripted loop is held to one pass a second.
|
|
1753
|
+
"cpm_on_read": env("TRUEPPM_THROTTLE_CPM_ON_READ_RATE", default=_STANDARD_RATE),
|
|
1754
|
+
# On-read merged program CPM (#2839), GET /programs/<pk>/schedule/. Same tier
|
|
1755
|
+
# as cpm_on_read: a 5,000-task merged pass measured ~470 ms, MORE than the
|
|
1756
|
+
# derivation's single-project pass, so it must not be laxer. 60/min holds a
|
|
1757
|
+
# loop to ~0.5 core (vs ~7.8 at the 1000/min default). Per account, so tabs
|
|
1758
|
+
# share it; one open tab can send up to 200/min (shared 300 ms debounce) in
|
|
1759
|
+
# an edit storm, and on a 429 the page keeps its last chart. See
|
|
1760
|
+
# ProgramScheduleThrottle.
|
|
1761
|
+
"program_schedule": env("TRUEPPM_THROTTLE_PROGRAM_SCHEDULE_RATE", default=_STANDARD_RATE),
|
|
1762
|
+
# Bundled-sample demo loader (#2402). POST /programs/load-sample/ is the
|
|
1763
|
+
# most expensive single write in the API: it hard-deletes the caller's
|
|
1764
|
+
# prior program with the same code (memberships -> projects -> cascade)
|
|
1765
|
+
# and rebuilds the whole fixture — three projects, per-row saves with
|
|
1766
|
+
# simple_history rows, the v2 replay timeline, and a forecast backfill —
|
|
1767
|
+
# synchronously inside one transaction. The default sample measures ~3.4 s
|
|
1768
|
+
# and ~3,700 sequential DB round-trips locally, several times that on
|
|
1769
|
+
# modest hardware. Left on the general "user" default (1000/min) one
|
|
1770
|
+
# authenticated member could drive a thousand full teardown-and-rebuilds a
|
|
1771
|
+
# minute. 6/min matches monte_carlo_whatif — the same "one call is costly
|
|
1772
|
+
# enough that even a modest loop is abusive" tier — and is ample for a
|
|
1773
|
+
# human clicking "Load demo data" and trying a couple of samples.
|
|
1774
|
+
"sample_load": env("TRUEPPM_THROTTLE_SAMPLE_LOAD_RATE", default=_STRICT_ABUSE_RATE),
|
|
1775
|
+
# Caller-supplied seed import (#2402). POST /programs/import/ runs the
|
|
1776
|
+
# *same* importer as load-sample on a payload the caller sizes, under the
|
|
1777
|
+
# same IsAuthenticated-only gate, so it is the cheaper of the two
|
|
1778
|
+
# exhaustion vectors and needs the same bound. A separate bucket from
|
|
1779
|
+
# sample_load so loading a demo never spends a user's real import
|
|
1780
|
+
# allowance — importing a handful of program bundles in a sitting is
|
|
1781
|
+
# normal; sixty a minute is not.
|
|
1782
|
+
"seed_import": env("TRUEPPM_THROTTLE_SEED_IMPORT_RATE", default=_STRICT_ABUSE_RATE),
|
|
1783
|
+
# Seed dry run (#2418). POST /programs/import/validate/ parses and
|
|
1784
|
+
# JSON-Schema-checks a caller-sized document but writes nothing, so it
|
|
1785
|
+
# costs CPU proportional to the payload and none of the teardown-rebuild
|
|
1786
|
+
# that makes seed_import expensive — a looser bound is right. It gets its
|
|
1787
|
+
# own bucket rather than sharing seed_import's for a usability reason as
|
|
1788
|
+
# much as a cost one: the whole point of a dry run is to iterate on a file
|
|
1789
|
+
# until it passes, and spending the real import allowance to do that would
|
|
1790
|
+
# lock an operator out of the import their fixed file just earned.
|
|
1791
|
+
"seed_validate": env("TRUEPPM_THROTTLE_SEED_VALIDATE_RATE", default=_MODERATE_RATE),
|
|
1792
|
+
# Bundled-fixture download (#2490). GET /programs/samples/{key}/download/
|
|
1793
|
+
# streams a file off disk (<= ~78 KB) and touches no table, so it is orders
|
|
1794
|
+
# of magnitude cheaper than sample_load and gets a correspondingly generous
|
|
1795
|
+
# bound. It gets its own bucket rather than sharing sample_load's for an
|
|
1796
|
+
# intent reason as much as a cost one: auditing all four fixtures before
|
|
1797
|
+
# importing is the behavior this feature exists to encourage, and it must
|
|
1798
|
+
# not spend the allowance a user needs to actually load a demo.
|
|
1799
|
+
# Not unthrottled: it serves a file keyed by a client-supplied string, so
|
|
1800
|
+
# even with the registry lookup closing traversal it would otherwise be a
|
|
1801
|
+
# bandwidth amplifier and a free oracle for enumerating valid keys — and
|
|
1802
|
+
# the scope becomes load-bearing the moment a deployment allowlists the
|
|
1803
|
+
# route for anonymous access (ADR-0685).
|
|
1804
|
+
"sample_download": env("TRUEPPM_THROTTLE_SAMPLE_DOWNLOAD_RATE", default=_STANDARD_RATE),
|
|
1805
|
+
# MCP read surface per-token rate limits (#1808 finding F4). These bound
|
|
1806
|
+
# token-authenticated reads on any McpReadableViewMixin view ONLY — human
|
|
1807
|
+
# JWT/Session traffic on the same views is unaffected (the throttles'
|
|
1808
|
+
# get_cache_key returns None for non-token callers, so DRF skips them; see
|
|
1809
|
+
# apps/access/throttles.py). "mcp_read" is the baseline cap every
|
|
1810
|
+
# MCP-readable view applies per token; "mcp_read_compute" is the tighter
|
|
1811
|
+
# bucket the compute-heavy reads (whatif, monte-carlo/latest, forecast,
|
|
1812
|
+
# sprint-forecast, schedule derivation #2839, and the program schedule
|
|
1813
|
+
# action) STACK on top, since each triggers a CPM or Monte Carlo
|
|
1814
|
+
# recompute per call and a read-only token loop must not burn arbitrary CPU.
|
|
1815
|
+
# Env-tunable so an operator can widen the budget for a trusted agent fleet
|
|
1816
|
+
# or tighten it under load. OSS ships basic per-token limits; per-agent
|
|
1817
|
+
# budgets and anomaly auto-suspend are Phase 4 / Enterprise.
|
|
1818
|
+
"mcp_read": env("TRUEPPM_THROTTLE_MCP_READ_RATE", default=_RELAXED_RATE),
|
|
1819
|
+
"mcp_read_compute": env("TRUEPPM_THROTTLE_MCP_READ_COMPUTE_RATE", default="12/min"),
|
|
1820
|
+
# Public board share-link endpoints (#283, ADR-0245). "share_mint" bounds
|
|
1821
|
+
# how fast one Admin account can spray share links; "share_access" bounds
|
|
1822
|
+
# scraping/abuse of the unauthenticated public board endpoint (the 256-bit
|
|
1823
|
+
# token is already non-enumerable — this stops a leaked/viral link becoming
|
|
1824
|
+
# an unthrottled load source on a self-hosted box, per Omar's VoC concern).
|
|
1825
|
+
"share_mint": env("TRUEPPM_THROTTLE_SHARE_MINT_RATE", default=_MODERATE_RATE),
|
|
1826
|
+
"share_access": env("TRUEPPM_THROTTLE_SHARE_ACCESS_RATE", default=_STANDARD_RATE),
|
|
1827
|
+
# Telemetry test-export probe (#2110). An admin-only button that opens one
|
|
1828
|
+
# outbound canary/reachability probe to the configured OTLP collector;
|
|
1829
|
+
# scoped-throttled so it can't be used to hammer the collector.
|
|
1830
|
+
"telemetry_test": env("TRUEPPM_THROTTLE_TELEMETRY_TEST_RATE", default=_STRICT_ABUSE_RATE),
|
|
1831
|
+
# Inbound Git-event webhook receiver, per client IP (#2881,
|
|
1832
|
+
# integrations.throttles.GitWebhookIpThrottle). The receiver's other
|
|
1833
|
+
# throttle keys on the project UUID *from the URL*, so rotating that UUID
|
|
1834
|
+
# minted a fresh bucket per request and the endpoint was effectively
|
|
1835
|
+
# unbounded for an anonymous caller. Deliberately loose: a provider delivers
|
|
1836
|
+
# from a small pool of shared egress addresses, so every repo on the install
|
|
1837
|
+
# lands in ONE bucket and a tight limit would throttle honest traffic across
|
|
1838
|
+
# unrelated projects. 600/min (10/s) sits far above any realistic aggregate
|
|
1839
|
+
# PR/MR event rate while still bounding a single source. Env-tunable for an
|
|
1840
|
+
# install with unusually heavy Git traffic.
|
|
1841
|
+
"git_webhook_ip": env("TRUEPPM_THROTTLE_GIT_WEBHOOK_IP_RATE", default="600/min"),
|
|
1842
|
+
},
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
# Global rate-limiting kill switch (ADR-0604): when an operator has disabled rate
|
|
1846
|
+
# limiting (with acknowledgment — see RATE_LIMIT_ENABLED above), neutralize every
|
|
1847
|
+
# DRF SimpleRateThrottle-family throttle in one place — drop the default throttle
|
|
1848
|
+
# classes and set every named scope's rate to None (DRF treats rate=None as "do not
|
|
1849
|
+
# throttle"). This covers all present and future scopes (anon, user, login,
|
|
1850
|
+
# monte_carlo, mcp_read, share_*, …) with no per-class edits. The custom Redis
|
|
1851
|
+
# BaseThrottle classes don't read these rates, so they bypass at request time via
|
|
1852
|
+
# the bypass_when_disabled decorator instead.
|
|
1853
|
+
if not RATE_LIMIT_ENABLED:
|
|
1854
|
+
apply_rate_limit_disable(REST_FRAMEWORK)
|
|
1855
|
+
|
|
1856
|
+
# MCP administration controls (#2021, ADR-0497).
|
|
1857
|
+
#
|
|
1858
|
+
# Instance-wide MCP kill switch. Default True preserves backward compatibility —
|
|
1859
|
+
# existing deployments keep agent (mcp:read token) access. Set False to deny every
|
|
1860
|
+
# MCP-token read at the McpReadableViewMixin chokepoint (403), even though the
|
|
1861
|
+
# token exists; human JWT/session auth on the same viewsets is unaffected. This is
|
|
1862
|
+
# the operator lever for "no agent access on this instance, period."
|
|
1863
|
+
TRUEPPM_MCP_ENABLED: bool = env.bool("TRUEPPM_MCP_ENABLED", default=True)
|
|
1864
|
+
|
|
1865
|
+
# Whether a member project's ADR-0678 agent-read opt-out withholds a PROGRAM-LEVEL
|
|
1866
|
+
# BULK EXPORT from an agent (`mcp:read`) token — the sync JSON seed and the async
|
|
1867
|
+
# .tar.gz bundle both carry every member project's data verbatim (#3014).
|
|
1868
|
+
#
|
|
1869
|
+
# "withhold" (default) — an agent token is refused (403) when ANY member project
|
|
1870
|
+
# has opted out. The cascade's rule is that a team's "no" is final, and a
|
|
1871
|
+
# bulk export is the widest possible read of the data it covers.
|
|
1872
|
+
# "allow" — pre-0.4 behavior: the export is treated as a program-level artifact
|
|
1873
|
+
# governed by the program's own `mcp_enabled` alone.
|
|
1874
|
+
#
|
|
1875
|
+
# OPERATOR-LEVEL ON PURPOSE, and the reason is the whole design (see
|
|
1876
|
+
# projects/mcp_settings.py, which refuses an `mcp_override_policy` field outright).
|
|
1877
|
+
# A workspace- or program-scoped toggle for this would be a *tenant* scope granting
|
|
1878
|
+
# over a team's denial — precisely the "consent that only an admin can grant or
|
|
1879
|
+
# revoke on the team's behalf is consent in name only" objection (#2415) the opt-out
|
|
1880
|
+
# exists to answer. An operator switch is the same kind of lever as
|
|
1881
|
+
# TRUEPPM_MCP_ENABLED above: the person running the server, not a scope above the
|
|
1882
|
+
# team inside it. Do not promote this to a model field.
|
|
1883
|
+
#
|
|
1884
|
+
# Humans are never affected by either value.
|
|
1885
|
+
TRUEPPM_MCP_PROGRAM_EXPORT_POLICY: str = env.str(
|
|
1886
|
+
"TRUEPPM_MCP_PROGRAM_EXPORT_POLICY", default="withhold"
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1889
|
+
# Env-overridable safety caps for API tokens and inbound task-sync. Each keeps its
|
|
1890
|
+
# historical hardcoded default; surfacing them as settings lets an operator tune the
|
|
1891
|
+
# blast-radius/limits per deployment without a code change. Read at request time so
|
|
1892
|
+
# override_settings (and a live env change) takes effect.
|
|
1893
|
+
#
|
|
1894
|
+
# Max ACTIVE personal access tokens per user (ADR-0214). Bounds the blast radius of
|
|
1895
|
+
# a leaked account and keeps the /me/api-tokens/ list navigable.
|
|
1896
|
+
TRUEPPM_MAX_PERSONAL_ACCESS_TOKENS: int = env.int("TRUEPPM_MAX_PERSONAL_ACCESS_TOKENS", default=10)
|
|
1897
|
+
# Max pinned projects + programs per user (#2390, ADR-0627). A pin is a personal
|
|
1898
|
+
# wayfinding shortcut, so the cap exists to bound the row count and keep the rail
|
|
1899
|
+
# and /me/pinned/ navigable — not as a security control. Exceeding it is a 400
|
|
1900
|
+
# carrying `code: "pin_limit_reached"` so the client can say which limit was hit.
|
|
1901
|
+
TRUEPPM_MAX_USER_PINS: int = env.int("TRUEPPM_MAX_USER_PINS", default=100)
|
|
1902
|
+
# Token-issuance rate cap (req/min/user) on the mint endpoint (ADR-0068). Caps the
|
|
1903
|
+
# blast radius of a compromised admin session even when RBAC is satisfied.
|
|
1904
|
+
TRUEPPM_TOKEN_ISSUANCE_PER_MINUTE: int = env.int("TRUEPPM_TOKEN_ISSUANCE_PER_MINUTE", default=5)
|
|
1905
|
+
# Token-revocation rate cap (req/min/user) on the revoke endpoint (#2878). Set well
|
|
1906
|
+
# above the issuance cap deliberately: revocation is the containment action, and a
|
|
1907
|
+
# user cutting off a leak may legitimately revoke every token they hold in one burst.
|
|
1908
|
+
TRUEPPM_TOKEN_REVOCATION_PER_MINUTE: int = env.int(
|
|
1909
|
+
"TRUEPPM_TOKEN_REVOCATION_PER_MINUTE", default=30
|
|
1910
|
+
)
|
|
1911
|
+
# Inbound task-sync per-project rate caps (ADR-0068). Steady-state limit applies
|
|
1912
|
+
# after the 60-minute backfill window; the higher backfill limit applies to a
|
|
1913
|
+
# freshly minted token so a large first import gets headroom.
|
|
1914
|
+
TRUEPPM_TASK_SYNC_STEADY_STATE_LIMIT: int = env.int(
|
|
1915
|
+
"TRUEPPM_TASK_SYNC_STEADY_STATE_LIMIT", default=100
|
|
1916
|
+
)
|
|
1917
|
+
TRUEPPM_TASK_SYNC_BACKFILL_LIMIT: int = env.int("TRUEPPM_TASK_SYNC_BACKFILL_LIMIT", default=1000)
|
|
1918
|
+
|
|
1919
|
+
# Public read-only sharing — board (#283, ADR-0245) and schedule (#1486, ADR-0265).
|
|
1920
|
+
# The instance kill switch governs BOTH share kinds: when False, minting new links
|
|
1921
|
+
# (403) and every public share endpoint (uniform 404, retroactively disabling
|
|
1922
|
+
# existing links) are turned off org-wide — the operator/PMO off lever for the
|
|
1923
|
+
# unauthenticated egress surface. One lever, one mental model: a deployment can
|
|
1924
|
+
# never leak schedules while "board sharing" reads off. Default True is safe because
|
|
1925
|
+
# no data is exposed until an Owner/Admin explicitly mints a link. The SHARE_*_MAX_*
|
|
1926
|
+
# caps bound each public snapshot payload (the projection flags "truncated" past the
|
|
1927
|
+
# cap rather than silently dropping content). The env var keeps its #283 name for
|
|
1928
|
+
# back-compat with deployments that already set it.
|
|
1929
|
+
TRUEPPM_PUBLIC_BOARD_SHARING_ENABLED = env.bool(
|
|
1930
|
+
"TRUEPPM_PUBLIC_BOARD_SHARING_ENABLED", default=True
|
|
1931
|
+
)
|
|
1932
|
+
SHARE_BOARD_MAX_CARDS = env.int("TRUEPPM_SHARE_BOARD_MAX_CARDS", default=1000)
|
|
1933
|
+
SHARE_SCHEDULE_MAX_TASKS = env.int("TRUEPPM_SHARE_SCHEDULE_MAX_TASKS", default=1000)
|
|
1934
|
+
|
|
1935
|
+
# ---------------------------------------------------------------------------
|
|
1936
|
+
# django-allauth
|
|
1937
|
+
# ---------------------------------------------------------------------------
|
|
1938
|
+
|
|
1939
|
+
SITE_ID = 1
|
|
1940
|
+
# "none" is safe *because allauth's self-service account flows are never reached*
|
|
1941
|
+
# (#2248): we do NOT mount `allauth.urls`, so there is no allauth signup/login/
|
|
1942
|
+
# email-confirmation surface. Account creation flows only through (a) the invite
|
|
1943
|
+
# path — the email is fixed to the invite, not user-chosen — and (b) SSO, which
|
|
1944
|
+
# enforces its own `email_verified is True` + admin domain allow-list gate in
|
|
1945
|
+
# `apps.sso.services.resolve_user`. Do NOT mount `allauth.urls` (which would open
|
|
1946
|
+
# self-service registration) without first flipping this to a real verification
|
|
1947
|
+
# policy — otherwise unverified self-service signup would be silently enabled.
|
|
1948
|
+
ACCOUNT_EMAIL_VERIFICATION = "none"
|
|
1949
|
+
|
|
1950
|
+
# ModelBackend FIRST so username/password login keeps working exactly as before
|
|
1951
|
+
# (ADR-0517 §1). The allauth backend is appended only so allauth's own account
|
|
1952
|
+
# machinery resolves; our SSO views never call allauth's login() — they mint the
|
|
1953
|
+
# simplejwt cookie session via the JWT bridge (apps/sso/views.py).
|
|
1954
|
+
AUTHENTICATION_BACKENDS = [
|
|
1955
|
+
"django.contrib.auth.backends.ModelBackend",
|
|
1956
|
+
"allauth.account.auth_backends.AuthenticationBackend",
|
|
1957
|
+
]
|
|
1958
|
+
|
|
1959
|
+
# SOCIALACCOUNT_PROVIDERS is used purely as a per-provider CONFIG REGISTRY — it
|
|
1960
|
+
# pins the server-fixed OSS scopes and PKCE. Per-provider APPS are NOT hardcoded
|
|
1961
|
+
# here: each configured IdP lives as a DB `SocialApp` row (provider_id == our
|
|
1962
|
+
# registry slug) written through the admin API, so the APPS list stays empty
|
|
1963
|
+
# (ADR-0517 §1). We deliberately do NOT set LOGIN_REDIRECT_URL or
|
|
1964
|
+
# SOCIALACCOUNT_ADAPTER — our own views own the flow, allauth is a library.
|
|
1965
|
+
SOCIALACCOUNT_PROVIDERS = {
|
|
1966
|
+
"openid_connect": {
|
|
1967
|
+
"APPS": [],
|
|
1968
|
+
"OAUTH_PKCE_ENABLED": True,
|
|
1969
|
+
# Server-fixed in OSS — `groups`/custom claims are an Enterprise widening.
|
|
1970
|
+
"SCOPE": ["openid", "email", "profile"],
|
|
1971
|
+
},
|
|
1972
|
+
"github": {
|
|
1973
|
+
# OIDC-equivalent scopes: identity via GET /user + /user/emails.
|
|
1974
|
+
"SCOPE": ["read:user", "user:email"],
|
|
1975
|
+
},
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
# ---------------------------------------------------------------------------
|
|
1979
|
+
# Outbound email transport (#639 read-only status page, #764)
|
|
1980
|
+
# ---------------------------------------------------------------------------
|
|
1981
|
+
# Bind Django's standard EMAIL_* settings from the environment so operators
|
|
1982
|
+
# configure SMTP via container env vars / Helm values with no settings override.
|
|
1983
|
+
# The Beat-driven notification drain runs on the api, celery, and celery-beat
|
|
1984
|
+
# workloads, so all three must receive the same env. EMAIL_HOST_PASSWORD is read
|
|
1985
|
+
# from the environment but never exposed by the API or logged (#639).
|
|
1986
|
+
#
|
|
1987
|
+
# EMAIL_HOST defaults to "" (not Django's implicit "localhost") so an
|
|
1988
|
+
# unconfigured deployment reports "Not configured" on the read-only status page
|
|
1989
|
+
# and the drain skips sending, rather than silently attempting localhost:25.
|
|
1990
|
+
EMAIL_BACKEND = env("EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend")
|
|
1991
|
+
EMAIL_HOST = env("EMAIL_HOST", default="")
|
|
1992
|
+
EMAIL_PORT = env.int("EMAIL_PORT", default=587)
|
|
1993
|
+
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=True)
|
|
1994
|
+
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", default=False)
|
|
1995
|
+
EMAIL_HOST_USER = env("EMAIL_HOST_USER", default="")
|
|
1996
|
+
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD", default="")
|
|
1997
|
+
EMAIL_TIMEOUT = env.int("EMAIL_TIMEOUT", default=10)
|
|
1998
|
+
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL", default="notifications@trueppm.local")
|
|
1999
|
+
|
|
2000
|
+
# ---------------------------------------------------------------------------
|
|
2001
|
+
# drf-spectacular (OpenAPI)
|
|
2002
|
+
# ---------------------------------------------------------------------------
|
|
2003
|
+
|
|
2004
|
+
# ---------------------------------------------------------------------------
|
|
2005
|
+
# Object change history (django-simple-history)
|
|
2006
|
+
# ---------------------------------------------------------------------------
|
|
2007
|
+
|
|
2008
|
+
# Retention window in days. Records older than this are purged nightly by the
|
|
2009
|
+
# Celery beat task in trueppm_api.apps.history.tasks.
|
|
2010
|
+
# Set to None to disable automatic purging (enterprise unlimited retention).
|
|
2011
|
+
# Operator env var standardized on the TRUEPPM_ prefix pre-0.3 (#1325) so all
|
|
2012
|
+
# tunables share one namespace in ConfigMaps/Secrets; the legacy bare name is read
|
|
2013
|
+
# as a fallback so deploys that already set it keep working until they migrate.
|
|
2014
|
+
HISTORY_RETENTION_DAYS: int | None = env.int(
|
|
2015
|
+
"TRUEPPM_HISTORY_RETENTION_DAYS", default=env.int("HISTORY_RETENTION_DAYS", default=90)
|
|
2016
|
+
)
|
|
2017
|
+
|
|
2018
|
+
# ---------------------------------------------------------------------------
|
|
2019
|
+
# Signal-privacy ceiling-raise ratification (ADR-0104 Amendment A, #930)
|
|
2020
|
+
# ---------------------------------------------------------------------------
|
|
2021
|
+
|
|
2022
|
+
# How long a team-ratification proposal to raise a signal's ceiling stays OPEN
|
|
2023
|
+
# before it lazily expires UNRATIFIED (the ceiling is left unchanged — silence is
|
|
2024
|
+
# never consent for widening a team signal's exposure). 72h is long enough for an
|
|
2025
|
+
# async team yet short enough not to stall a pending share. Evaluated lazily on
|
|
2026
|
+
# read/vote/propose; no Celery/Beat sweep is required.
|
|
2027
|
+
SIGNAL_CEILING_PROPOSAL_TTL_HOURS: int = env.int("SIGNAL_CEILING_PROPOSAL_TTL_HOURS", default=72)
|
|
2028
|
+
|
|
2029
|
+
# ---------------------------------------------------------------------------
|
|
2030
|
+
# Task run retention (trueppm_api.apps.taskruns)
|
|
2031
|
+
# ---------------------------------------------------------------------------
|
|
2032
|
+
|
|
2033
|
+
# Retention window in days for completed/failed/cancelled TaskRun records.
|
|
2034
|
+
# Set to None to disable automatic purging. Env var standardized on the TRUEPPM_
|
|
2035
|
+
# prefix pre-0.3 (#1325); legacy bare name read as a fallback.
|
|
2036
|
+
TASK_RUN_RETENTION_DAYS: int | None = env.int(
|
|
2037
|
+
"TRUEPPM_TASK_RUN_RETENTION_DAYS", default=env.int("TASK_RUN_RETENTION_DAYS", default=30)
|
|
2038
|
+
)
|
|
2039
|
+
|
|
2040
|
+
# ---------------------------------------------------------------------------
|
|
2041
|
+
# Time tracking (trueppm_api.apps.timetracking, ADR-0185)
|
|
2042
|
+
# ---------------------------------------------------------------------------
|
|
2043
|
+
|
|
2044
|
+
# Stale-timer ceiling: a running timer that exceeds this many minutes is flagged
|
|
2045
|
+
# ``stale`` on GET /me/timer/, and ``stop`` caps the logged minutes at it rather
|
|
2046
|
+
# than the raw elapsed (so a timer left running over a weekend logs the ceiling,
|
|
2047
|
+
# not thousands of minutes). Default 600 = 10 h.
|
|
2048
|
+
TIMETRACKING_TIMER_MAX_MINUTES: int = env.int("TIMETRACKING_TIMER_MAX_MINUTES", default=600)
|
|
2049
|
+
|
|
2050
|
+
# Manual-entry backdate window: a manual ``entry_date`` is rejected if it is in the
|
|
2051
|
+
# future or older than this many days, so a contributor can fill in last week but
|
|
2052
|
+
# not rewrite arbitrary history. Default 60 days.
|
|
2053
|
+
TIMETRACKING_BACKDATE_DAYS: int = env.int("TIMETRACKING_BACKDATE_DAYS", default=60)
|
|
2054
|
+
|
|
2055
|
+
# ---------------------------------------------------------------------------
|
|
2056
|
+
# Dead-letter inspector bulk actions (ADR-0210)
|
|
2057
|
+
# ---------------------------------------------------------------------------
|
|
2058
|
+
|
|
2059
|
+
# Upper bound on how many parked tasks one `requeue_all` / `drop_all` call may act
|
|
2060
|
+
# on, oldest-first; the response's `capped` flag tells the operator more remain
|
|
2061
|
+
# (ADR-0210 §4). Documented as tunable in docs → Administration → System Health, and
|
|
2062
|
+
# for its whole life it was not: `scheduling/views.py` read it with
|
|
2063
|
+
# `getattr(settings, "FAILED_TASK_BULK_ACTION_MAX", 500)` and no settings module
|
|
2064
|
+
# bound it, so the fallback always won and setting the env var did nothing (#2880 —
|
|
2065
|
+
# the same class as TRUEPPM_INTEGRATION_ALLOWED_HOSTS in #2860). The env var is
|
|
2066
|
+
# TRUEPPM_-prefixed per #1355; the setting name keeps its established spelling
|
|
2067
|
+
# because the view constant and its tests already reference it.
|
|
2068
|
+
FAILED_TASK_BULK_ACTION_MAX: int = env.int("TRUEPPM_FAILED_TASK_BULK_ACTION_MAX", default=500)
|
|
2069
|
+
|
|
2070
|
+
# ---------------------------------------------------------------------------
|
|
2071
|
+
# Outbox retention + Beat liveness (ADR-0081)
|
|
2072
|
+
# ---------------------------------------------------------------------------
|
|
2073
|
+
|
|
2074
|
+
# Retention window in days for terminal (SUCCESS/FAILED) WebhookDelivery rows.
|
|
2075
|
+
# Set to None to disable the nightly purge. New tunables are TRUEPPM_-prefixed
|
|
2076
|
+
# for env-var namespacing in shared ConfigMaps/Secrets (ADR-0081 §D).
|
|
2077
|
+
TRUEPPM_WEBHOOK_RETENTION_DAYS: int | None = env.int("TRUEPPM_WEBHOOK_RETENTION_DAYS", default=7)
|
|
2078
|
+
|
|
2079
|
+
# Retention window in days for terminal (DONE/DEAD) ImportRequest rows. These
|
|
2080
|
+
# carry multi-MB file_content_b64 blobs. Set to None to disable the nightly purge.
|
|
2081
|
+
TRUEPPM_IMPORT_RETENTION_DAYS: int | None = env.int("TRUEPPM_IMPORT_RETENTION_DAYS", default=7)
|
|
2082
|
+
|
|
2083
|
+
# Download-link validity (days) for a completed WorkspaceExportJob; past this the
|
|
2084
|
+
# nightly purge deletes the job row and its stored archive (ADR-0174). The full
|
|
2085
|
+
# archive can be large and contains every project's data, so it is not kept
|
|
2086
|
+
# indefinitely. Set to None to disable expiry/purge (links never lapse).
|
|
2087
|
+
TRUEPPM_EXPORT_RETENTION_DAYS: int | None = env.int("TRUEPPM_EXPORT_RETENTION_DAYS", default=7)
|
|
2088
|
+
|
|
2089
|
+
# Retention window (days) for the ⌘Z undo ledger rows behind paste-many and
|
|
2090
|
+
# cascade classification (ADR-0810, #2756) — undone or not. 30 days comfortably
|
|
2091
|
+
# exceeds any plausible "wait, I meant to undo that" gap while staying inside
|
|
2092
|
+
# the 90-day HISTORY_RETENTION_DAYS these tables sit alongside. Set to None to
|
|
2093
|
+
# disable the purge (ledger rows kept indefinitely).
|
|
2094
|
+
TRUEPPM_BATCH_OPERATION_RETENTION_DAYS: int | None = env.int(
|
|
2095
|
+
"TRUEPPM_BATCH_OPERATION_RETENTION_DAYS", default=30
|
|
2096
|
+
)
|
|
2097
|
+
|
|
2098
|
+
# Window in seconds during which the same person repeating the change whose
|
|
2099
|
+
# config-change notice was the last one sent for a project is not re-notified
|
|
2100
|
+
# (ADR-1174 §D). A different change, a different person, or an intervening
|
|
2101
|
+
# different notice always sends. 0 disables the cooldown.
|
|
2102
|
+
TRUEPPM_CONFIG_NOTICE_COOLDOWN_SECONDS: int = env.int(
|
|
2103
|
+
"TRUEPPM_CONFIG_NOTICE_COOLDOWN_SECONDS", default=600
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
# Age in seconds past which the Beat heartbeat is considered stale. Drives both
|
|
2107
|
+
# the GET /api/v1/health/beat/ stale flag and the beat.check_stale_heartbeat
|
|
2108
|
+
# WARNING log. Default 120 s = four missed 30 s beats.
|
|
2109
|
+
TRUEPPM_BEAT_STALE_SECONDS: int = env.int("TRUEPPM_BEAT_STALE_SECONDS", default=120)
|
|
2110
|
+
|
|
2111
|
+
# Escape hatch for a *deliberate* image-only rollback (#2806). /api/v1/readyz
|
|
2112
|
+
# reports not-ready when the database records migrations this image does not ship
|
|
2113
|
+
# ("migration_state": "ahead"), because the probe cannot tell an additive upgrade
|
|
2114
|
+
# — whose schema the old code can usually still serve — from a destructive one it
|
|
2115
|
+
# certainly cannot. Fail-closed is the right default, but it also blocks the
|
|
2116
|
+
# additive-only rollback the upgrade runbook recommends, so an operator who has
|
|
2117
|
+
# read the release's migrations and knows they are additive can set this to true
|
|
2118
|
+
# to let the older image serve. It never suppresses the "behind" gate: a
|
|
2119
|
+
# mid-upgrade pod is still kept out of the Service.
|
|
2120
|
+
TRUEPPM_READYZ_ALLOW_DB_AHEAD: bool = env.bool("TRUEPPM_READYZ_ALLOW_DB_AHEAD", default=False)
|
|
2121
|
+
|
|
2122
|
+
# Freshness/dedup window in hours for mobile sync upload batches (ADR-0082). A
|
|
2123
|
+
# duplicate upload with the same client_batch_id within this window replays the
|
|
2124
|
+
# stored response; past it, the id is allowed to re-run and the nightly
|
|
2125
|
+
# sync.purge_sync_batches task reaps the stale row.
|
|
2126
|
+
TRUEPPM_SYNC_BATCH_RETENTION_HOURS: int = env.int("TRUEPPM_SYNC_BATCH_RETENTION_HOURS", default=24)
|
|
2127
|
+
|
|
2128
|
+
# Retention window in hours for the WebSocket event replay buffer (ADR-0236,
|
|
2129
|
+
# #321). BoardEvent rows older than this are reaped by the nightly
|
|
2130
|
+
# sync.purge_board_events task so the buffer stays bounded; a client reconnecting
|
|
2131
|
+
# with a ?since= older than the retained window gets a resync_required frame and
|
|
2132
|
+
# refetches. Kept deliberately short (a reconnect gap is minutes, not days).
|
|
2133
|
+
TRUEPPM_BOARD_EVENT_RETENTION_HOURS: int = env.int(
|
|
2134
|
+
"TRUEPPM_BOARD_EVENT_RETENTION_HOURS", default=24
|
|
2135
|
+
)
|
|
2136
|
+
|
|
2137
|
+
# Retention window in days for soft-deleted (trashed) projects. Once a project has
|
|
2138
|
+
# been soft-deleted for longer than this, the consolidated retention purge
|
|
2139
|
+
# (retention.run_purge, ADR-0173) HARD-deletes it and all its child data (tasks,
|
|
2140
|
+
# dependencies, sprints, baselines, …) via DB CASCADE. Set to None to disable the
|
|
2141
|
+
# purge (trashed projects are retained unbounded). A project soft-deleted before
|
|
2142
|
+
# this column existed has a NULL deleted_at and is never auto-purged.
|
|
2143
|
+
TRUEPPM_PROJECT_SOFT_DELETE_RETENTION_DAYS: int | None = env.int(
|
|
2144
|
+
"TRUEPPM_PROJECT_SOFT_DELETE_RETENTION_DAYS", default=30
|
|
2145
|
+
)
|
|
2146
|
+
|
|
2147
|
+
# How long a manual retention purge may be considered "in progress" before the
|
|
2148
|
+
# run endpoint stops treating a RUNNING PurgeRow as blocking (ADR-0173 §G). Bounds
|
|
2149
|
+
# the API-level single-flight guard to the coordinator's Redis lock window so a
|
|
2150
|
+
# worker that died mid-run can't block all future manual runs with a stale row.
|
|
2151
|
+
RETENTION_PURGE_INFLIGHT_SECONDS: int = env.int("RETENTION_PURGE_INFLIGHT_SECONDS", default=600)
|
|
2152
|
+
|
|
2153
|
+
# Maximum rows (created + updated + deleted) in a single mobile sync upload
|
|
2154
|
+
# batch (ADR-0082). The batch applies in one transaction; this bounds how long
|
|
2155
|
+
# that transaction (and its per-task row locks) can be held by one request.
|
|
2156
|
+
TRUEPPM_SYNC_BATCH_MAX_ROWS: int = env.int("TRUEPPM_SYNC_BATCH_MAX_ROWS", default=500)
|
|
2157
|
+
|
|
2158
|
+
# Batch-wide cap on inline ``owners`` entries across a sync upload batch (#3643):
|
|
2159
|
+
# summed over every created/updated task row's ``owners`` list, independent of
|
|
2160
|
+
# the row cap above (few rows can still carry many owners each).
|
|
2161
|
+
TRUEPPM_SYNC_BATCH_MAX_OWNERS: int = env.int("TRUEPPM_SYNC_BATCH_MAX_OWNERS", default=500)
|
|
2162
|
+
|
|
2163
|
+
# Max simultaneously in-flight sync upload batches per user (#1756, defense in
|
|
2164
|
+
# depth over the SyncUploadThrottle rate cap). Each accepted batch is a heavy
|
|
2165
|
+
# transaction.atomic() (up to TRUEPPM_SYNC_BATCH_MAX_ROWS row locks + a CPM recalc
|
|
2166
|
+
# enqueue); under lock contention a burst of concurrent batches from one user can
|
|
2167
|
+
# tie up worker/DB resources *within* the per-minute rate cap. A per-user Redis
|
|
2168
|
+
# semaphore (INCR on entry / DECR in finally) bounds simultaneous heavy
|
|
2169
|
+
# transactions. Fails OPEN on a Redis error — parity with the rate throttle's
|
|
2170
|
+
# offline-client-retries reasoning would over-penalize here, and the rate cap
|
|
2171
|
+
# remains the hard bound. The counter carries a TTL so a worker that dies holding
|
|
2172
|
+
# a slot cannot leak it permanently.
|
|
2173
|
+
TRUEPPM_SYNC_MAX_CONCURRENT_BATCHES: int = env.int("TRUEPPM_SYNC_MAX_CONCURRENT_BATCHES", default=4)
|
|
2174
|
+
# TTL (seconds) on the per-user in-flight counter. A crashed worker's slot is
|
|
2175
|
+
# reclaimed once no further batch refreshes the key within this window; set
|
|
2176
|
+
# comfortably above the longest legitimate batch-apply time so an in-progress
|
|
2177
|
+
# slot is never reclaimed out from under a live request.
|
|
2178
|
+
TRUEPPM_SYNC_INFLIGHT_TTL_SECONDS: int = env.int("TRUEPPM_SYNC_INFLIGHT_TTL_SECONDS", default=120)
|
|
2179
|
+
|
|
2180
|
+
# Cursor pagination for the offline delta PULL (#1013). A cold start (since=0)
|
|
2181
|
+
# on a large project would otherwise materialize every row into one unbounded
|
|
2182
|
+
# multi-MB response, past the "500-task delta < 3s" mobile target. The pull now
|
|
2183
|
+
# returns at most PAGE_SIZE rows (across all collections) per request and the
|
|
2184
|
+
# client loops on the cursor. PAGE_SIZE is the default; MAX_PAGE_SIZE clamps a
|
|
2185
|
+
# client-requested page_size so one request can never re-open the unbounded cliff.
|
|
2186
|
+
TRUEPPM_SYNC_PULL_PAGE_SIZE: int = env.int("TRUEPPM_SYNC_PULL_PAGE_SIZE", default=1000)
|
|
2187
|
+
TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE: int = env.int("TRUEPPM_SYNC_PULL_MAX_PAGE_SIZE", default=5000)
|
|
2188
|
+
|
|
2189
|
+
# Retention window in days for per-row soft-deleted tombstones in live projects
|
|
2190
|
+
# (is_deleted=True rows on Task, Risk, Sprint, Dependency). Rows older than this
|
|
2191
|
+
# are hard-deleted by the nightly sync.reap_domain_tombstones task. The 90-day
|
|
2192
|
+
# default aligns with the HistoricalTask history window so tombstones are never
|
|
2193
|
+
# retained longer than the audit trail that references them (#1321).
|
|
2194
|
+
TRUEPPM_TOMBSTONE_RETENTION_DAYS: int = env.int("TRUEPPM_TOMBSTONE_RETENTION_DAYS", default=90)
|
|
2195
|
+
|
|
2196
|
+
# Look-ahead horizon (days) for lazy recurring-task occurrence generation (ADR-0090).
|
|
2197
|
+
# The hourly projects.generate_recurring_occurrences sweep materializes only
|
|
2198
|
+
# occurrences due within this window — a bounded look-ahead, never the full series.
|
|
2199
|
+
TRUEPPM_RECURRENCE_HORIZON_DAYS: int = env.int("TRUEPPM_RECURRENCE_HORIZON_DAYS", default=14)
|
|
2200
|
+
|
|
2201
|
+
# ---------------------------------------------------------------------------
|
|
2202
|
+
# Workflow execution engine (ADR-0080)
|
|
2203
|
+
# ---------------------------------------------------------------------------
|
|
2204
|
+
|
|
2205
|
+
# Dotted path to the WorkflowBackend implementation. The OSS default composes
|
|
2206
|
+
# the transactional outbox + Celery; enterprise editions register an alternate
|
|
2207
|
+
# (e.g. Temporal) by overriding this — the edition-routing pattern of ADR-0030.
|
|
2208
|
+
# Env vars standardized on the TRUEPPM_ prefix pre-0.3 (#1325); legacy bare names
|
|
2209
|
+
# read as a fallback so existing deploys keep working until they migrate.
|
|
2210
|
+
WORKFLOW_BACKEND = env.str(
|
|
2211
|
+
"TRUEPPM_WORKFLOW_BACKEND",
|
|
2212
|
+
default=env.str(
|
|
2213
|
+
"WORKFLOW_BACKEND",
|
|
2214
|
+
default="trueppm_api.workflows.backends.default.DefaultWorkflowBackend",
|
|
2215
|
+
),
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
# Retention window in days for WorkflowHistoryEvent rows (purged nightly by
|
|
2219
|
+
# workflows.purge_old_records). Set to None / 0 to disable history purging.
|
|
2220
|
+
WORKFLOW_HISTORY_RETENTION_DAYS: int | None = env.int(
|
|
2221
|
+
"TRUEPPM_WORKFLOW_HISTORY_RETENTION_DAYS",
|
|
2222
|
+
default=env.int("WORKFLOW_HISTORY_RETENTION_DAYS", default=30),
|
|
2223
|
+
)
|
|
2224
|
+
|
|
2225
|
+
# Max rows the workflow outbox/timer drains process per tick. Bounds the work
|
|
2226
|
+
# per run so a large backlog (e.g. after a broker outage) can't exceed the task
|
|
2227
|
+
# time_limit — subsequent ticks drain the remainder.
|
|
2228
|
+
WORKFLOW_DRAIN_BATCH_SIZE = env.int(
|
|
2229
|
+
"TRUEPPM_WORKFLOW_DRAIN_BATCH_SIZE", default=env.int("WORKFLOW_DRAIN_BATCH_SIZE", default=200)
|
|
2230
|
+
)
|
|
2231
|
+
|
|
2232
|
+
# Rows deleted per statement by the nightly workflow retention purge. The purge
|
|
2233
|
+
# deletes in bounded chunks rather than one unbounded statement so the first run
|
|
2234
|
+
# on a mature install (e.g. after WORKFLOW_HISTORY_RETENTION_DAYS is first set)
|
|
2235
|
+
# cannot take a long lock over a large slice of the history/outbox tables.
|
|
2236
|
+
WORKFLOW_PURGE_BATCH_SIZE = env.int(
|
|
2237
|
+
"TRUEPPM_WORKFLOW_PURGE_BATCH_SIZE", default=env.int("WORKFLOW_PURGE_BATCH_SIZE", default=500)
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
# ---------------------------------------------------------------------------
|
|
2241
|
+
# Idempotency-Key retention (trueppm_api.apps.idempotency, ADR-0170)
|
|
2242
|
+
# ---------------------------------------------------------------------------
|
|
2243
|
+
|
|
2244
|
+
# Retention window in hours for stored Idempotency-Key responses. Purged hourly by
|
|
2245
|
+
# the Celery beat task in trueppm_api.apps.idempotency.tasks. After expiry, a retry
|
|
2246
|
+
# with the same key re-runs the mutation. Set to None to disable automatic purging.
|
|
2247
|
+
# Env vars standardized on the TRUEPPM_ prefix pre-0.3 (#1325); legacy bare names
|
|
2248
|
+
# read as a fallback so existing deploys keep working until they migrate.
|
|
2249
|
+
IDEMPOTENCY_RETENTION_HOURS: int | None = env.int(
|
|
2250
|
+
"TRUEPPM_IDEMPOTENCY_RETENTION_HOURS",
|
|
2251
|
+
default=env.int("IDEMPOTENCY_RETENTION_HOURS", default=24),
|
|
2252
|
+
)
|
|
2253
|
+
# Maximum stored response body size (bytes). Responses larger than this are not stored
|
|
2254
|
+
# (the claim row is dropped, so a retry re-runs). Mutation responses are single objects
|
|
2255
|
+
# and effectively never approach this.
|
|
2256
|
+
IDEMPOTENCY_MAX_BODY_BYTES: int = env.int(
|
|
2257
|
+
"TRUEPPM_IDEMPOTENCY_MAX_BODY_BYTES",
|
|
2258
|
+
default=env.int("IDEMPOTENCY_MAX_BODY_BYTES", default=1 * 1024 * 1024),
|
|
2259
|
+
)
|
|
2260
|
+
|
|
2261
|
+
# ---------------------------------------------------------------------------
|
|
2262
|
+
# drf-spectacular (OpenAPI)
|
|
2263
|
+
# ---------------------------------------------------------------------------
|
|
2264
|
+
|
|
2265
|
+
SPECTACULAR_SETTINGS = {
|
|
2266
|
+
"TITLE": "TruePPM API",
|
|
2267
|
+
"DESCRIPTION": "REST API for the TruePPM project scheduling platform.",
|
|
2268
|
+
# Derived, never restated (#2605). This read "0.3.0" while the package was
|
|
2269
|
+
# 0.4.0-beta.1, so every generated client and every published schema consumer
|
|
2270
|
+
# was told the wrong version — and the literal sat ~900 lines below
|
|
2271
|
+
# _package_version(), the anti-drift helper written to prevent exactly this.
|
|
2272
|
+
# A version that has to be updated by hand at tag time is a version that will
|
|
2273
|
+
# be wrong between tags.
|
|
2274
|
+
"VERSION": TRUEPPM_VERSION,
|
|
2275
|
+
"SERVE_INCLUDE_SCHEMA": False,
|
|
2276
|
+
"COMPONENT_SPLIT_REQUEST": True,
|
|
2277
|
+
# Meaningful top-level tag block (#1333). drf-spectacular defaults every
|
|
2278
|
+
# /api/v1/ operation to the tag "v1", which collapses a generated client into a
|
|
2279
|
+
# single API class. Defining tags here (plus the resource-tag assignment in the
|
|
2280
|
+
# post-processing hook below) splits the SDK into ProjectsApi, SprintsApi,
|
|
2281
|
+
# SchedulingApi, … the way an SDK consumer expects. Kept in lockstep with the
|
|
2282
|
+
# tag-assignment map in trueppm_api.core.openapi (defined inline rather than
|
|
2283
|
+
# imported: settings must not import DRF-dependent app code, which would force
|
|
2284
|
+
# rest_framework's api_settings to cache before REST_FRAMEWORK is defined).
|
|
2285
|
+
"TAGS": [
|
|
2286
|
+
{
|
|
2287
|
+
"name": "auth",
|
|
2288
|
+
"description": "Authentication, login, token issuance and OIDC single sign-on.",
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
"name": "me",
|
|
2292
|
+
"description": "The authenticated user's own account, credentials, timers and work.",
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
"name": "workspace",
|
|
2296
|
+
"description": "Workspace-level settings, membership, invites and branding.",
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
"name": "projects",
|
|
2300
|
+
"description": "Projects and their project-level views (overview, status, history).",
|
|
2301
|
+
},
|
|
2302
|
+
{
|
|
2303
|
+
"name": "programs",
|
|
2304
|
+
"description": "Programs — grouping of related projects managed by one PM.",
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
"name": "tasks",
|
|
2308
|
+
"description": "Tasks, dependencies, milestones, acceptance criteria and task runs.",
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
"name": "task-relations",
|
|
2312
|
+
"description": "Informational task-to-task relations (relates to, blocks, "
|
|
2313
|
+
"duplicates) — cross-references with no scheduling effect.",
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
"name": "scheduling",
|
|
2317
|
+
"description": "CPM scheduling, baselines, Monte Carlo forecasts and slip analysis.",
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
"name": "sprints",
|
|
2321
|
+
"description": "Sprints, backlog, velocity, retrospectives and agile ceremonies.",
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
"name": "resources",
|
|
2325
|
+
"description": "Resources, skills, allocation, utilization and assignments.",
|
|
2326
|
+
},
|
|
2327
|
+
{"name": "calendars", "description": "Working calendars, exceptions and recurrence rules."},
|
|
2328
|
+
{"name": "teams", "description": "Teams and team membership."},
|
|
2329
|
+
{"name": "members", "description": "Project and program membership management."},
|
|
2330
|
+
{
|
|
2331
|
+
"name": "integrations",
|
|
2332
|
+
"description": "External task sources, Git automation and personal credentials.",
|
|
2333
|
+
},
|
|
2334
|
+
{
|
|
2335
|
+
"name": "assets",
|
|
2336
|
+
"description": "Workspace-scoped Assets feed: file/URL attachments and task links "
|
|
2337
|
+
"aggregated across every project the caller can read.",
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
"name": "import-export",
|
|
2341
|
+
"description": (
|
|
2342
|
+
"File import and export: MS Project, Jira XML, CSV/Excel spreadsheets, "
|
|
2343
|
+
"import templates, and data exports."
|
|
2344
|
+
),
|
|
2345
|
+
},
|
|
2346
|
+
{"name": "webhooks", "description": "Outbound webhook subscriptions."},
|
|
2347
|
+
{
|
|
2348
|
+
"name": "sync",
|
|
2349
|
+
"description": "Offline delta-sync protocol and WebSocket connection tickets.",
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
"name": "share",
|
|
2353
|
+
"description": "Public read-only board share links: mint, list, revoke, and the "
|
|
2354
|
+
"public unauthenticated read endpoint.",
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
"name": "agent-actions",
|
|
2358
|
+
"description": "Append-only, hash-chained audit trail of MCP/agent read actions "
|
|
2359
|
+
"and refusals, scoped to project membership.",
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
"name": "meta",
|
|
2363
|
+
"description": "Deployment metadata, health probes and administrative endpoints.",
|
|
2364
|
+
},
|
|
2365
|
+
],
|
|
2366
|
+
# Fill SDK-quality facets (operation summaries, resource tags, global + public
|
|
2367
|
+
# security) that per-view annotations leave thin. Runs after the built-in enum
|
|
2368
|
+
# post-processor, which must be preserved (drf-spectacular default).
|
|
2369
|
+
"POSTPROCESSING_HOOKS": [
|
|
2370
|
+
"drf_spectacular.hooks.postprocess_schema_enums",
|
|
2371
|
+
"trueppm_api.core.openapi.postprocess_openapi",
|
|
2372
|
+
],
|
|
2373
|
+
# Serve the Swagger UI / ReDoc assets from the bundled sidecar package (Django
|
|
2374
|
+
# static, same origin) instead of the default jsdelivr CDN. Our strict CSP
|
|
2375
|
+
# (script-src / style-src 'self', no CDN host) blocks the CDN-hosted bundles,
|
|
2376
|
+
# leaving the docs pages blank. SIDECAR resolves every asset under 'self', so
|
|
2377
|
+
# the pages work under CSP and offline/air-gapped. Paired with the split
|
|
2378
|
+
# Swagger view in urls.py, which moves the bootstrap JS out of an inline
|
|
2379
|
+
# <script> (also blocked by script-src 'self') to a same-origin request.
|
|
2380
|
+
"SWAGGER_UI_DIST": "SIDECAR",
|
|
2381
|
+
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
|
2382
|
+
"REDOC_DIST": "SIDECAR",
|
|
2383
|
+
# Self-hosted deployments serve the API from their own origin, so the schema
|
|
2384
|
+
# advertises a single templated server (scheme + host variables) whose
|
|
2385
|
+
# defaults are the local-dev address. Without a top-level `servers` array,
|
|
2386
|
+
# openapi-typescript / Orval / openapi-generator emit base-URL-less clients
|
|
2387
|
+
# or fail outright (#1329). Paths already carry the `/api/v1/` prefix, so the
|
|
2388
|
+
# server URL is just the deployment origin.
|
|
2389
|
+
"SERVERS": [
|
|
2390
|
+
{
|
|
2391
|
+
"url": "{scheme}://{host}",
|
|
2392
|
+
"description": "Your TruePPM deployment",
|
|
2393
|
+
"variables": {
|
|
2394
|
+
"scheme": {"default": "https", "enum": ["https", "http"]},
|
|
2395
|
+
"host": {
|
|
2396
|
+
"default": "localhost:8000",
|
|
2397
|
+
"description": "Host (and port) of your TruePPM API server.",
|
|
2398
|
+
},
|
|
2399
|
+
},
|
|
2400
|
+
},
|
|
2401
|
+
],
|
|
2402
|
+
# Pin state-enum names (ADR-0173). PurgeRun.state shares the field name "state"
|
|
2403
|
+
# with the sprint lifecycle enum; introducing a second "state" choice set makes
|
|
2404
|
+
# drf-spectacular disambiguate *both* by model prefix, renaming the sprint enum
|
|
2405
|
+
# away from the stable `StateEnum` component (a schema regression). Pinning the
|
|
2406
|
+
# sprint enum to `StateEnum` and ours to `PurgeRunStateEnum` keeps both stable.
|
|
2407
|
+
# Likewise for "frequency": TaskRecurrenceRule.frequency (#736) introduces a
|
|
2408
|
+
# second choice set sharing the field name with RetentionSchedule.frequency,
|
|
2409
|
+
# so drf-spectacular would disambiguate both by model prefix and rename the
|
|
2410
|
+
# stable `FrequencyEnum` component (a regression). Pin the retention enum back
|
|
2411
|
+
# to `FrequencyEnum` and the recurrence enum to `RecurrenceFrequencyEnum`.
|
|
2412
|
+
"ENUM_NAME_OVERRIDES": {
|
|
2413
|
+
# #3037: the bulk refusal vocabulary. Pinned rather than left to
|
|
2414
|
+
# drf-spectacular's derived naming because these two names are the handle a
|
|
2415
|
+
# generated client's types carry — the whole point of publishing the enum is
|
|
2416
|
+
# that an integrator can branch on it, and a component that renames itself
|
|
2417
|
+
# when an unrelated field's choices change would break exactly that.
|
|
2418
|
+
# `code` appears on three buckets of one response and must resolve to ONE
|
|
2419
|
+
# component, not three postfixed variants.
|
|
2420
|
+
"TaskBulkRefusalCodeEnum": "trueppm_api.apps.projects.refusal_codes.BulkRefusalCode",
|
|
2421
|
+
"StructuralUndoBlockedReasonEnum": (
|
|
2422
|
+
"trueppm_api.apps.projects.refusal_codes.StructuralUndoBlockedReason"
|
|
2423
|
+
),
|
|
2424
|
+
# #3680: MonteCarloForecastDiagnosticSerializer.reason introduces a SECOND
|
|
2425
|
+
# "reason" choice set alongside TaskGroupLeftAloneEntrySerializer.reason
|
|
2426
|
+
# (an inline tuple, not an enum class — hence the raw-list value, same
|
|
2427
|
+
# shape as OutcomeEnum below). Without a pin drf-spectacular disambiguates
|
|
2428
|
+
# both by serializer prefix and renames the published `ReasonEnum` — the
|
|
2429
|
+
# project_drf_enum_name_collision regression every pin in this block
|
|
2430
|
+
# exists to prevent. Pin the incumbent to its stable name; the new
|
|
2431
|
+
# Monte Carlo set is then free to take the prefixed form.
|
|
2432
|
+
"ReasonEnum": ["ancestor_selected", "different_parent"],
|
|
2433
|
+
"StateEnum": "trueppm_api.apps.projects.models.SprintState",
|
|
2434
|
+
"PurgeRunStateEnum": "trueppm_api.apps.observability.models.PurgeRun.State",
|
|
2435
|
+
"PurgeRunTriggerEnum": "trueppm_api.apps.observability.models.PurgeRun.Trigger",
|
|
2436
|
+
"FrequencyEnum": "trueppm_api.apps.observability.models.RetentionSchedule.Frequency",
|
|
2437
|
+
"RecurrenceFrequencyEnum": "trueppm_api.apps.projects.models.TaskRecurrenceFrequency",
|
|
2438
|
+
# ADR-0102: SprintScopeChange.status (pending|accepted|rejected) introduces
|
|
2439
|
+
# a new "status" choice set; without a pin drf-spectacular disambiguates
|
|
2440
|
+
# every "status" field by model prefix and renames existing stable
|
|
2441
|
+
# components (a schema-drift regression — project memory
|
|
2442
|
+
# project_drf_enum_name_collision). Pin ours to a stable name.
|
|
2443
|
+
"ScopeChangeStatusEnum": "trueppm_api.apps.projects.models.ScopeChangeStatus",
|
|
2444
|
+
# ADR-0789 (#2729): ProjectTemplate.source_kind introduces a SECOND
|
|
2445
|
+
# "source_kind" choice set alongside Task.source_kind (ADR-0786). Without
|
|
2446
|
+
# a pin drf-spectacular disambiguates both by model prefix, which renames
|
|
2447
|
+
# the already-published `SourceKindEnum` to `TaskSourceKindEnum` — a
|
|
2448
|
+
# removed component, and exactly the schema-drift regression the two pins
|
|
2449
|
+
# above exist to prevent. Pin Task's to its existing stable name; the
|
|
2450
|
+
# template's own set is then free to take the model-prefixed name.
|
|
2451
|
+
"SourceKindEnum": "trueppm_api.apps.projects.models.TaskSource",
|
|
2452
|
+
# ADR-0880 (#2974): StructuralOperation.kind introduces a SECOND "kind"
|
|
2453
|
+
# choice set alongside AssetItemSerializer.kind (file|link). Without a pin
|
|
2454
|
+
# drf-spectacular disambiguates both by prefix, renaming the published
|
|
2455
|
+
# `KindEnum` to `AssetItemKindEnum` — a removed component referenced by
|
|
2456
|
+
# AgentAction, AssetItem, ShareLink, ShareLinkCreateRequest,
|
|
2457
|
+
# ShareLinkCreateResponse and Task. Same regression class as every pin in
|
|
2458
|
+
# this block. Keep the incumbent's name; the new set takes the prefixed one.
|
|
2459
|
+
"KindEnum": "trueppm_api.apps.projects.asset_feed.KIND_CHOICES",
|
|
2460
|
+
# #3525: health_band_source is declared on BOTH the single-project status
|
|
2461
|
+
# summary and the my-projects health summary. Identical choices already
|
|
2462
|
+
# resolve to one component, but the pin is proactive rather than
|
|
2463
|
+
# incidental: the next endpoint that grows a "source" choice set would
|
|
2464
|
+
# otherwise make drf-spectacular disambiguate every one of them by prefix
|
|
2465
|
+
# and rename the published component — the project_drf_enum_name_collision
|
|
2466
|
+
# regression every pin in this block exists to prevent. Pinned to the
|
|
2467
|
+
# shared constant the two declarations reference, so the name cannot drift
|
|
2468
|
+
# from the vocabulary.
|
|
2469
|
+
"HealthBandSourceEnum": "trueppm_api.apps.projects.views.HEALTH_BAND_SOURCE_CHOICES",
|
|
2470
|
+
"TemplateSourceEnum": "trueppm_api.apps.projects.models.TemplateSource",
|
|
2471
|
+
# ADR-0116: Workspace.iteration_label_override_policy adds an
|
|
2472
|
+
# INHERIT/SUGGEST/ENFORCE choice set. Pin it so drf-spectacular does not
|
|
2473
|
+
# rename existing stable enums (the same schema-drift regression class as
|
|
2474
|
+
# ScopeChangeStatus above — project_drf_enum_name_collision).
|
|
2475
|
+
"TermOverridePolicyEnum": "trueppm_api.apps.workspace.models.TermOverridePolicy",
|
|
2476
|
+
# #2723: TaskBulkAppliedEntry.outcome adds a SECOND "outcome" choice set,
|
|
2477
|
+
# alongside the telemetry export result's. Without a pin drf-spectacular
|
|
2478
|
+
# disambiguates both by serializer prefix and *renames* the existing stable
|
|
2479
|
+
# OutcomeEnum to TelemetryTestExportResultOutcomeEnum — a published component
|
|
2480
|
+
# disappearing, which breaks generated SDKs and trips schema-check. Pinning
|
|
2481
|
+
# keeps the incumbent's name and lets the new one take the prefixed form.
|
|
2482
|
+
# Same regression class as ScopeChangeStatus above (project_drf_enum_name_collision).
|
|
2483
|
+
"OutcomeEnum": ["success", "reachable", "failure"],
|
|
2484
|
+
# ADR-0107: Workspace gains a third use of Methodology (after Project +
|
|
2485
|
+
# Program) and methodology_override_policy reuses TermOverridePolicy. Pin
|
|
2486
|
+
# Methodology to its stable component name proactively so the third use does
|
|
2487
|
+
# not trip the drf-spectacular enum-name-collision / schema-drift regression
|
|
2488
|
+
# (project_drf_enum_name_collision), same class as the entries above.
|
|
2489
|
+
"MethodologyEnum": "trueppm_api.apps.projects.models.Methodology",
|
|
2490
|
+
# ADR-0105: Task gains task_type / readiness and Project gains
|
|
2491
|
+
# backlog_scoring_model. Each is a new choice set on a *_type / status-adjacent
|
|
2492
|
+
# field; pin to model-prefixed names so drf-spectacular does not rename the
|
|
2493
|
+
# existing stable enums (same regression class as ScopeChangeStatus above).
|
|
2494
|
+
"TaskTypeEnum": "trueppm_api.apps.projects.models.TaskType",
|
|
2495
|
+
# ADR-0638 (#2333): the program label view adds a second serializer
|
|
2496
|
+
# exposing Task.status, which was enough for drf-spectacular to stop
|
|
2497
|
+
# resolving the stable `TaskStatusEnum` and emit a hashed
|
|
2498
|
+
# `Status3ebEnum` in its place — renaming the component on every
|
|
2499
|
+
# *existing* task endpoint, i.e. a breaking change to generated clients
|
|
2500
|
+
# for a purely additive read view. Same regression class as the entries
|
|
2501
|
+
# above (project_drf_enum_name_collision); pin it.
|
|
2502
|
+
"TaskStatusEnum": "trueppm_api.apps.projects.models.TaskStatus",
|
|
2503
|
+
"DorStateEnum": "trueppm_api.apps.projects.models.DorState",
|
|
2504
|
+
"PrioritizationModelEnum": "trueppm_api.apps.projects.models.PrioritizationModel",
|
|
2505
|
+
# ADR-0078 (#927): TeamMembership.role (member|admin) is a second "role"
|
|
2506
|
+
# choice set sharing the field name with the access Role ordinal. Without
|
|
2507
|
+
# a pin drf-spectacular disambiguates both by hash and renames the stable
|
|
2508
|
+
# `RoleEnum` component (same regression class as ScopeChangeStatus above —
|
|
2509
|
+
# project memory project_drf_enum_name_collision). Pin the access role back
|
|
2510
|
+
# to `RoleEnum` and the team role to `TeamRoleEnum`. `RoleEnum` never reaches
|
|
2511
|
+
# the published schema: the post-processing hook finds it by this pinned name
|
|
2512
|
+
# and replaces it with the banded-integer `RoleOrdinal` (ADR-1291), so the
|
|
2513
|
+
# pin is what keeps that hook from silently missing a renamed component.
|
|
2514
|
+
"RoleEnum": "trueppm_api.apps.access.models.Role",
|
|
2515
|
+
"TeamRoleEnum": "trueppm_api.apps.teams.models.TeamRole",
|
|
2516
|
+
# ADR-0036 (#407): Task.governance_class / .delivery_mode add two new choice
|
|
2517
|
+
# sets. Pin to stable model-prefixed names so a later delivery_mode on another
|
|
2518
|
+
# model (e.g. Project, #410) cannot trigger a drf-spectacular rename of the
|
|
2519
|
+
# existing component (same regression class as ScopeChangeStatus — project
|
|
2520
|
+
# memory project_drf_enum_name_collision).
|
|
2521
|
+
"GovernanceClassEnum": "trueppm_api.apps.projects.models.GovernanceClass",
|
|
2522
|
+
"DeliveryModeEnum": "trueppm_api.apps.projects.models.DeliveryMode",
|
|
2523
|
+
# ADR-0106 §5 (#860): ForecastSnapshot.basis / .confidence introduce two
|
|
2524
|
+
# new choice sets on "basis" / "confidence" fields. Pin to model-prefixed
|
|
2525
|
+
# names so drf-spectacular does not rename existing stable components
|
|
2526
|
+
# (same regression class as ScopeChangeStatus — project memory
|
|
2527
|
+
# project_drf_enum_name_collision).
|
|
2528
|
+
"ForecastBasisEnum": "trueppm_api.apps.projects.models.ForecastBasis",
|
|
2529
|
+
"ForecastConfidenceEnum": "trueppm_api.apps.projects.models.ForecastConfidence",
|
|
2530
|
+
# ADR-0104 (#553): SignalAudience is a fresh ladder enum used in the
|
|
2531
|
+
# signal-privacy serializer; pin it to a stable name so a future "audience"
|
|
2532
|
+
# field cannot collide-and-rename it (project memory
|
|
2533
|
+
# project_drf_enum_name_collision).
|
|
2534
|
+
"SignalAudienceEnum": "trueppm_api.apps.projects.models.SignalAudience",
|
|
2535
|
+
# #983: Sprint.goal_outcome (MET|PARTIAL|MISSED) is a new choice set; pin
|
|
2536
|
+
# to a stable model-prefixed name so drf-spectacular does not rename
|
|
2537
|
+
# existing components (same regression class as ScopeChangeStatus —
|
|
2538
|
+
# project memory project_drf_enum_name_collision).
|
|
2539
|
+
"SprintGoalOutcomeEnum": "trueppm_api.apps.projects.models.SprintGoalOutcome",
|
|
2540
|
+
# #985: SprintTaskDisposition is exposed in the /outcome/ read serializer
|
|
2541
|
+
# (DidntShipItemSerializer.disposition). Pin to a stable name so
|
|
2542
|
+
# drf-spectacular does not hash-disambiguate it (project memory
|
|
2543
|
+
# project_drf_enum_name_collision).
|
|
2544
|
+
"SprintTaskDispositionEnum": "trueppm_api.apps.projects.models.SprintTaskDisposition",
|
|
2545
|
+
# ADR-0124 (#1135): Task.blocker_type adds a new structured-blocker choice
|
|
2546
|
+
# set. Pin to a stable model-prefixed name so drf-spectacular does not
|
|
2547
|
+
# hash-disambiguate or rename existing components (same regression class as
|
|
2548
|
+
# ScopeChangeStatus above — project memory project_drf_enum_name_collision).
|
|
2549
|
+
"BlockerTypeEnum": "trueppm_api.apps.projects.models.BlockerType",
|
|
2550
|
+
# #3416: annotating the four batch-undo actions adds a SECOND serializer
|
|
2551
|
+
# exposing StructuralOperation.kind and TemplateApplication.status (the
|
|
2552
|
+
# `*Undo` response wrappers subclass the ledger serializers). A second
|
|
2553
|
+
# exposure of an already-resolved choice set is all it takes for
|
|
2554
|
+
# drf-spectacular to stop deriving the stable name and emit a hashed
|
|
2555
|
+
# `KindD4dEnum` / `StatusCf2Enum` instead — renaming the component on every
|
|
2556
|
+
# *existing* endpoint that serves those rows, i.e. a Breaking change under
|
|
2557
|
+
# `api/stability.md` for a purely additive response annotation. Exactly the
|
|
2558
|
+
# TaskStatusEnum case above, and the same regression class as every pin in
|
|
2559
|
+
# this block (project memory project_drf_enum_name_collision).
|
|
2560
|
+
"StructuralOperationKindEnum": ("trueppm_api.apps.projects.models.StructuralOperationKind"),
|
|
2561
|
+
"TemplateApplicationStatusEnum": (
|
|
2562
|
+
"trueppm_api.apps.projects.models.TemplateApplicationStatus"
|
|
2563
|
+
),
|
|
2564
|
+
# ADR-0219 (#1266): ProjectExportJob.status introduces a second export-job
|
|
2565
|
+
# status choice set with the SAME members (pending|running|success|failed)
|
|
2566
|
+
# as workspace.ExportJobStatus. drf-spectacular unifies identical value-sets
|
|
2567
|
+
# into ONE component, and without a pin it hash-disambiguates and DROPS the
|
|
2568
|
+
# existing WorkspaceExportJobStatusEnum (a schema-drift regression — project
|
|
2569
|
+
# memory project_drf_enum_name_collision). Pin the shared value-set to the
|
|
2570
|
+
# already-published WorkspaceExportJobStatusEnum name so no component is
|
|
2571
|
+
# removed; the project export field simply reuses that stable component.
|
|
2572
|
+
"WorkspaceExportJobStatusEnum": "trueppm_api.apps.workspace.models.ExportJobStatus",
|
|
2573
|
+
# ADR-0112 RC1 (#1805): the AgentAction read serializer exposes verdict,
|
|
2574
|
+
# refusal_reason, and actor_kind choice sets. "verdict"/"refusal_reason" are
|
|
2575
|
+
# generic field names a future model could reuse; pin to stable model-prefixed
|
|
2576
|
+
# names so drf-spectacular does not hash-disambiguate or rename them (same
|
|
2577
|
+
# regression class as ScopeChangeStatus — project_drf_enum_name_collision).
|
|
2578
|
+
"AgentActionVerdictEnum": "trueppm_api.apps.agents.models.AgentActionVerdict",
|
|
2579
|
+
"AgentActionRefusalReasonEnum": "trueppm_api.apps.agents.models.AgentActionRefusalReason",
|
|
2580
|
+
"AgentActorKindEnum": "trueppm_api.apps.agents.models.AgentActorKind",
|
|
2581
|
+
# ADR-0421 (#1850): the refusal side-car exposes a "constraint" choice set;
|
|
2582
|
+
# "constraint" is a generic field name, so pin it to a stable model-prefixed
|
|
2583
|
+
# component name (same rationale as the verdict/refusal_reason enums above).
|
|
2584
|
+
"RefusalConstraintEnum": "trueppm_api.apps.agents.models.RefusalConstraint",
|
|
2585
|
+
# #3679: ProjectSprintHealthSignalSerializer.key ("orphan" |
|
|
2586
|
+
# "phase_span" | "summary_in_sprint") introduces a second "key" choice
|
|
2587
|
+
# set alongside RetentionPolicy.key, which is enough for drf-spectacular
|
|
2588
|
+
# to stop resolving the stable `KeyEnum` and rename it to
|
|
2589
|
+
# `RetentionPolicyWriteKeyEnum` — a removed component (same regression
|
|
2590
|
+
# class as every pin in this block, project_drf_enum_name_collision).
|
|
2591
|
+
# Pin the incumbent back to its stable name; the sprint-health set is
|
|
2592
|
+
# then free to take the serializer-prefixed name it already resolves to.
|
|
2593
|
+
"KeyEnum": "trueppm_api.apps.observability.retention.RETENTION_KEYS",
|
|
2594
|
+
# #3684: giving the weekly time-entry rollup (`GET /me/time-entries/`) a
|
|
2595
|
+
# real response schema exposes `TimeEntryWeeklySerializer.source`, a SECOND
|
|
2596
|
+
# serializer surfacing `TimeEntry.source` alongside `TimeEntrySerializer`'s.
|
|
2597
|
+
# A second exposure of an already-resolved choice set is exactly what makes
|
|
2598
|
+
# drf-spectacular stop deriving the stable `TimeEntrySourceEnum` name and
|
|
2599
|
+
# hash-disambiguate both instead — renaming the component on every
|
|
2600
|
+
# *existing* time-entry endpoint for a purely additive response annotation
|
|
2601
|
+
# (same regression class as ScopeChangeStatus above —
|
|
2602
|
+
# project_drf_enum_name_collision). Pin it.
|
|
2603
|
+
"TimeEntrySourceEnum": "trueppm_api.apps.timetracking.models.TimeEntrySource",
|
|
2604
|
+
},
|
|
2605
|
+
}
|