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