Add release notes and project prompts
This commit is contained in:
@@ -59,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
BCSSettingsView,
|
||||
BCSReadmeView,
|
||||
BCSVersionsView,
|
||||
BCSReleaseNotesView,
|
||||
BCSRepoDetailView,
|
||||
BCSCustomRepoView,
|
||||
BCSInstallView,
|
||||
@@ -74,6 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.http.register_view(BCSSettingsView(core))
|
||||
hass.http.register_view(BCSReadmeView(core))
|
||||
hass.http.register_view(BCSVersionsView(core))
|
||||
hass.http.register_view(BCSReleaseNotesView(core))
|
||||
hass.http.register_view(BCSRepoDetailView(core))
|
||||
hass.http.register_view(BCSCustomRepoView(core))
|
||||
hass.http.register_view(BCSInstallView(core))
|
||||
@@ -88,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
frontend_url_path="bahmcloud-store",
|
||||
webcomponent_name="bahmcloud-store-panel",
|
||||
# IMPORTANT: bump v to avoid caching old JS
|
||||
module_url="/api/bahmcloud_store_static/panel.js?v=109",
|
||||
module_url="/api/bahmcloud_store_static/panel.js?v=110",
|
||||
sidebar_title="Bahmcloud Store",
|
||||
sidebar_icon="mdi:store",
|
||||
require_admin=True,
|
||||
|
||||
@@ -20,7 +20,14 @@ from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.util import yaml as ha_yaml
|
||||
|
||||
from .storage import BCSStorage, CustomRepo
|
||||
from .providers import fetch_repo_info, detect_provider, RepoInfo, fetch_readme_markdown, fetch_repo_versions
|
||||
from .providers import (
|
||||
fetch_repo_info,
|
||||
detect_provider,
|
||||
RepoInfo,
|
||||
fetch_readme_markdown,
|
||||
fetch_repo_versions,
|
||||
fetch_release_notes_markdown,
|
||||
)
|
||||
from .metadata import fetch_repo_metadata, RepoMetadata
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -1160,6 +1167,23 @@ class BCSCore:
|
||||
default_branch=repo.default_branch,
|
||||
)
|
||||
|
||||
async def fetch_release_notes_markdown(self, repo_id: str, ref: str | None = None) -> str | None:
|
||||
repo = self.get_repo(repo_id)
|
||||
if not repo:
|
||||
return None
|
||||
|
||||
target_ref = (ref or "").strip() or (repo.latest_version or "").strip()
|
||||
if not target_ref:
|
||||
return None
|
||||
|
||||
return await fetch_release_notes_markdown(
|
||||
self.hass,
|
||||
repo.url,
|
||||
ref=target_ref,
|
||||
provider=repo.provider,
|
||||
github_token=self.config.github_token,
|
||||
)
|
||||
|
||||
def _pick_ref_for_install(self, repo: RepoItem) -> str:
|
||||
if repo.latest_version and str(repo.latest_version).strip():
|
||||
return str(repo.latest_version).strip()
|
||||
@@ -1790,4 +1814,4 @@ class BCSCore:
|
||||
return await self.install_repo(repo_id, version=version)
|
||||
|
||||
async def request_restart(self) -> None:
|
||||
await self.hass.services.async_call("homeassistant", "restart", {}, blocking=False)
|
||||
await self.hass.services.async_call("homeassistant", "restart", {}, blocking=False)
|
||||
|
||||
@@ -56,6 +56,10 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._versionsCache = {}; // repo_id -> [{ref,label,source}, ...]
|
||||
this._versionsLoadingRepoId = null;
|
||||
this._selectedVersionByRepoId = {}; // repo_id -> ref ("" means latest)
|
||||
this._releaseNotesLoading = false;
|
||||
this._releaseNotesText = null;
|
||||
this._releaseNotesHtml = null;
|
||||
this._releaseNotesError = null;
|
||||
|
||||
// History handling (mobile back button should go back to list, not exit panel)
|
||||
this._historyBound = false;
|
||||
@@ -442,6 +446,10 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._readmeError = null;
|
||||
this._readmeExpanded = false;
|
||||
this._readmeCanToggle = false;
|
||||
this._releaseNotesLoading = false;
|
||||
this._releaseNotesText = null;
|
||||
this._releaseNotesHtml = null;
|
||||
this._releaseNotesError = null;
|
||||
|
||||
// Versions dropdown
|
||||
if (!(repoId in this._selectedVersionByRepoId)) {
|
||||
@@ -452,6 +460,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._loadRepoDetails(repoId);
|
||||
this._loadReadme(repoId);
|
||||
this._loadVersions(repoId);
|
||||
this._loadReleaseNotes(repoId);
|
||||
}
|
||||
|
||||
|
||||
@@ -499,6 +508,41 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
async _loadReleaseNotes(repoId) {
|
||||
if (!this._hass || !repoId) return;
|
||||
|
||||
this._releaseNotesLoading = true;
|
||||
this._releaseNotesText = null;
|
||||
this._releaseNotesHtml = null;
|
||||
this._releaseNotesError = null;
|
||||
this._update();
|
||||
|
||||
try {
|
||||
const sel = this._safeText(this._selectedVersionByRepoId?.[repoId] || "").trim();
|
||||
const qv = sel ? `&ref=${encodeURIComponent(sel)}` : "";
|
||||
const resp = await this._hass.callApi(
|
||||
"get",
|
||||
`bcs/release_notes?repo_id=${encodeURIComponent(repoId)}${qv}`,
|
||||
);
|
||||
|
||||
if (resp?.ok && typeof resp.release_notes === "string" && resp.release_notes.trim()) {
|
||||
this._releaseNotesText = resp.release_notes;
|
||||
this._releaseNotesHtml =
|
||||
typeof resp.html === "string" && resp.html.trim() ? resp.html : null;
|
||||
} else {
|
||||
this._releaseNotesError =
|
||||
this._safeText(resp?.message) || "Release notes not available for this version.";
|
||||
}
|
||||
} catch (e) {
|
||||
this._releaseNotesError = e?.message
|
||||
? String(e.message)
|
||||
: "Release notes not available for this version.";
|
||||
} finally {
|
||||
this._releaseNotesLoading = false;
|
||||
this._update();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadReadme(repoId) {
|
||||
if (!this._hass) return;
|
||||
this._readmeLoading = true;
|
||||
@@ -1250,6 +1294,31 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
</div>
|
||||
`;
|
||||
|
||||
const releaseNotesBlock = this._releaseNotesLoading
|
||||
? `<div class="card" style="margin-top:12px;">Loading release notes...</div>`
|
||||
: this._releaseNotesText
|
||||
? `
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<div class="row" style="align-items:center;">
|
||||
<div><strong>Release Notes</strong></div>
|
||||
<div class="muted small">${this._esc(selectedRef || latestVersion || "-")}</div>
|
||||
</div>
|
||||
<div id="releaseNotesPretty" class="md" style="margin-top:12px;"></div>
|
||||
<details>
|
||||
<summary>Show raw release notes</summary>
|
||||
<div style="margin-top:10px;">
|
||||
<pre class="readme">${this._esc(this._releaseNotesText)}</pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<div><strong>Release Notes</strong></div>
|
||||
<div class="muted" style="margin-top:8px;">${this._esc(this._releaseNotesError || "Release notes not available for this version.")}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const installBtn = `<button class="primary" id="btnInstall" ${installed || busy ? "disabled" : ""}>${busyInstall ? "Installing…" : installed ? "Installed" : "Install"}</button>`;
|
||||
const updateBtn = `<button class="primary" id="btnUpdate" ${!updateAvailable || busy ? "disabled" : ""}>${busyUpdate ? "Updating…" : updateAvailable ? "Update" : "Up to date"}</button>`;
|
||||
const uninstallBtn = `<button class="primary" id="btnUninstall" ${!installed || busy ? "disabled" : ""}>${busyUninstall ? "Uninstalling…" : "Uninstall"}</button>`;
|
||||
@@ -1303,6 +1372,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
</div>
|
||||
|
||||
${versionSelect}
|
||||
${releaseNotesBlock}
|
||||
|
||||
<div class="row" style="margin-top:14px; gap:10px; flex-wrap:wrap;">
|
||||
${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 = `<div class="muted">Rendered HTML not available. Use "Show raw release notes".</div>`;
|
||||
}
|
||||
} 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 = `<div class="muted">Rendered HTML not available. Use "Show raw release notes".</div>`;
|
||||
}
|
||||
} else {
|
||||
releaseMount.innerHTML = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_wireRestoreModal() {
|
||||
@@ -1554,4 +1654,4 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
|
||||
customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user