stemtrace 0.3.6__tar.gz → 0.3.7__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 (158) hide show
  1. {stemtrace-0.3.6 → stemtrace-0.3.7}/.bumpversion.toml +16 -1
  2. {stemtrace-0.3.6 → stemtrace-0.3.7}/.dockerignore +16 -5
  3. {stemtrace-0.3.6 → stemtrace-0.3.7}/.github/dependabot.yml +15 -0
  4. {stemtrace-0.3.6 → stemtrace-0.3.7}/.github/workflows/ci.yml +2 -2
  5. {stemtrace-0.3.6 → stemtrace-0.3.7}/.github/workflows/e2e.yml +1 -1
  6. {stemtrace-0.3.6 → stemtrace-0.3.7}/.github/workflows/release.yml +2 -2
  7. {stemtrace-0.3.6 → stemtrace-0.3.7}/.pre-commit-config.yaml +5 -3
  8. {stemtrace-0.3.6 → stemtrace-0.3.7}/CHANGELOG.md +17 -0
  9. {stemtrace-0.3.6 → stemtrace-0.3.7}/Dockerfile +3 -2
  10. {stemtrace-0.3.6 → stemtrace-0.3.7}/PKG-INFO +19 -3
  11. {stemtrace-0.3.6 → stemtrace-0.3.7}/README.md +18 -2
  12. {stemtrace-0.3.6 → stemtrace-0.3.7}/pyproject.toml +2 -6
  13. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/__init__.py +21 -49
  14. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/core/graph.py +238 -5
  15. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/signals.py +11 -4
  16. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/__main__.py +22 -41
  17. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/api/__init__.py +2 -0
  18. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/api/routes.py +64 -36
  19. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/api/schemas.py +6 -0
  20. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/api/websocket.py +11 -6
  21. stemtrace-0.3.7/src/stemtrace/server/fastapi/form_auth.py +342 -0
  22. stemtrace-0.3.7/src/stemtrace/server/fastapi/form_auth_middleware.py +154 -0
  23. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/fastapi/login_routes.py +14 -0
  24. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/store.py +145 -60
  25. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/biome.json +2 -2
  26. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/package-lock.json +128 -136
  27. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/package.json +6 -6
  28. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/api/client.ts +115 -21
  29. stemtrace-0.3.7/src/stemtrace/server/ui/frontend/src/components/LogoutButton.tsx +20 -0
  30. stemtrace-0.3.7/src/stemtrace/server/ui/frontend/src/components/SessionRejectedNotice.tsx +31 -0
  31. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +67 -28
  32. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +7 -2
  33. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +27 -20
  34. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/main.tsx +3 -0
  35. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +29 -14
  36. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +14 -55
  37. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +2 -0
  38. stemtrace-0.3.7/src/stemtrace/server/ui/frontend/tests/auth.spec.ts +119 -0
  39. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +2 -0
  40. stemtrace-0.3.7/src/stemtrace/server/ui/frontend/tests/header.spec.ts +186 -0
  41. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +119 -1
  42. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/static.py +22 -75
  43. stemtrace-0.3.7/tests/conftest.py +97 -0
  44. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/e2e/conftest.py +44 -0
  45. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/e2e/test_api_e2e.py +14 -4
  46. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/e2e/test_workers_e2e.py +1 -1
  47. stemtrace-0.3.7/tests/unit/test_cli_form_login.py +280 -0
  48. stemtrace-0.3.7/tests/unit/test_form_auth.py +389 -0
  49. stemtrace-0.3.7/tests/unit/test_form_login.py +808 -0
  50. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_graph.py +626 -1
  51. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_routes.py +280 -12
  52. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_signals.py +28 -3
  53. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_store.py +238 -2
  54. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_ui_static_prefix.py +39 -4
  55. {stemtrace-0.3.6 → stemtrace-0.3.7}/uv.lock +592 -329
  56. stemtrace-0.3.6/src/stemtrace/server/fastapi/form_auth.py +0 -157
  57. stemtrace-0.3.6/tests/conftest.py +0 -18
  58. stemtrace-0.3.6/tests/unit/test_form_auth.py +0 -135
  59. stemtrace-0.3.6/tests/unit/test_form_login.py +0 -293
  60. {stemtrace-0.3.6 → stemtrace-0.3.7}/.gitignore +0 -0
  61. {stemtrace-0.3.6 → stemtrace-0.3.7}/.python-version +0 -0
  62. {stemtrace-0.3.6 → stemtrace-0.3.7}/CONTRIBUTING.md +0 -0
  63. {stemtrace-0.3.6 → stemtrace-0.3.7}/Dockerfile.e2e +0 -0
  64. {stemtrace-0.3.6 → stemtrace-0.3.7}/LICENSE +0 -0
  65. {stemtrace-0.3.6 → stemtrace-0.3.7}/Makefile +0 -0
  66. {stemtrace-0.3.6 → stemtrace-0.3.7}/build_ui.py +0 -0
  67. {stemtrace-0.3.6 → stemtrace-0.3.7}/docker-compose.e2e.rabbitmq.yml +0 -0
  68. {stemtrace-0.3.6 → stemtrace-0.3.7}/docker-compose.e2e.yml +0 -0
  69. {stemtrace-0.3.6 → stemtrace-0.3.7}/docker-compose.rabbitmq.yml +0 -0
  70. {stemtrace-0.3.6 → stemtrace-0.3.7}/docker-compose.yml +0 -0
  71. {stemtrace-0.3.6 → stemtrace-0.3.7}/docs/brand/stemtrace-mark-light.svg +0 -0
  72. {stemtrace-0.3.6 → stemtrace-0.3.7}/docs/brand/stemtrace-mark.svg +0 -0
  73. {stemtrace-0.3.6 → stemtrace-0.3.7}/docs/screenshots/task_details.png +0 -0
  74. {stemtrace-0.3.6 → stemtrace-0.3.7}/docs/screenshots/unregistered.png +0 -0
  75. {stemtrace-0.3.6 → stemtrace-0.3.7}/docs/screenshots/workflow.png +0 -0
  76. {stemtrace-0.3.6 → stemtrace-0.3.7}/examples/celery_app.py +0 -0
  77. {stemtrace-0.3.6 → stemtrace-0.3.7}/examples/fastapi_integration.py +0 -0
  78. {stemtrace-0.3.6 → stemtrace-0.3.7}/examples/with_auth.py +0 -0
  79. {stemtrace-0.3.6 → stemtrace-0.3.7}/examples/with_login.py +0 -0
  80. {stemtrace-0.3.6 → stemtrace-0.3.7}/progress.md +0 -0
  81. {stemtrace-0.3.6 → stemtrace-0.3.7}/scripts/wait_for_http.py +0 -0
  82. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/core/__init__.py +0 -0
  83. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/core/events.py +0 -0
  84. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/core/exceptions.py +0 -0
  85. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/core/ports.py +0 -0
  86. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/__init__.py +0 -0
  87. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/bootsteps.py +0 -0
  88. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/config.py +0 -0
  89. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/scrubbing.py +0 -0
  90. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/transports/__init__.py +0 -0
  91. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/transports/memory.py +0 -0
  92. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/transports/rabbitmq.py +0 -0
  93. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/library/transports/redis.py +0 -0
  94. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/py.typed +0 -0
  95. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/__init__.py +0 -0
  96. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/consumer.py +0 -0
  97. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/fastapi/__init__.py +0 -0
  98. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/fastapi/auth.py +0 -0
  99. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/fastapi/extension.py +0 -0
  100. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/fastapi/router.py +0 -0
  101. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/__init__.py +0 -0
  102. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/index.html +0 -0
  103. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
  104. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
  105. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
  106. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
  107. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
  108. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/SproutMark.tsx +0 -0
  109. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
  110. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
  111. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
  112. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
  113. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
  114. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
  115. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
  116. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
  117. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
  118. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
  119. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
  120. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
  121. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
  122. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
  123. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
  124. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
  125. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
  126. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
  127. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
  128. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
  129. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/ui/templates/login.html +0 -0
  130. {stemtrace-0.3.6 → stemtrace-0.3.7}/src/stemtrace/server/websocket.py +0 -0
  131. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/__init__.py +0 -0
  132. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/e2e/__init__.py +0 -0
  133. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/e2e/tasks.py +0 -0
  134. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/__init__.py +0 -0
  135. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_embedded_static.py +0 -0
  136. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_fastapi_integration.py +0 -0
  137. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_rabbitmq_transport.py +0 -0
  138. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_redis_resilience_docker.py +0 -0
  139. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_redis_transport.py +0 -0
  140. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_static_ui.py +0 -0
  141. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_wait_for_http_redirects.py +0 -0
  142. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/integration/test_workers_refresh.py +0 -0
  143. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/__init__.py +0 -0
  144. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_auth.py +0 -0
  145. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_bootsteps.py +0 -0
  146. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_build_ui_hook.py +0 -0
  147. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_config.py +0 -0
  148. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_consumer.py +0 -0
  149. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_events.py +0 -0
  150. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_examples.py +0 -0
  151. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_exceptions.py +0 -0
  152. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_favicon.py +0 -0
  153. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_init.py +0 -0
  154. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_redis_resilience.py +0 -0
  155. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_scrubbing.py +0 -0
  156. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_transports.py +0 -0
  157. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_wait_for_http_script.py +0 -0
  158. {stemtrace-0.3.6 → stemtrace-0.3.7}/tests/unit/test_websocket.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.3.6"
2
+ current_version = "0.3.7"
3
3
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
4
4
  serialize = ["{major}.{minor}.{patch}"]
5
5
  commit = true
@@ -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"
@@ -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"
@@ -56,7 +56,7 @@ jobs:
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
@@ -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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.7] - 2026-10-10
11
+
12
+ ### Changed
13
+ - 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).
14
+ - 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`.
15
+ - 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.
16
+ - UI: updated `@tanstack/react-query` 5.94.5 → 5.104.1 in the bundled frontend.
17
+
18
+ ### Fixed
19
+ - 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.
20
+ - 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.
21
+ - 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.
22
+ - 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.
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+
10
27
  ## [0.3.6] - 2026-10-10
11
28
 
12
29
  ### Security
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stemtrace
3
- Version: 0.3.6
3
+ Version: 0.3.7
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.7-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)
@@ -443,6 +443,10 @@ stemtrace server \
443
443
  --login-secret change-me
444
444
  ```
445
445
 
446
+ Set `--login-secret` (or `STEMTRACE_LOGIN_SECRET`) to a long random value, and use the same
447
+ value on every replica. Without it, each process generates its own random secret, so sessions
448
+ only work within a single process and are lost on restart.
449
+
446
450
  #### High-Scale Production Setup
447
451
 
448
452
  Note: `stemtrace server` includes an embedded consumer today (single-process). A multi-process deployment mode is planned.
@@ -512,7 +516,19 @@ stemtrace.init_app(
512
516
  )
513
517
  ```
514
518
 
515
- This serves a sign-in page at `/stemtrace/login` and protects everything under `/stemtrace`.
519
+ This serves a sign-in page at `/stemtrace/login` and protects the UI, API and WebSocket under
520
+ `/stemtrace`. The static JS/CSS bundle under `/stemtrace/assets/` is the public UI build and is
521
+ served without a session. So is `/stemtrace/api/health/live`, a liveness probe for load
522
+ balancers and container health checks: it always answers 200 while the process is serving,
523
+ with only `{"status": "ok"}`, or `{"status": "degraded"}` when the embedded consumer stopped
524
+ unexpectedly or can't read from the broker (the same rule as `/stemtrace/api/health`).
525
+
526
+ For any multi-process deployment (`uvicorn --workers N`, gunicorn, or several replicas behind
527
+ a load balancer), set `login_secret` (or the `STEMTRACE_LOGIN_SECRET` env var) to the same long
528
+ random value everywhere, e.g. `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
529
+ Without it, each process signs sessions with its own random secret, and users get logged out
530
+ at random. stemtrace logs a warning at startup in that case, and the login page and UI explain
531
+ the problem when a session signed by another process is rejected.
516
532
 
517
533
  #### Built-in Auth Helpers (Basic / API key)
518
534
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Zero-infrastructure Celery task flow visualizer**
6
6
 
7
- [![PyPI version](https://img.shields.io/badge/pypi-v0.3.6-darklime)](https://pypi.org/project/stemtrace)
7
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.3.7-darklime)](https://pypi.org/project/stemtrace)
8
8
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
9
9
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
10
10
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -388,6 +388,10 @@ stemtrace server \
388
388
  --login-secret change-me
389
389
  ```
390
390
 
391
+ Set `--login-secret` (or `STEMTRACE_LOGIN_SECRET`) to a long random value, and use the same
392
+ value on every replica. Without it, each process generates its own random secret, so sessions
393
+ only work within a single process and are lost on restart.
394
+
391
395
  #### High-Scale Production Setup
392
396
 
393
397
  Note: `stemtrace server` includes an embedded consumer today (single-process). A multi-process deployment mode is planned.
@@ -457,7 +461,19 @@ stemtrace.init_app(
457
461
  )
458
462
  ```
459
463
 
460
- This serves a sign-in page at `/stemtrace/login` and protects everything under `/stemtrace`.
464
+ This serves a sign-in page at `/stemtrace/login` and protects the UI, API and WebSocket under
465
+ `/stemtrace`. The static JS/CSS bundle under `/stemtrace/assets/` is the public UI build and is
466
+ served without a session. So is `/stemtrace/api/health/live`, a liveness probe for load
467
+ balancers and container health checks: it always answers 200 while the process is serving,
468
+ with only `{"status": "ok"}`, or `{"status": "degraded"}` when the embedded consumer stopped
469
+ unexpectedly or can't read from the broker (the same rule as `/stemtrace/api/health`).
470
+
471
+ For any multi-process deployment (`uvicorn --workers N`, gunicorn, or several replicas behind
472
+ a load balancer), set `login_secret` (or the `STEMTRACE_LOGIN_SECRET` env var) to the same long
473
+ random value everywhere, e.g. `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
474
+ Without it, each process signs sessions with its own random secret, and users get logged out
475
+ at random. stemtrace logs a warning at startup in that case, and the login page and UI explain
476
+ the problem when a session signed by another process is rejected.
461
477
 
462
478
  #### Built-in Auth Helpers (Basic / API key)
463
479
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stemtrace"
3
- version = "0.3.6"
3
+ version = "0.3.7"
4
4
  description = "A lightweight Celery task flow visualizer"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -115,10 +115,6 @@ ignore_missing_imports = true
115
115
  module = "kombu.*"
116
116
  ignore_missing_imports = true
117
117
 
118
- [[tool.mypy.overrides]]
119
- module = "amqp.*"
120
- ignore_missing_imports = true
121
-
122
118
  # =============================================================================
123
119
  # ruff
124
120
  # =============================================================================
@@ -204,6 +200,6 @@ exclude_lines = [
204
200
  "if TYPE_CHECKING:",
205
201
  "raise NotImplementedError",
206
202
  "@abstractmethod",
207
- "\\.\\.\\.", # Protocol method stubs
203
+ "^\\s*\\.\\.\\.\\s*$", # Protocol method stubs (a bare `...` body line)
208
204
  "class.*Protocol.*:",
209
205
  ]
@@ -16,9 +16,6 @@ Usage:
16
16
  """
17
17
 
18
18
  import os
19
- import secrets
20
- import urllib.parse
21
- from collections.abc import Awaitable, Callable
22
19
  from typing import TYPE_CHECKING, Any
23
20
 
24
21
  from stemtrace.core.events import TaskEvent, TaskState
@@ -36,14 +33,15 @@ from stemtrace.server.fastapi import (
36
33
  require_api_key,
37
34
  require_basic_auth,
38
35
  )
39
- from stemtrace.server.fastapi.form_auth import FormAuthConfig, is_authenticated_cookie
36
+ from stemtrace.server.fastapi.form_auth import FormAuthConfig, resolve_session_secret
37
+ from stemtrace.server.fastapi.form_auth_middleware import create_form_auth_middleware
40
38
  from stemtrace.server.fastapi.login_routes import create_login_router
41
39
 
42
40
  if TYPE_CHECKING:
43
41
  from celery import Celery
44
42
  from fastapi import FastAPI
45
43
 
46
- __version__ = "0.3.6"
44
+ __version__ = "0.3.7"
47
45
  __all__ = [
48
46
  "ConfigurationError",
49
47
  "StemtraceConfig",
@@ -178,7 +176,10 @@ def init_app(
178
176
  login_password: Enable built-in form login if set (password for login_username).
179
177
  login_secret: Secret used to sign session cookies. If not provided, uses
180
178
  STEMTRACE_LOGIN_SECRET env var. If still missing, a random secret is
181
- generated (sessions invalidated on app restart).
179
+ generated per process and a warning is logged: sessions then only
180
+ work within one process (and are invalidated on restart), so set the
181
+ same long random value on every worker/replica of a multi-process
182
+ deployment.
182
183
  login_ttl_seconds: Session TTL in seconds (default: 24 hours).
183
184
  node_alias_from_arguments: Derive graph node display name from task arguments.
184
185
  Digit string for args[index], string for kwargs[key], or None (default).
@@ -208,10 +209,8 @@ def init_app(
208
209
  effective_login_username = login_username or os.getenv("STEMTRACE_LOGIN_USERNAME")
209
210
  effective_login_password = login_password or os.getenv("STEMTRACE_LOGIN_PASSWORD")
210
211
  if effective_login_username and effective_login_password:
211
- effective_secret = (
212
- login_secret
213
- or os.getenv("STEMTRACE_LOGIN_SECRET")
214
- or secrets.token_urlsafe(32)
212
+ effective_secret, secret_is_generated = resolve_session_secret(
213
+ login_secret or os.getenv("STEMTRACE_LOGIN_SECRET")
215
214
  )
216
215
  form_auth_config = FormAuthConfig(
217
216
  username=effective_login_username,
@@ -220,6 +219,7 @@ def init_app(
220
219
  ttl_seconds=login_ttl_seconds,
221
220
  cookie_name="stemtrace_session",
222
221
  cookie_path=mount_prefix,
222
+ secret_is_generated=secret_is_generated,
223
223
  )
224
224
 
225
225
  _install_stemtrace_form_auth(
@@ -256,51 +256,23 @@ def _install_stemtrace_form_auth(
256
256
  The login router must be added before stemtrace UI's SPA catch-all route,
257
257
  otherwise `/login` would be handled by the SPA fallback.
258
258
  """
259
- from fastapi import Request
260
- from fastapi.responses import JSONResponse, RedirectResponse
261
- from starlette.responses import Response
262
-
263
259
  login_router = create_login_router(
264
260
  form_auth_config,
265
261
  default_next_path=f"{mount_prefix}/",
266
262
  )
267
263
  app.include_router(login_router, prefix=mount_prefix)
268
264
 
269
- login_path = f"{mount_prefix}/login"
270
- logout_path = f"{mount_prefix}/logout"
271
-
272
- @app.middleware("http")
273
- async def _stemtrace_form_auth_middleware(
274
- request: Request,
275
- call_next: Callable[[Request], Awaitable[Response]],
276
- ) -> Response:
277
- path = request.url.path
278
- if not path.startswith(mount_prefix):
279
- return await call_next(request)
280
-
281
- # Allow login/logout without auth.
282
- if path in (login_path, logout_path):
283
- return await call_next(request)
284
-
285
- cookie_value = request.cookies.get(form_auth_config.cookie_name)
286
- if is_authenticated_cookie(
287
- cookie_value,
288
- secret=form_auth_config.secret,
289
- expected_username=form_auth_config.username,
290
- ):
291
- return await call_next(request)
292
-
293
- # Not authenticated.
294
- if path.startswith(f"{mount_prefix}/api"):
295
- return JSONResponse({"detail": "Authentication required"}, status_code=401)
296
- if path.startswith(f"{mount_prefix}/assets"):
297
- return JSONResponse({"detail": "Authentication required"}, status_code=401)
298
-
299
- next_target = path
300
- if request.url.query:
301
- next_target = f"{path}?{request.url.query}"
302
- next_qs = urllib.parse.urlencode({"next": next_target})
303
- return RedirectResponse(url=f"{login_path}?{next_qs}", status_code=303)
265
+ app.middleware("http")(
266
+ create_form_auth_middleware(
267
+ form_auth_config,
268
+ login_path=f"{mount_prefix}/login",
269
+ logout_path=f"{mount_prefix}/logout",
270
+ api_prefix=f"{mount_prefix}/api",
271
+ assets_prefix=f"{mount_prefix}/assets",
272
+ protected_prefix=mount_prefix,
273
+ public_paths=(f"{mount_prefix}/api/health/live",),
274
+ )
275
+ )
304
276
 
305
277
 
306
278
  def _reset() -> None: