feat(dashboard): notify users of available juice updates
Summary
Adds a dashboard widget on the Home (landing) page that displays the running version of OrangeQS Juice and warns users when a newer stable release is available on PyPI.
- The check runs off the event loop, so it never blocks dashboard rendering.
- The result is cached in runtime data, so the registry is polled at most once every 24h.
- Network/registry failures are caught and logged — the widget falls back to a "last check failed" state with the last known result instead of crashing.
- The version comparison, registry call, and widget are all generic and package-agnostic (configurable package name, registry URL, and docs link). Juice is just the first caller.
Closes #262 (closed)
What changed
client/package_registry.py(new) — generic, package-agnostic core:fetch_release_versions(package, registry_url)— thin registry JSON-API client.latest_stable_version(versions)— pure helper; ignores alpha/beta/rc/dev and unparseable versions.is_newer_version(current, latest)— pure version comparison.check_for_update(...)— cache-aware orchestrator: 24h polling interval + graceful failure fallback.
schemas/runtime.py—PackageUpdateState+PackageUpdateChecks(RuntimeData, keyed per package).dashboard/widgets/update_notification_widget.py(new) —UpdateNotificationWidget; runs the check viaasyncio.to_thread+pn.state.executeand renders the banner into a bokehDiv.dashboard/pages/home.py+home.html— instantiate and embed the widget.pyproject.toml— addpackagingdependency.
Screenshots
The widget rendered in each state — an update-available warning banner (with current/latest versions, last-checked timestamp, and a link to update instructions), the up-to-date state, the graceful "last check failed" fallback, and the initial loading state:
Reuse example
The widget works for any Python package:
import importlib.metadata
import panel
from orangeqs.juice.dashboard.widgets import UpdateNotificationWidget
widget = UpdateNotificationWidget(
package_name="some-other-pkg",
current_version=importlib.metadata.version("some-other-pkg"),
docs_url="https://example.com/docs/upgrading",
registry_url="https://pypi.org/pypi", # any registry exposing the same JSON API
)
panel.state.execute(widget.check) # off-event-loop check
template.add_panel("update_notification", widget.root)Testing
tests/juice/client/test_package_registry.py:- Pure version parsing/comparison (no mocking) — pre-releases ignored, unparsable skipped.
- Mocked-registry fetch (integration with a mock registry API).
- Checker scenarios: poll + cache write, cache reuse within interval, re-poll after interval, graceful failure fallback, and "no stable release is not a failure".
- Verified end to end against the live PyPI JSON API.
ruff(lint + format) andpyrightclean on in-scope files; existing dashboard/settings suites pass.
Notes
- A local dev/git build (e.g.
26.23.0+dirty) may show the banner against the matching stable release — expected, and consistent with "ignore pre-releases in the latest comparison". - The docs link points to the existing
installation.html#updatingsection (no new docs page needed).
