stemtrace 0.3.6__tar.gz → 0.3.8__tar.gz

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 (167) hide show
  1. {stemtrace-0.3.6 → stemtrace-0.3.8}/.bumpversion.toml +25 -3
  2. {stemtrace-0.3.6 → stemtrace-0.3.8}/.dockerignore +16 -5
  3. {stemtrace-0.3.6 → stemtrace-0.3.8}/.github/dependabot.yml +15 -0
  4. {stemtrace-0.3.6 → stemtrace-0.3.8}/.github/workflows/ci.yml +2 -2
  5. {stemtrace-0.3.6 → stemtrace-0.3.8}/.github/workflows/e2e.yml +1 -1
  6. {stemtrace-0.3.6 → stemtrace-0.3.8}/.github/workflows/release.yml +27 -27
  7. {stemtrace-0.3.6 → stemtrace-0.3.8}/.gitignore +1 -0
  8. {stemtrace-0.3.6 → stemtrace-0.3.8}/.pre-commit-config.yaml +5 -3
  9. {stemtrace-0.3.6 → stemtrace-0.3.8}/CHANGELOG.md +37 -1
  10. {stemtrace-0.3.6 → stemtrace-0.3.8}/CONTRIBUTING.md +12 -0
  11. {stemtrace-0.3.6 → stemtrace-0.3.8}/Dockerfile +3 -2
  12. {stemtrace-0.3.6 → stemtrace-0.3.8}/Makefile +47 -12
  13. {stemtrace-0.3.6 → stemtrace-0.3.8}/PKG-INFO +78 -8
  14. {stemtrace-0.3.6 → stemtrace-0.3.8}/README.md +77 -7
  15. {stemtrace-0.3.6 → stemtrace-0.3.8}/pyproject.toml +2 -6
  16. stemtrace-0.3.8/scripts/changelog_section.py +59 -0
  17. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/__init__.py +65 -56
  18. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/core/graph.py +238 -5
  19. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/config.py +5 -4
  20. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/signals.py +13 -6
  21. stemtrace-0.3.8/src/stemtrace/library/transports/breaker.py +426 -0
  22. stemtrace-0.3.8/src/stemtrace/library/transports/rabbitmq.py +649 -0
  23. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/__main__.py +46 -41
  24. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/api/__init__.py +2 -0
  25. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/api/routes.py +64 -36
  26. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/api/schemas.py +6 -0
  27. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/api/websocket.py +11 -6
  28. stemtrace-0.3.8/src/stemtrace/server/fastapi/form_auth.py +342 -0
  29. stemtrace-0.3.8/src/stemtrace/server/fastapi/form_auth_middleware.py +154 -0
  30. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/login_routes.py +14 -0
  31. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/store.py +145 -60
  32. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/biome.json +2 -2
  33. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/package-lock.json +128 -136
  34. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/package.json +6 -6
  35. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/api/client.ts +115 -21
  36. stemtrace-0.3.8/src/stemtrace/server/ui/frontend/src/components/LogoutButton.tsx +20 -0
  37. stemtrace-0.3.8/src/stemtrace/server/ui/frontend/src/components/SessionRejectedNotice.tsx +31 -0
  38. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +67 -28
  39. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +7 -2
  40. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +27 -20
  41. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/main.tsx +3 -0
  42. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +29 -14
  43. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +14 -55
  44. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +2 -0
  45. stemtrace-0.3.8/src/stemtrace/server/ui/frontend/tests/auth.spec.ts +119 -0
  46. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +2 -0
  47. stemtrace-0.3.8/src/stemtrace/server/ui/frontend/tests/header.spec.ts +186 -0
  48. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +119 -1
  49. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/static.py +22 -75
  50. stemtrace-0.3.8/tests/conftest.py +97 -0
  51. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/e2e/conftest.py +44 -0
  52. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/e2e/test_api_e2e.py +14 -4
  53. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/e2e/test_workers_e2e.py +1 -1
  54. stemtrace-0.3.8/tests/integration/test_rabbitmq_publish.py +349 -0
  55. stemtrace-0.3.8/tests/integration/test_rabbitmq_transport.py +342 -0
  56. stemtrace-0.3.8/tests/unit/test_changelog_section_script.py +102 -0
  57. stemtrace-0.3.8/tests/unit/test_cli.py +171 -0
  58. stemtrace-0.3.8/tests/unit/test_cli_form_login.py +280 -0
  59. stemtrace-0.3.8/tests/unit/test_form_auth.py +389 -0
  60. stemtrace-0.3.8/tests/unit/test_form_login.py +808 -0
  61. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_graph.py +626 -1
  62. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_init.py +169 -0
  63. stemtrace-0.3.8/tests/unit/test_publish_breaker.py +771 -0
  64. stemtrace-0.3.8/tests/unit/test_rabbitmq_publish.py +705 -0
  65. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_routes.py +280 -12
  66. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_signals.py +50 -3
  67. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_store.py +238 -2
  68. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_transports.py +755 -35
  69. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_ui_static_prefix.py +39 -4
  70. {stemtrace-0.3.6 → stemtrace-0.3.8}/uv.lock +592 -329
  71. stemtrace-0.3.6/src/stemtrace/library/transports/rabbitmq.py +0 -250
  72. stemtrace-0.3.6/src/stemtrace/server/fastapi/form_auth.py +0 -157
  73. stemtrace-0.3.6/tests/conftest.py +0 -18
  74. stemtrace-0.3.6/tests/integration/test_rabbitmq_transport.py +0 -139
  75. stemtrace-0.3.6/tests/unit/test_form_auth.py +0 -135
  76. stemtrace-0.3.6/tests/unit/test_form_login.py +0 -293
  77. {stemtrace-0.3.6 → stemtrace-0.3.8}/.python-version +0 -0
  78. {stemtrace-0.3.6 → stemtrace-0.3.8}/Dockerfile.e2e +0 -0
  79. {stemtrace-0.3.6 → stemtrace-0.3.8}/LICENSE +0 -0
  80. {stemtrace-0.3.6 → stemtrace-0.3.8}/build_ui.py +0 -0
  81. {stemtrace-0.3.6 → stemtrace-0.3.8}/docker-compose.e2e.rabbitmq.yml +0 -0
  82. {stemtrace-0.3.6 → stemtrace-0.3.8}/docker-compose.e2e.yml +0 -0
  83. {stemtrace-0.3.6 → stemtrace-0.3.8}/docker-compose.rabbitmq.yml +0 -0
  84. {stemtrace-0.3.6 → stemtrace-0.3.8}/docker-compose.yml +0 -0
  85. {stemtrace-0.3.6 → stemtrace-0.3.8}/docs/brand/stemtrace-mark-light.svg +0 -0
  86. {stemtrace-0.3.6 → stemtrace-0.3.8}/docs/brand/stemtrace-mark.svg +0 -0
  87. {stemtrace-0.3.6 → stemtrace-0.3.8}/docs/screenshots/task_details.png +0 -0
  88. {stemtrace-0.3.6 → stemtrace-0.3.8}/docs/screenshots/unregistered.png +0 -0
  89. {stemtrace-0.3.6 → stemtrace-0.3.8}/docs/screenshots/workflow.png +0 -0
  90. {stemtrace-0.3.6 → stemtrace-0.3.8}/examples/celery_app.py +0 -0
  91. {stemtrace-0.3.6 → stemtrace-0.3.8}/examples/fastapi_integration.py +0 -0
  92. {stemtrace-0.3.6 → stemtrace-0.3.8}/examples/with_auth.py +0 -0
  93. {stemtrace-0.3.6 → stemtrace-0.3.8}/examples/with_login.py +0 -0
  94. {stemtrace-0.3.6 → stemtrace-0.3.8}/progress.md +0 -0
  95. {stemtrace-0.3.6 → stemtrace-0.3.8}/scripts/wait_for_http.py +0 -0
  96. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/core/__init__.py +0 -0
  97. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/core/events.py +0 -0
  98. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/core/exceptions.py +0 -0
  99. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/core/ports.py +0 -0
  100. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/__init__.py +0 -0
  101. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/bootsteps.py +0 -0
  102. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/scrubbing.py +0 -0
  103. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/transports/__init__.py +0 -0
  104. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/transports/memory.py +0 -0
  105. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/library/transports/redis.py +0 -0
  106. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/py.typed +0 -0
  107. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/__init__.py +0 -0
  108. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/consumer.py +0 -0
  109. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/__init__.py +0 -0
  110. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/auth.py +0 -0
  111. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/extension.py +0 -0
  112. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/fastapi/router.py +0 -0
  113. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/__init__.py +0 -0
  114. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/index.html +0 -0
  115. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
  116. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
  117. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
  118. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
  119. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
  120. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/SproutMark.tsx +0 -0
  121. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
  122. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
  123. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
  124. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
  125. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
  126. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
  127. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
  128. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
  129. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
  130. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
  131. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
  132. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
  133. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
  134. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
  135. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
  136. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
  137. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
  138. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
  139. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
  140. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
  141. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/ui/templates/login.html +0 -0
  142. {stemtrace-0.3.6 → stemtrace-0.3.8}/src/stemtrace/server/websocket.py +0 -0
  143. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/__init__.py +0 -0
  144. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/e2e/__init__.py +0 -0
  145. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/e2e/tasks.py +0 -0
  146. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/__init__.py +0 -0
  147. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_embedded_static.py +0 -0
  148. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_fastapi_integration.py +0 -0
  149. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_redis_resilience_docker.py +0 -0
  150. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_redis_transport.py +0 -0
  151. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_static_ui.py +0 -0
  152. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_wait_for_http_redirects.py +0 -0
  153. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/integration/test_workers_refresh.py +0 -0
  154. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/__init__.py +0 -0
  155. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_auth.py +0 -0
  156. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_bootsteps.py +0 -0
  157. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_build_ui_hook.py +0 -0
  158. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_config.py +0 -0
  159. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_consumer.py +0 -0
  160. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_events.py +0 -0
  161. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_examples.py +0 -0
  162. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_exceptions.py +0 -0
  163. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_favicon.py +0 -0
  164. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_redis_resilience.py +0 -0
  165. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_scrubbing.py +0 -0
  166. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_wait_for_http_script.py +0 -0
  167. {stemtrace-0.3.6 → stemtrace-0.3.8}/tests/unit/test_websocket.py +0 -0
@@ -1,10 +1,10 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.3.6"
2
+ current_version = "0.3.8"
3
3
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
4
4
  serialize = ["{major}.{minor}.{patch}"]
5
5
  commit = true
6
- tag = false # Tag created via GitHub Release UI
7
- commit_message = "chore: release v{new_version}"
6
+ tag = false # Tagged after the release PR merges (make release-tag)
7
+ message = "chore: release v{new_version}"
8
8
 
9
9
  # Python package version in pyproject.toml
10
10
  [[tool.bumpversion.files]]
@@ -24,6 +24,21 @@ filename = "src/stemtrace/server/ui/frontend/package.json"
24
24
  search = '"version": "{current_version}"'
25
25
  replace = '"version": "{new_version}"'
26
26
 
27
+ # Frontend package-lock.json: root version and the root package entry
28
+ [[tool.bumpversion.files]]
29
+ filename = "src/stemtrace/server/ui/frontend/package-lock.json"
30
+ search = """"name": "stemtrace-ui",
31
+ "version": "{current_version}\""""
32
+ replace = """"name": "stemtrace-ui",
33
+ "version": "{new_version}\""""
34
+
35
+ [[tool.bumpversion.files]]
36
+ filename = "src/stemtrace/server/ui/frontend/package-lock.json"
37
+ search = """"name": "stemtrace-ui",
38
+ "version": "{current_version}\""""
39
+ replace = """"name": "stemtrace-ui",
40
+ "version": "{new_version}\""""
41
+
27
42
  # uv.lock - keep workspace lockfile in sync (uv stores the local project version)
28
43
  [[tool.bumpversion.files]]
29
44
  filename = "uv.lock"
@@ -45,3 +60,10 @@ replace = """## [Unreleased]
45
60
  filename = "README.md"
46
61
  search = "[![PyPI version](https://img.shields.io/badge/pypi-v{current_version}-darklime)](https://pypi.org/project/stemtrace)"
47
62
  replace = "[![PyPI version](https://img.shields.io/badge/pypi-v{new_version}-darklime)](https://pypi.org/project/stemtrace)"
63
+
64
+ # CHANGELOG compare links: advance [unreleased], add the new version's link
65
+ [[tool.bumpversion.files]]
66
+ filename = "CHANGELOG.md"
67
+ search = "[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v{current_version}...HEAD"
68
+ replace = """[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v{new_version}...HEAD
69
+ [{new_version}]: https://github.com/iansokolskyi/stemtrace/compare/v{current_version}...v{new_version}"""
@@ -2,9 +2,9 @@
2
2
  .git
3
3
  .gitignore
4
4
 
5
- # Python
6
- __pycache__
7
- *.py[cod]
5
+ # Python (patterns without **/ only match at the context root)
6
+ **/__pycache__
7
+ **/*.py[cod]
8
8
  *$py.class
9
9
  *.so
10
10
  .Python
@@ -37,13 +37,23 @@ ENV/
37
37
 
38
38
  # Testing
39
39
  .pytest_cache/
40
+ .mypy_cache/
41
+ .ruff_cache/
40
42
  .coverage
41
43
  htmlcov/
42
44
  .tox/
43
45
  .nox/
44
46
 
45
47
  # Node (rebuilt in container)
46
- node_modules/
48
+ **/node_modules/
49
+
50
+ # Frontend build output and test artifacts (the UI is built in the
51
+ # frontend stage; a local dist/ would be merged into the image's copy)
52
+ src/stemtrace/server/ui/frontend/dist/
53
+ src/stemtrace/server/ui/frontend/test-results/
54
+ src/stemtrace/server/ui/frontend/playwright-report/
55
+ src/stemtrace/server/ui/frontend/.tanstack/
56
+ src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo
47
57
 
48
58
  # Documentation
49
59
  docs/_build/
@@ -53,9 +63,10 @@ docs/_build/
53
63
  .env.local
54
64
  *.log
55
65
 
56
- # Cursor/Editor
66
+ # Cursor/Editor/agent tooling
57
67
  .cursor/
58
68
  .context/
69
+ .claude/
59
70
 
60
71
  # Keep these (they're built in Dockerfile)
61
72
  !src/stemtrace/server/ui/frontend/package*.json
@@ -7,6 +7,10 @@ updates:
7
7
  interval: weekly
8
8
  commit-message:
9
9
  prefix: "chore(deps)"
10
+ ignore:
11
+ # Dependabot's uv updater fails on the build backend; updated by the
12
+ # weekly maintenance routine
13
+ - dependency-name: "hatchling"
10
14
 
11
15
  - package-ecosystem: npm
12
16
  directory: /src/stemtrace/server/ui/frontend
@@ -14,6 +18,10 @@ updates:
14
18
  interval: weekly
15
19
  commit-message:
16
20
  prefix: "chore(deps)"
21
+ ignore:
22
+ # Pinned to ^22: types must match the Node 22 runtime used in CI and Docker
23
+ - dependency-name: "@types/node"
24
+ update-types: ["version-update:semver-major"]
17
25
 
18
26
  - package-ecosystem: github-actions
19
27
  directory: /
@@ -29,3 +37,10 @@ updates:
29
37
  interval: weekly
30
38
  commit-message:
31
39
  prefix: "chore(deps)"
40
+ ignore:
41
+ # The shipped Python runtime is deliberate: stay on 3.12.x patch updates
42
+ - dependency-name: "python"
43
+ update-types: ["version-update:semver-major", "version-update:semver-minor"]
44
+ # UI is built with Node 22 LTS: allow minor/patch updates only
45
+ - dependency-name: "node"
46
+ update-types: ["version-update:semver-major"]
@@ -135,7 +135,7 @@ jobs:
135
135
  - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
136
136
 
137
137
  - name: Set up Node.js
138
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
138
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
139
139
  with:
140
140
  node-version: "22"
141
141
  cache: "npm"
@@ -170,7 +170,7 @@ jobs:
170
170
  enable-cache: true
171
171
 
172
172
  - name: Set up Node.js
173
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
173
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
174
174
  with:
175
175
  node-version: "22"
176
176
  cache: "npm"
@@ -153,7 +153,7 @@ jobs:
153
153
  sleep 5
154
154
 
155
155
  - name: Set up Node.js
156
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
156
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
157
157
  with:
158
158
  node-version: "22"
159
159
  cache: "npm"
@@ -1,19 +1,9 @@
1
1
  name: Release
2
2
 
3
3
  on:
4
+ # Releases are tagged on main after the release PR merges (make release-tag).
4
5
  push:
5
6
  tags: ["v*"]
6
- # Manual trigger from Actions tab
7
- workflow_dispatch:
8
- inputs:
9
- version:
10
- description: "Version to release (e.g., 0.1.0) - must match version in pyproject.toml"
11
- required: true
12
- type: string
13
-
14
- env:
15
- # Determine version from tag or manual input
16
- VERSION: ${{ github.event.inputs.version || github.ref_name }}
17
7
 
18
8
  jobs:
19
9
  build:
@@ -25,17 +15,14 @@ jobs:
25
15
  tag: ${{ steps.version.outputs.tag }}
26
16
  steps:
27
17
  - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
18
+ with:
19
+ fetch-depth: 0 # full history for the "tag is on main" check
28
20
 
29
21
  - name: Determine version
30
22
  id: version
31
23
  run: |
32
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
33
- echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
34
- echo "tag=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
35
- else
36
- echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
37
- echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
38
- fi
24
+ echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
25
+ echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
39
26
 
40
27
  - name: Verify version matches pyproject.toml
41
28
  run: |
@@ -50,13 +37,26 @@ jobs:
50
37
  fi
51
38
  echo "✅ Version $PYPROJECT_VERSION matches"
52
39
 
40
+ - name: Verify the tagged commit is on main
41
+ run: |
42
+ # Releases are tagged after the release PR merges; never from a branch.
43
+ git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
44
+ echo "❌ ${GITHUB_REF_NAME} points at a commit that isn't on main"; exit 1; }
45
+
46
+ - name: Check release notes exist in CHANGELOG.md
47
+ run: |
48
+ # Fail before anything is published: every release needs notes.
49
+ python3 scripts/changelog_section.py "$VERSION"
50
+ env:
51
+ VERSION: ${{ steps.version.outputs.version }}
52
+
53
53
  - name: Set up Python
54
54
  uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
55
55
  with:
56
56
  python-version: "3.12"
57
57
 
58
58
  - name: Set up Node.js
59
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
59
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
60
60
  with:
61
61
  node-version: "22"
62
62
  cache: "npm"
@@ -112,7 +112,7 @@ jobs:
112
112
  - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
113
113
 
114
114
  - name: Set up QEMU
115
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
115
+ uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
116
116
 
117
117
  - name: Set up Docker Buildx
118
118
  uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
@@ -158,17 +158,17 @@ jobs:
158
158
  name: dist
159
159
  path: dist/
160
160
 
161
- - name: Create tag (for manual dispatch)
162
- if: github.event_name == 'workflow_dispatch'
163
- run: |
164
- git config user.name "github-actions[bot]"
165
- git config user.email "github-actions[bot]@users.noreply.github.com"
166
- git tag -a "${{ needs.build.outputs.tag }}" -m "Release ${{ needs.build.outputs.tag }}"
167
- git push origin "${{ needs.build.outputs.tag }}"
161
+ - name: Extract release notes from CHANGELOG.md
162
+ run: python3 scripts/changelog_section.py "$VERSION" > "$RUNNER_TEMP/release_notes.md"
163
+ env:
164
+ VERSION: ${{ needs.build.outputs.version }}
168
165
 
169
166
  - name: Create GitHub Release
170
167
  uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
171
168
  with:
172
169
  tag_name: ${{ needs.build.outputs.tag }}
170
+ # CHANGELOG.md section first; GitHub appends contributors, PR list
171
+ # and the compare link.
172
+ body_path: ${{ runner.temp }}/release_notes.md
173
173
  generate_release_notes: true
174
174
  files: dist/*
@@ -33,3 +33,4 @@ src/stemtrace/server/ui/frontend/src/routeTree.gen.ts
33
33
  src/stemtrace/server/ui/frontend/playwright-report/
34
34
  src/stemtrace/server/ui/frontend/test-results/
35
35
  src/stemtrace/server/ui/frontend/playwright/.cache/
36
+ .release-notes.md
@@ -11,13 +11,15 @@ repos:
11
11
  - id: debug-statements
12
12
 
13
13
  - repo: https://github.com/astral-sh/ruff-pre-commit
14
- rev: v0.14.0
14
+ rev: v0.16.10
15
15
  hooks:
16
16
  # Linter (auto-fix enabled)
17
- - id: ruff
17
+ - id: ruff-check
18
18
  args: [--fix]
19
- # Formatter
19
+ # Formatter (Python only: the hook also formats Markdown code blocks,
20
+ # which would undo the hand-aligned comments in the README examples)
20
21
  - id: ruff-format
22
+ types_or: [python, pyi]
21
23
 
22
24
  # Local hooks use project environment (uv) for proper dependency access
23
25
  - repo: local
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.8] - 2026-10-10
11
+
12
+ ### Fixed
13
+ - `node_alias_from_arguments` can now be set on the standalone server and Docker image: `stemtrace server --node-alias-from-arguments <key>` or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=<key>` (a kwarg name or a positional index). `init_app()` reads the same env var when the argument isn't passed (#69).
14
+ - RabbitMQ: publishing from workers no longer blocks tasks while the broker is down, stalled or in a memory/disk alarm. Each publish tries every host of the URL once with a 2s timeout; repeated or slow failures open a circuit breaker that drops events (instead of queueing them) until the broker answers again, and the worker logs at most four warnings a minute with counts of dropped events. The README's broker section has the details and limits.
15
+ - RabbitMQ: the server's consumer now reconnects after the broker restarts or drops the connection; it used to retry forever on a dead channel.
16
+ - RabbitMQ: a broker outage no longer floods the server log with one ERROR traceback per second; reconnects back off from 1s to 30s and log one line per attempt, plus one when the connection is back (thanks @ohadmata — #70, #71).
17
+ - RabbitMQ: a message the consumer can't decode is dropped with a warning instead of blocking the queue forever.
18
+ - RabbitMQ: stopping the server no longer waits out the reconnect delay or hangs on a frozen broker.
19
+
20
+ ### Added
21
+ - Health endpoint: `consumer_connected` is now reported for RabbitMQ, so `/api/health` shows `degraded` while the server can't read from the broker.
22
+
23
+ ### Deprecated
24
+ - `init_worker(node_alias_from_arguments=...)` has no effect (node names are resolved by the server) and now logs a deprecation warning; it will be removed in a future major release.
25
+
26
+ ## [0.3.7] - 2026-10-10
27
+
28
+ ### Changed
29
+ - Docker: the image's `HEALTHCHECK` now calls the new `<prefix>/api/health/live` endpoint instead of `/stemtrace/api/health`, which returns 401 under form login (so containers with login enabled reported unhealthy). The liveness endpoint needs no session and checks only that the process is serving: it always answers 200, with an informational `{"status": "ok"}`, or `{"status": "degraded"}` by the same rule as `/api/health` (the event consumer stopped unexpectedly or can't read from the broker; a consumer stopped on purpose or whose event source finished is not degraded).
30
+ - Docker image: updated the locked runtime dependencies `fastapi` 0.141.1 → 0.142.2 (which now pulls in `opentelemetry-api` 1.45.1; stemtrace doesn't configure tracing, so it stays a no-op), `pydantic` 2.12.5 → 2.13.5 (`pydantic-core` 2.41.5 → 2.46.5), `celery` 5.6.0 → 5.6.3, `typer` 0.21.0 → 0.27.3, `typing-extensions` 4.15.0 → 4.16.0 and `websockets` 15.0.1 → 16.1.1 (used by uvicorn's WebSocket support). Only the lockfile changed: the package's dependency ranges are the same, so pip installs are unaffected. With typer 0.27, `--help` shows option types as `<str>`/`<int>` instead of `TEXT`/`INTEGER`.
31
+ - Docker image: building the image from a local checkout no longer copies the frontend's `node_modules/`, `dist/` or test artifacts into the build context, so stale local UI assets can't end up in the image.
32
+ - UI: updated `@tanstack/react-query` 5.94.5 → 5.104.1 in the bundled frontend.
33
+
34
+ ### Fixed
35
+ - Form login: clear warnings and an in-app explanation when sessions are rejected because workers/replicas don't share `STEMTRACE_LOGIN_SECRET`. Without an explicit secret each process signs cookies with its own random secret, so under `uvicorn --workers N`, gunicorn or several replicas a cookie issued by one process is rejected by the others (random 401s, unstyled UI, login loops). stemtrace now logs a startup warning when form login runs without a secret (mentioning `WEB_CONCURRENCY` > 1 or gunicorn when detected). In that mode, when a well-formed, unexpired session cookie fails signature verification and the issue time it carries (`iat`, now included in the signed session payload; unverified in this case and used only to classify the rejection, never to grant access) is after the process started (so it most likely came from a sibling worker or replica), it also logs a rate-limited warning (also for rejected WebSocket handshakes) and returns `"reason": "session_secret_mismatch"` in API 401 responses, and HTML routes redirect to the login page with `reason=session_mismatch`, which shows an explanation. Cookies from before a restart, and any badly signed cookie when a secret is configured, get a plain 401 / login redirect.
36
+ - Form login: the static UI bundle under `/assets/` (the public open-source JS/CSS build, no data) is now served without a session, so the UI always loads and is styled instead of rendering a blank or unstyled page when a bundle request is rejected. The UI routes, API and WebSocket stay protected, and paths containing `.` or `..` segments (including percent-encoded variants), or an encoded `?` or `#` (`%3F`, `%23`), are rejected with 404 before any exemption applies. Exemptions are decided on the decoded path that routing matches, so e.g. `/api/health/live%3Fx` can't pass as the liveness probe. Paths under `assets/` that aren't real bundle files return 404 instead of the app shell.
37
+ - UI: the Logout control is now a button in the header instead of a floating overlay injected by the server, which covered the connection status. On narrow screens the header wraps onto extra rows instead of overflowing, and the status tooltip stays inside the viewport.
38
+ - UI: a 401 from the API (missing or expired session) now redirects to the login page with `next` set to the current page, instead of showing "Failed to load ..." errors. A 401 with `reason: "session_secret_mismatch"` shows an explanation instead, since signing in again wouldn't help. Redirects happen at most once per page and not again within a few seconds of the last one, which prevents redirect loops. The WebSocket reconnect now backs off exponentially (3s up to 60s) instead of retrying every 3s forever.
39
+ - Task state no longer regresses when events arrive out of order. The sender-side `PENDING` event (from `task_sent`, which fires after publish) could reach the stream after the worker's `STARTED`/`SUCCESS`, and the task showed as `PENDING` again. Node state now comes from the most advanced event, and the result is the same for every arrival order. A terminal state (`SUCCESS`/`FAILURE`/`REVOKED`/`REJECTED`) always beats a non-terminal one, and between terminals the later one wins. Among other states, a newer attempt (higher `retries`) wins, then lifecycle order (`PENDING` < `RECEIVED` < `STARTED`/`RETRY`), with `STARTED` vs `RETRY` in the same attempt decided by time (a redelivered run after a connection-loss retry shows as `STARTED`). A task's events are kept in lifecycle order: the first attempt's `PENDING` then `RECEIVED` first (even when stamped after the task was received or finished), then everything else by timestamp, with a fixed order for identical timestamps. `first_seen`, `last_updated`, `duration_ms`, date filters, sorting and eviction follow that order instead of arrival order. Node aliases from task arguments (`node_alias_from_arguments`) are read from the first event that carries arguments, so a leading `RECEIVED` (which has none) no longer hides them. Task lists, task details and graphs are read from a single snapshot taken under the store lock, so a concurrent event can't make a response mix old and new data (e.g. a `?state=STARTED` filter returning a task that just finished). Naive event timestamps are treated as UTC. Exact duplicate events are ignored and not re-broadcast over the WebSocket.
40
+ - UI: the task timeline and event history follow the same order as the API. They used to sort by retry count first, so a failure raised in the worker main process (hard time limit, lost worker), which Celery reports with `retries=0`, appeared before later attempts and produced negative durations. A late-stamped `PENDING` is shown first, with its time clamped to the first run.
41
+ - The signal handler no longer raises `TypeError` when Celery cancels a running task (lost broker connection, cold shutdown): Celery sends `task_retry` without a `reason` on that path. The `RETRY` event is now created, with the exception text "cancelled by Celery". During a broker outage it still can't be delivered if stemtrace publishes through that same broker.
42
+
10
43
  ## [0.3.6] - 2026-10-10
11
44
 
12
45
  ### Security
@@ -148,7 +181,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
148
181
  - **E2E test suite**: Docker API tests + Playwright browser tests
149
182
  - **Comprehensive test suite**: 350+ Python tests, 90%+ coverage
150
183
 
151
- [unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.5...HEAD
184
+ [unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.8...HEAD
185
+ [0.3.8]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.7...v0.3.8
186
+ [0.3.7]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.6...v0.3.7
187
+ [0.3.6]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.5...v0.3.6
152
188
  [0.3.5]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.4...v0.3.5
153
189
  [0.3.4]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.3...v0.3.4
154
190
  [0.3.3]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.2...v0.3.3
@@ -136,6 +136,18 @@ uv lock --upgrade
136
136
  - Fire-and-forget pattern for publishers (never block)
137
137
  - Fakes over mocks in tests
138
138
 
139
+ ## Releasing (maintainers)
140
+
141
+ Every release goes through a pull request into `main` and is published by CI from a tag on `main`.
142
+
143
+ 1. Add user-facing entries under `## [Unreleased]` in `CHANGELOG.md`.
144
+ 2. `make release-pr BUMP=patch|minor|major` runs the checks, creates `release/vX.Y.Z`, bumps the
145
+ version and opens a PR whose description is the changelog section.
146
+ 3. When CI is green, merge the PR with a merge commit (not squash or rebase).
147
+ 4. `make release-tag` tags the merge on `main`. The tag triggers the Release workflow, which
148
+ publishes to PyPI and GHCR and creates the GitHub Release from the changelog section. It
149
+ refuses to publish if the section is empty or the tag isn't on `main`.
150
+
139
151
  ## Questions?
140
152
 
141
153
  - Open an issue for bugs or feature requests
@@ -60,9 +60,10 @@ ENV PORT="8000"
60
60
 
61
61
  EXPOSE 8000
62
62
 
63
- # Health check (CLI server mounts API at /stemtrace prefix)
63
+ # Health check (CLI server mounts API at /stemtrace prefix). The liveness
64
+ # endpoint needs no login session and answers 200 while the process serves.
64
65
  HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
65
- CMD python -c "import httpx; httpx.get('http://localhost:8000/stemtrace/api/health').raise_for_status()"
66
+ CMD python -c "import httpx; httpx.get('http://localhost:8000/stemtrace/api/health/live').raise_for_status()"
66
67
 
67
68
  # Default command: run server
68
69
  ENTRYPOINT ["stemtrace"]
@@ -1,4 +1,4 @@
1
- .PHONY: install check types lint format test coverage clean ui-install ui-dev ui-build e2e e2e-api e2e-playwright build release-check version bump-dry bump-patch bump-minor bump-major release
1
+ .PHONY: install check types lint format test coverage clean ui-install ui-dev ui-build e2e e2e-api e2e-playwright build release-check version bump-dry bump-patch bump-minor bump-major release release-pr release-tag
2
2
 
3
3
  # Install all dependencies
4
4
  install:
@@ -99,6 +99,8 @@ build:
99
99
  # Full pre-release checklist
100
100
  release-check:
101
101
  @echo "=== Pre-release Checklist ==="
102
+ @echo "0. Checking CHANGELOG.md has [Unreleased] entries..."
103
+ @uv run python scripts/changelog_section.py Unreleased > /dev/null
102
104
  @echo "1. Running all checks..."
103
105
  $(MAKE) check
104
106
  @echo ""
@@ -107,7 +109,6 @@ release-check:
107
109
  @echo ""
108
110
  @echo "✅ Ready to release!"
109
111
  @echo ""
110
- @echo "Next: make release"
111
112
 
112
113
  # =============================================================================
113
114
  # Versioning (bump-my-version)
@@ -126,7 +127,7 @@ bump-patch:
126
127
  @NEW_VER=$$(uv run bump-my-version show current_version); \
127
128
  echo "✅ Version bumped to $$NEW_VER"; \
128
129
  echo ""; \
129
- echo "Next: make release"
130
+ echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
130
131
 
131
132
  # Bump minor version (0.1.0 -> 0.2.0)
132
133
  bump-minor:
@@ -134,7 +135,7 @@ bump-minor:
134
135
  @NEW_VER=$$(uv run bump-my-version show current_version); \
135
136
  echo "✅ Version bumped to $$NEW_VER"; \
136
137
  echo ""; \
137
- echo "Next: make release"
138
+ echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
138
139
 
139
140
  # Bump major version (0.1.0 -> 1.0.0)
140
141
  bump-major:
@@ -142,20 +143,54 @@ bump-major:
142
143
  @NEW_VER=$$(uv run bump-my-version show current_version); \
143
144
  echo "✅ Version bumped to $$NEW_VER"; \
144
145
  echo ""; \
145
- echo "Next: make release"
146
+ echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
146
147
 
147
148
  # Tag and push to trigger release workflow
148
149
  release:
149
- @VERSION=$$(uv run bump-my-version show current_version); \
150
+ @echo "Releases go through a PR: make release-pr BUMP=patch|minor|major, then make release-tag."
151
+ @exit 1
152
+
153
+ # Open a release PR: preflight, checks, release/vX.Y.Z branch, version bump, PR with notes.
154
+ # Usage: make release-pr BUMP=patch|minor|major
155
+ release-pr:
156
+ @set -e; \
157
+ case "$(BUMP)" in patch|minor|major) ;; *) echo "Usage: make release-pr BUMP=patch|minor|major"; exit 1;; esac; \
158
+ test "$$(git rev-parse --abbrev-ref HEAD)" = main || { echo "❌ Run from main"; exit 1; }; \
159
+ test -z "$$(git status --porcelain)" || { echo "❌ Working tree not clean"; exit 1; }; \
160
+ gh auth status >/dev/null 2>&1 || { echo "❌ gh is not authenticated (gh auth login)"; exit 1; }; \
161
+ git fetch -q --tags origin main; \
162
+ test -z "$$(git log HEAD..origin/main --oneline)" || { echo "❌ main is behind origin/main"; exit 1; }; \
163
+ NEW=$$(uv run bump-my-version show new_version --increment $(BUMP)); \
164
+ ! git rev-parse -q --verify "refs/heads/release/v$$NEW" >/dev/null || { echo "❌ Local branch release/v$$NEW exists"; exit 1; }; \
165
+ test -z "$$(git ls-remote --heads origin "release/v$$NEW")" || { echo "❌ Remote branch release/v$$NEW exists"; exit 1; }; \
166
+ ! git rev-parse -q --verify "refs/tags/v$$NEW" >/dev/null || { echo "❌ Tag v$$NEW exists"; exit 1; }; \
167
+ $(MAKE) release-check; \
168
+ git switch -c "release/v$$NEW"; \
169
+ uv run bump-my-version bump $(BUMP); \
170
+ NOTES=$$(mktemp); trap 'rm -f "$$NOTES"' EXIT; \
171
+ uv run python scripts/changelog_section.py "$$NEW" > "$$NOTES"; \
172
+ git push -u origin "release/v$$NEW" || { echo "❌ Push failed; local branch release/v$$NEW holds the bump commit"; exit 1; }; \
173
+ gh pr create --base main --head "release/v$$NEW" --title "chore: release v$$NEW" --body-file "$$NOTES" \
174
+ || { echo "❌ PR creation failed; release/v$$NEW is pushed, open the PR manually"; exit 1; }; \
175
+ echo ""; \
176
+ echo "✅ Release PR opened for v$$NEW"; \
177
+ echo "Next: gh pr checks --watch, merge with a merge commit, then make release-tag"
178
+
179
+ # Tag the merged release on main; the tag push triggers the Release workflow.
180
+ release-tag:
181
+ @set -e; \
182
+ git switch -q main; \
183
+ git pull -q --ff-only origin main; \
184
+ VERSION=$$(uv run bump-my-version show current_version); \
150
185
  TAG="v$$VERSION"; \
151
- echo "Tagging $$TAG..."; \
186
+ SUBJECT=$$(git log -1 --format=%s); \
187
+ test "$$(git rev-list --parents -n 1 HEAD | wc -w)" -eq 3 || { echo "❌ HEAD of main is not a merge commit (merge the release PR with a merge commit)"; exit 1; }; \
188
+ case "$$SUBJECT" in *"/release/$$TAG"|*"release $$TAG"|*"release $$TAG "*) ;; *) echo "❌ HEAD of main is not the $$TAG release PR merge: $$SUBJECT"; exit 1;; esac; \
189
+ git fetch -q --tags origin; \
190
+ ! git rev-parse -q --verify "refs/tags/$$TAG" >/dev/null || { echo "❌ $$TAG already exists"; exit 1; }; \
152
191
  git tag -a "$$TAG" -m "Release $$TAG"; \
153
- echo "Pushing to origin..."; \
154
- git push origin main; \
155
192
  git push origin "$$TAG"; \
156
- echo ""; \
157
- echo "✅ Released $$TAG"; \
158
- echo " → GitHub Actions will publish to PyPI and Docker"
193
+ echo "✅ Tagged $$TAG → Release workflow publishes PyPI, GHCR and the GitHub Release"
159
194
 
160
195
  # =============================================================================
161
196
  # E2E Testing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stemtrace
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: A lightweight Celery task flow visualizer
5
5
  Project-URL: Homepage, https://github.com/iansokolskyi/stemtrace
6
6
  Project-URL: Documentation, https://github.com/iansokolskyi/stemtrace#readme
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
59
59
 
60
60
  **Zero-infrastructure Celery task flow visualizer**
61
61
 
62
- [![PyPI version](https://img.shields.io/badge/pypi-v0.3.6-darklime)](https://pypi.org/project/stemtrace)
62
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.3.8-darklime)](https://pypi.org/project/stemtrace)
63
63
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
64
64
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
65
65
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -253,10 +253,6 @@ stemtrace.init_worker(
253
253
  scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
254
254
  additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
255
255
  safe_keys=frozenset({"public_key"}), # Never scrub these keys
256
-
257
- # UI display
258
- node_alias_from_arguments="operator_type", # Use kwargs["operator_type"] as node name
259
- # node_alias_from_arguments="0", # Or use args[0] (digit string = positional index)
260
256
  )
261
257
 
262
258
  # Introspection (after init)
@@ -336,6 +332,7 @@ stemtrace.init_app(
336
332
  |----------|-------------|---------|
337
333
  | `STEMTRACE_BROKER_URL` | Celery broker URL (used for on-demand worker/registry inspection). Also used as the default for `STEMTRACE_TRANSPORT_URL`. | `redis://localhost:6379/0` |
338
334
  | `STEMTRACE_TRANSPORT_URL` | Event transport URL (where stemtrace publishes/consumes events). | Defaults to `STEMTRACE_BROKER_URL`. |
335
+ | `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS` | Server only: show a task argument as the graph node name (see [Node Names from Task Arguments](#node-names-from-task-arguments)). Same as `--node-alias-from-arguments` / `init_app(node_alias_from_arguments=...)`. | Unset (task names are shown). |
339
336
 
340
337
  ### Supported Brokers
341
338
 
@@ -356,6 +353,36 @@ again once events are re-consumed.
356
353
  - Events already consumed/acked by the server are **gone**.
357
354
  - Events published while the server is down are only visible after restart if the server’s
358
355
  durable per-consumer queue still exists and the messages are still within TTL.
356
+ - Events that can't be published to RabbitMQ are dropped, not buffered on the worker, and a
357
+ broker outage doesn't slow tasks down. Each publish tries every host of the broker URL once
358
+ (2s connect timeout, 2s per read or write after that). A single quick failure, such as a
359
+ refused connection, drops only that event; a failure that took 0.5s or more (a timeout) or
360
+ three quick failures in a row make the worker skip publishing for a cooldown that grows from
361
+ 1s to 30s and resets on the next successful publish.
362
+ - During a RabbitMQ memory or disk alarm, the broker accepts connections but stops reading
363
+ from publishers, so a publish is sent but never confirmed. The worker counts such events as
364
+ "sent but unconfirmed" (RabbitMQ may still deliver them once the alarm clears); after two
365
+ unconfirmed attempts in a row it retries only every 30s. Each attempt leaves one blocked
366
+ connection on the broker until the alarm clears, so expect 2 to 3 connections per worker
367
+ process in the first minute of an alarm and about 2 per minute after that, all released
368
+ when the alarm ends. Each retry also logs `Received method (10, 60) during closing channel 0`
369
+ from py-amqp's `amqp` logger (not stemtrace's): that's RabbitMQ's "connection blocked"
370
+ notice arriving while the connection closes.
371
+ - Logging is bounded: at most four warnings a minute however the broker behaves (failures,
372
+ an outage starting, an outage ending, and a summary). Each reports the number of events
373
+ dropped, and sent but unconfirmed, since the previous one, and losses are reported within
374
+ about 1.5 minutes even when the broker flaps.
375
+ - Limitation: a broker that answers but slowly (say, close to the 2s timeout for each step)
376
+ still costs every event several seconds, because publishes that eventually succeed don't
377
+ open the breaker.
378
+ - A failover broker URL (`amqp://h1//;amqp://h2//`) must list nodes of the same RabbitMQ
379
+ cluster: workers keep publishing to whichever host last worked, so the server must be able to
380
+ consume those events from any of them. Entries resolve as in kombu: a bare host (`;h2`)
381
+ takes user, password, port, virtual host and TLS options from the first entry, while a
382
+ URL-form entry (`;amqp://h2`) gets guest/guest unless it names credentials, and takes port
383
+ and virtual host from the previous entry only when it omits them (`//` means `/`). A host
384
+ that refuses the login ends that publish attempt without trying the later hosts, as in
385
+ kombu.
359
386
  - **Workers + Registry tabs**: stemtrace uses **Celery inspect** on demand to populate workers
360
387
  and registered tasks, so those pages work even if the server missed `worker_ready` events.
361
388
  - **If you need durable history across restarts**: point stemtrace events at Redis even if your
@@ -387,6 +414,15 @@ docker run -p 8000:8000 \
387
414
  ghcr.io/iansokolskyi/stemtrace
388
415
  ```
389
416
 
417
+ Show a task argument as the graph node name (here `kwargs["operator_type"]`):
418
+
419
+ ```bash
420
+ docker run -p 8000:8000 \
421
+ -e STEMTRACE_BROKER_URL=redis://host.docker.internal:6379/0 \
422
+ -e STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=operator_type \
423
+ ghcr.io/iansokolskyi/stemtrace
424
+ ```
425
+
390
426
  Or with Docker Compose:
391
427
 
392
428
  ```yaml
@@ -430,9 +466,26 @@ stemtrace server \
430
466
  --transport-url redis://myredis:6379/0 \
431
467
  --host 0.0.0.0 \
432
468
  --port 8000 \
469
+ --node-alias-from-arguments operator_type \
433
470
  --reload # For development
434
471
  ```
435
472
 
473
+ #### Node Names from Task Arguments
474
+
475
+ By default, graph nodes are labeled with the task name. Generic tasks (e.g. one `run_operator`
476
+ task that does different things depending on its arguments) are easier to tell apart when the
477
+ graph shows an argument instead:
478
+
479
+ - `--node-alias-from-arguments operator_type` uses `kwargs["operator_type"]`
480
+ - `--node-alias-from-arguments 0` uses `args[0]` (a digit string is a positional index)
481
+
482
+ If the argument is missing, the task name is shown. This is a **server** setting: set it on
483
+ `stemtrace server` (flag or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS`, e.g. in Docker) or pass
484
+ `node_alias_from_arguments` to `stemtrace.init_app(...)`. Surrounding whitespace is ignored, and
485
+ `stemtrace server` prints `Node alias: <key>` at startup when it is set. Workers need
486
+ `capture_args=True` (the default) so the arguments are available. Passing it to
487
+ `stemtrace.init_worker(...)` has no effect and is deprecated.
488
+
436
489
  #### Protecting the Server (Built-in Login Page)
437
490
 
438
491
  ```bash
@@ -443,6 +496,10 @@ stemtrace server \
443
496
  --login-secret change-me
444
497
  ```
445
498
 
499
+ Set `--login-secret` (or `STEMTRACE_LOGIN_SECRET`) to a long random value, and use the same
500
+ value on every replica. Without it, each process generates its own random secret, so sessions
501
+ only work within a single process and are lost on restart.
502
+
446
503
  #### High-Scale Production Setup
447
504
 
448
505
  Note: `stemtrace server` includes an embedded consumer today (single-process). A multi-process deployment mode is planned.
@@ -482,6 +539,7 @@ extension = stemtrace.init_app(
482
539
  embedded_consumer=True, # Run consumer in FastAPI process
483
540
  serve_ui=True, # Serve React dashboard
484
541
  auth_dependency=None, # Optional auth (see below)
542
+ node_alias_from_arguments=None, # e.g. "operator_type" or "0"; see Node Names from Task Arguments
485
543
  )
486
544
  ```
487
545
 
@@ -512,7 +570,19 @@ stemtrace.init_app(
512
570
  )
513
571
  ```
514
572
 
515
- This serves a sign-in page at `/stemtrace/login` and protects everything under `/stemtrace`.
573
+ This serves a sign-in page at `/stemtrace/login` and protects the UI, API and WebSocket under
574
+ `/stemtrace`. The static JS/CSS bundle under `/stemtrace/assets/` is the public UI build and is
575
+ served without a session. So is `/stemtrace/api/health/live`, a liveness probe for load
576
+ balancers and container health checks: it always answers 200 while the process is serving,
577
+ with only `{"status": "ok"}`, or `{"status": "degraded"}` when the embedded consumer stopped
578
+ unexpectedly or can't read from the broker (the same rule as `/stemtrace/api/health`).
579
+
580
+ For any multi-process deployment (`uvicorn --workers N`, gunicorn, or several replicas behind
581
+ a load balancer), set `login_secret` (or the `STEMTRACE_LOGIN_SECRET` env var) to the same long
582
+ random value everywhere, e.g. `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
583
+ Without it, each process signs sessions with its own random secret, and users get logged out
584
+ at random. stemtrace logs a warning at startup in that case, and the login page and UI explain
585
+ the problem when a session signed by another process is rejected.
516
586
 
517
587
  #### Built-in Auth Helpers (Basic / API key)
518
588
 
@@ -551,7 +621,7 @@ Because the status code stays 200, check `status` in the body if you want monito
551
621
  What is detected depends on the transport:
552
622
 
553
623
  - **Redis:** refused or reset connections, connect timeouts, repeated read timeouts, and consumer loop failures. On redis-py < 8 the default socket timeout is unbounded, so a half-open or hung broker connection may go unnoticed; add `?socket_timeout=…` or `?health_check_interval=…` to the broker URL if you need that.
554
- - **RabbitMQ:** connectivity isn't reported (`consumer_connected` is `null`), so `"degraded"` only reflects consumer loop failures.
624
+ - **RabbitMQ:** refused or dropped connections and channel errors while connecting or draining, and consumer loop failures. The consumer connection is read about once a second and a read that times out counts as connected, so a half-open connection, or a broker that is frozen but keeps the socket open, may go unnoticed. Messages the consumer can't decode (not JSON) are dropped with a warning and don't count as failures.
555
625
 
556
626
  ## 🗺️ Roadmap
557
627