diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/Projects.iml b/.idea/Projects.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/Projects.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/changes.md b/.idea/changes.md new file mode 100644 index 0000000..1c8afb7 --- /dev/null +++ b/.idea/changes.md @@ -0,0 +1,39 @@ +# Changes Log + +## 2026-03-23 + +### Added +- Created `.idea/start prompt.md` as a persistent project working prompt for future sessions. +- Added `.idea/changes.md` as the preferred in-project location for the detailed session change log. +- Added release notes support in the active Bahmcloud Store runtime path: backend provider fetching, a dedicated API endpoint, and panel display for the currently selected version when provider release notes are available. +- Bumped the Home Assistant panel asset cache-buster from `v=109` to `v=110` so the updated frontend loads reliably after deployment. + +### Documented +- Captured the verified project identity from the repository and README files: Bahmcloud Store is a Home Assistant custom integration intended to behave like a provider-neutral store for custom integrations, similar to HACS but broader than GitHub-only workflows. +- Recorded the actual active architecture from source analysis, including the config-entry-only setup, the fixed Bahmcloud store index, delayed startup refresh, periodic refresh, repo merge flow, cache usage, install/update/uninstall pipeline, backup/restore pipeline, update entities, and Repairs-based restart handling. +- Recorded the current provider reality from code: GitHub, GitLab, and Gitea-compatible repositories are the concrete supported paths today, while truly generic "all git providers" support is still an intention and must be validated case by case. +- Recorded the public API endpoints exposed by `views.py` so future work preserves the current backend contract unless a deliberate breaking change is approved. +- Recorded storage facts from `storage.py`, including the `bcs_store` Home Assistant storage key and the persisted sections for custom repositories, installed repositories, settings, HACS cache, and repo enrichment cache. +- Recorded frontend facts from the active panel registration in `__init__.py` and the active frontend implementation in `panel/panel.js`, including the cache-busting panel asset version query. +- Updated the persistent start prompt to point future work to `.idea/changes.md` as the canonical detailed work log. +- Release notes are intentionally tied to provider release objects, so tags or branches without release bodies now return a clear "not available" state instead of misleading fallback text. + +### Important findings from code analysis +- Identified `custom_components/bahmcloud_store/panel/panel.js` as the active Home Assistant panel script currently loaded by the integration. +- Identified `custom_components/bahmcloud_store/panel/app.js`, `custom_components/bahmcloud_store/panel/index.html`, and `custom_components/bahmcloud_store/panel/styles.css` as likely legacy or secondary assets that should not be treated as authoritative without verification. +- Identified `custom_components/bahmcloud_store/store.py` as an older implementation with a different data model and API shape than the active `BCSCore` runtime. +- Identified `custom_components/bahmcloud_store/custom_repo_view.py` as duplicate or legacy API code because the active custom-repo handling already exists in `views.py`. +- Noted that the README set is directionally useful but not fully authoritative where it conflicts with current code behavior. +- Noted that some repository files contain encoding or mojibake artifacts, so future edits should preserve valid UTF-8 and avoid spreading broken text. + +### Project rules written into the start prompt +- Never push, commit, tag, or create a release without explicit user approval. +- Always append a dated and detailed entry to `.idea/changes.md` for every change made. +- When a release is created, collect all relevant changes since the last release into `CHANGELOG.md`. + +### Verification +- Reviewed repository structure and current git status. +- Read `README.md`, `README_DEVELOPER.md`, `README_FULL.md`, `bcs.yaml`, and the current `CHANGELOG.md`. +- Analyzed the active backend files: `__init__.py`, `const.py`, `core.py`, `providers.py`, `metadata.py`, `storage.py`, `views.py`, `config_flow.py`, `update.py`, and `repairs.py`. +- Checked panel and legacy-related files to distinguish the currently active UI path from older or duplicated files. +- Verified that the active panel (`panel/panel.js`) now requests release notes from the new backend route and reloads them when the selected install version changes. diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7ed86b6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/start prompt.md b/.idea/start prompt.md new file mode 100644 index 0000000..95f944b --- /dev/null +++ b/.idea/start prompt.md @@ -0,0 +1,119 @@ +## Bahmcloud Store Start Prompt + +You are working in the `Bahmcloud Store` repository. + +Project identity: +- This is a Home Assistant custom integration named `bahmcloud_store`. +- The product goal is a provider-neutral store for Home Assistant custom integrations, similar in spirit to HACS, but not limited to GitHub. +- Current real provider implementation is strongest for GitHub, GitLab, and Gitea-compatible providers. Unknown providers currently fall through the Gitea-style code paths, so do not assume every arbitrary Git provider works without verification. + +Working rules: +- Never push, commit, tag, or create a release without explicit user approval. +- Always document every change in `.idea/changes.md` with the current date and a detailed description of what changed, why it changed, and any verification done. +- If a new release is created, update `CHANGELOG.md` with all relevant changes since the last released version. +- Prefer changing the real active code paths, not legacy or duplicate files. +- When docs and code disagree, trust the current code first, then update docs to match verified behavior. +- Do not remove user changes or perform destructive git actions unless the user explicitly asks for them. + +Repository facts to keep in mind: +- Main integration path: `custom_components/bahmcloud_store/` +- Entry point: `__init__.py` +- Main runtime/service layer: `core.py` +- HTTP API layer: `views.py` +- Provider detection and remote version/readme fetching: `providers.py` +- Repo metadata loading (`bcs.yaml`, `hacs.yaml`, `hacs.json`): `metadata.py` +- Persistent storage: `storage.py` +- Update entities: `update.py` +- Repairs restart flow: `repairs.py` +- Frontend panel actually loaded by Home Assistant: `panel/panel.js` +- Panel registration uses `/api/bahmcloud_store_static/panel.js?v=109`; if frontend assets change in a real release, bump the version query to break HA browser cache. + +Current architecture summary: +- Setup is config-entry only. YAML configuration is intentionally unsupported and only logs a warning if present. +- Only one integration instance is allowed. +- The official store index URL is fixed in `const.py`: + `https://git.bahmcloud.de/bahmcloud/ha_store/raw/branch/main/store.yaml` +- The store index format currently used by the active code is a YAML mapping with: + - `refresh_seconds` + - `repos` + - each repo entry contains at least `url`, optionally `name` and `category` +- Refresh flow: + - integration initializes storage and caches + - after `homeassistant_started`, a delayed refresh runs + - periodic refresh also runs on an interval from the store index + - refresh merges official index repos, optional HACS repos, and user custom repos + - cached enrichment is applied first, installed repos are refreshed eagerly, and the rest are enriched in the background +- Optional HACS support exists behind the `hacs_enabled` setting and currently loads the official HACS integration list from `data-v2.hacs.xyz`. That path is GitHub-only metadata, not a general provider abstraction. +- Install/update flow: + - picks a ref from selected version, latest version, or default branch + - downloads a ZIP from the provider-specific archive endpoint + - extracts the repository + - finds `custom_components` + - installs every integration folder inside `custom_components/*` that contains `manifest.json` + - stores the installed ref and manifest version in HA storage + - creates backups before overwriting existing domains + - marks restart required through Repairs +- Backup/restore behavior: + - backups live under `/config/.bcs_backups///` + - restore updates stored installed-version info so the UI and update entities reflect the restored state + - retention is currently 5 backups per domain +- Installed-state reconciliation exists: + - if folders are deleted from `/config/custom_components`, stale installed entries are removed from storage + - BCS also tries to self-reconcile as installed when it was deployed externally + +Public/API contract to preserve unless intentionally changed: +- `GET /api/bcs` +- `POST /api/bcs?action=refresh` +- `GET /api/bcs/settings` +- `POST /api/bcs/settings` +- `GET /api/bcs/readme?repo_id=...` +- `GET /api/bcs/versions?repo_id=...` +- `GET /api/bcs/repo?repo_id=...` +- `POST /api/bcs/install?repo_id=...&version=...` +- `POST /api/bcs/update?repo_id=...&version=...` +- `POST /api/bcs/uninstall?repo_id=...` +- `GET /api/bcs/backups?repo_id=...` +- `POST /api/bcs/restore?repo_id=...&backup_id=...` +- `POST /api/bcs/restart` +- `DELETE /api/bcs/custom_repo?id=...` + +Storage model: +- Home Assistant storage key: `bcs_store` +- Stored sections: + - `custom_repos` + - `installed_repos` + - `settings` + - `hacs_cache` + - `repo_cache` + +Frontend/UI facts: +- The active HA panel script is `custom_components/bahmcloud_store/panel/panel.js`. +- The richer UI is implemented there: source filtering, HACS toggle, repo detail loading, version selection, backups restore modal, restart action, and history handling. +- `panel/app.js`, `panel/index.html`, and `panel/styles.css` look like older standalone or legacy panel assets. Treat them as secondary unless you confirm they are still used for a real path. + +Code-analysis findings that should influence future work: +- `store.py` represents an older store implementation with a different data model (`packages`, `source_path`, older API routes). It does not appear to be the active runtime path for the current integration flow. +- `custom_repo_view.py` duplicates logic that already exists in `views.py` and looks legacy/unreferenced. +- README files describe the project direction correctly at a high level, but some wording overstates provider generality. The actual code is provider-neutral in intent, but concretely implemented around GitHub, GitLab, and Gitea-style endpoints. +- The end-user and full READMEs contain some stale or inconsistent details compared with the current UI and code. Verify behavior in source before using README text as specification. +- There are visible encoding/mojibake issues in some documentation and older UI assets. Preserve valid UTF-8 when editing. + +Project constraints to respect in future edits: +- Keep async I/O non-blocking in Home Assistant. +- Avoid startup-heavy network work before HA is fully started. +- Preserve repo-cache and HACS-cache behavior unless intentionally redesigning refresh logic. +- Preserve backup-before-overwrite safety for install/update/restore flows. +- Preserve update-entity behavior for installed repos. +- Keep the integration UI-admin-only and config-entry-based. + +Recommended workflow for future tasks: +1. Read `README.md`, `README_DEVELOPER.md`, and relevant source files. +2. Verify whether the requested change belongs in active code or in legacy files. +3. Implement the change in the active runtime path. +4. Update documentation if behavior changed. +5. Append a detailed dated entry to `.idea/changes.md`. +6. If and only if a release is being prepared with user approval, fold release-worthy changes into `CHANGELOG.md`. + +Current release baseline: +- `manifest.json` version is `0.7.2` +- Latest documented release in `CHANGELOG.md` is `0.7.2` dated `2026-01-20` diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/custom_components/bahmcloud_store/__init__.py b/custom_components/bahmcloud_store/__init__.py index 77c6c4e..ef72e48 100644 --- a/custom_components/bahmcloud_store/__init__.py +++ b/custom_components/bahmcloud_store/__init__.py @@ -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, diff --git a/custom_components/bahmcloud_store/core.py b/custom_components/bahmcloud_store/core.py index 47fc126..e8ac070 100644 --- a/custom_components/bahmcloud_store/core.py +++ b/custom_components/bahmcloud_store/core.py @@ -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) \ No newline at end of file + await self.hass.services.async_call("homeassistant", "restart", {}, blocking=False) diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js index aa6bb4e..99ec4dc 100644 --- a/custom_components/bahmcloud_store/panel/panel.js +++ b/custom_components/bahmcloud_store/panel/panel.js @@ -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 { `; + const releaseNotesBlock = this._releaseNotesLoading + ? `
Loading release notes...
` + : this._releaseNotesText + ? ` +
+
+
Release Notes
+
${this._esc(selectedRef || latestVersion || "-")}
+
+
+
+ Show raw release notes +
+
${this._esc(this._releaseNotesText)}
+
+
+
+ ` + : ` +
+
Release Notes
+
${this._esc(this._releaseNotesError || "Release notes not available for this version.")}
+
+ `; + const installBtn = ``; const updateBtn = ``; const uninstallBtn = ``; @@ -1303,6 +1372,7 @@ class BahmcloudStorePanel extends HTMLElement { ${versionSelect} + ${releaseNotesBlock}
${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"