Loading release notes...
`
+ : this._releaseNotesText
+ ? `
+
${installBtn}
@@ -1342,6 +1412,7 @@ class BahmcloudStorePanel extends HTMLElement {
if (!this._detailRepoId) return;
const v = selVersion.value != null ? String(selVersion.value) : "";
this._selectedVersionByRepoId[this._detailRepoId] = v;
+ this._loadReleaseNotes(this._detailRepoId);
});
}
@@ -1380,7 +1451,22 @@ class BahmcloudStorePanel extends HTMLElement {
}
const mount = root.getElementById("readmePretty");
- if (!mount) return;
+ if (!mount) {
+ const releaseMount = root.getElementById("releaseNotesPretty");
+ if (releaseMount) {
+ if (this._releaseNotesText) {
+ if (this._releaseNotesHtml) {
+ releaseMount.innerHTML = this._releaseNotesHtml;
+ this._postprocessRenderedMarkdown(releaseMount);
+ } else {
+ releaseMount.innerHTML = `
Rendered HTML not available. Use "Show raw release notes".
`;
+ }
+ } else {
+ releaseMount.innerHTML = "";
+ }
+ }
+ return;
+ }
if (this._readmeText) {
if (this._readmeHtml) {
@@ -1392,6 +1478,20 @@ class BahmcloudStorePanel extends HTMLElement {
} else {
mount.innerHTML = "";
}
+
+ const releaseMount = root.getElementById("releaseNotesPretty");
+ if (releaseMount) {
+ if (this._releaseNotesText) {
+ if (this._releaseNotesHtml) {
+ releaseMount.innerHTML = this._releaseNotesHtml;
+ this._postprocessRenderedMarkdown(releaseMount);
+ } else {
+ releaseMount.innerHTML = `
Rendered HTML not available. Use "Show raw release notes".
`;
+ }
+ } else {
+ releaseMount.innerHTML = "";
+ }
+ }
}
_wireRestoreModal() {
@@ -1554,4 +1654,4 @@ class BahmcloudStorePanel extends HTMLElement {
}
}
-customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
\ No newline at end of file
+customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
diff --git a/custom_components/bahmcloud_store/providers.py b/custom_components/bahmcloud_store/providers.py
index 0d24480..70172c3 100644
--- a/custom_components/bahmcloud_store/providers.py
+++ b/custom_components/bahmcloud_store/providers.py
@@ -678,4 +678,78 @@ async def fetch_repo_versions(
except Exception:
_LOGGER.debug("fetch_repo_versions failed for %s", repo_url, exc_info=True)
- return out
\ No newline at end of file
+ return out
+
+
+async def fetch_release_notes_markdown(
+ hass: HomeAssistant,
+ repo_url: str,
+ *,
+ ref: str | None,
+ provider: str | None = None,
+ github_token: str | None = None,
+) -> str | None:
+ """Fetch release notes for a specific release tag."""
+
+ repo_url = (repo_url or "").strip()
+ target_ref = (ref or "").strip()
+ if not repo_url or not target_ref:
+ return None
+
+ prov = (provider or "").strip().lower() if provider else ""
+ if not prov:
+ prov = detect_provider(repo_url)
+
+ owner, repo = _split_owner_repo(repo_url)
+ if not owner or not repo:
+ return None
+
+ session = async_get_clientsession(hass)
+
+ try:
+ if prov == "github":
+ data, status = await _safe_json(
+ session,
+ f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{quote_plus(target_ref)}",
+ headers=_github_headers(github_token),
+ )
+ if status == 200 and isinstance(data, dict):
+ body = data.get("body")
+ if isinstance(body, str) and body.strip():
+ return body
+ return None
+
+ if prov == "gitlab":
+ u = urlparse(repo_url.rstrip("/"))
+ base = f"{u.scheme}://{u.netloc}"
+ project = quote_plus(f"{owner}/{repo}")
+ data, status = await _safe_json(
+ session,
+ f"{base}/api/v4/projects/{project}/releases/{quote_plus(target_ref)}",
+ headers={"User-Agent": UA},
+ )
+ if status == 200 and isinstance(data, dict):
+ body = data.get("description")
+ if isinstance(body, str) and body.strip():
+ return body
+ return None
+
+ u = urlparse(repo_url.rstrip("/"))
+ base = f"{u.scheme}://{u.netloc}"
+ data, status = await _safe_json(
+ session,
+ f"{base}/api/v1/repos/{owner}/{repo}/releases/tags/{quote_plus(target_ref)}",
+ headers={"User-Agent": UA},
+ )
+ if status == 200 and isinstance(data, dict):
+ body = data.get("body")
+ if isinstance(body, str) and body.strip():
+ return body
+ note = data.get("note")
+ if isinstance(note, str) and note.strip():
+ return note
+ return None
+
+ except Exception:
+ _LOGGER.debug("fetch_release_notes_markdown failed for %s ref=%s", repo_url, target_ref, exc_info=True)
+ return None
diff --git a/custom_components/bahmcloud_store/views.py b/custom_components/bahmcloud_store/views.py
index 2dac305..6b7b7e5 100644
--- a/custom_components/bahmcloud_store/views.py
+++ b/custom_components/bahmcloud_store/views.py
@@ -349,6 +349,41 @@ class BCSVersionsView(HomeAssistantView):
return web.json_response({"ok": False, "message": str(e) or "List versions failed"}, status=500)
+class BCSReleaseNotesView(HomeAssistantView):
+ url = "/api/bcs/release_notes"
+ name = "api:bcs_release_notes"
+ requires_auth = True
+
+ def __init__(self, core: Any) -> None:
+ self.core: BCSCore = core
+
+ async def get(self, request: web.Request) -> web.Response:
+ repo_id = request.query.get("repo_id")
+ if not repo_id:
+ return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400)
+
+ ref = request.query.get("ref")
+ ref = str(ref).strip() if ref is not None else None
+
+ try:
+ notes = await self.core.fetch_release_notes_markdown(repo_id, ref=ref)
+ if not notes or not str(notes).strip():
+ return web.json_response(
+ {"ok": False, "message": "Release notes not found for this version."},
+ status=404,
+ )
+
+ notes_str = str(notes)
+ html = _render_markdown_server_side(notes_str)
+ return web.json_response(
+ {"ok": True, "ref": ref, "release_notes": notes_str, "html": html},
+ status=200,
+ )
+ except Exception as e:
+ _LOGGER.exception("BCS release notes failed: %s", e)
+ return web.json_response({"ok": False, "message": str(e) or "Release notes failed"}, status=500)
+
+
class BCSInstallView(HomeAssistantView):
url = "/api/bcs/install"
name = "api:bcs_install"