starbash 0.3.3__tar.gz → 0.3.4__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 (128) hide show
  1. {starbash-0.3.3 → starbash-0.3.4}/PKG-INFO +8 -2
  2. {starbash-0.3.3 → starbash-0.3.4}/README.md +7 -2
  3. {starbash-0.3.3 → starbash-0.3.4}/pyproject.toml +9 -2
  4. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/processed_target.py +21 -2
  5. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/github.py +145 -5
  6. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/github_service.py +1 -1
  7. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/target.md.jinja +20 -5
  8. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/main_window.py +3 -0
  9. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/processing.py +24 -0
  10. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/wizard.py +35 -0
  11. {starbash-0.3.3 → starbash-0.3.4}/LICENSE +0 -0
  12. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/__init__.py +0 -0
  13. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/aliases.py +0 -0
  14. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/analytics.py +0 -0
  15. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/app.py +0 -0
  16. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/__init__.py +0 -0
  17. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/check.png +0 -0
  18. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/favicon.ico +0 -0
  19. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/icon.png +0 -0
  20. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/icon.svg +0 -0
  21. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/assets/starbash.desktop.in +0 -0
  22. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/check_version.py +0 -0
  23. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/__init__.py +0 -0
  24. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/gui.py +0 -0
  25. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/info.py +0 -0
  26. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/process.py +0 -0
  27. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/publish.py +0 -0
  28. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/repo.py +0 -0
  29. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/select.py +0 -0
  30. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/commands/user.py +0 -0
  31. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/database.py +0 -0
  32. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/defaults/__init__.py +0 -0
  33. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/defaults/starbash.toml +0 -0
  34. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/doit.py +0 -0
  35. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/doit_types.py +0 -0
  36. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/dwarf3.py +0 -0
  37. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/events.py +0 -0
  38. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/exception.py +0 -0
  39. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/filtering.py +0 -0
  40. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/fits.py +0 -0
  41. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/interaction.py +0 -0
  42. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/linux.py +0 -0
  43. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/main.py +0 -0
  44. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/os.py +0 -0
  45. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/parameters.py +0 -0
  46. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/paths.py +0 -0
  47. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/preferences.py +0 -0
  48. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/processing.py +0 -0
  49. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/processing_like.py +0 -0
  50. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/__init__.py +0 -0
  51. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/credentials.py +0 -0
  52. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/publish/github_publish.py +0 -0
  53. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/README.md +0 -0
  54. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/__init__.py +0 -0
  55. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/crop.py +0 -0
  56. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/osc.py +0 -0
  57. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/recipes/report_registration.py +0 -0
  58. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/report.py +0 -0
  59. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/rich.py +0 -0
  60. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/run_state.py +0 -0
  61. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/safety.py +0 -0
  62. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/score.py +0 -0
  63. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/selection.py +0 -0
  64. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/__init__.py +0 -0
  65. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/connection.py +0 -0
  66. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/enums.py +0 -0
  67. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/sim_siril/utility.py +0 -0
  68. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/siril/import_registration.py +0 -0
  69. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/stage_utils.py +0 -0
  70. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/stages.py +0 -0
  71. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/__init__.py +0 -0
  72. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/repo/master.toml +0 -0
  73. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/repo/processed.toml +0 -0
  74. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/Gemfile +0 -0
  75. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/README.md.jinja +0 -0
  76. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/__init__.py +0 -0
  77. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/_config.yml +0 -0
  78. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/default.html +0 -0
  79. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/report/index.md.jinja +0 -0
  80. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/master.toml +0 -0
  81. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/processed/about.toml +0 -0
  82. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/processed/main.toml +0 -0
  83. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/target/processed/sessions.toml +0 -0
  84. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/templates/userconfig.toml +0 -0
  85. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/toml.py +0 -0
  86. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/__init__.py +0 -0
  87. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/base.py +0 -0
  88. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/context.py +0 -0
  89. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/graxpert.py +0 -0
  90. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/python.py +0 -0
  91. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/rcastro.py +0 -0
  92. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/siril.py +0 -0
  93. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/tool/starnet.py +0 -0
  94. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/__init__.py +0 -0
  95. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/cli.py +0 -0
  96. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/cli_events.py +0 -0
  97. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/__init__.py +0 -0
  98. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/app.py +0 -0
  99. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/bridge.py +0 -0
  100. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/desktop.py +0 -0
  101. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/interaction.py +0 -0
  102. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/jobs.py +0 -0
  103. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/models.py +0 -0
  104. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/__init__.py +0 -0
  105. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/base.py +0 -0
  106. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/dashboard.py +0 -0
  107. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/masters.py +0 -0
  108. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/publish.py +0 -0
  109. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/repositories.py +0 -0
  110. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/sessions.py +0 -0
  111. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/settings.py +0 -0
  112. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/pages/targets.py +0 -0
  113. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/services.py +0 -0
  114. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/theme.py +0 -0
  115. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/__init__.py +0 -0
  116. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/busy_indicator.py +0 -0
  117. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/file_links.py +0 -0
  118. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/github_login.py +0 -0
  119. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/hover_preview.py +0 -0
  120. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/image_viewer.py +0 -0
  121. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/log_view.py +0 -0
  122. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/master_picker.py +0 -0
  123. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/selection_panel.py +0 -0
  124. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/stat_card.py +0 -0
  125. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/widgets/tool_warning.py +0 -0
  126. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/ui/qt/workers.py +0 -0
  127. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/url.py +0 -0
  128. {starbash-0.3.3 → starbash-0.3.4}/src/starbash/windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starbash
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: A tool for automating/standardizing/sharing astrophotography workflows.
5
5
  License-File: LICENSE
6
6
  Author: Kevin Hester
@@ -79,7 +79,12 @@ This project is still very young - but making good progress 😊!
79
79
 
80
80
  If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
81
81
 
82
- ![Sample session movie](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
82
+ (new!) We now have a full GUI (click the video to play)...
83
+
84
+ <video src="https://github.com/user-attachments/assets/0791c47f-f8fa-4279-be06-f869d7212d8c" autoplay controls loop muted playsinline width="100%"></video>
85
+
86
+ We have a CLI interface also...
87
+ ![CLI sample session](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
83
88
 
84
89
  ## Current features
85
90
 
@@ -321,3 +326,4 @@ However, in some of my other open-source projects, I've seen the current hell PR
321
326
 
322
327
  Copyright 2025 Kevin Hester, kevinh@geeksville.com.
323
328
  Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
329
+
@@ -46,7 +46,12 @@ This project is still very young - but making good progress 😊!
46
46
 
47
47
  If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
48
48
 
49
- ![Sample session movie](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
49
+ (new!) We now have a full GUI (click the video to play)...
50
+
51
+ <video src="https://github.com/user-attachments/assets/0791c47f-f8fa-4279-be06-f869d7212d8c" autoplay controls loop muted playsinline width="100%"></video>
52
+
53
+ We have a CLI interface also...
54
+ ![CLI sample session](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
50
55
 
51
56
  ## Current features
52
57
 
@@ -287,4 +292,4 @@ However, in some of my other open-source projects, I've seen the current hell PR
287
292
  ## License
288
293
 
289
294
  Copyright 2025 Kevin Hester, kevinh@geeksville.com.
290
- Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
295
+ Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "starbash"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "A tool for automating/standardizing/sharing astrophotography workflows."
5
5
  authors = ["Kevin Hester <kevinh@geeksville.com>"]
6
6
  readme = "README.md"
@@ -61,6 +61,7 @@ doit-graph = ">=0.3.0"
61
61
  # harlequin = "^2.4.1"
62
62
  library-skills = "^0.0.19"
63
63
  semble = {version = "^0.6.0", extras = ["mcp"]}
64
+ imageio-ffmpeg = "^0.6.0"
64
65
 
65
66
  [tool.poetry.scripts]
66
67
  starbash = "starbash.main:app"
@@ -71,6 +72,7 @@ markers = [
71
72
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
72
73
  "integration: marks tests as integration tests requiring /test-data (deselect with '-m \"not integration\"')",
73
74
  "gui: marks tests that build real Qt widgets (offscreen; deselect with '-m \"not gui\"')",
75
+ "gui_integration: marks the scripted gui_integration movie runs (needs ffmpeg + /test-data; deselect with '-m \"not gui_integration\"')",
74
76
  ]
75
77
  # -n auto enables parallel test execution plugin
76
78
  # By default we exclude only slow and integration tests. GUI tests DO run: PySide6
@@ -80,7 +82,12 @@ markers = [
80
82
  # --tb=short shows shorter tracebacks
81
83
  # -rfE shows summary of failed and error test outcomes at end (not passed)
82
84
  # --ignore excludes third-party test directories
83
- addopts = "-m 'not slow and not integration' -n auto --tb=short -rfE --ignore=GraXpert --ignore=starbash-recipes"
85
+ addopts = "-m 'not slow and not integration and not gui_integration' -n auto --tb=short -rfE --ignore=GraXpert --ignore=starbash-recipes"
86
+
87
+ # Note: the `gui_integration` movies are deselected by default on purpose: they drive a
88
+ # whole user journey (setup wizard -> reindex -> processing run) and take minutes, so they
89
+ # run on demand - `just test-integration-gui`, or `pytest -m gui_integration -n 0`.
90
+ # The marker name uses an underscore so it is usable as `pytest.mark.gui_integration`.
84
91
 
85
92
  # Note: To limit captured output further, you can:
86
93
  # 1. Use --tb=line for minimal tracebacks
@@ -85,6 +85,10 @@ class StageOption:
85
85
  parameters: list[ParameterOption]
86
86
  #: URL (``https://`` or ``file://``) of the recipe that declares this stage.
87
87
  recipe_url: str | None = None
88
+ #: Tool this stage runs (e.g. ``"siril"``), as declared by the recipe.
89
+ tool: str | None = None
90
+ #: Interchangeable role (e.g. ``"stack"``) the recipe declares, if any.
91
+ role: str | None = None
88
92
 
89
93
 
90
94
  @dataclass
@@ -189,7 +193,8 @@ def stage_declarations(recipes: Any) -> dict[str, dict[str, Any]]:
189
193
  Recipes are the authoritative source of a parameter's default and description
190
194
  (declared as ``[[stages.parameters]]``). Scanning the recipe repos is cheap
191
195
  because their config is already loaded; later definitions win, mirroring repo
192
- precedence.
196
+ precedence. The stage's declared ``tool`` and optional ``role`` are carried
197
+ along too, for report rendering.
193
198
  """
194
199
  declarations: dict[str, dict[str, Any]] = {}
195
200
  for repo in recipes:
@@ -205,7 +210,14 @@ def stage_declarations(recipes: Any) -> dict[str, dict[str, Any]]:
205
210
  if not name:
206
211
  continue
207
212
  entry = declarations.setdefault(
208
- str(name), {"description": None, "parameters": {}, "recipe_url": None}
213
+ str(name),
214
+ {
215
+ "description": None,
216
+ "parameters": {},
217
+ "recipe_url": None,
218
+ "tool": None,
219
+ "role": None,
220
+ },
209
221
  )
210
222
  if stage.get("description"):
211
223
  entry["description"] = stage.get("description")
@@ -213,6 +225,11 @@ def stage_declarations(recipes: Any) -> dict[str, dict[str, Any]]:
213
225
  recipe_url = getattr(source, "url", None) if source is not None else None
214
226
  if recipe_url:
215
227
  entry["recipe_url"] = str(recipe_url)
228
+ tool = stage.get("tool")
229
+ if isinstance(tool, dict) and tool.get("name"):
230
+ entry["tool"] = str(tool["name"])
231
+ if stage.get("role"):
232
+ entry["role"] = str(stage["role"])
216
233
  for param in stage.get("parameters") or []:
217
234
  param_name = param.get("name")
218
235
  if not param_name:
@@ -1027,6 +1044,8 @@ class ProcessedTarget:
1027
1044
  excluded=bool(entry.get("excluded", False)),
1028
1045
  parameters=parameters,
1029
1046
  recipe_url=declared.get("recipe_url"),
1047
+ tool=declared.get("tool"),
1048
+ role=declared.get("role"),
1030
1049
  )
1031
1050
  )
1032
1051
  return stages
@@ -2,9 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import html
5
6
  import re
6
7
  import shutil
7
8
  import warnings
9
+ from collections.abc import Sequence
8
10
  from importlib import resources
9
11
  from pathlib import Path
10
12
  from typing import Any
@@ -16,7 +18,7 @@ from tomlkit.exceptions import ParseError
16
18
 
17
19
  from starbash import console
18
20
  from starbash.paths import get_publish_site_dir
19
- from starbash.processed_target import ProcessedTarget
21
+ from starbash.processed_target import ProcessedTarget, StageOption, stage_declarations
20
22
 
21
23
 
22
24
  def slugify(value: str) -> str:
@@ -63,6 +65,123 @@ def equipment_rows(equipment: Any) -> list[dict[str, str | None]]:
63
65
  return rows
64
66
 
65
67
 
68
+ #: Stylesheet for :func:`stage_tree_html`, scoped to its wrapper so it cannot
69
+ #: fight the site theme. Colours are tuned for the dark ``jekyll-theme-midnight``
70
+ #: look: defaults are a muted grey-blue, overrides a bold amber accent.
71
+ _STAGE_TREE_CSS = """\
72
+ .sb-stages{margin:1.25em 0;font-size:.92rem;line-height:1.5}
73
+ .sb-stages ul{list-style:none;margin:.25rem 0;padding:0}
74
+ .sb-stages .sb-stage{margin:.35rem 0;padding-left:1.1rem;border-left:2px solid rgba(128,150,180,.45)}
75
+ .sb-stages .sb-stage-name{font-weight:600}
76
+ .sb-stages a.sb-stage-name{color:inherit;text-decoration:underline dotted;text-underline-offset:.15em}
77
+ .sb-stages .sb-tool,.sb-stages .sb-role,.sb-stages .sb-skip{display:inline-block;margin-left:.45rem;padding:.05rem .45rem;border:1px solid rgba(128,150,180,.55);border-radius:.7rem;font-size:.7rem;letter-spacing:.05em;text-transform:uppercase;opacity:.85;vertical-align:.1em}
78
+ .sb-stages .sb-skip{border-style:dashed}
79
+ .sb-stages .sb-stage.excluded{opacity:.55}
80
+ .sb-stages .sb-stage.excluded .sb-stage-name{text-decoration:line-through}
81
+ .sb-stages .sb-recipe{margin-left:.45rem;font-size:.78rem;opacity:.9}
82
+ .sb-stages .sb-stage-desc{margin-left:.45rem;opacity:.7;font-size:.85rem}
83
+ .sb-stages .sb-params{padding-left:1.35rem;margin:.1rem 0}
84
+ .sb-stages .sb-param{margin:.05rem 0}
85
+ .sb-stages .sb-pname{opacity:.92}
86
+ .sb-stages .sb-default{color:#9fb0c3;font-weight:400}
87
+ .sb-stages .sb-override{color:#ffb454;font-weight:700}
88
+ .sb-stages .sb-legend{font-size:.8rem;opacity:.85;margin:.2rem 0 .6rem}"""
89
+
90
+
91
+ def _display_value(value: Any) -> str:
92
+ """Render a parameter value the way its TOML line would be written."""
93
+ if value is None:
94
+ return ""
95
+ if isinstance(value, bool):
96
+ return "true" if value else "false"
97
+ if isinstance(value, str):
98
+ return f'"{value}"'
99
+ return str(value)
100
+
101
+
102
+ def _esc(value: Any) -> str:
103
+ """HTML-escape any value bound for the stage tree."""
104
+ return html.escape(str(value), quote=True)
105
+
106
+
107
+ def _param_row(param: Any) -> str:
108
+ """One parameter line: grey default, or a bold amber override beside it."""
109
+ bits = ['<li class="sb-param"']
110
+ if param.description:
111
+ bits.append(f' title="{_esc(param.description)}"')
112
+ bits.append(f'><span class="sb-pname">{_esc(param.name)}</span>')
113
+ if param.is_overridden:
114
+ bits.append(f' <span class="sb-override">= {_esc(_display_value(param.value))}</span>')
115
+ if param.default is not None:
116
+ bits.append(
117
+ f' <span class="sb-default">(default {_esc(_display_value(param.default))})</span>'
118
+ )
119
+ elif param.default is not None:
120
+ bits.append(f' <span class="sb-default">= {_esc(_display_value(param.default))}</span>')
121
+ else:
122
+ bits.append(' <span class="sb-default">(no default)</span>')
123
+ bits.append("</li>")
124
+ return "".join(bits)
125
+
126
+
127
+ def _stage_row(stage: StageOption) -> list[str]:
128
+ """The ``<li>`` for one stage: header line plus its parameter lines."""
129
+ css = "sb-stage excluded" if stage.excluded else "sb-stage"
130
+ # The stage name links to the recipe its TOML came from, when that recipe
131
+ # has a browsable (http(s)) URL; local/`pkg://` sources stay plain text.
132
+ recipe_url = str(stage.recipe_url) if stage.recipe_url else ""
133
+ if recipe_url.startswith("http"):
134
+ name_html = f'<a class="sb-stage-name" href="{_esc(recipe_url)}">{_esc(stage.name)}</a>'
135
+ else:
136
+ name_html = f'<span class="sb-stage-name">{_esc(stage.name)}</span>'
137
+ head = [name_html]
138
+ if stage.tool:
139
+ head.append(f'<span class="sb-tool">{_esc(stage.tool)}</span>')
140
+ if stage.role:
141
+ head.append(f'<span class="sb-role">{_esc(stage.role)}</span>')
142
+ if stage.excluded:
143
+ head.append('<span class="sb-skip">skipped</span>')
144
+ if stage.description:
145
+ head.append(f'<span class="sb-stage-desc">{_esc(stage.description)}</span>')
146
+
147
+ lines = [f'<li class="{css}">', f'<div class="sb-stage-head">{" ".join(head)}</div>']
148
+ if stage.parameters:
149
+ lines.append('<ul class="sb-params">')
150
+ lines.extend(_param_row(param) for param in stage.parameters)
151
+ lines.append("</ul>")
152
+ lines.append("</li>")
153
+ return lines
154
+
155
+
156
+ def stage_tree_html(stages: Sequence[StageOption]) -> str:
157
+ """Render a target's stages as an HTML tree for its report page.
158
+
159
+ Every stage the target records is shown, in order, with the parameters its
160
+ recipe declares: the recipe *defaults* render in muted grey, while values
161
+ this target *overrides* render in a bolder amber accent right beside the
162
+ default they replaced. Excluded stages stay visible but dimmed and marked
163
+ "skipped", so the tree shows exactly which stages were used.
164
+
165
+ Returns ``""`` for a target with no recorded stages.
166
+ """
167
+ if not stages:
168
+ return ""
169
+
170
+ lines = [
171
+ '<div class="sb-stages">',
172
+ "<style>",
173
+ _STAGE_TREE_CSS,
174
+ "</style>",
175
+ '<div class="sb-legend">Parameters: <span class="sb-default">grey = recipe default</span>'
176
+ ' &middot; <span class="sb-override">amber = this target&rsquo;s override</span></div>',
177
+ '<ul class="sb-stage-list">',
178
+ ]
179
+ for stage in stages:
180
+ lines.extend(_stage_row(stage))
181
+ lines.extend(["</ul>", "</div>"])
182
+ return "\n".join(lines)
183
+
184
+
66
185
  class GitHubPublisher:
67
186
  """Generate a complete local Jekyll site from one processed repository."""
68
187
 
@@ -103,8 +222,26 @@ class GitHubPublisher:
103
222
  heroes = [path for path in images if path.name.lower().startswith("hero")]
104
223
  return heroes or images
105
224
 
106
- def _targets(self, root: Path) -> list[tuple[Path, dict[str, Any]]]:
107
- targets: list[tuple[Path, dict[str, Any]]] = []
225
+ def _stage_declarations(self) -> dict[str, dict[str, Any]]:
226
+ """Map stage name -> declared description/parameters from the recipe repos.
227
+
228
+ Best effort: contexts without ``get_recipes`` (bare front ends, tests)
229
+ get an empty map, and the report then simply omits recipe-declared
230
+ defaults while still showing the target's recorded stages.
231
+ """
232
+ get_recipes = getattr(self.sb, "get_recipes", None)
233
+ if not callable(get_recipes):
234
+ return {}
235
+ try:
236
+ return stage_declarations(get_recipes())
237
+ except Exception: # noqa: BLE001 - a broken recipe repo must not kill the report
238
+ warnings.warn(
239
+ "Could not read recipe declarations; the report omits defaults", stacklevel=2
240
+ )
241
+ return {}
242
+
243
+ def _targets(self, root: Path) -> list[tuple[Path, dict[str, Any], ProcessedTarget]]:
244
+ targets: list[tuple[Path, dict[str, Any], ProcessedTarget]] = []
108
245
  for directory in sorted(
109
246
  (path for path in root.iterdir() if path.is_dir()),
110
247
  key=lambda path: path.name.lower(),
@@ -126,7 +263,7 @@ class GitHubPublisher:
126
263
  document.update(plain(target.about))
127
264
  document.update(plain(target.sessions))
128
265
  document["_main_config"] = main_config
129
- targets.append((directory, document))
266
+ targets.append((directory, document, target))
130
267
  except (OSError, ParseError) as exc:
131
268
  warnings.warn(f"Skipping malformed target {metadata_dir}: {exc}", stacklevel=2)
132
269
  return targets
@@ -135,6 +272,7 @@ class GitHubPublisher:
135
272
  """Regenerate the complete site and return its root directory."""
136
273
  root = self._processed_root()
137
274
  targets = self._targets(root)
275
+ declarations = self._stage_declarations()
138
276
 
139
277
  # Wipe any previously generated site so stale files don't persist.
140
278
  if self.site_dir.exists():
@@ -178,7 +316,7 @@ class GitHubPublisher:
178
316
  shutil.copyfileobj(source, destination)
179
317
  progress.update(task, description="Copied static assets", advance=1)
180
318
  index_targets: list[dict[str, Any]] = []
181
- for directory, document in targets:
319
+ for directory, document, processed in targets:
182
320
  about = document.get("about", {})
183
321
  if not isinstance(about, dict):
184
322
  about = {}
@@ -231,6 +369,7 @@ class GitHubPublisher:
231
369
  )
232
370
  page_images = [f"../../{url}" for url in image_urls]
233
371
  seo_image = f"/{image_urls[0]}" if image_urls else None
372
+ stage_tree = stage_tree_html(processed.stage_options(declarations))
234
373
  page_name = f"{slug}.md"
235
374
  post = self.environment.get_template("target.md.jinja").render(
236
375
  target={**target, "name": name},
@@ -240,6 +379,7 @@ class GitHubPublisher:
240
379
  images=page_images,
241
380
  image=seo_image,
242
381
  sessions=sessions,
382
+ stage_tree=stage_tree,
243
383
  workflow_url=f"../../assets/targets/{slug}/main.toml",
244
384
  )
245
385
  (posts / page_name).write_text(post)
@@ -407,7 +407,7 @@ class GitHubService:
407
407
  {
408
408
  "name": name,
409
409
  # note: markdown is not supported in the description field
410
- "description": "Semiautomatic (beta) image workflows by Starbash",
410
+ "description": "(mostly) automatic (beta) image workflows by Starbash",
411
411
  "private": False,
412
412
  # we want to make main ourselves
413
413
  "auto_init": False,
@@ -9,10 +9,30 @@ description: {{ description | default('Processed Starbash target.', true) | tojs
9
9
 
10
10
  {{ about.summary | default('Processed astrophotography target.') }}
11
11
 
12
+ {% if target.ra is defined and target.ra and target.ra != 'N/A' and target.dec is defined and target.dec and target.dec != 'N/A' %}
13
+
14
+ **Coordinates:** RA {{ target.ra }} / Dec {{ target.dec }}
15
+
16
+ {% endif %}
17
+
12
18
  {% for image in images %}
13
19
  ![{{ target.name }}]({{ image }})
14
20
  {% endfor %}
15
21
 
22
+ ## Workflow
23
+
24
+ {% if stage_tree %}
25
+
26
+ The stages used to process this target, with each stage's parameters:
27
+
28
+ {{ stage_tree }}
29
+
30
+ {% endif %}
31
+
32
+ {% if workflow_url %}
33
+ [View processing workflow]({{ workflow_url }})
34
+ {% endif %}
35
+
16
36
  ## Sessions
17
37
 
18
38
  {% for session in sessions %}
@@ -35,8 +55,3 @@ description: {{ description | default('Processed Starbash target.', true) | tojs
35
55
 
36
56
  {% endfor %}
37
57
 
38
- ## Workflow
39
-
40
- {% if workflow_url %}
41
- [View processing workflow]({{ workflow_url }})
42
- {% endif %}
@@ -61,6 +61,9 @@ class MainWindow(QMainWindow):
61
61
  super().__init__(parent)
62
62
  self.setWindowTitle("Starbash")
63
63
  self.resize(1280, 820)
64
+ # A stable handle for the gui-integration movie (doc/plans/gui-integration-video.md
65
+ # §6): a name, not a behaviour - nothing is styled by it.
66
+ self.setObjectName("MainWindow")
64
67
 
65
68
  self._bus = EventBusBridge(self)
66
69
  self._sb = sb
@@ -150,6 +150,7 @@ class ProcessingPage(Page):
150
150
  layout.addWidget(self._caption)
151
151
 
152
152
  self._tasks = _RunTree()
153
+ self._tasks.setObjectName("processingTree")
153
154
  self._tasks.setHeaderLabels(["Target / stage / task", "Status / details"])
154
155
  self._tasks.setAlternatingRowColors(True)
155
156
  layout.addWidget(self._tasks, 1)
@@ -160,6 +161,29 @@ class ProcessingPage(Page):
160
161
  if self.bus is not None:
161
162
  self.bus.received.connect(self._on_event) # type: ignore[attr-defined]
162
163
 
164
+ # --- public handles ---------------------------------------------------
165
+ @property
166
+ def run_button(self) -> QPushButton:
167
+ """The *Run auto pipeline* button — the movie script's handle for it.
168
+
169
+ It cannot carry an ``objectName`` of its own (it is ``#Primary``, and a widget
170
+ has one ``objectName``), so it is reached through this accessor instead — see
171
+ ``doc/plans/gui-integration-video.md`` §6.
172
+ """
173
+ return self._run
174
+
175
+ @property
176
+ def cancel_button(self) -> QPushButton:
177
+ """The *Cancel* button beside the run button — same §6 workaround as the
178
+ run button (no ``objectName`` of its own)."""
179
+ return self._cancel
180
+
181
+ @property
182
+ def caption_label(self) -> QLabel:
183
+ """The one-line run caption (e.g. ``Idle.``) — same §6 workaround as above:
184
+ it is ``#PageSubtitle``, so its ``objectName`` is spoken for."""
185
+ return self._caption
186
+
163
187
  # --- actions ----------------------------------------------------------
164
188
  def start_run(self) -> None:
165
189
  """Clear the last run's view and start processing in the background.
@@ -274,12 +274,18 @@ class YouPage(SetupPage):
274
274
 
275
275
  repo = sb.user_repo
276
276
  self._name = QLineEdit(str(repo.get("user.name", "") or ""))
277
+ # objectName is the gui-integration script's handle (doc/plans/gui-integration-video.md
278
+ # §6). These four are named, never restyled - the theme styles by type here.
279
+ self._name.setObjectName("wizardName")
277
280
  self._email = QLineEdit(str(repo.get("user.email", "") or ""))
281
+ self._email.setObjectName("wizardEmail")
278
282
  self._analytics = QCheckBox("Send anonymous crash reports and usage data (please!)")
283
+ self._analytics.setObjectName("wizardAnalytics")
279
284
  self._analytics.setChecked(analytics_enabled(repo))
280
285
  self._include_email = QCheckBox(
281
286
  "Include my email with crash reports (so we can contact you)"
282
287
  )
288
+ self._include_email.setObjectName("wizardIncludeEmail")
283
289
  self._include_email.setChecked(analytics_include_user(repo))
284
290
 
285
291
  # The *include email* option only means something once an email is typed.
@@ -548,6 +554,17 @@ class ImagesPage(SetupPage):
548
554
 
549
555
  self.refresh()
550
556
 
557
+ @property
558
+ def choose_button(self) -> QPushButton:
559
+ """The *Choose folder…* button — the movie script's handle for it.
560
+
561
+ It cannot carry an ``objectName`` of its own: it is ``#Primary`` (the theme's
562
+ accent style) and a widget has only one ``objectName``. A public accessor is
563
+ the workaround the plan settled on — see ``doc/plans/gui-integration-video.md``
564
+ §6.
565
+ """
566
+ return self._choose
567
+
551
568
  def _known_paths(self) -> set[str]:
552
569
  """The filesystem paths of the raw-image repos we already know about."""
553
570
  known: set[str] = set()
@@ -869,6 +886,9 @@ class SetupWizard(QWizard):
869
886
  self.action: str | None = None
870
887
 
871
888
  self.setWindowTitle("Starbash setup")
889
+ # A stable handle for the gui-integration movie (doc/plans/gui-integration-video.md
890
+ # §6); names, not behaviour.
891
+ self.setObjectName("SetupWizard")
872
892
  self.setWizardStyle(QWizard.WizardStyle.ModernStyle)
873
893
  # Page 1 is the start of the story, so a greyed-out *Back* is just noise.
874
894
  self.setOption(QWizard.WizardOption.NoBackButtonOnStartPage)
@@ -901,6 +921,11 @@ class SetupWizard(QWizard):
901
921
  self.setOption(QWizard.WizardOption.HaveCustomButton2)
902
922
  self.setButtonText(QWizard.WizardButton.CustomButton1, "Process all my targets")
903
923
  self.setButtonText(QWizard.WizardButton.CustomButton2, "Pick a target to process")
924
+ # The script's handles for those two actions (doc/plans/gui-integration-video.md
925
+ # §6). Qt pre-created the buttons, so naming them here is enough - and the
926
+ # names are new ids, so the theme (which styles nothing of theirs) is untouched.
927
+ self._name_action_button(QWizard.WizardButton.CustomButton1, "wizardProcessAllTargets")
928
+ self._name_action_button(QWizard.WizardButton.CustomButton2, "wizardPickTarget")
904
929
  # Qt creates those two *enabled* and shows them on every page, and it never
905
930
  # consults isComplete() for custom buttons - so without this the wizard
906
931
  # offered a live "Process all my targets" on page 1 and only switched it off
@@ -959,6 +984,16 @@ class SetupWizard(QWizard):
959
984
  buttons.append(button)
960
985
  return buttons
961
986
 
987
+ def _name_action_button(self, which: QWizard.WizardButton, name: str) -> None:
988
+ """Give one of Qt's custom buttons an ``objectName`` (see the constructor).
989
+
990
+ Qt types :meth:`button` as ``QAbstractButton``; the closing actions are the
991
+ ``QPushButton`` it actually created.
992
+ """
993
+ button = self.button(which)
994
+ if isinstance(button, QPushButton):
995
+ button.setObjectName(name)
996
+
962
997
  def _disable_action_buttons(self) -> None:
963
998
  """Switch the closing actions off, and say when they come alive.
964
999
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes