trueppm-api 0.4.0b2__py3-none-any.whl

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