bobframes 0.2.7__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. {bobframes-0.2.7 → bobframes-0.2.8}/CHANGELOG.md +31 -1
  2. {bobframes-0.2.7 → bobframes-0.2.8}/PKG-INFO +17 -1
  3. {bobframes-0.2.7 → bobframes-0.2.8}/README.md +16 -0
  4. bobframes-0.2.8/bobframes/_version.py +1 -0
  5. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/cli.py +22 -13
  6. bobframes-0.2.8/bobframes/serve.py +36 -0
  7. bobframes-0.2.8/bobframes/tests/_ui_util.py +57 -0
  8. bobframes-0.2.8/bobframes/tests/test_ui_ab_theme.py +162 -0
  9. bobframes-0.2.8/bobframes/tests/test_ui_browser.py +78 -0
  10. bobframes-0.2.8/bobframes/tests/test_ui_jobs.py +82 -0
  11. bobframes-0.2.8/bobframes/tests/test_ui_js_parses.py +34 -0
  12. bobframes-0.2.8/bobframes/tests/test_ui_progress.py +64 -0
  13. bobframes-0.2.8/bobframes/tests/test_ui_security.py +44 -0
  14. bobframes-0.2.8/bobframes/tests/test_ui_share.py +135 -0
  15. bobframes-0.2.8/bobframes/tests/test_ui_smoke.py +45 -0
  16. bobframes-0.2.8/bobframes/tests/test_ui_state.py +33 -0
  17. bobframes-0.2.8/bobframes/ui/__init__.py +17 -0
  18. bobframes-0.2.8/bobframes/ui/assets/panel.css +50 -0
  19. bobframes-0.2.8/bobframes/ui/assets/panel.js +151 -0
  20. bobframes-0.2.8/bobframes/ui/jobs.py +119 -0
  21. bobframes-0.2.8/bobframes/ui/progress.py +64 -0
  22. bobframes-0.2.8/bobframes/ui/server.py +513 -0
  23. bobframes-0.2.7/bobframes/_version.py +0 -1
  24. {bobframes-0.2.7 → bobframes-0.2.8}/.gitignore +0 -0
  25. {bobframes-0.2.7 → bobframes-0.2.8}/LICENSE +0 -0
  26. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/__init__.py +0 -0
  27. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/_default_config.toml +0 -0
  28. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/aggregates.py +0 -0
  29. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/catalog.py +0 -0
  30. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/config.py +0 -0
  31. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derive_post_merge.py +0 -0
  32. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/__init__.py +0 -0
  33. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/classifier.py +0 -0
  34. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/draw_classifier.toml +0 -0
  35. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/pass_class_breakdown.py +0 -0
  36. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/presets/custom-template.toml +0 -0
  37. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/presets/godot.toml +0 -0
  38. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/presets/unity.toml +0 -0
  39. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/derives/texture_usage.py +0 -0
  40. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/discovery.py +0 -0
  41. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/errors.py +0 -0
  42. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/global_entities.py +0 -0
  43. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/health.py +0 -0
  44. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/html/__init__.py +0 -0
  45. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/html/template.py +0 -0
  46. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/lint.py +0 -0
  47. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/lint_banlist.toml +0 -0
  48. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/manifest.py +0 -0
  49. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/package.py +0 -0
  50. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/parquetize.py +0 -0
  51. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/parsers/__init__.py +0 -0
  52. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/parsers/derive_program_transitions.py +0 -0
  53. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/parsers/parse_init_state.py +0 -0
  54. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/paths.py +0 -0
  55. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/probes/__init__.py +0 -0
  56. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/probes/whatif.py +0 -0
  57. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/qrd_harness.py +0 -0
  58. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/query_examples.py +0 -0
  59. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/rdcmd.py +0 -0
  60. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/replay/__init__.py +0 -0
  61. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/replay/replay_main.py +0 -0
  62. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/__init__.py +0 -0
  63. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/_tokens.py +0 -0
  64. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/ab.py +0 -0
  65. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/Inter-OFL.txt +0 -0
  66. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/README.md +0 -0
  67. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/chrome.css +0 -0
  68. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/components.css +0 -0
  69. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/components.js +0 -0
  70. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/container.css +0 -0
  71. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/design_tokens.css +0 -0
  72. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/icon_sprite.html +0 -0
  73. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/inter-subset.woff2 +0 -0
  74. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/link_kind.css +0 -0
  75. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/per_drop.css +0 -0
  76. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/print.css +0 -0
  77. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/rdc_table.css +0 -0
  78. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/rdc_table.js +0 -0
  79. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/assets/sticky.css +0 -0
  80. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/base.py +0 -0
  81. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/cache.py +0 -0
  82. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/charts.py +0 -0
  83. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/chrome.py +0 -0
  84. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/cli.py +0 -0
  85. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/dashboard.py +0 -0
  86. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/delta.py +0 -0
  87. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/design_tokens.toml +0 -0
  88. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/discovery.py +0 -0
  89. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/draws_by_class.py +0 -0
  90. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/formatters.py +0 -0
  91. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/instancing_opportunities.py +0 -0
  92. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/orchestrator.py +0 -0
  93. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/overdraw.py +0 -0
  94. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/pass_gpu.py +0 -0
  95. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/preview.py +0 -0
  96. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/shader_hotlist.py +0 -0
  97. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/summary.py +0 -0
  98. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/reports/trend_table.py +0 -0
  99. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/resource_labels.py +0 -0
  100. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/run.py +0 -0
  101. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/schemas.py +0 -0
  102. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/stable_keys.py +0 -0
  103. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/__init__.py +0 -0
  104. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/_render_util.py +0 -0
  105. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_pagedata/catalog.js +0 -0
  106. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/draws_by_class.html +0 -0
  107. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  108. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  109. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  110. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  111. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  112. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  113. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  114. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  115. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  116. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  117. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  118. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  119. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  120. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  121. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  122. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  123. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  124. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  125. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  126. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  127. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  128. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  129. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  130. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  131. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  132. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  133. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  134. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/index.html +0 -0
  135. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/instancing_opportunities.html +0 -0
  136. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/overdraw.html +0 -0
  137. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/pass_gpu.html +0 -0
  138. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/draws_by_class.html +0 -0
  139. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/index.html +0 -0
  140. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -0
  141. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/overdraw.html +0 -0
  142. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/pass_gpu.html +0 -0
  143. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -0
  144. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/summary.html +0 -0
  145. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/shader_hotlist.html +0 -0
  146. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/summary.html +0 -0
  147. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/trend_table.html +0 -0
  148. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden/index.html +0 -0
  149. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/README.txt +0 -0
  150. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_pagedata/catalog.js +0 -0
  151. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/draws_by_class.html +0 -0
  152. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  153. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  154. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  155. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  156. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  157. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  158. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  159. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  160. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  161. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  162. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  163. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  164. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  165. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  166. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  167. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  168. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  169. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  170. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  171. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  172. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  173. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  174. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  175. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  176. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  177. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  178. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  179. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/index.html +0 -0
  180. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/instancing_opportunities.html +0 -0
  181. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/overdraw.html +0 -0
  182. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/pass_gpu.html +0 -0
  183. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/draws_by_class.html +0 -0
  184. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/index.html +0 -0
  185. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -0
  186. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/overdraw.html +0 -0
  187. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/pass_gpu.html +0 -0
  188. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -0
  189. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/summary.html +0 -0
  190. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/shader_hotlist.html +0 -0
  191. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/summary.html +0 -0
  192. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/trend_table.html +0 -0
  193. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/index.html +0 -0
  194. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/README.txt +0 -0
  195. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/catalog.css +0 -0
  196. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/catalog.js +0 -0
  197. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/report.css +0 -0
  198. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/report.js +0 -0
  199. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_pagedata/catalog.js +0 -0
  200. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/draws_by_class.html +0 -0
  201. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  202. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  203. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  204. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  205. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  206. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  207. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  208. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  209. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  210. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  211. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  212. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  213. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  214. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  215. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  216. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  217. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  218. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  219. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  220. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  221. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  222. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  223. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  224. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  225. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  226. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  227. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  228. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/index.html +0 -0
  229. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/instancing_opportunities.html +0 -0
  230. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/overdraw.html +0 -0
  231. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/pass_gpu.html +0 -0
  232. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/draws_by_class.html +0 -0
  233. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/index.html +0 -0
  234. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -0
  235. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/overdraw.html +0 -0
  236. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/pass_gpu.html +0 -0
  237. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -0
  238. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/summary.html +0 -0
  239. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/shader_hotlist.html +0 -0
  240. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/summary.html +0 -0
  241. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/trend_table.html +0 -0
  242. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/index.html +0 -0
  243. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/README.txt +0 -0
  244. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/catalog.css +0 -0
  245. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/catalog.js +0 -0
  246. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/report.css +0 -0
  247. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/report.js +0 -0
  248. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_pagedata/catalog.js +0 -0
  249. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/draws_by_class.html +0 -0
  250. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  251. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  252. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  253. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  254. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  255. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  256. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  257. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  258. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  259. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  260. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  261. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  262. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  263. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  264. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  265. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  266. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  267. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  268. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  269. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  270. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  271. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  272. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  273. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  274. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  275. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  276. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  277. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/index.html +0 -0
  278. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/instancing_opportunities.html +0 -0
  279. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/overdraw.html +0 -0
  280. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/pass_gpu.html +0 -0
  281. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/draws_by_class.html +0 -0
  282. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/index.html +0 -0
  283. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -0
  284. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/overdraw.html +0 -0
  285. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/pass_gpu.html +0 -0
  286. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -0
  287. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/summary.html +0 -0
  288. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/shader_hotlist.html +0 -0
  289. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/summary.html +0 -0
  290. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/trend_table.html +0 -0
  291. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/index.html +0 -0
  292. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_parquet/digests.json +0 -0
  293. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/golden_preview/_chrome_preview.html +0 -0
  294. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +0 -0
  295. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  296. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  297. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  298. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  299. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  300. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  301. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  302. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  303. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  304. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  305. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  306. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  307. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  308. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  309. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  310. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  311. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  312. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  313. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  314. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  315. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  316. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  317. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  318. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  319. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  320. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  321. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  322. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +0 -0
  323. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  324. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  325. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  326. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  327. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  328. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  329. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  330. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  331. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  332. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  333. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  334. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  335. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  336. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  337. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  338. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  339. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  340. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  341. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  342. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  343. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  344. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  345. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  346. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  347. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  348. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  349. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  350. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/make_golden.py +0 -0
  351. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/make_package_golden.py +0 -0
  352. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/make_parquet_golden.py +0 -0
  353. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/make_preview_golden.py +0 -0
  354. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/make_synthetic.py +0 -0
  355. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/smoke.py +0 -0
  356. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_aggregates.py +0 -0
  357. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_assets.py +0 -0
  358. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_browser_shots.py +0 -0
  359. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_cache.py +0 -0
  360. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_catalog_backup_dedup.py +0 -0
  361. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_charts.py +0 -0
  362. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_classifier.py +0 -0
  363. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_components.py +0 -0
  364. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_config.py +0 -0
  365. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_contrast.py +0 -0
  366. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_delta.py +0 -0
  367. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_design_tokens.py +0 -0
  368. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_determinism.py +0 -0
  369. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_discovery.py +0 -0
  370. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_draws_by_class_kpis.py +0 -0
  371. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_element_builder.py +0 -0
  372. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_fonts.py +0 -0
  373. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_hardening.py +0 -0
  374. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_head_assets.py +0 -0
  375. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_health.py +0 -0
  376. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_js_coupled_classes.py +0 -0
  377. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_manifest_guard.py +0 -0
  378. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_multicapture_normalize.py +0 -0
  379. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_package.py +0 -0
  380. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_parity.py +0 -0
  381. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_parquet_parity.py +0 -0
  382. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_parquetize.py +0 -0
  383. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_perf.py +0 -0
  384. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_replay_drift.py +0 -0
  385. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_report_polish.py +0 -0
  386. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_report_structure.py +0 -0
  387. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_run_model.py +0 -0
  388. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_schemas.py +0 -0
  389. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_schemas_unit.py +0 -0
  390. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_stable_keys.py +0 -0
  391. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_summary.py +0 -0
  392. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_table_component.py +0 -0
  393. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_theme.py +0 -0
  394. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_token_guard.py +0 -0
  395. {bobframes-0.2.7 → bobframes-0.2.8}/bobframes/tests/test_trend_regression_basis.py +0 -0
  396. {bobframes-0.2.7 → bobframes-0.2.8}/pyproject.toml +0 -0
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.8] - 2026-06-24
11
+
12
+ A guided local-web control panel (`bobframes ui`) for QA and product teammates who are not comfortable
13
+ in a terminal. Zero new runtime dependency (stdlib `http.server` only) and no report-output change --
14
+ the panel drives the existing verbs and emits no report HTML, so the rendered HTML and parquet digests
15
+ stay byte-identical on the same captures. No schema change (still schema 3). `_version` 0.2.7 -> 0.2.8.
16
+
17
+ ### Added
18
+ - `bobframes ui`: a zero-dependency local-web control panel that drives the whole pipeline from a
19
+ browser (ADR-47). It detects the RenderDoc tools and discovers capture drops, runs ingest /
20
+ re-generate / package / A-B as subprocesses with live per-capture progress streamed over Server-Sent
21
+ Events, opens or serves the rendered report, applies accent theming to a re-render, and can scaffold a
22
+ convention-correct capture folder. Server-rendered HTML + vanilla JS -- no framework, no build step,
23
+ no client router (the hard governance line in ADR-47). Binds `127.0.0.1` only and gates every
24
+ `/api/*` call with a per-session token. Install with `pipx install bobframes`, then run `bobframes ui`.
25
+
26
+ ### Changed
27
+ - The static preview server body moved from `cli._cmd_serve` into a reusable `serve.make_server` /
28
+ `serve.serve_forever` so the panel's background serve and the `bobframes serve` verb share it; the
29
+ `serve` verb's behavior is unchanged.
30
+ - The control panel's JS and CSS are now served as static files (`bobframes/ui/assets/panel.{js,css}`,
31
+ at `GET /panel.js` / `GET /panel.css`) instead of being embedded in a Python string -- so the client
32
+ is a real file that `node --check`/lint can validate (no build step; still no framework or router).
33
+
34
+ ### Tests / CI
35
+ - Added an automated guard that the panel JS parses and runs: a `node --check` gate (a pytest test +
36
+ an unconditional CI step) and an opt-in `browser`-marked headless smoke that drives the live panel in
37
+ Chrome and asserts it populates -- closing the gap that let a broken `<script>` ship undetected.
38
+
10
39
  ## [0.2.7] - 2026-06-24
11
40
 
12
41
  An aggregation-consistency pass plus four report/sharing correctness fixes. Every KPI now names its
@@ -160,7 +189,8 @@ First standalone release. v1 is Windows-only (the replay stage drives `qrenderdo
160
189
  `python -m _analysis.*` entry points no longer work; switch to the `bobframes` commands (see the
161
190
  migration table in the README). This is a hard rename with no compatibility shim.
162
191
 
163
- [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.2.7...HEAD
192
+ [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.2.8...HEAD
193
+ [0.2.8]: https://github.com/altpsyche/bobframes/compare/v0.2.7...v0.2.8
164
194
  [0.2.7]: https://github.com/altpsyche/bobframes/compare/v0.2.6...v0.2.7
165
195
  [0.2.6]: https://github.com/altpsyche/bobframes/compare/v0.2.0...v0.2.6
166
196
  [0.2.0]: https://github.com/altpsyche/bobframes/compare/v0.1.0...v0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bobframes
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: RenderDoc capture pipeline: ingest, analyze, render.
5
5
  Project-URL: Homepage, https://github.com/altpsyche/bobframes
6
6
  Project-URL: Issues, https://github.com/altpsyche/bobframes/issues
@@ -55,6 +55,21 @@ bobframes check
55
55
  `bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
56
56
  either is missing, so you can confirm the toolchain before a long ingest.
57
57
 
58
+ ## Guided mode (recommended for QA / product)
59
+
60
+ Not comfortable in a terminal? `bobframes ui` opens a local-web control panel in your browser that
61
+ drives the whole pipeline point-and-click: detect the RenderDoc tools, pick a capture folder, ingest
62
+ with live per-capture progress, then open / serve / package the report or compare two runs.
63
+
64
+ ```
65
+ pipx install bobframes
66
+ bobframes ui # opens http://127.0.0.1:8765 in your browser
67
+ ```
68
+
69
+ The panel binds `127.0.0.1` only and carries a one-time session token in the opened URL, so only your
70
+ own browser can drive it. It is stdlib-only -- one `pipx install` gives every feature, no extra setup --
71
+ and it emits no report files of its own; it just runs the same verbs as the CLI.
72
+
58
73
  ## Quickstart
59
74
 
60
75
  ```
@@ -78,6 +93,7 @@ time to rebuild the HTML from existing Parquet without re-replaying captures.
78
93
  | `lint <file>...` | Check HTML or markdown against the banlist. |
79
94
  | `check` | Print resolved tool paths; non-zero when a tool is missing. |
80
95
  | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
96
+ | `ui [root] [--port 8765] [--bind 127.0.0.1] [--no-open]` | Guided local-web control panel (ADR-47): ingest / render / package / A-B / open / serve / scaffold from a browser, with live progress. Localhost-bound + per-session token; emits no report output of its own. |
81
97
  | `package [root] [--inline] [--light] [--redact] [--redact-paths {strip,fail}] [--out PATH] [--run KEY] [--no-summary-file] [--stage]` | Bundle a rendered tree into a shareable `<project>-<rundate>-report.zip` + a standalone `<project>-<rundate>-summary.html`, both written OUTSIDE `<root>` (non-mutating). `--redact` scrubs device/host provenance + absolute paths for external sharing. |
82
98
  | `preview [root] [--accent OKLCH] [--accent-data OKLCH]` | Render the chrome gallery to `_reports/_chrome_preview.html`; no capture data needed. `--accent` previews a theme override before you commit it. |
83
99
  | `export-tokens [--format toml\|json\|css] [--theme-template]` | Print the design tokens to stdout in the chosen format. `--theme-template` emits a paste-ready `[theme]` block for `.bobframes.toml`. |
@@ -24,6 +24,21 @@ bobframes check
24
24
  `bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
25
25
  either is missing, so you can confirm the toolchain before a long ingest.
26
26
 
27
+ ## Guided mode (recommended for QA / product)
28
+
29
+ Not comfortable in a terminal? `bobframes ui` opens a local-web control panel in your browser that
30
+ drives the whole pipeline point-and-click: detect the RenderDoc tools, pick a capture folder, ingest
31
+ with live per-capture progress, then open / serve / package the report or compare two runs.
32
+
33
+ ```
34
+ pipx install bobframes
35
+ bobframes ui # opens http://127.0.0.1:8765 in your browser
36
+ ```
37
+
38
+ The panel binds `127.0.0.1` only and carries a one-time session token in the opened URL, so only your
39
+ own browser can drive it. It is stdlib-only -- one `pipx install` gives every feature, no extra setup --
40
+ and it emits no report files of its own; it just runs the same verbs as the CLI.
41
+
27
42
  ## Quickstart
28
43
 
29
44
  ```
@@ -47,6 +62,7 @@ time to rebuild the HTML from existing Parquet without re-replaying captures.
47
62
  | `lint <file>...` | Check HTML or markdown against the banlist. |
48
63
  | `check` | Print resolved tool paths; non-zero when a tool is missing. |
49
64
  | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
65
+ | `ui [root] [--port 8765] [--bind 127.0.0.1] [--no-open]` | Guided local-web control panel (ADR-47): ingest / render / package / A-B / open / serve / scaffold from a browser, with live progress. Localhost-bound + per-session token; emits no report output of its own. |
50
66
  | `package [root] [--inline] [--light] [--redact] [--redact-paths {strip,fail}] [--out PATH] [--run KEY] [--no-summary-file] [--stage]` | Bundle a rendered tree into a shareable `<project>-<rundate>-report.zip` + a standalone `<project>-<rundate>-summary.html`, both written OUTSIDE `<root>` (non-mutating). `--redact` scrubs device/host provenance + absolute paths for external sharing. |
51
67
  | `preview [root] [--accent OKLCH] [--accent-data OKLCH]` | Render the chrome gallery to `_reports/_chrome_preview.html`; no capture data needed. `--accent` previews a theme override before you commit it. |
52
68
  | `export-tokens [--format toml\|json\|css] [--theme-template]` | Print the design tokens to stdout in the chosen format. `--theme-template` emits a paste-ready `[theme]` block for `.bobframes.toml`. |
@@ -0,0 +1 @@
1
+ __version__ = "0.2.8"
@@ -251,20 +251,19 @@ def _cmd_check(args: argparse.Namespace) -> int:
251
251
 
252
252
 
253
253
  def _cmd_serve(args: argparse.Namespace) -> int:
254
- import functools
255
- import http.server
256
- import socketserver
254
+ # The static preview server body lives in serve.serve_forever so the `ui` panel can reuse the
255
+ # same builder (serve.make_server) for its background click-to-serve (v028_3).
256
+ from . import serve
257
+ return serve.serve_forever(os.path.abspath(args.root), bind=args.bind, port=args.port)
257
258
 
258
- root = os.path.abspath(args.root)
259
- handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=root)
260
- log = logging.getLogger('bobframes')
261
- try:
262
- with socketserver.TCPServer((args.bind, args.port), handler) as httpd:
263
- log.info(f'serving {root} at http://{args.bind}:{args.port} (Ctrl+C to stop)')
264
- httpd.serve_forever()
265
- except KeyboardInterrupt:
266
- return 4
267
- return 0
259
+
260
+ def _cmd_ui(args: argparse.Namespace) -> int:
261
+ # ADR-47: a zero-dependency local-web control panel that DRIVES the verbs for non-terminal users.
262
+ # A frontend SURFACE above the verb taxonomy -- it emits no report HTML (golden gate untouched) and
263
+ # pulls no dep into core. Lazy-imported so a core install never loads the ui package.
264
+ from .ui import server
265
+ return server.serve(os.path.abspath(args.root), bind=args.bind, port=args.port,
266
+ open_browser=not args.no_open)
268
267
 
269
268
 
270
269
  def _cmd_package(args: argparse.Namespace) -> int:
@@ -380,6 +379,16 @@ def _build_parser() -> argparse.ArgumentParser:
380
379
  sp.add_argument('--bind', default='127.0.0.1')
381
380
  sp.set_defaults(func=_cmd_serve)
382
381
 
382
+ sp = sub.add_parser('ui', parents=[common],
383
+ help='guided local-web control panel (ingest/generate/package in a browser)')
384
+ sp.add_argument('root', nargs='?', default='.')
385
+ sp.add_argument('--port', type=int, default=8765)
386
+ sp.add_argument('--bind', default='127.0.0.1',
387
+ help='bind address; localhost only by design (ADR-47)')
388
+ sp.add_argument('--no-open', action='store_true',
389
+ help="don't auto-open the browser")
390
+ sp.set_defaults(func=_cmd_ui)
391
+
383
392
  sp = sub.add_parser('package', parents=[common],
384
393
  help='bundle a rendered tree into a shareable zip + standalone summary')
385
394
  sp.add_argument('root', nargs='?', default='.')
@@ -0,0 +1,36 @@
1
+ """Static preview file server (extracted from ``cli._cmd_serve`` for reuse by ``bobframes ui``).
2
+
3
+ A stdlib ``http.server`` serving ``<root>`` read-only over localhost. ``make_server`` builds (does not
4
+ start) the server; ``serve_forever`` builds it and blocks until Ctrl+C -- the body of the ``serve`` verb.
5
+ The ``ui`` panel reuses ``make_server`` to run the static server in a background daemon thread alongside
6
+ the control panel (so a click-to-serve returns a URL instead of blocking the request handler).
7
+
8
+ No new dependency; behavior is identical to the prior inlined ``_cmd_serve`` (same ``socketserver``
9
+ TCPServer + ``SimpleHTTPRequestHandler(directory=root)`` + ``[HH:MM:SS]`` log line + exit codes).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import functools
14
+ import http.server
15
+ import logging
16
+ import socketserver
17
+
18
+ log = logging.getLogger('bobframes')
19
+
20
+
21
+ def make_server(root: str, *, bind: str = '127.0.0.1', port: int = 8000) -> socketserver.TCPServer:
22
+ """Build (do not start) a static file server over ``<root>``. ``port=0`` binds an ephemeral port
23
+ (read it back from ``server.server_address[1]``); used by the panel's background serve."""
24
+ handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=root)
25
+ return socketserver.TCPServer((bind, port), handler)
26
+
27
+
28
+ def serve_forever(root: str, *, bind: str = '127.0.0.1', port: int = 8000) -> int:
29
+ """Serve ``<root>`` and block until Ctrl+C. Returns a CLI exit code (0 clean, 4 interrupted)."""
30
+ try:
31
+ with make_server(root, bind=bind, port=port) as httpd:
32
+ log.info(f'serving {root} at http://{bind}:{port} (Ctrl+C to stop)')
33
+ httpd.serve_forever()
34
+ except KeyboardInterrupt:
35
+ return 4
36
+ return 0
@@ -0,0 +1,57 @@
1
+ """Shared helpers for the `bobframes ui` panel tests (v028_1).
2
+
3
+ Starts the stdlib panel server on an ephemeral port in a daemon thread and hits it with `urllib`,
4
+ so the tests exercise the real HTTP surface (token gating, JSON shape) without a browser or GPU.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ import json
10
+ import os
11
+ import threading
12
+ import urllib.request
13
+
14
+ from ..ui import server as _server
15
+
16
+
17
+ def make_capture_root(tmp_path) -> str:
18
+ """A tmp project root with the `<Area>/<YYYY-MM-DD[_label]>/*.rdc` convention (mirrors
19
+ test_discovery): Town has the newest dated drop with 3 captures; Bay a single undated-label drop."""
20
+ root = str(tmp_path / 'proj')
21
+ layout = (('Town', '2026-05-27_old', ('1',)),
22
+ ('Town', '2026-05-28_new', ('1', '2', '10')),
23
+ ('Bay', '2026-01-01', ('cap',)))
24
+ for area, key, caps in layout:
25
+ for cap in caps:
26
+ p = os.path.join(root, area, key, f'{cap}.rdc')
27
+ os.makedirs(os.path.dirname(p), exist_ok=True)
28
+ open(p, 'w', encoding='utf-8').close()
29
+ return root
30
+
31
+
32
+ @contextlib.contextmanager
33
+ def running(root: str):
34
+ """Run the panel server on an ephemeral port; yield (httpd, port)."""
35
+ httpd = _server.build_server(root, port=0)
36
+ t = threading.Thread(target=httpd.serve_forever, daemon=True)
37
+ t.start()
38
+ try:
39
+ yield httpd, httpd.server_address[1]
40
+ finally:
41
+ _server._shutdown_aux(httpd) # stop the background static serve if /api/serve started one
42
+ httpd.shutdown()
43
+ t.join(timeout=5)
44
+
45
+
46
+ def get(port: int, path: str, headers: dict | None = None):
47
+ """GET http://127.0.0.1:<port><path>; returns the response (raises HTTPError on >=400)."""
48
+ req = urllib.request.Request(f'http://127.0.0.1:{port}{path}', headers=headers or {})
49
+ return urllib.request.urlopen(req, timeout=5)
50
+
51
+
52
+ def post(port: int, path: str, token: str, body: dict):
53
+ """POST JSON with the session token; returns the response (raises HTTPError on >=400)."""
54
+ req = urllib.request.Request(
55
+ f'http://127.0.0.1:{port}{path}', data=json.dumps(body).encode('utf-8'), method='POST',
56
+ headers={'Content-Type': 'application/json', 'X-Bobframes-Token': token})
57
+ return urllib.request.urlopen(req, timeout=5)
@@ -0,0 +1,162 @@
1
+ """v028_4: A/B comparison + accent theming + the opt-in capture-folder scaffold.
2
+
3
+ A/B and the accent re-render are streamed subprocess jobs mocked at the spawn seam (no GPU/RenderDoc --
4
+ ADR-6): `jobs.spawn` (render reuses `python -m bobframes.run`) and `jobs.spawn_cli` (`bobframes ab`)
5
+ are monkeypatched with a fake process. The run list backing the picker is asserted via `/api/state`
6
+ (`discovery.discover_drops` monkeypatched). Scaffold creates a real folder under a tmp root.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import urllib.error
13
+
14
+ import pytest
15
+
16
+ from ..ui import jobs as _jobs
17
+ from ._ui_util import get, post, running
18
+
19
+
20
+ class _FakeProc:
21
+ def __init__(self, lines, rc=0):
22
+ self.stdout = iter(lines)
23
+ self.returncode = None
24
+ self._rc = rc
25
+
26
+ def wait(self):
27
+ self.returncode = self._rc
28
+ return self._rc
29
+
30
+ def poll(self):
31
+ return self.returncode
32
+
33
+ def terminate(self):
34
+ self.returncode = -15
35
+
36
+
37
+ class _FakeRun:
38
+ def __init__(self, key, label, date, n):
39
+ self.key, self.label, self.date, self.n_captures = key, label, date, n
40
+
41
+
42
+ # --- argv builders ------------------------------------------------------------------------------
43
+
44
+ def test_build_render_argv_threads_accent():
45
+ argv = _jobs.build_render_argv('/proj', accent='oklch(0.5 0.2 250)', accent_data='red')
46
+ assert '--render-only' in argv
47
+ assert argv[argv.index('--accent') + 1] == 'oklch(0.5 0.2 250)'
48
+ assert argv[argv.index('--accent-data') + 1] == 'red'
49
+ plain = _jobs.build_render_argv('/proj')
50
+ assert '--accent' not in plain and '--accent-data' not in plain and '--render-only' in plain
51
+
52
+
53
+ def test_build_ab_argv_mirrors_ab_verb():
54
+ argv = _jobs.build_ab_argv('/proj', baseline_label='r1', compare_label='r2',
55
+ baseline_date='2026-05-27', compare_date='2026-05-28')
56
+ assert argv[0] == 'ab'
57
+ assert argv[argv.index('--baseline-label') + 1] == 'r1'
58
+ assert argv[argv.index('--compare-label') + 1] == 'r2'
59
+ assert argv[argv.index('--baseline-date') + 1] == '2026-05-27'
60
+ assert argv[argv.index('--compare-date') + 1] == '2026-05-28'
61
+ bare = _jobs.build_ab_argv('/proj', baseline_label='r1', compare_label='r2')
62
+ assert '--baseline-date' not in bare and '--compare-date' not in bare
63
+
64
+
65
+ # --- /api/render accent -------------------------------------------------------------------------
66
+
67
+ def test_render_threads_accent_into_spawn(tmp_path, monkeypatch):
68
+ seen = {}
69
+
70
+ def fake_spawn(argv):
71
+ seen['argv'] = argv
72
+ return _FakeProc(["[10:00:00] render-only done"])
73
+ monkeypatch.setattr(_jobs, 'spawn', fake_spawn)
74
+ with running(str(tmp_path)) as (httpd, port):
75
+ jid = json.loads(post(port, '/api/render', httpd.bobframes_token,
76
+ {'accent': 'oklch(0.6 0.2 30)', 'accent_data': ''}).read())['job']
77
+ stream = get(port, f'/api/stream/{jid}?t={httpd.bobframes_token}').read().decode('utf-8')
78
+ assert '--render-only' in seen['argv']
79
+ assert seen['argv'][seen['argv'].index('--accent') + 1] == 'oklch(0.6 0.2 30)'
80
+ assert '--accent-data' not in seen['argv'] # blank field -> flag omitted
81
+ assert '"rc": 0' in stream
82
+
83
+
84
+ # --- /api/ab ------------------------------------------------------------------------------------
85
+
86
+ def test_ab_spawns_cli_and_streams(tmp_path, monkeypatch):
87
+ seen = {}
88
+
89
+ def fake_spawn_cli(argv):
90
+ seen['argv'] = argv
91
+ return _FakeProc(["[10:00:00] a/b: 2026-05-27_r1 (2 captures) vs 2026-05-28_r2 (3 captures)"])
92
+ monkeypatch.setattr(_jobs, 'spawn_cli', fake_spawn_cli)
93
+ with running(str(tmp_path)) as (httpd, port):
94
+ body = {'baseline_label': 'r1', 'baseline_date': '2026-05-27',
95
+ 'compare_label': 'r2', 'compare_date': '2026-05-28'}
96
+ jid = json.loads(post(port, '/api/ab', httpd.bobframes_token, body).read())['job']
97
+ stream = get(port, f'/api/stream/{jid}?t={httpd.bobframes_token}').read().decode('utf-8')
98
+ assert seen['argv'][0] == 'ab'
99
+ assert seen['argv'][seen['argv'].index('--baseline-label') + 1] == 'r1'
100
+ assert seen['argv'][seen['argv'].index('--compare-label') + 1] == 'r2'
101
+ assert 'a/b:' in stream and '"rc": 0' in stream
102
+
103
+
104
+ def test_ab_requires_both_runs(tmp_path):
105
+ with running(str(tmp_path)) as (httpd, port):
106
+ with pytest.raises(urllib.error.HTTPError) as e:
107
+ post(port, '/api/ab', httpd.bobframes_token, {'baseline_label': 'r1'}) # no compare
108
+ assert e.value.code == 400
109
+
110
+
111
+ # --- /api/state runs (the picker source) --------------------------------------------------------
112
+
113
+ def test_state_lists_rendered_runs(tmp_path, monkeypatch):
114
+ from ..reports import discovery as rdisc
115
+ runs = [_FakeRun('2026-05-27_r1', 'r1', '2026-05-27', 2),
116
+ _FakeRun('2026-05-28_r2', 'r2', '2026-05-28', 3)]
117
+ monkeypatch.setattr(rdisc, 'discover_drops', lambda root: runs)
118
+ with running(str(tmp_path)) as (httpd, port):
119
+ s = json.load(get(port, '/api/state?t=' + httpd.bobframes_token))
120
+ assert [r['key'] for r in s['runs']] == ['2026-05-27_r1', '2026-05-28_r2']
121
+ assert s['runs'][1]['n_captures'] == 3
122
+
123
+
124
+ def test_state_runs_empty_without_catalog(tmp_path):
125
+ with running(str(tmp_path)) as (httpd, port):
126
+ s = json.load(get(port, '/api/state?t=' + httpd.bobframes_token))
127
+ assert s['runs'] == [] # no catalog yet -> no runs
128
+
129
+
130
+ # --- /api/scaffold ------------------------------------------------------------------------------
131
+
132
+ def test_scaffold_creates_convention_folder(tmp_path):
133
+ root = str(tmp_path / 'proj')
134
+ os.makedirs(root)
135
+ with running(root) as (httpd, port):
136
+ r = json.loads(post(port, '/api/scaffold', httpd.bobframes_token,
137
+ {'area': 'Town', 'date': '2026-06-24', 'label': 'r110600'}).read())
138
+ assert r['created'] is True
139
+ assert os.path.basename(r['path']) == '2026-06-24_r110600'
140
+ assert os.path.isdir(os.path.join(root, 'Town', '2026-06-24_r110600'))
141
+ # idempotent: a second call reports already-exists.
142
+ again = json.loads(post(port, '/api/scaffold', httpd.bobframes_token,
143
+ {'area': 'Town', 'date': '2026-06-24', 'label': 'r110600'}).read())
144
+ assert again['created'] is False
145
+
146
+
147
+ def test_scaffold_rejects_bad_date_and_traversal(tmp_path):
148
+ with running(str(tmp_path)) as (httpd, port):
149
+ with pytest.raises(urllib.error.HTTPError) as bad_date:
150
+ post(port, '/api/scaffold', httpd.bobframes_token, {'area': 'Town', 'date': 'June 24'})
151
+ assert bad_date.value.code == 400
152
+ with pytest.raises(urllib.error.HTTPError) as traversal:
153
+ post(port, '/api/scaffold', httpd.bobframes_token, {'area': '../escape', 'date': '2026-06-24'})
154
+ assert traversal.value.code == 400
155
+
156
+
157
+ def test_ab_and_scaffold_require_token(tmp_path):
158
+ with running(str(tmp_path)) as (httpd, port):
159
+ for path in ('/api/ab', '/api/scaffold'):
160
+ with pytest.raises(urllib.error.HTTPError) as e:
161
+ post(port, path, 'wrong-token', {})
162
+ assert e.value.code == 403, path
@@ -0,0 +1,78 @@
1
+ """v028_7: opt-in headless-browser smoke that the panel JS actually RUNS and populates state.
2
+
3
+ Marked ``browser`` -- DESELECTED by the default suite (``-m "not browser"``) and only run on demand
4
+ (``pytest -m browser``) where a local Chrome exists, mirroring ``test_browser_shots.py`` (ADR-43 gate-d).
5
+ This is the gate the v028_6 bug needed and node --check cannot give: ``node --check`` proves the script
6
+ PARSES; this proves it RUNS. Chrome loads the LIVE panel over http (token in the URL), the page's
7
+ ``loadState()`` fetches ``/api/state``, and we assert the DOM populated (root path + tool rows + drops
8
+ table) -- i.e. the script parsed AND executed AND wired the fetch->render path.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import pathlib
13
+ import sys
14
+
15
+ import pytest
16
+
17
+ from ._ui_util import make_capture_root, running
18
+
19
+ _TOOLS = pathlib.Path(__file__).resolve().parents[2] / 'tools'
20
+ sys.path.insert(0, str(_TOOLS))
21
+ import shoot # noqa: E402 (path injected above; dev-only CDP harness, never shipped)
22
+
23
+ pytestmark = pytest.mark.browser
24
+
25
+
26
+ def _eval(chrome, expr, *, await_promise=False):
27
+ """Runtime.evaluate in the attached page; returns the by-value result. Raises on a JS exception
28
+ (a thrown panel script is a failure, not a None)."""
29
+ r = chrome.cdp.call('Runtime.evaluate',
30
+ {'expression': expr, 'returnByValue': True, 'awaitPromise': await_promise},
31
+ session=chrome.session)
32
+ if 'exceptionDetails' in r:
33
+ raise AssertionError(f'panel JS threw: {r["exceptionDetails"]}')
34
+ return r['result'].get('value')
35
+
36
+
37
+ # An awaited promise that resolves once render() has replaced the "Loading..." placeholder (loadState's
38
+ # fetch is async, so it completes AFTER Page.loadEventFired). Times out to a {timeout:true} marker so a
39
+ # dead script fails the assert instead of hanging.
40
+ _WAIT_POPULATED = """
41
+ new Promise(function(resolve){
42
+ var waited = 0, step = 50, deadline = 5000;
43
+ var timer = setInterval(function(){
44
+ var root = document.getElementById('root');
45
+ var ready = root && root.textContent.indexOf('Loading') === -1;
46
+ if (ready) {
47
+ clearInterval(timer);
48
+ resolve({
49
+ root: root.textContent,
50
+ tools: document.getElementById('tools').innerHTML,
51
+ drops: document.getElementById('drops').innerHTML
52
+ });
53
+ } else if ((waited += step) >= deadline) {
54
+ clearInterval(timer);
55
+ resolve({timeout: true, root: root ? root.textContent : null});
56
+ }
57
+ }, step);
58
+ })
59
+ """
60
+
61
+
62
+ def test_panel_js_runs_and_populates_state(tmp_path):
63
+ if not shoot.find_chrome():
64
+ pytest.skip('Chrome not found')
65
+ root = make_capture_root(tmp_path) # Town + Bay drops with .rdc files
66
+ with running(root) as (httpd, port):
67
+ url = f'http://127.0.0.1:{port}/?t={httpd.bobframes_token}'
68
+ with shoot.Chrome() as chrome:
69
+ s = chrome.session
70
+ chrome.cdp.call('Page.navigate', {'url': url}, session=s)
71
+ chrome.cdp.wait_event('Page.loadEventFired', session=s)
72
+ state = _eval(chrome, _WAIT_POPULATED, await_promise=True)
73
+
74
+ assert not state.get('timeout'), f'panel never populated -- the JS did not run: {state!r}'
75
+ # The script parsed, ran, fetched /api/state, and render() filled the DOM:
76
+ assert 'Loading' not in state['root'] and 'proj' in state['root'], state['root']
77
+ assert 'renderdoccmd' in state['tools'], 'tool rows did not render'
78
+ assert 'Town' in state['drops'] or 'Bay' in state['drops'], 'drops table did not render'
@@ -0,0 +1,82 @@
1
+ """v028_2: the subprocess job runner + SSE (mocked spawn -- no GPU/RenderDoc, the ADR-6 discipline).
2
+
3
+ `jobs.spawn` is monkeypatched with a fake process that emits a scripted stdout transcript; the test
4
+ asserts POST /api/ingest starts a job and GET /api/stream/<job> relays the classified lines + a final
5
+ `done` event carrying the return code.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import urllib.error
11
+
12
+ import pytest
13
+
14
+ from ..ui import jobs as _jobs
15
+ from ._ui_util import get, post, running
16
+
17
+ _SCRIPT = [
18
+ "[10:00:00] pipeline: 1 drop(s); root=.",
19
+ "[10:00:00] replay: 2 captures (sequential)",
20
+ "[10:00:10] 1: rc=0 10.0s",
21
+ "[10:00:20] 2: rc=0 10.0s",
22
+ "[10:00:21] merge + parquetize",
23
+ "[10:00:22] pipeline done: 1 drops processed",
24
+ ]
25
+
26
+
27
+ class _FakeProc:
28
+ def __init__(self, lines, rc=0):
29
+ self.stdout = iter(lines)
30
+ self.returncode = None
31
+ self._rc = rc
32
+
33
+ def wait(self):
34
+ self.returncode = self._rc
35
+ return self._rc
36
+
37
+ def poll(self):
38
+ return self.returncode
39
+
40
+ def terminate(self):
41
+ self.returncode = -15
42
+
43
+
44
+ def test_build_run_argv_mirrors_ingest():
45
+ argv = _jobs.build_run_argv('/proj', force=True, render_only=False, workers=8, pixel_grid=2)
46
+ assert '--root' in argv and '--force' in argv
47
+ assert argv[argv.index('--workers') + 1] == '8'
48
+ assert argv[argv.index('--pixel-grid') + 1] == '2'
49
+ assert '--render-only' not in argv
50
+
51
+
52
+ def test_ingest_streams_stdout_and_return_code(tmp_path, monkeypatch):
53
+ monkeypatch.setattr(_jobs, 'spawn', lambda argv: _FakeProc(list(_SCRIPT), rc=0))
54
+ with running(str(tmp_path)) as (httpd, port):
55
+ started = json.loads(post(port, '/api/ingest', httpd.bobframes_token, {'render_only': True}).read())
56
+ assert 'job' in started
57
+ stream = get(port, f"/api/stream/{started['job']}?t={httpd.bobframes_token}").read().decode('utf-8')
58
+ assert 'replay: 2 captures' in stream # raw line relayed verbatim
59
+ assert '"replay_total": 2' in stream and '"replay_done": 2' in stream
60
+ assert 'event: done' in stream and '"rc": 0' in stream
61
+
62
+
63
+ def test_ingest_reports_nonzero_rc(tmp_path, monkeypatch):
64
+ monkeypatch.setattr(_jobs, 'spawn', lambda argv: _FakeProc(['[10:00:00] boom'], rc=1))
65
+ with running(str(tmp_path)) as (httpd, port):
66
+ jid = json.loads(post(port, '/api/ingest', httpd.bobframes_token, {}).read())['job']
67
+ stream = get(port, f'/api/stream/{jid}?t={httpd.bobframes_token}').read().decode('utf-8')
68
+ assert 'event: done' in stream and '"rc": 1' in stream
69
+
70
+
71
+ def test_ingest_requires_token(tmp_path):
72
+ with running(str(tmp_path)) as (httpd, port):
73
+ with pytest.raises(urllib.error.HTTPError) as e:
74
+ post(port, '/api/ingest', 'wrong-token', {})
75
+ assert e.value.code == 403
76
+
77
+
78
+ def test_stream_unknown_job_404(tmp_path):
79
+ with running(str(tmp_path)) as (httpd, port):
80
+ with pytest.raises(urllib.error.HTTPError) as e:
81
+ get(port, f'/api/stream/nope?t={httpd.bobframes_token}')
82
+ assert e.value.code == 404
@@ -0,0 +1,34 @@
1
+ """v028_7/8: the panel JS must PARSE -- an automated gate that runs the parser (the v028_6 lesson).
2
+
3
+ pytest exercises the panel's HTTP surface in Python but never parses or executes the panel JS; the
4
+ v028_2 bug (a normal triple-quoted page literal turned the embedded JS ``"\\n"`` into a REAL newline
5
+ mid-string-literal -> the WHOLE script was a syntax error -> nothing populated) shipped undetected
6
+ through five commits for exactly that reason. v028_8 externalized the JS to
7
+ ``bobframes/ui/assets/panel.js`` (a real file -- the embedding bug class is now impossible), and this
8
+ runs ``node --check`` on it (syntax-only, so browser globals like ``document`` / ``fetch`` /
9
+ ``EventSource`` are fine -- it parses, it does not resolve).
10
+
11
+ Skips when node is absent locally; CI (`windows-latest` ships node) also runs an UNCONDITIONAL
12
+ ``node --check bobframes/ui/assets/panel.js`` step + ``node --version``, so the gate can never silently
13
+ skip everywhere (ADR-23).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import pathlib
18
+ import shutil
19
+ import subprocess
20
+
21
+ import pytest
22
+
23
+ from ..ui import server as _server
24
+
25
+ _PANEL_JS = pathlib.Path(_server.__file__).parent / 'assets' / 'panel.js'
26
+
27
+
28
+ def test_panel_js_parses_with_node():
29
+ node = shutil.which('node')
30
+ if node is None:
31
+ pytest.skip('node not installed (CI runs an unconditional node --check step)')
32
+ assert _PANEL_JS.exists(), f'missing {_PANEL_JS}'
33
+ proc = subprocess.run([node, '--check', str(_PANEL_JS)], capture_output=True, text=True)
34
+ assert proc.returncode == 0, f'node --check rejected panel.js:\n{proc.stdout}\n{proc.stderr}'