v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

v2.1.0 - #159

Merged
thusser merged 29 commits into
mainfrom
develop
Sep 1, 2026
Merged

v2.1.0#159
thusser merged 29 commits into
mainfrom
develop

Conversation

@thusser

Copy link
Copy Markdown
Member

version 2.1.0

thusserand others added 29 commits August 28, 2026 19:37
RoboticWidget: running indicator, Start/Stop, current-task panel
(name, target, obsnum, started, ETA/countdown), next-up + cant_run_reason.
ScheduleWidget: schedule table (start/end/task/target/state/priority),
"Re-schedule now" (proxy -> run()), Start/Stop. Both registered in
DEFAULT_WIDGETS/DEFAULT_ICONS/DEFAULT_CONFIG.
Requires pyobs-core with IRobotic/IRoboticScheduler, which is on develop
but unreleased -- see specs/2026-08-31-irobotic-widgets.md.
test/robotic.yaml added for manual/visual testing against DummyMastermind
+ DummyScheduler.
…lity
Column widths now fit their content (start/end/task/target/state/
priority vary a lot) instead of the designer's equal-width default.
test/robotic.yaml: raised blocked_probability to 0.6 so the "next up"
panel is visible more often during manual testing -- it's only
populated during a simulated "can't run" wait, by design (matches the
real Mastermind), so it's often empty at the 0.2 default.
labelAutonomousWarning had no maximumHeight and was rendering huge in
practice (pre-existing pyobs-gui issue, unrelated to #825, surfaced
while visually testing this branch's test/robotic.yaml since
DummyMastermind auto-starts running). Capped to 32px.
ScheduleWidget: switched from per-column ResizeToContents to Stretch
(equal-width columns filling the full table width) -- looked bad with
content-fitted widths.
pyobs-core 2.1.0 is now released, resolving this PR's earlier blocker.
CI was failing pyrefly because uv.lock still pinned 2.0.0. Verified
locally against a real (non-editable) sync of the published package:
pytest, ruff, and pyrefly all clean.
Add RoboticWidget/ScheduleWidget for IRobotic/IRoboticScheduler (#825)
…add VideoWidget split follow-up
Revise 2026-08-28-gui-main-vs-sidebar-widgets.md: a sidebar_preferred promotion rule
replaces the naive "keep every match" collection (fixes camera+filters showing filter
controls twice), and the sidebar container becomes universal instead of >=2-widgets-only
(fixes custom sidebar: entries being silently invisible on modules like a bare roof).
Adds the paired_sidebar_widget mechanism (D6); its first consumer (splitting VideoWidget
into a live-view main widget + controls sidebar widget) is tracked in a new follow-up plan.
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.2 to 0.16.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.2...0.16.5)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.16.5
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
chore(deps-dev): bump ruff from 0.16.2 to 0.16.5
Replaces the interface -> widget dict (first-match-wins, silently dropping
every other interface a module implements) with an ordered MAIN_WIDGETS
registry plus collect_main_widgets(): a module matching several main
widgets now gets one nav entry whose page is a tab widget, one tab per
widget, with a shared sidebar (ModulePage) that's the universal page host
regardless of match count. Sidebar-preferred interfaces (filters, focuser,
temperatures, cooling) demote into the sidebar when another main widget
already matched, and promote back to their own page/tabs when nothing else
did. Partial widget-open failures drop just that tab instead of tearing
down the whole page. Custom widgets:/sidebar: config gained real
interface-targeted replace and overwrite semantics. The VideoWidget split
(D6's first paired_sidebar_widget consumer) is tracked separately.
Fake interface/widget registry so the promotion rule, universal
ModulePage, custom widgets:/sidebar: config, disconnect teardown,
FITS-header aggregation and D5 partial-open-failure handling are all
exercised without needing real comm plumbing. Updates the two existing
DEFAULT_WIDGETS/DEFAULT_ICONS assertions for the new registry shape.
…ract
Also records that the DEFAULT_CONFIG deletion (D1) has no maintainer
objection -- deleting outright, no fallback constant.
ALWAYS_SIDEBAR_WIDGETS unconditionally added FITS-header controls to
every module's sidebar (roof, weather, standalone filter wheel, ...),
but that panel only makes sense for modules that actually write FITS
files. Move it to a per-entry sidebar=((None, FitsHeadersWidget), ...)
declaration on the ICamera and IVideo MAIN_WIDGETS rows instead (and
CameraWidget.sidebar_fills, restoring its pre-registry behavior), and
leave ALWAYS_SIDEBAR_WIDGETS empty -- kept as a mechanism for genuinely
interface-agnostic sidebar content, per D2, just unused for now.
1. HIGH, confirmed: Camera's/Telescope's declared sidebar tuples fully
overlapped their own sidebar_preferred registry entries (IFilters,
ICooling, ITemperatures, IFocuser), so every one of those widgets was
added to the sidebar twice. Drop the redundant declarations (registry
and sidebar_fills class attributes) and add an interface-keyed dedup
in open_module_page() as a second line of defense, so a future entry
can't silently reintroduce the same duplication. WidgetChoice now
carries the MAIN_WIDGETS interface it originated from (or the slot
it's replacing, for a custom widgets: entry) so the dedup still holds
when a demoted slot has been custom-replaced.
2. MEDIUM, spec deviation: implement D5's promised sidebar-widget
failure isolation in BaseWidget.add_to_sidebar() -- a failing sidebar
widget is now logged and dropped instead of propagating and tearing
down the whole page. Also drops the redundant explicit .open() call
on demoted sidebar_preferred widgets (add_to_sidebar already opens
them via _open_child), which is what makes their failures go through
the same isolation as every other sidebar fill.
3. LOW: interface: custom config can now target an interface that's
currently demoted into the sidebar (sidebar_preferred), not just a
plain main-widget slot -- collect_main_widgets() falls back to
looking it up there before logging "module doesn't implement it".
4. LOW: interface: combined with overwrite: true now logs a warning
that interface-replace wins and overwrite is ignored, instead of
silently doing so.
5. LOW: ModuleWindow.open() now raises when open_module_page() reports
every main widget failed, matching how the old single-widget code
surfaced a total failure, instead of leaving a silent empty page.
Nit: base.py's register_event() docstring still referenced the deleted
DEFAULT_WIDGETS.
Adds 5 regression tests (79 total, up from 74).
horizontalLayout had no explicit margins, so its QGroupBox border sat
inset by Qt's default layout margin instead of flush with the sidebar
column like the other two sidebar-preferred widgets.
The shared sidebar (D2) aggregates fills across every tab, so it can
grow taller than any single old widget's hand-picked sidebar did.
Wrap widgetSidebar in a QScrollArea: vertical-as-needed, horizontal
always off, frameless to keep the same visual look when it fits.
Main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules (#150)
Check off the implementation checklist, note the deviation from the
literal _add_client sketch and the post-merge follow-up fixes, and
unblock the VideoWidget-split follow-up plan.
Adds StructuredConfigWidget: a nested form auto-built from a module's
ConfigSchema (str/int/float/bool/enum plus recursive objects), populated
from ConfigAppliedState and applied via set_config(). No widget code
needed per module. Registered in MAIN_WIDGETS as a fallback/companion
page for any IStructuredConfig module.
…dget verification
Used to smoke-test the widget against DummyStructuredConfig (pyobs-core#841) in a
local MultiModule + LocalComm setup, same pattern as the other test/*.yaml fixtures.
Generic IStructuredConfig widget, schema-driven config form (#154)
Current pyobs-core develop's stricter leftover-kwarg check in Object.__init__
has no `name` parameter anywhere in the Module/Object chain, so every fixture's
redundant module-level `name: <module>` (the dict key already names it; only
comm: needs its own `name:`) now raises TypeError on construction. Verified
against the unmodified files before this fix (e.g. roof.yaml failed the same
way). full.yaml/video.yaml still fail separately on a stale `port` kwarg to
DummyVideo -- same root cause, different field, left for a follow-up.
@thusser
thusser merged commit 929ad7d into mainSep 1, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@thusser