expr_tracker 0.5.0__tar.gz → 0.6.1__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 (300) hide show
  1. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/Cargo.lock +1 -1
  2. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/Cargo.toml +1 -1
  3. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/PKG-INFO +2 -2
  4. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/README.md +1 -1
  5. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/web-viewer.md +127 -22
  6. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/pyproject.toml +2 -1
  7. expr_tracker-0.6.1/rust/viewer/aggregate.rs +217 -0
  8. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/auth_store.rs +27 -0
  9. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/index.rs +3 -1
  10. expr_tracker-0.6.1/rust/viewer/log_files.rs +30 -0
  11. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/stream.rs +13 -3
  12. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/tests.rs +42 -1
  13. expr_tracker-0.6.1/rust/viewer/workspace.rs +157 -0
  14. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer.rs +78 -46
  15. expr_tracker-0.6.1/src/expr_tracker/_web/assets/curve-worker-B9xuMx07.js +2 -0
  16. expr_tracker-0.6.1/src/expr_tracker/_web/assets/index-BVS-StHr.js +57 -0
  17. expr_tracker-0.6.1/src/expr_tracker/_web/assets/index-BfrrriPo.css +1 -0
  18. expr_tracker-0.6.1/src/expr_tracker/_web/favicon.svg +3 -0
  19. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/index.html +3 -2
  20. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_viewer.py +149 -0
  21. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/uv.lock +7 -7
  22. expr_tracker-0.6.1/web/e2e/viewer-fixtures.ts +194 -0
  23. expr_tracker-0.6.1/web/e2e/viewer-layout.spec.ts +573 -0
  24. expr_tracker-0.6.1/web/e2e/viewer-sources.spec.ts +468 -0
  25. expr_tracker-0.6.1/web/e2e/viewer-streaming.spec.ts +333 -0
  26. expr_tracker-0.6.1/web/e2e/viewer.spec.ts +571 -0
  27. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/index.html +1 -0
  28. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/package-lock.json +2 -2
  29. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/package.json +1 -1
  30. expr_tracker-0.6.1/web/public/favicon.svg +3 -0
  31. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/api.test.ts +6 -2
  32. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/api.ts +15 -1
  33. expr_tracker-0.6.1/web/src/app.tsx +269 -0
  34. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-export.ts +12 -6
  35. expr_tracker-0.6.1/web/src/chart-layout.test.ts +57 -0
  36. expr_tracker-0.6.1/web/src/chart-layout.ts +57 -0
  37. expr_tracker-0.6.1/web/src/chart-settings-dialog.tsx +174 -0
  38. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-settings.test.ts +11 -1
  39. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-settings.ts +17 -5
  40. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-ticks.test.ts +18 -1
  41. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/chart-ticks.ts +14 -0
  42. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/dialog.tsx +10 -3
  43. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/input.tsx +1 -1
  44. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/select.tsx +5 -5
  45. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/sheet.tsx +1 -1
  46. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-data.test.ts +8 -3
  47. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-data.ts +6 -7
  48. expr_tracker-0.6.1/web/src/curve-groups.tsx +265 -0
  49. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-worker.ts +3 -2
  50. expr_tracker-0.6.1/web/src/metric-chart.tsx +363 -0
  51. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/run-picker.tsx +2 -2
  52. expr_tracker-0.6.1/web/src/runs-panel.tsx +136 -0
  53. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/session.tsx +1 -1
  54. expr_tracker-0.6.1/web/src/source-aggregation.test.ts +22 -0
  55. expr_tracker-0.6.1/web/src/source-aggregation.ts +49 -0
  56. expr_tracker-0.6.1/web/src/styles.css +240 -0
  57. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/theme.tsx +1 -0
  58. expr_tracker-0.6.1/web/src/use-chart-drag.ts +101 -0
  59. expr_tracker-0.6.1/web/src/workspace.test.ts +46 -0
  60. expr_tracker-0.6.1/web/src/workspace.ts +101 -0
  61. expr_tracker-0.5.0/src/expr_tracker/_web/assets/curve-worker-BkxJLhdR.js +0 -2
  62. expr_tracker-0.5.0/src/expr_tracker/_web/assets/index-Cl3CI7bq.css +0 -1
  63. expr_tracker-0.5.0/src/expr_tracker/_web/assets/index-Czg8NFJT.js +0 -53
  64. expr_tracker-0.5.0/web/e2e/viewer.spec.ts +0 -698
  65. expr_tracker-0.5.0/web/src/app.tsx +0 -236
  66. expr_tracker-0.5.0/web/src/chart-settings-dialog.tsx +0 -83
  67. expr_tracker-0.5.0/web/src/curve-groups.tsx +0 -135
  68. expr_tracker-0.5.0/web/src/metric-chart.tsx +0 -206
  69. expr_tracker-0.5.0/web/src/styles.css +0 -134
  70. expr_tracker-0.5.0/web/src/workspace-storage.test.ts +0 -47
  71. expr_tracker-0.5.0/web/src/workspace-storage.ts +0 -54
  72. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.github/workflows/ci.yaml +0 -0
  73. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.github/workflows/docs.yaml +0 -0
  74. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.github/workflows/release.yaml +0 -0
  75. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/.gitignore +0 -0
  76. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/LICENSE +0 -0
  77. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/compare_native.py +0 -0
  78. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/native_workloads.py +0 -0
  79. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/profile_logging.py +0 -0
  80. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/architecture-simplification.json +0 -0
  81. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/artifact-atime-isolation.json +0 -0
  82. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/artifact-metadata-investigation.json +0 -0
  83. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/file-splitting.json +0 -0
  84. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/logging-100-metrics.json +0 -0
  85. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/native-migration.json +0 -0
  86. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/native-optimized.json +0 -0
  87. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/notification-simplification.json +0 -0
  88. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/observer-projection.json +0 -0
  89. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/serializer-cache-probe.json +0 -0
  90. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/series-cache-probe.json +0 -0
  91. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/transport-optimized.json +0 -0
  92. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/validation-reuse.json +0 -0
  93. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/results/viewer-lock-scope.json +0 -0
  94. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/benchmarks/viewer.py +0 -0
  95. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/architecture.md +0 -0
  96. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/benchmarks/native-migration.md +0 -0
  97. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/design.md +0 -0
  98. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/examples.md +0 -0
  99. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/getting-started.md +0 -0
  100. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/alerts.md +0 -0
  101. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/artifacts.md +0 -0
  102. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/backends.md +0 -0
  103. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/cli.md +0 -0
  104. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/distributed.md +0 -0
  105. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/history.md +0 -0
  106. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/logging.md +0 -0
  107. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/spans.md +0 -0
  108. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/guide/streams.md +0 -0
  109. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/index.md +0 -0
  110. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/native-development.md +0 -0
  111. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/reference/api.md +0 -0
  112. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/reference/configuration.md +0 -0
  113. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/docs/reference/expressions.md +0 -0
  114. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/README.md +0 -0
  115. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/alert_rules.py +0 -0
  116. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/checkpoints.py +0 -0
  117. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/early_stopping.py +0 -0
  118. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/multiprocess_pipeline.py +0 -0
  119. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/profile_step.py +0 -0
  120. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/examples/quickstart.py +0 -0
  121. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/mkdocs.yml +0 -0
  122. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/delivery.rs +0 -0
  123. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/evaluation.rs +0 -0
  124. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/rules.rs +0 -0
  125. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts/watchdog.rs +0 -0
  126. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts.rs +0 -0
  127. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts_parser.rs +0 -0
  128. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/alerts_plan.rs +0 -0
  129. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/artifacts.rs +0 -0
  130. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/json.rs +0 -0
  131. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/notifications.rs +0 -0
  132. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/reader/segments.rs +0 -0
  133. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/reader.rs +0 -0
  134. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/record.rs +0 -0
  135. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/series.rs +0 -0
  136. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/state/operations.rs +0 -0
  137. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/state.rs +0 -0
  138. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/tests.rs +0 -0
  139. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/writer/persistence.rs +0 -0
  140. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/writer/tests.rs +0 -0
  141. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history/writer.rs +0 -0
  142. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/history.rs +0 -0
  143. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/lib.rs +0 -0
  144. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/spans.rs +0 -0
  145. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/summary.rs +0 -0
  146. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/trace/lanes.rs +0 -0
  147. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/trace.rs +0 -0
  148. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/auth.rs +0 -0
  149. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/directories.rs +0 -0
  150. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer/identity.rs +0 -0
  151. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/rust/viewer_json.rs +0 -0
  152. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/__init__.py +0 -0
  153. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_compat.py +0 -0
  154. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_process.py +0 -0
  155. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  156. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  157. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  158. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  159. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/_web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  160. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/__init__.py +0 -0
  161. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/backends/__init__.py +0 -0
  162. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/backends/base.py +0 -0
  163. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/backends/cards.py +0 -0
  164. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/dispatch.py +0 -0
  165. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/engine.py +0 -0
  166. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/__init__.py +0 -0
  167. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/eval.py +0 -0
  168. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/functions.py +0 -0
  169. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/lexer.py +0 -0
  170. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/nodes.py +0 -0
  171. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/parser.py +0 -0
  172. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/expr/rule.py +0 -0
  173. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/alerts/models.py +0 -0
  174. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/artifacts.py +0 -0
  175. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/cli.py +0 -0
  176. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/encoders.py +0 -0
  177. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/__init__.py +0 -0
  178. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/_store_types.py +0 -0
  179. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/_store_writes.py +0 -0
  180. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/codec.py +0 -0
  181. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/frame.py +0 -0
  182. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/naming.py +0 -0
  183. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/reader.py +0 -0
  184. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/rotation.py +0 -0
  185. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/series.py +0 -0
  186. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/store.py +0 -0
  187. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/history/writer.py +0 -0
  188. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/plugins.py +0 -0
  189. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/py.typed +0 -0
  190. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/run.py +0 -0
  191. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/spans.py +0 -0
  192. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/summary.py +0 -0
  193. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/trace.py +0 -0
  194. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/tracker.py +0 -0
  195. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/src/expr_tracker/types.py +0 -0
  196. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_artifact_metadata.py +0 -0
  197. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_callback_helpers.py +0 -0
  198. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_reader_edge_helpers.py +0 -0
  199. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_span_helpers.py +0 -0
  200. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_span_plugin_helpers.py +0 -0
  201. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/_trace_helpers.py +0 -0
  202. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/conftest.py +0 -0
  203. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_backends.py +0 -0
  204. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_delivery.py +0 -0
  205. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_dispatch.py +0 -0
  206. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_engine.py +0 -0
  207. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_models.py +0 -0
  208. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_native.py +0 -0
  209. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_alert_routing.py +0 -0
  210. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_artifact_metadata.py +0 -0
  211. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_artifacts.py +0 -0
  212. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_benchmark.py +0 -0
  213. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_cache.py +0 -0
  214. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_callback_isolation.py +0 -0
  215. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_callback_lifecycle.py +0 -0
  216. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_cli.py +0 -0
  217. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_commit_timestamps.py +0 -0
  218. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_correctness.py +0 -0
  219. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_distributed.py +0 -0
  220. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_e2e.py +0 -0
  221. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_examples.py +0 -0
  222. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_builder.py +0 -0
  223. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_eval.py +0 -0
  224. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_functions.py +0 -0
  225. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_parser.py +0 -0
  226. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_expr_properties.py +0 -0
  227. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_failure_modes.py +0 -0
  228. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_features.py +0 -0
  229. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_fork_safety.py +0 -0
  230. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_frame_codec_summary.py +0 -0
  231. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_history.py +0 -0
  232. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_history_admission.py +0 -0
  233. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_history_concurrency.py +0 -0
  234. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_hot_paths.py +0 -0
  235. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_integration.py +0 -0
  236. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_lark.py +0 -0
  237. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_native_commit_queue.py +0 -0
  238. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_native_fast_paths.py +0 -0
  239. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_native_metric_projection.py +0 -0
  240. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_nonfinite_conditions.py +0 -0
  241. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_perf.py +0 -0
  242. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_cpu_time.py +0 -0
  243. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_gpu_stats.py +0 -0
  244. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_resources.py +0 -0
  245. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_plugin_torch_memory.py +0 -0
  246. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_process_compat.py +0 -0
  247. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_public_surfaces.py +0 -0
  248. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_reader_edge_cases.py +0 -0
  249. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_reader_live_queries.py +0 -0
  250. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_review_regressions.py +0 -0
  251. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rotation.py +0 -0
  252. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rule_lifecycle.py +0 -0
  253. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rule_overrides.py +0 -0
  254. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_rule_quoted_names.py +0 -0
  255. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_run_backends.py +0 -0
  256. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_run_dir.py +0 -0
  257. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_scenarios.py +0 -0
  258. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_serialization_edge_cases.py +0 -0
  259. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_series_edge_cases.py +0 -0
  260. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_source_size.py +0 -0
  261. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_coordination.py +0 -0
  262. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_lifecycle.py +0 -0
  263. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_plugins.py +0 -0
  264. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_printing.py +0 -0
  265. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_span_value_encoding.py +0 -0
  266. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_spans.py +0 -0
  267. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_step_commit.py +0 -0
  268. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_step_types.py +0 -0
  269. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_streams.py +0 -0
  270. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_stress.py +0 -0
  271. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace.py +0 -0
  272. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace_export_failures.py +0 -0
  273. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace_perf.py +0 -0
  274. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trace_queries.py +0 -0
  275. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_trackio.py +0 -0
  276. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_unicode_records.py +0 -0
  277. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_value_encoding.py +0 -0
  278. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_wandb.py +0 -0
  279. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_async.py +0 -0
  280. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_buffer.py +0 -0
  281. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_bytes.py +0 -0
  282. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_diagnostics.py +0 -0
  283. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/tests/test_writer_durability.py +0 -0
  284. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/components.json +0 -0
  285. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/playwright.config.ts +0 -0
  286. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/badge.tsx +0 -0
  287. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/button.tsx +0 -0
  288. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/card.tsx +0 -0
  289. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/checkbox.tsx +0 -0
  290. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/switch.tsx +0 -0
  291. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/components/ui/tabs.tsx +0 -0
  292. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/curve-stream.ts +0 -0
  293. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/inspector.tsx +0 -0
  294. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/lib/utils.ts +0 -0
  295. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/main.tsx +0 -0
  296. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/plot-scheduler.test.ts +0 -0
  297. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/src/plot-scheduler.ts +0 -0
  298. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/tsconfig.json +0 -0
  299. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/vite.config.ts +0 -0
  300. {expr_tracker-0.5.0 → expr_tracker-0.6.1}/web/vitest.config.ts +0 -0
@@ -205,7 +205,7 @@ dependencies = [
205
205
 
206
206
  [[package]]
207
207
  name = "expr-tracker-native"
208
- version = "0.5.0"
208
+ version = "0.6.1"
209
209
  dependencies = [
210
210
  "axum",
211
211
  "base64 0.22.1",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "expr-tracker-native"
3
- version = "0.5.0"
3
+ version = "0.6.1"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  publish = false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: expr_tracker
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -100,7 +100,7 @@ and view config, summary, and spans across rotation segments.
100
100
  Enter the random key printed at startup in the viewer's login form. The default listener is
101
101
  `0.0.0.0`; use a trusted network or HTTPS/SSH for remote access.
102
102
  Use **Add run** to select each run's directory on the server.
103
- Added runs are remembered locally across reloads; subdirectories are not scanned for runs.
103
+ Added runs are saved on the server and shared across browsers; subdirectories are not scanned for runs.
104
104
  See the [viewer guide](docs/guide/web-viewer.md) and
105
105
  [native build instructions](docs/native-development.md).
106
106
 
@@ -50,7 +50,7 @@ and view config, summary, and spans across rotation segments.
50
50
  Enter the random key printed at startup in the viewer's login form. The default listener is
51
51
  `0.0.0.0`; use a trusted network or HTTPS/SSH for remote access.
52
52
  Use **Add run** to select each run's directory on the server.
53
- Added runs are remembered locally across reloads; subdirectories are not scanned for runs.
53
+ Added runs are saved on the server and shared across browsers; subdirectories are not scanned for runs.
54
54
  See the [viewer guide](docs/guide/web-viewer.md) and
55
55
  [native build instructions](docs/native-development.md).
56
56
 
@@ -18,13 +18,13 @@ through HTTPS). Keys are not stored in browser local storage or URLs.
18
18
  Login is remembered for 30 days, including browser and server restarts.
19
19
  **Sign out** revokes the session and clears its cookie. Scripted clients can still use HTTP Basic
20
20
  with username `et` and the startup key. Previously added runs are restored after login.
21
- `--root` only sets the directory picker's starting location. Use `--port` to
21
+ `--root` sets the directory picker's initial location until a shared location is saved. Use `--port` to
22
22
  choose another local port. Add up to six runs to the workspace and use their
23
23
  checkboxes to compare them. Removing a run from the sidebar only removes it
24
24
  from the workspace, not from disk. Each run has its own stream/rank selector;
25
25
  sources are never silently merged.
26
26
 
27
- `et serve` stores the key and active sessions in
27
+ `et serve` stores the key, active sessions, and shared workspace in
28
28
  `$XDG_STATE_HOME/expr_tracker/viewer-auth-<port>.json`, or
29
29
  `~/.local/state/expr_tracker/viewer-auth-<port>.json` when that variable is unset
30
30
  or relative. The file is private (`0600` on Unix), atomically updated, and locked
@@ -44,7 +44,7 @@ Display names come from `tracker.name` and `tracker.project` in the run's config
44
44
  not necessarily the directory basename. The viewer prefers `config.json`, then
45
45
  `config.trainer-r0.json`, then the first stream config. This optional identity
46
46
  read is size-limited and cached until the chosen config changes; missing or
47
- invalid identity metadata falls back to directory names. Saved browser entries
47
+ invalid identity metadata falls back to directory names. Saved workspace entries
48
48
  refresh their display names automatically.
49
49
  The directory path is shown in the picker's editable path field.
50
50
  This is not a browser upload picker: with SSH forwarding, paths
@@ -52,30 +52,72 @@ refer to the remote training host, not your laptop. Relative paths are resolved
52
52
  against the startup `--root`; absolute paths can select any directory accessible
53
53
  to the server process, including directories outside the startup root.
54
54
 
55
- Added runs, their selected sources, colors, and checkbox states are saved in this
56
- browser's local storage and restored on reload. Removing a run updates that saved
57
- list. No metric data, config contents, or access keys are saved there. Different
58
- tabs do not change each other's live selection; a new tab or reload reads the
59
- last saved list. Live and manual refresh only reload explicitly added run
60
- directories. The URL separately saves the picker's starting location.
61
- If browser storage is unavailable, a warning is shown and runs remain usable
62
- for the current page. Unavailable saved directories remain listed with errors
63
- rather than being silently discarded.
64
- Files are never uploaded, modified, or deleted. Browsing directories
55
+ Added runs, their selected sources, colors, checkbox states, and the picker's
56
+ last added directory are saved on the server, alongside its private authentication
57
+ state. All browsers signed in to the same viewer share this workspace; visible
58
+ pages check for changes every three seconds, even when **Live** is off, and also
59
+ when focused. Reloading or restarting the server retains the saved workspace.
60
+ Concurrent edits are revision-checked: a stale edit is rejected and the latest
61
+ workspace is reloaded instead of silently overwriting another browser's changes.
62
+ Loading or saving failures display an error and retry control; failed saves do
63
+ not replace the last confirmed state. Removing a run only updates the shared
64
+ list, not the experiment files. Unavailable saved directories remain listed
65
+ with errors rather than being silently discarded.
66
+
67
+ Run directories are no longer read from or written to browser local storage.
68
+ Legacy browser-only lists are not automatically published to the shared server;
69
+ add those directories once to share them. Theme and per-chart preferences remain
70
+ local to each browser. No metric data or experiment config contents are persisted
71
+ in the shared workspace. Live and manual refresh only reload explicitly added
72
+ run directories. A `?root=...` bookmark can override the picker's starting location
73
+ for that page until a run is added.
74
+
75
+ Experiment files are never uploaded, modified, or deleted. Browsing directories
65
76
  with more than 10,000 entries fails explicitly; enter a child directory path
66
77
  directly instead.
67
78
 
68
79
  ## Curve comparison
69
80
 
70
- The React + shadcn/ui workspace uses single-line run rows. Metrics are grouped
81
+ The React + shadcn/ui workspace arranges independent rounded panels on a shared
82
+ canvas. The Runs panel contains the application icon and name, run search, and
83
+ the run list. The plus icon beside run search adds a directory. Its bottom row
84
+ groups the **Live** toggle button, **Refresh**, and **Settings**;
85
+ settings contains theme switching and sign out. On desktop, drag the panel's
86
+ right edge to resize its width without changing its height. The focused edge
87
+ also accepts Left/Right (Shift for larger changes), Home to reset, and Escape to cancel a drag.
88
+ The button beside **Experiment tracker** collapses the desktop panel into a narrow
89
+ rail. The application icon becomes the expand button, followed by Add and each
90
+ run's colored checkbox; hover or focus a checkbox to see its run name.
91
+ The rail shows all runs even when the expanded list is filtered, and keeps Live,
92
+ Refresh, and Settings available. Expand again to restore the previous width and
93
+ search without changing selection or reloading charts.
94
+ Desktop collapse and mobile management expansion are saved independently in this
95
+ browser and restored on reload. A warning appears if browser storage is unavailable.
96
+ Chart panels resize with the available space without reloading their data.
97
+ Source, details, and remove actions appear when a run is hovered or keyboard
98
+ focused, remain visible while its source menu is open, and are always available
99
+ on touch screens. Narrow screens use wider canvas margins and a compact Runs panel:
100
+ scroll the named run chips horizontally to select runs, or expand management for
101
+ source, details, and remove actions. Add, Live, Refresh, and Settings remain available.
102
+ Run and metric search fields are hidden on narrow screens, and `/` does not focus search.
103
+ Chart pin, settings, and expand actions likewise appear on hover or keyboard focus,
104
+ and stay visible on touch screens. The resize grip appears only when hovered,
105
+ keyboard focused, or dragged; search fields use a thin inset focus indicator.
106
+ Dropdown menus share the panels' compact radii, surface colors, and subtle borders.
107
+
108
+ The workspace uses single-line run rows. Metrics are grouped
71
109
  by the first segment of their name:
72
110
  `train/loss` and `train/optimizer/lr` appear under **train**; names without a
73
- namespace appear under **Other**. Each section shows six charts per page, with
74
- left/right arrows for navigation. Search resets pagination to the first matching
111
+ namespace appear under **Other**. Each section has independent **Columns** and
112
+ **Rows** dropdowns, both accepting 1–5 (default 4 columns and 2 rows).
113
+ Page capacity is columns times rows, so the default desktop
114
+ page shows up to eight charts in two rows. Left/right arrows navigate pages.
115
+ Narrow screens use one column and the same configured row count.
116
+ Search or a layout change resets pagination to the first matching
75
117
  page; groups can also be collapsed. Only the current page is queried and rendered.
76
118
  The sidebar contains
77
119
  only runs; each row provides source selection, details, and removal controls.
78
- Search, axis, and layout share one compact sticky toolbar (horizontally
120
+ Search and axis share one rounded sticky toolbar panel (horizontally
79
121
  scrollable on narrow screens). Search filters charts immediately, matching full metric
80
122
  names case-insensitively (multiple words must all match). Press `/` to focus
81
123
  search and Escape to clear it. The header theme button switches between light
@@ -84,9 +126,31 @@ and dark themes, including chart axes and series colors, and saves the preferenc
84
126
  Each uPlot chart overlays the selected runs with consistent colors, with its
85
127
  legend centered above the plot. Values and the hovered step/time appear only in
86
128
  that chart's hover tooltip; cursors are not synchronized across charts.
87
- Use **Step** or **Wall time** to change
88
- alignment, and the layout buttons for
89
- one or two columns. Each chart can expand into a focused view. On narrow screens,
129
+ Each series shows its actual nearest sample, never an interpolated value.
130
+ Ties choose the left sample. The matched sample is highlighted with a larger
131
+ marker, and the tooltip heading snaps to an actual sampled step/time.
132
+ Use **Step**, **Wall time**, or **Relative time** to change alignment.
133
+ Plots show only the left and bottom axis borders, with short outward ticks and
134
+ horizontal grid lines. Y-axis space fits its labels to avoid excessive left
135
+ padding. The X axis includes the first and last plotted
136
+ data values, suppressing nearby regular ticks; endpoint labels are stacked when
137
+ needed to remain readable in narrow charts. These details also appear in PNG exports.
138
+ Relative time is elapsed time since the selected source's earliest valid
139
+ timestamp, independently for each run, displayed in seconds, minutes, hours, or
140
+ days. Filtering steps or downsampling does not reset that origin; records without
141
+ valid timestamps are skipped with a warning.
142
+ Each chart's pin button adds a copy to a separate **Pinned** section above
143
+ the namespace groups, keeping the chart in its original section. Unpinning
144
+ only removes the pinned copy. Both copies share the metric's chart settings.
145
+ Pinned charts support the same rows, paging, filtering, and focus controls.
146
+ Hover a chart title to reveal its grab handle, then drag it before or after
147
+ another chart in the same section. Hold within 48 pixels of either section edge
148
+ for 650 ms to turn a page while dragging; release over a chart to place it.
149
+ Escape cancels a drag. Focused titles also support arrow-key reordering:
150
+ left/right move one position, up/down move one row. Pinned and original sections
151
+ have independent ordering. Pins, ordering, and each section's column and row counts are remembered in the current browser, independently of
152
+ the server-synchronized run directories.
153
+ Each chart can expand into a focused full-width view. On narrow screens,
90
154
  the sidebar controls collapse so curves remain the primary content.
91
155
 
92
156
  ### Per-chart settings
@@ -109,6 +173,41 @@ changes do not reread logs or alter recorded values. Hover tooltips show the
109
173
  displayed values and identify active EMA smoothing. **Reset** restores the
110
174
  default controls; apply them to save the reset.
111
175
 
176
+ ### Combining sources
177
+
178
+ Open an existing chart's settings and change **Source mode** from **Selected source**
179
+ to **Overlay**, **Sum**, **Mean**, **Maximum**, or **Minimum**. The chart always
180
+ shows the same metric; aggregation combines its values across source files within
181
+ each experiment, never different metrics or different experiments.
182
+ Choose sources with the checkboxes, or enter a wildcard under **Source pattern**:
183
+ `rollout-*` selects `metrics.rollout-0.jsonl` through `metrics.rollout-7.jsonl`;
184
+ `trainer-*` selects trainer sources. Full filenames such as
185
+ `metrics.rollout-*.jsonl` also work. `*` matches any text and `?` matches one
186
+ character. A nonempty pattern replaces the manual selection and automatically
187
+ matches new available sources. Up to 128 inputs
188
+ are supported; an empty or excessive match is reported explicitly.
189
+
190
+ Overlay draws the selected sources as separate curves. Source colors distinguish
191
+ sources, and different run line styles distinguish runs; legends and tooltips
192
+ include both names. Reducers produce one curve per run, combining the selected
193
+ sources in the same recorded step. Runs are never aggregated together.
194
+ Only finite values present at that step participate: missing values are not
195
+ interpolated, forward-filled, or replaced with zero, and an entirely missing step
196
+ produces no sample. Means divide by the number of present values.
197
+ Reduction happens on the server before downsampling, EMA smoothing, and log filtering.
198
+ Time axes use the earliest selected-source timestamp at each step. Relative time
199
+ starts at the earliest timestamp across the selected sources' entire histories,
200
+ independently of metric presence, query window, and downsampling.
201
+ Numeric overflow is reported and omitted.
202
+
203
+ The original metric remains the chart's title and position; its subtitle identifies
204
+ the combination mode and input count. Other charts and source logs are unchanged.
205
+ Choices are saved with that chart's browser-local settings, shared by its pinned
206
+ copy, and included in PNG exports. Source aggregation does not change the run's
207
+ selected source in the sidebar. Cached requests are reused when returning to the
208
+ same operation and inputs. Legacy metric-aggregation settings are removed on load,
209
+ while smoothing, axes, height, and other appearance settings are preserved.
210
+
112
211
  ### Records and streaming
113
212
 
114
213
  Each run's **Details** button opens config/summary, paginated records, and spans
@@ -132,7 +231,8 @@ There is no workspace-wide metric limit. A Web Worker streams history over
132
231
  `/api/history/stream` as NDJSON and prepares finite per-run x/y arrays
133
232
  off the UI thread. Transferable `Float64Array` buffers move to uPlot's shared-axis,
134
233
  per-series data mode without cloning thousands of boxed numbers on the UI thread.
135
- Queries contain up to six metrics for the current page and share two concurrent
234
+ Queries contain only metrics on the current section page (up to 20 with four
235
+ columns and five rows) and share two concurrent
136
236
  streams. Each native chunk contains at most 256 points, with a bounded two-chunk
137
237
  channel providing backpressure. Disconnecting cancels the scan.
138
238
 
@@ -141,6 +241,10 @@ row (including 5,000+ points); larger ranges use extrema-preserving downsampling
141
241
  Sections initialize when approached. Scrolling away pauses subscriptions, drawing,
142
242
  after a short grace period; initialized canvases stay
143
243
  mounted so scrolling back does not repeatedly destroy and recreate plots.
244
+ Completed views with unchanged request and transform keys reuse their current
245
+ data without resubscribing or transferring worker snapshots. Canvas drawing is
246
+ skipped unless data, dimensions, theme, or chart settings change. Changed source
247
+ metadata and manual refresh still invalidate offscreen views when revisited.
144
248
  Hover values update lightweight DOM labels rather than rerendering React charts.
145
249
  Shared worker streams avoid
146
250
  duplicate scans for neighboring charts, and completed sources are cached with
@@ -161,7 +265,8 @@ workspace or refetching successful sources.
161
265
 
162
266
  Live is off by default. When enabled, it polls metadata every three seconds while the page is visible, fetching new
163
267
  curves only when source metadata changes. Visible charts update in place; old
164
- data remains visible with a loading indicator during refresh. Manual **Refresh**
268
+ data remains visible during refresh. Each loading chart's border pulses instead
269
+ of displaying a spinner; reduced-motion mode uses a steady border. Manual **Refresh**
165
270
  also invalidates curve queries. It refreshes selected runs, not unselected
166
271
  sidebar entries, and sees only complete rows already written to disk. It cannot display
167
272
  an open metric row or records still held in the training process's write buffer.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "expr_tracker"
3
- version = "0.5.0"
3
+ version = "0.6.1"
4
4
  description = "Local-first experiment tracking with queryable history and expression-based alerts on your training metrics"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -97,6 +97,7 @@ include = [
97
97
  { path = "web/package-lock.json", format = "sdist" },
98
98
  { path = "web/components.json", format = "sdist" },
99
99
  { path = "web/index.html", format = "sdist" },
100
+ { path = "web/public/**/*", format = "sdist" },
100
101
  { path = "web/tsconfig.json", format = "sdist" },
101
102
  { path = "web/vite.config.ts", format = "sdist" },
102
103
  { path = "web/vitest.config.ts", format = "sdist" },
@@ -0,0 +1,217 @@
1
+ use super::*;
2
+
3
+ #[derive(Default)]
4
+ struct Reduction {
5
+ count: usize,
6
+ scale: f64,
7
+ sum: f64,
8
+ min: f64,
9
+ max: f64,
10
+ }
11
+
12
+ impl Reduction {
13
+ fn push(&mut self, value: f64) {
14
+ if self.count == 0 {
15
+ self.min = value;
16
+ self.max = value;
17
+ } else {
18
+ self.min = self.min.min(value);
19
+ self.max = self.max.max(value);
20
+ }
21
+ let scale = value.abs();
22
+ if scale > self.scale {
23
+ self.sum *= self.scale / scale;
24
+ self.scale = scale;
25
+ }
26
+ if self.scale > 0.0 {
27
+ self.sum += value / self.scale;
28
+ }
29
+ self.count += 1;
30
+ }
31
+
32
+ fn value(&self, mode: &str) -> Option<f64> {
33
+ if self.count == 0 {
34
+ return None;
35
+ }
36
+ Some(match mode {
37
+ "min" => self.min,
38
+ "max" => self.max,
39
+ "mean" => (self.sum / self.count as f64) * self.scale,
40
+ _ => self.sum * self.scale,
41
+ })
42
+ }
43
+ }
44
+
45
+ pub(super) fn sources(
46
+ viewer: &Viewer,
47
+ run: &Run,
48
+ query: &HistoryQuery,
49
+ cancelled: impl Fn() -> bool,
50
+ ) -> Result<CachedLog> {
51
+ let files: Vec<String> = serde_json::from_str(
52
+ query
53
+ .files
54
+ .as_deref()
55
+ .ok_or_else(|| invalid("Source aggregation requires files"))?,
56
+ )?;
57
+ let mode = query.aggregation.as_deref().unwrap_or("");
58
+ if query.file.is_some() || !["sum", "mean", "max", "min"].contains(&mode) {
59
+ return Err(invalid(
60
+ "Choose sum, mean, max or min with files, not a single file",
61
+ ));
62
+ }
63
+ if files.is_empty() || files.len() > 128 {
64
+ return Err(invalid("Select between 1 and 128 metric sources per run"));
65
+ }
66
+ let paths = files
67
+ .iter()
68
+ .map(|file| {
69
+ let (path, spans) = viewer.file(run, Some(file))?;
70
+ if spans {
71
+ return Err(invalid("Aggregation requires metric sources"));
72
+ }
73
+ Ok(path)
74
+ })
75
+ .collect::<Result<Vec<_>>>()?;
76
+ if paths.iter().collect::<BTreeSet<_>>().len() != paths.len() {
77
+ return Err(invalid("Duplicate aggregation sources"));
78
+ }
79
+ let metrics: Vec<String> = serde_json::from_str(&query.metrics)?;
80
+ if metrics.is_empty() || metrics.len() > 128 || metrics.iter().any(|metric| metric.len() > 4096)
81
+ {
82
+ return Err(invalid("Select between 1 and 128 metrics"));
83
+ }
84
+ let mut output = CachedLog::new(viewer.stopping.clone())?;
85
+ output
86
+ .db
87
+ .execute_batch("CREATE TABLE source_rows(step TEXT, sort TEXT, time REAL, data TEXT);")?;
88
+ let transaction = output.db.transaction()?;
89
+ let mut missing_sources = 0;
90
+ {
91
+ let mut insert = transaction.prepare("INSERT INTO source_rows VALUES (?1,?2,?3,?4)")?;
92
+ for path in paths {
93
+ viewer.with_log(&path, false, |log| {
94
+ output.corrupt += log.corrupt;
95
+ output.surrogates |= log.surrogates;
96
+ if !metrics.iter().any(|metric| log.metrics.contains(metric)) {
97
+ missing_sources += 1;
98
+ }
99
+ let mut statement = log.db.prepare("SELECT step,sort,time,data FROM records")?;
100
+ let mut rows = statement.query([])?;
101
+ while let Some(row) = rows.next()? {
102
+ if cancelled() {
103
+ return Err(invalid("Aggregation disconnected"));
104
+ }
105
+ let raw: String = row.get(3)?;
106
+ let value: Value = serde_json::from_str(&raw)?;
107
+ let values: Map<String, Value> = metrics
108
+ .iter()
109
+ .filter_map(|metric| {
110
+ value
111
+ .get(metric)
112
+ .map(|value| (metric.clone(), value.clone()))
113
+ })
114
+ .collect();
115
+ insert.execute(params![
116
+ row.get::<_, String>(0)?,
117
+ row.get::<_, String>(1)?,
118
+ row.get::<_, Option<f64>>(2)?,
119
+ serde_json::to_string(&values)?
120
+ ])?;
121
+ }
122
+ Ok(())
123
+ })?;
124
+ }
125
+ }
126
+ let mut overflow = 0;
127
+ let mut nonfinite = 0;
128
+ {
129
+ let mut statement =
130
+ transaction.prepare("SELECT step,sort,time,data FROM source_rows ORDER BY sort")?;
131
+ let mut rows = statement.query([])?;
132
+ let mut insert = transaction
133
+ .prepare("INSERT INTO records(step,sort,ordinal,time,data) VALUES (?1,?2,?3,?4,?5)")?;
134
+ let mut current: Option<(String, String)> = None;
135
+ let mut time: Option<f64> = None;
136
+ let mut values: Vec<Reduction> = (0..metrics.len()).map(|_| Reduction::default()).collect();
137
+ let mut ordinal = 0i64;
138
+ let mut flush =
139
+ |step: &str, sort: &str, time: Option<f64>, values: &[Reduction]| -> Result<()> {
140
+ let mut data = Map::new();
141
+ data.insert("_step".into(), serde_json::from_str(step)?);
142
+ if let Some(time) = time {
143
+ data.insert("_time".into(), Value::from(time));
144
+ }
145
+ for (metric, reduction) in metrics.iter().zip(values) {
146
+ if let Some(value) = reduction.value(mode) {
147
+ if value.is_finite() {
148
+ data.insert(metric.clone(), Value::from(value));
149
+ } else {
150
+ overflow += 1;
151
+ }
152
+ }
153
+ }
154
+ insert.execute(params![
155
+ step,
156
+ sort,
157
+ ordinal,
158
+ time,
159
+ serde_json::to_string(&data)?
160
+ ])?;
161
+ ordinal += 1;
162
+ Ok(())
163
+ };
164
+ while let Some(row) = rows.next()? {
165
+ if cancelled() {
166
+ return Err(invalid("Aggregation disconnected"));
167
+ }
168
+ let step: String = row.get(0)?;
169
+ let sort: String = row.get(1)?;
170
+ if current
171
+ .as_ref()
172
+ .is_some_and(|(previous, _)| previous != &step)
173
+ {
174
+ let (previous, previous_sort) = current.as_ref().unwrap();
175
+ flush(previous, previous_sort, time, &values)?;
176
+ values = (0..metrics.len()).map(|_| Reduction::default()).collect();
177
+ time = None;
178
+ }
179
+ current = Some((step, sort));
180
+ if let Some(timestamp) = row.get::<_, Option<f64>>(2)? {
181
+ time = Some(time.map_or(timestamp, |previous| previous.min(timestamp)));
182
+ }
183
+ let raw: String = row.get(3)?;
184
+ let data: Value = serde_json::from_str(&raw)?;
185
+ for (metric, reduction) in metrics.iter().zip(&mut values) {
186
+ if let Some(value) = data.get(metric) {
187
+ if let Some(value) = finite_number(value) {
188
+ reduction.push(value);
189
+ } else if value.get("$et.nonfinite").is_some() || value.is_number() {
190
+ nonfinite += 1;
191
+ }
192
+ }
193
+ }
194
+ }
195
+ if let Some((step, sort)) = current {
196
+ flush(&step, &sort, time, &values)?;
197
+ }
198
+ }
199
+ transaction.execute("DROP TABLE source_rows", [])?;
200
+ transaction.commit()?;
201
+ if missing_sources > 0 {
202
+ output.extra_warnings.push(format!(
203
+ "{missing_sources} selected sources have no recorded values for the requested metrics; missing values are ignored."
204
+ ));
205
+ }
206
+ if overflow > 0 {
207
+ output.extra_warnings.push(format!(
208
+ "{overflow} aggregated values exceeded the numeric range and were omitted."
209
+ ));
210
+ }
211
+ if nonfinite > 0 {
212
+ output.extra_warnings.push(format!(
213
+ "{nonfinite} non-finite source observations ignored."
214
+ ));
215
+ }
216
+ Ok(output)
217
+ }
@@ -16,6 +16,8 @@ struct Saved {
16
16
  version: u8,
17
17
  key: String,
18
18
  sessions: Vec<Session>,
19
+ #[serde(default)]
20
+ workspace: workspace::Workspace,
19
21
  }
20
22
 
21
23
  pub(super) struct AuthStore {
@@ -49,6 +51,7 @@ impl AuthStore {
49
51
  version: 1,
50
52
  key,
51
53
  sessions: Vec::new(),
54
+ workspace: workspace::Workspace::default(),
52
55
  };
53
56
  let mut lock = None;
54
57
  let path = path
@@ -115,6 +118,7 @@ impl AuthStore {
115
118
  {
116
119
  return Err(invalid_state());
117
120
  }
121
+ saved.workspace.validate().map_err(|_| invalid_state())?;
118
122
  let store = Self {
119
123
  data: Mutex::new(saved),
120
124
  path,
@@ -144,6 +148,29 @@ impl AuthStore {
144
148
  self.data.lock().key.clone()
145
149
  }
146
150
 
151
+ pub(super) fn workspace(&self) -> workspace::Workspace {
152
+ self.data.lock().workspace.clone()
153
+ }
154
+
155
+ pub(super) fn save_workspace(&self, mut workspace: workspace::Workspace) -> Result<workspace::Workspace> {
156
+ workspace.validate()?;
157
+ let mut data = self.data.lock();
158
+ if workspace.revision != data.workspace.revision {
159
+ return Err(Error(
160
+ StatusCode::CONFLICT,
161
+ "Workspace changed in another browser. Shared runs have been reloaded; please try again.".into(),
162
+ ));
163
+ }
164
+ workspace.revision = workspace.revision.checked_add(1)
165
+ .filter(|revision| *revision < (1u64 << 53))
166
+ .ok_or_else(|| invalid("Workspace revision limit reached"))?;
167
+ let mut next = data.clone();
168
+ next.workspace = workspace.clone();
169
+ self.persist(&next)?;
170
+ *data = next;
171
+ Ok(workspace)
172
+ }
173
+
147
174
  pub(super) fn contains_at(&self, token: &str, now: u64) -> bool {
148
175
  self.data.lock().sessions.iter().any(|session| {
149
176
  session.expires > now && auth::matches(token.as_bytes(), session.token.as_bytes())
@@ -21,6 +21,7 @@ pub(super) struct CachedLog {
21
21
  pub(super) metrics: BTreeSet<String>,
22
22
  pub(super) corrupt: u64,
23
23
  pub(super) surrogates: bool,
24
+ pub(super) extra_warnings: Vec<String>,
24
25
  pub(super) stopping: Arc<AtomicBool>,
25
26
  }
26
27
 
@@ -48,12 +49,13 @@ impl CachedLog {
48
49
  metrics: BTreeSet::new(),
49
50
  corrupt: 0,
50
51
  surrogates: false,
52
+ extra_warnings: Vec::new(),
51
53
  stopping,
52
54
  })
53
55
  }
54
56
 
55
57
  pub(super) fn warnings(&self) -> Vec<String> {
56
- let mut warnings = Vec::new();
58
+ let mut warnings = self.extra_warnings.clone();
57
59
  if self.corrupt > 0 {
58
60
  warnings.push(format!("Skipped {} malformed records.", self.corrupt));
59
61
  }
@@ -0,0 +1,30 @@
1
+ use super::LogFile;
2
+
3
+ pub(super) fn parse(name: &str) -> Option<LogFile> {
4
+ let stem = name.strip_suffix(".jsonl")?;
5
+ let mut parts: Vec<&str> = stem.split('.').collect();
6
+ let kind = parts.first()?;
7
+ if !["metrics", "spans"].contains(kind) {
8
+ return None;
9
+ }
10
+ let mut rank = 0;
11
+ if parts.len() > 1 {
12
+ if let Some(number) = parts
13
+ .last()?
14
+ .strip_prefix("rank")
15
+ .and_then(|rank| rank.parse::<u64>().ok())
16
+ {
17
+ rank = number;
18
+ parts.pop();
19
+ }
20
+ }
21
+ if parts.len() > 2 {
22
+ return None;
23
+ }
24
+ Some(LogFile {
25
+ name: name.into(),
26
+ kind: parts[0].into(),
27
+ stream: parts.get(1).map(|stream| (*stream).into()),
28
+ rank,
29
+ })
30
+ }
@@ -44,7 +44,9 @@ fn query(log: &CachedLog, query: &HistoryQuery, metrics: &[String], sender: &Sen
44
44
  sender,
45
45
  json!({"type":"start", "metrics":metrics, "total":total, "warnings":log.warnings()}),
46
46
  )?;
47
- let time = query.axis.as_deref() == Some("time");
47
+ let axis = query.axis.as_deref().unwrap_or("step");
48
+ let time = axis != "step";
49
+ let origin = time_origin(log, axis)?;
48
50
  let sql = if time {
49
51
  "SELECT data FROM records WHERE sort>=?1 AND sort<?2 ORDER BY time,sort"
50
52
  } else {
@@ -88,7 +90,11 @@ fn query(log: &CachedLog, query: &HistoryQuery, metrics: &[String], sender: &Sen
88
90
  number
89
91
  })
90
92
  .collect();
91
- let point = Point { at, x, ys };
93
+ let point = Point {
94
+ at,
95
+ x: x - origin,
96
+ ys,
97
+ };
92
98
  if total <= limit {
93
99
  points.push(point);
94
100
  } else {
@@ -138,7 +144,7 @@ pub(super) async fn history(
138
144
  "max_points is too small for the selected metric count",
139
145
  ));
140
146
  }
141
- if !["step", "time"].contains(&query_args.axis.as_deref().unwrap_or("step")) {
147
+ if !["step", "time", "relative"].contains(&query_args.axis.as_deref().unwrap_or("step")) {
142
148
  return Err(invalid("Unknown chart axis"));
143
149
  }
144
150
  let low = bound(query_args.from_step.as_deref())?;
@@ -155,6 +161,10 @@ pub(super) async fn history(
155
161
  let result = (|| {
156
162
  let root = viewer.resolve_root(query_args.root.as_deref())?;
157
163
  let run = viewer.run(&root, &query_args.run)?;
164
+ if query_args.files.is_some() || query_args.aggregation.is_some() {
165
+ let log = aggregate::sources(&viewer, &run, &query_args, || sender.is_closed())?;
166
+ return query(&log, &query_args, &metrics, &sender);
167
+ }
158
168
  let (path, spans) = viewer.file(&run, query_args.file.as_deref())?;
159
169
  if spans {
160
170
  return Err(invalid("Charts require a metric source"));