Prepare blueprint install support
This commit is contained in:
3
.idea/changes.md
generated
3
.idea/changes.md
generated
@@ -15,6 +15,9 @@
|
|||||||
- Added a persistent language rule that all project-facing repository content must stay in English regardless of the chat language.
|
- Added a persistent language rule that all project-facing repository content must stay in English regardless of the chat language.
|
||||||
- Added persistent pinned repositories support: favorites are stored in settings, exposed by the backend, filterable and sortable in the store view, and toggleable from the detail view without forcing a full repository refresh.
|
- Added persistent pinned repositories support: favorites are stored in settings, exposed by the backend, filterable and sortable in the store view, and toggleable from the detail view without forcing a full repository refresh.
|
||||||
- Bumped the integration version from `0.7.3` to `0.7.4` and added the `0.7.4` release entry to `CHANGELOG.md` for the pinned-repositories feature.
|
- Bumped the integration version from `0.7.3` to `0.7.4` and added the `0.7.4` release entry to `CHANGELOG.md` for the pinned-repositories feature.
|
||||||
|
- Added the broader product roadmap to the persistent project prompt: blueprints, templates, Lovelace designs, and more category support as future store targets.
|
||||||
|
- Started the blueprint/category-aware installer preparation: installation metadata now distinguishes install type and installed paths, repo payloads expose install targets, and the active panel shows install-target context for future non-integration categories.
|
||||||
|
- Added initial blueprint install-path handling groundwork so the codebase is no longer fully hard-wired to `custom_components/`.
|
||||||
|
|
||||||
### Documented
|
### 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.
|
- 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.
|
||||||
|
|||||||
11
.idea/start prompt.md
generated
11
.idea/start prompt.md
generated
@@ -6,6 +6,7 @@ Project identity:
|
|||||||
- This is a Home Assistant custom integration named `bahmcloud_store`.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
- The long-term product direction is broader than integrations only: the store should evolve into a Home Assistant content store that can manage integrations, blueprints, templates, Lovelace designs, and additional future categories from multiple git providers.
|
||||||
|
|
||||||
Working rules:
|
Working rules:
|
||||||
- All project-facing work must be done in English only, regardless of the language the user speaks in chat. This applies to code comments, documentation, changelog entries, release notes, commit messages, PR text, UI text, issue text, and any other project artifacts unless the user explicitly requests a specific exception for repository content.
|
- All project-facing work must be done in English only, regardless of the language the user speaks in chat. This applies to code comments, documentation, changelog entries, release notes, commit messages, PR text, UI text, issue text, and any other project artifacts unless the user explicitly requests a specific exception for repository content.
|
||||||
@@ -102,6 +103,16 @@ Code-analysis findings that should influence future work:
|
|||||||
- 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.
|
- 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.
|
- There are visible encoding/mojibake issues in some documentation and older UI assets. Preserve valid UTF-8 when editing.
|
||||||
|
|
||||||
|
Planned product expansion:
|
||||||
|
1. Add support for Home Assistant blueprints and install them directly into the correct Home Assistant blueprint location from the store.
|
||||||
|
2. Add templates and Lovelace designs to the store so they can be discovered and installed from the same UI.
|
||||||
|
3. Add support for more categories beyond integrations and design the architecture so category-specific install targets and validation rules are explicit.
|
||||||
|
|
||||||
|
Implications for future architecture:
|
||||||
|
- The current install pipeline is integration-centric because it assumes repository content under `custom_components/`.
|
||||||
|
- Future category support should move toward category-aware install strategies instead of one universal install path.
|
||||||
|
- Store metadata and index entries will likely need stronger category typing, install-target definitions, and validation rules per category.
|
||||||
|
|
||||||
Project constraints to respect in future edits:
|
Project constraints to respect in future edits:
|
||||||
- Keep async I/O non-blocking in Home Assistant.
|
- Keep async I/O non-blocking in Home Assistant.
|
||||||
- Avoid startup-heavy network work before HA is fully started.
|
- Avoid startup-heavy network work before HA is fully started.
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
frontend_url_path="bahmcloud-store",
|
frontend_url_path="bahmcloud-store",
|
||||||
webcomponent_name="bahmcloud-store-panel",
|
webcomponent_name="bahmcloud-store-panel",
|
||||||
# IMPORTANT: bump v to avoid caching old JS
|
# IMPORTANT: bump v to avoid caching old JS
|
||||||
module_url="/api/bahmcloud_store_static/panel.js?v=111",
|
module_url="/api/bahmcloud_store_static/panel.js?v=112",
|
||||||
sidebar_title="Bahmcloud Store",
|
sidebar_title="Bahmcloud Store",
|
||||||
sidebar_icon="mdi:store",
|
sidebar_icon="mdi:store",
|
||||||
require_admin=True,
|
require_admin=True,
|
||||||
|
|||||||
@@ -1172,13 +1172,17 @@ class BCSCore:
|
|||||||
"latest_version": r.latest_version,
|
"latest_version": r.latest_version,
|
||||||
"latest_version_source": r.latest_version_source,
|
"latest_version_source": r.latest_version_source,
|
||||||
"category": r.meta_category,
|
"category": r.meta_category,
|
||||||
|
"category_key": self._repo_install_type(r),
|
||||||
|
"install_target": self._repo_install_target(r),
|
||||||
"meta_author": r.meta_author,
|
"meta_author": r.meta_author,
|
||||||
"meta_maintainer": r.meta_maintainer,
|
"meta_maintainer": r.meta_maintainer,
|
||||||
"meta_source": r.meta_source,
|
"meta_source": r.meta_source,
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
|
"install_type": str(inst.get("install_type") if isinstance(inst, dict) else self._repo_install_type(r)),
|
||||||
"installed_version": installed_version,
|
"installed_version": installed_version,
|
||||||
"installed_manifest_version": installed_manifest_version,
|
"installed_manifest_version": installed_manifest_version,
|
||||||
"installed_domains": installed_domains,
|
"installed_domains": installed_domains,
|
||||||
|
"installed_paths": list(inst.get("installed_paths") or []) if isinstance(inst, dict) else [],
|
||||||
"favorite": self.is_favorite_repo(r.id),
|
"favorite": self.is_favorite_repo(r.id),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1220,6 +1224,32 @@ class BCSCore:
|
|||||||
return str(repo.default_branch).strip()
|
return str(repo.default_branch).strip()
|
||||||
return "main"
|
return "main"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_category_key(category: str | None) -> str:
|
||||||
|
raw = str(category or "").strip().lower()
|
||||||
|
if raw in ("integration", "integrations"):
|
||||||
|
return "integration"
|
||||||
|
if raw in ("blueprint", "blueprints"):
|
||||||
|
return "blueprint"
|
||||||
|
if raw in ("template", "templates"):
|
||||||
|
return "template"
|
||||||
|
if raw in ("lovelace", "dashboard", "dashboards", "lovelace design", "lovelace designs"):
|
||||||
|
return "lovelace"
|
||||||
|
return "integration"
|
||||||
|
|
||||||
|
def _repo_install_type(self, repo: RepoItem | None) -> str:
|
||||||
|
return self._normalize_category_key(getattr(repo, "meta_category", None))
|
||||||
|
|
||||||
|
def _repo_install_target(self, repo: RepoItem | None) -> str:
|
||||||
|
install_type = self._repo_install_type(repo)
|
||||||
|
if install_type == "blueprint":
|
||||||
|
return "/config/blueprints"
|
||||||
|
if install_type == "template":
|
||||||
|
return "/config"
|
||||||
|
if install_type == "lovelace":
|
||||||
|
return "/config"
|
||||||
|
return "/config/custom_components"
|
||||||
|
|
||||||
def _build_zip_url(self, repo_url: str, ref: str) -> str:
|
def _build_zip_url(self, repo_url: str, ref: str) -> str:
|
||||||
ref = (ref or "").strip()
|
ref = (ref or "").strip()
|
||||||
if not ref:
|
if not ref:
|
||||||
@@ -1286,6 +1316,18 @@ class BCSCore:
|
|||||||
return candidate
|
return candidate
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_blueprints_root(extract_root: Path) -> Path | None:
|
||||||
|
direct = extract_root / "blueprints"
|
||||||
|
if direct.exists() and direct.is_dir():
|
||||||
|
return direct
|
||||||
|
|
||||||
|
for child in extract_root.iterdir():
|
||||||
|
candidate = child / "blueprints"
|
||||||
|
if candidate.exists() and candidate.is_dir():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
async def _ensure_backup_root(self) -> None:
|
async def _ensure_backup_root(self) -> None:
|
||||||
"""Create backup root directory if needed."""
|
"""Create backup root directory if needed."""
|
||||||
def _mkdir() -> None:
|
def _mkdir() -> None:
|
||||||
@@ -1640,15 +1682,25 @@ class BCSCore:
|
|||||||
|
|
||||||
for it in items:
|
for it in items:
|
||||||
domains = [str(d) for d in (it.domains or []) if str(d).strip()]
|
domains = [str(d) for d in (it.domains or []) if str(d).strip()]
|
||||||
|
install_type = str(getattr(it, "install_type", "integration") or "integration").strip() or "integration"
|
||||||
|
installed_paths = [str(p) for p in (getattr(it, "installed_paths", None) or []) if str(p).strip()]
|
||||||
|
|
||||||
# A repo is considered "present" if at least one of its domains
|
# A repo is considered "present" if at least one of its domains
|
||||||
# exists and contains a manifest.json.
|
# exists and contains a manifest.json.
|
||||||
present = False
|
present = False
|
||||||
for d in domains:
|
if install_type == "integration":
|
||||||
p = cc_root / d
|
for d in domains:
|
||||||
if p.is_dir() and (p / "manifest.json").exists():
|
p = cc_root / d
|
||||||
present = True
|
if p.is_dir() and (p / "manifest.json").exists():
|
||||||
break
|
present = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
cfg_root = Path(self.hass.config.path(""))
|
||||||
|
for rel in installed_paths:
|
||||||
|
p = cfg_root / rel
|
||||||
|
if p.exists():
|
||||||
|
present = True
|
||||||
|
break
|
||||||
|
|
||||||
if not present:
|
if not present:
|
||||||
to_remove.append(it.repo_id)
|
to_remove.append(it.repo_id)
|
||||||
@@ -1657,6 +1709,8 @@ class BCSCore:
|
|||||||
cache[it.repo_id] = {
|
cache[it.repo_id] = {
|
||||||
"installed": True,
|
"installed": True,
|
||||||
"domains": domains,
|
"domains": domains,
|
||||||
|
"install_type": install_type,
|
||||||
|
"installed_paths": installed_paths,
|
||||||
"installed_version": it.installed_version,
|
"installed_version": it.installed_version,
|
||||||
"installed_manifest_version": it.installed_manifest_version,
|
"installed_manifest_version": it.installed_manifest_version,
|
||||||
"ref": it.ref,
|
"ref": it.ref,
|
||||||
@@ -1692,24 +1746,50 @@ class BCSCore:
|
|||||||
if path.exists() and path.is_dir():
|
if path.exists() and path.is_dir():
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
|
|
||||||
for domain in inst.domains:
|
def _remove_file(path: Path) -> None:
|
||||||
d = str(domain).strip()
|
if path.exists() and path.is_file():
|
||||||
if not d:
|
path.unlink(missing_ok=True)
|
||||||
continue
|
|
||||||
target = cc_root / d
|
def _prune_empty_parents(path: Path, stop_at: Path) -> None:
|
||||||
await self.hass.async_add_executor_job(_remove_dir, target)
|
cur = path.parent
|
||||||
removed.append(d)
|
while cur != stop_at and str(cur).startswith(str(stop_at)):
|
||||||
|
try:
|
||||||
|
if any(cur.iterdir()):
|
||||||
|
break
|
||||||
|
cur.rmdir()
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
cur = cur.parent
|
||||||
|
|
||||||
|
install_type = str(getattr(inst, "install_type", "integration") or "integration").strip() or "integration"
|
||||||
|
|
||||||
|
if install_type == "integration":
|
||||||
|
for domain in inst.domains:
|
||||||
|
d = str(domain).strip()
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
target = cc_root / d
|
||||||
|
await self.hass.async_add_executor_job(_remove_dir, target)
|
||||||
|
removed.append(d)
|
||||||
|
elif install_type == "blueprint":
|
||||||
|
cfg_root = Path(self.hass.config.path(""))
|
||||||
|
blueprints_root = Path(self.hass.config.path("blueprints"))
|
||||||
|
for rel in [str(p).strip() for p in (inst.installed_paths or []) if str(p).strip()]:
|
||||||
|
target = cfg_root / rel
|
||||||
|
await self.hass.async_add_executor_job(_remove_file, target)
|
||||||
|
await self.hass.async_add_executor_job(_prune_empty_parents, target, blueprints_root)
|
||||||
|
removed.append(rel)
|
||||||
|
|
||||||
await self.storage.remove_installed_repo(repo_id)
|
await self.storage.remove_installed_repo(repo_id)
|
||||||
await self._refresh_installed_cache()
|
await self._refresh_installed_cache()
|
||||||
|
|
||||||
# Show restart required in Settings.
|
# Show restart required in Settings.
|
||||||
if removed:
|
if removed and install_type == "integration":
|
||||||
self._mark_restart_required()
|
self._mark_restart_required()
|
||||||
|
|
||||||
_LOGGER.info("BCS uninstall complete: repo_id=%s removed_domains=%s", repo_id, removed)
|
_LOGGER.info("BCS uninstall complete: repo_id=%s removed_domains=%s", repo_id, removed)
|
||||||
self.signal_updated()
|
self.signal_updated()
|
||||||
return {"ok": True, "repo_id": repo_id, "removed": removed, "restart_required": bool(removed)}
|
return {"ok": True, "repo_id": repo_id, "removed": removed, "restart_required": bool(removed) if install_type == "integration" else False}
|
||||||
|
|
||||||
async def install_repo(self, repo_id: str, *, version: str | None = None) -> dict[str, Any]:
|
async def install_repo(self, repo_id: str, *, version: str | None = None) -> dict[str, Any]:
|
||||||
repo = self.get_repo(repo_id)
|
repo = self.get_repo(repo_id)
|
||||||
@@ -1724,7 +1804,9 @@ class BCSCore:
|
|||||||
_LOGGER.info("BCS install started: repo_id=%s ref=%s zip_url=%s", repo_id, ref, zip_url)
|
_LOGGER.info("BCS install started: repo_id=%s ref=%s zip_url=%s", repo_id, ref, zip_url)
|
||||||
|
|
||||||
installed_domains: list[str] = []
|
installed_domains: list[str] = []
|
||||||
|
installed_paths: list[str] = []
|
||||||
backups: dict[str, Path] = {}
|
backups: dict[str, Path] = {}
|
||||||
|
install_type = self._repo_install_type(repo)
|
||||||
|
|
||||||
inst_before = self.get_installed(repo_id) or {}
|
inst_before = self.get_installed(repo_id) or {}
|
||||||
backup_meta = {
|
backup_meta = {
|
||||||
@@ -1745,57 +1827,85 @@ class BCSCore:
|
|||||||
await self._download_zip(zip_url, zip_path)
|
await self._download_zip(zip_url, zip_path)
|
||||||
await self._extract_zip(zip_path, extract_dir)
|
await self._extract_zip(zip_path, extract_dir)
|
||||||
|
|
||||||
cc_root = self._find_custom_components_root(extract_dir)
|
if install_type == "blueprint":
|
||||||
if not cc_root:
|
blueprints_root = self._find_blueprints_root(extract_dir)
|
||||||
raise BCSInstallError("custom_components folder not found in repository ZIP")
|
if not blueprints_root:
|
||||||
|
raise BCSInstallError("blueprints folder not found in repository ZIP")
|
||||||
|
|
||||||
dest_root = Path(self.hass.config.path("custom_components"))
|
config_root = Path(self.hass.config.path(""))
|
||||||
|
target_root = Path(self.hass.config.path("blueprints"))
|
||||||
|
|
||||||
for domain_dir in cc_root.iterdir():
|
def _copy_blueprints() -> list[str]:
|
||||||
if not domain_dir.is_dir():
|
copied: list[str] = []
|
||||||
continue
|
target_root.mkdir(parents=True, exist_ok=True)
|
||||||
manifest = domain_dir / "manifest.json"
|
for src in blueprints_root.rglob("*"):
|
||||||
if not manifest.exists():
|
if not src.is_file():
|
||||||
continue
|
continue
|
||||||
|
rel = src.relative_to(blueprints_root)
|
||||||
|
dest = target_root / rel
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dest)
|
||||||
|
copied.append(str(Path("blueprints") / rel).replace("\\", "/"))
|
||||||
|
return copied
|
||||||
|
|
||||||
domain = domain_dir.name
|
installed_paths = await self.hass.async_add_executor_job(_copy_blueprints)
|
||||||
target = dest_root / domain
|
if not installed_paths:
|
||||||
|
raise BCSInstallError("No blueprint files found under blueprints/")
|
||||||
|
else:
|
||||||
|
cc_root = self._find_custom_components_root(extract_dir)
|
||||||
|
if not cc_root:
|
||||||
|
raise BCSInstallError("custom_components folder not found in repository ZIP")
|
||||||
|
|
||||||
# Backup only if we are going to overwrite an existing domain.
|
dest_root = Path(self.hass.config.path("custom_components"))
|
||||||
if target.exists() and target.is_dir():
|
|
||||||
m = dict(backup_meta)
|
|
||||||
m["domain"] = domain
|
|
||||||
bkp = await self._backup_domain(domain, meta=m)
|
|
||||||
if bkp:
|
|
||||||
backups[domain] = bkp
|
|
||||||
else:
|
|
||||||
created_new.add(domain)
|
|
||||||
|
|
||||||
await self._copy_domain_dir(domain_dir, domain)
|
for domain_dir in cc_root.iterdir():
|
||||||
installed_domains.append(domain)
|
if not domain_dir.is_dir():
|
||||||
|
continue
|
||||||
|
manifest = domain_dir / "manifest.json"
|
||||||
|
if not manifest.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
if not installed_domains:
|
domain = domain_dir.name
|
||||||
raise BCSInstallError("No integrations found under custom_components/ (missing manifest.json)")
|
target = dest_root / domain
|
||||||
|
|
||||||
installed_manifest_version = await self._read_installed_manifest_version(installed_domains[0])
|
# Backup only if we are going to overwrite an existing domain.
|
||||||
|
if target.exists() and target.is_dir():
|
||||||
|
m = dict(backup_meta)
|
||||||
|
m["domain"] = domain
|
||||||
|
bkp = await self._backup_domain(domain, meta=m)
|
||||||
|
if bkp:
|
||||||
|
backups[domain] = bkp
|
||||||
|
else:
|
||||||
|
created_new.add(domain)
|
||||||
|
|
||||||
|
await self._copy_domain_dir(domain_dir, domain)
|
||||||
|
installed_domains.append(domain)
|
||||||
|
|
||||||
|
if not installed_domains:
|
||||||
|
raise BCSInstallError("No integrations found under custom_components/ (missing manifest.json)")
|
||||||
|
|
||||||
|
installed_manifest_version = await self._read_installed_manifest_version(installed_domains[0]) if installed_domains else None
|
||||||
installed_version = ref
|
installed_version = ref
|
||||||
|
|
||||||
await self.storage.set_installed_repo(
|
await self.storage.set_installed_repo(
|
||||||
repo_id=repo_id,
|
repo_id=repo_id,
|
||||||
url=repo.url,
|
url=repo.url,
|
||||||
domains=installed_domains,
|
domains=installed_domains,
|
||||||
|
install_type=install_type,
|
||||||
|
installed_paths=installed_paths,
|
||||||
installed_version=installed_version,
|
installed_version=installed_version,
|
||||||
installed_manifest_version=installed_manifest_version,
|
installed_manifest_version=installed_manifest_version,
|
||||||
ref=ref,
|
ref=ref,
|
||||||
)
|
)
|
||||||
await self._refresh_installed_cache()
|
await self._refresh_installed_cache()
|
||||||
|
|
||||||
self._mark_restart_required()
|
if install_type == "integration":
|
||||||
|
self._mark_restart_required()
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"BCS install complete: repo_id=%s domains=%s installed_ref=%s manifest_version=%s",
|
"BCS install complete: repo_id=%s domains=%s installed_ref=%s manifest_version=%s",
|
||||||
repo_id,
|
repo_id,
|
||||||
installed_domains,
|
installed_domains if installed_domains else installed_paths,
|
||||||
installed_version,
|
installed_version,
|
||||||
installed_manifest_version,
|
installed_manifest_version,
|
||||||
)
|
)
|
||||||
@@ -1804,9 +1914,11 @@ class BCSCore:
|
|||||||
"ok": True,
|
"ok": True,
|
||||||
"repo_id": repo_id,
|
"repo_id": repo_id,
|
||||||
"domains": installed_domains,
|
"domains": installed_domains,
|
||||||
|
"installed_paths": installed_paths,
|
||||||
|
"install_type": install_type,
|
||||||
"installed_version": installed_version,
|
"installed_version": installed_version,
|
||||||
"installed_manifest_version": installed_manifest_version,
|
"installed_manifest_version": installed_manifest_version,
|
||||||
"restart_required": True,
|
"restart_required": True if install_type == "integration" else False,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1270,6 +1270,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: -",
|
this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: -",
|
||||||
this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
|
this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
|
||||||
this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null,
|
this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null,
|
||||||
|
this._safeText(r?.install_target) ? `Target: ${this._safeText(r?.install_target)}` : null,
|
||||||
this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
|
this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
|
||||||
this._safeText(r?.meta_maintainer) ? `Maintainer: ${this._safeText(r?.meta_maintainer)}` : null,
|
this._safeText(r?.meta_maintainer) ? `Maintainer: ${this._safeText(r?.meta_maintainer)}` : null,
|
||||||
this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
|
this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
|
||||||
@@ -1312,8 +1313,10 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
const installed = this._asBoolStrict(r?.installed);
|
const installed = this._asBoolStrict(r?.installed);
|
||||||
const installedVersion = this._safeText(r?.installed_version);
|
const installedVersion = this._safeText(r?.installed_version);
|
||||||
const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : [];
|
const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : [];
|
||||||
|
const installedPaths = Array.isArray(r?.installed_paths) ? r.installed_paths : [];
|
||||||
const latestVersion = this._safeText(r?.latest_version);
|
const latestVersion = this._safeText(r?.latest_version);
|
||||||
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(repoId);
|
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(repoId);
|
||||||
|
const installType = this._safeText(r?.install_type) || "integration";
|
||||||
|
|
||||||
const busyInstall = this._installingRepoId === repoId;
|
const busyInstall = this._installingRepoId === repoId;
|
||||||
const busyUpdate = this._updatingRepoId === repoId;
|
const busyUpdate = this._updatingRepoId === repoId;
|
||||||
@@ -1376,7 +1379,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
const installBtn = `<button class="primary" id="btnInstall" ${installed || busy ? "disabled" : ""}>${busyInstall ? "Installing…" : installed ? "Installed" : "Install"}</button>`;
|
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 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>`;
|
const uninstallBtn = `<button class="primary" id="btnUninstall" ${!installed || busy ? "disabled" : ""}>${busyUninstall ? "Uninstalling…" : "Uninstall"}</button>`;
|
||||||
const restoreBtn = `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`;
|
const restoreBtn = installType === "integration"
|
||||||
|
? `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`
|
||||||
|
: ``;
|
||||||
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
|
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
|
||||||
|
|
||||||
const restartHint = this._restartRequired
|
const restartHint = this._restartRequired
|
||||||
@@ -1424,6 +1429,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
<div><strong>Installed version:</strong> ${this._esc(installedVersion || "-")}</div>
|
<div><strong>Installed version:</strong> ${this._esc(installedVersion || "-")}</div>
|
||||||
<div><strong>Latest version:</strong> ${this._esc(latestVersion || "-")}</div>
|
<div><strong>Latest version:</strong> ${this._esc(latestVersion || "-")}</div>
|
||||||
<div style="margin-top:6px;"><strong>Domains:</strong> ${installedDomains.length ? this._esc(installedDomains.join(", ")) : "-"}</div>
|
<div style="margin-top:6px;"><strong>Domains:</strong> ${installedDomains.length ? this._esc(installedDomains.join(", ")) : "-"}</div>
|
||||||
|
<div style="margin-top:6px;"><strong>Installed paths:</strong> ${installedPaths.length ? this._esc(installedPaths.join(", ")) : "-"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${versionSelect}
|
${versionSelect}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class InstalledRepo:
|
|||||||
url: str
|
url: str
|
||||||
domains: list[str]
|
domains: list[str]
|
||||||
installed_at: int
|
installed_at: int
|
||||||
|
install_type: str = "integration"
|
||||||
|
installed_paths: list[str] | None = None
|
||||||
installed_version: str | None = None # BCS "installed ref" (tag/release/branch)
|
installed_version: str | None = None # BCS "installed ref" (tag/release/branch)
|
||||||
installed_manifest_version: str | None = None # informational only
|
installed_manifest_version: str | None = None # informational only
|
||||||
ref: str | None = None # kept for backward compatibility / diagnostics
|
ref: str | None = None # kept for backward compatibility / diagnostics
|
||||||
@@ -197,6 +199,11 @@ class BCSStorage:
|
|||||||
if not isinstance(domains, list):
|
if not isinstance(domains, list):
|
||||||
domains = []
|
domains = []
|
||||||
domains = [str(d) for d in domains if str(d).strip()]
|
domains = [str(d) for d in domains if str(d).strip()]
|
||||||
|
installed_paths = entry.get("installed_paths") or []
|
||||||
|
if not isinstance(installed_paths, list):
|
||||||
|
installed_paths = []
|
||||||
|
installed_paths = [str(p) for p in installed_paths if str(p).strip()]
|
||||||
|
install_type = str(entry.get("install_type") or "integration").strip() or "integration"
|
||||||
|
|
||||||
installed_version = entry.get("installed_version")
|
installed_version = entry.get("installed_version")
|
||||||
ref = entry.get("ref")
|
ref = entry.get("ref")
|
||||||
@@ -213,6 +220,8 @@ class BCSStorage:
|
|||||||
url=str(entry.get("url") or ""),
|
url=str(entry.get("url") or ""),
|
||||||
domains=domains,
|
domains=domains,
|
||||||
installed_at=int(entry.get("installed_at") or 0),
|
installed_at=int(entry.get("installed_at") or 0),
|
||||||
|
install_type=install_type,
|
||||||
|
installed_paths=installed_paths,
|
||||||
installed_version=str(installed_version) if installed_version else None,
|
installed_version=str(installed_version) if installed_version else None,
|
||||||
installed_manifest_version=str(installed_manifest_version) if installed_manifest_version else None,
|
installed_manifest_version=str(installed_manifest_version) if installed_manifest_version else None,
|
||||||
ref=str(ref) if ref else None,
|
ref=str(ref) if ref else None,
|
||||||
@@ -238,9 +247,11 @@ class BCSStorage:
|
|||||||
repo_id: str,
|
repo_id: str,
|
||||||
url: str,
|
url: str,
|
||||||
domains: list[str],
|
domains: list[str],
|
||||||
installed_version: str | None,
|
installed_version: str | None = None,
|
||||||
installed_manifest_version: str | None = None,
|
installed_manifest_version: str | None = None,
|
||||||
ref: str | None,
|
ref: str | None = None,
|
||||||
|
install_type: str = "integration",
|
||||||
|
installed_paths: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
data = await self._load()
|
data = await self._load()
|
||||||
installed = data.get("installed_repos", {})
|
installed = data.get("installed_repos", {})
|
||||||
@@ -252,6 +263,8 @@ class BCSStorage:
|
|||||||
"repo_id": str(repo_id),
|
"repo_id": str(repo_id),
|
||||||
"url": str(url),
|
"url": str(url),
|
||||||
"domains": [str(d) for d in (domains or []) if str(d).strip()],
|
"domains": [str(d) for d in (domains or []) if str(d).strip()],
|
||||||
|
"install_type": str(install_type or "integration"),
|
||||||
|
"installed_paths": [str(p) for p in (installed_paths or []) if str(p).strip()],
|
||||||
"installed_at": int(time.time()),
|
"installed_at": int(time.time()),
|
||||||
# IMPORTANT: this is what BCS uses as "installed version" (ref/tag/branch)
|
# IMPORTANT: this is what BCS uses as "installed version" (ref/tag/branch)
|
||||||
"installed_version": installed_version,
|
"installed_version": installed_version,
|
||||||
|
|||||||
@@ -540,6 +540,9 @@ class BCSRepoDetailView(HomeAssistantView):
|
|||||||
domains = inst.get("domains") or []
|
domains = inst.get("domains") or []
|
||||||
if not isinstance(domains, list):
|
if not isinstance(domains, list):
|
||||||
domains = []
|
domains = []
|
||||||
|
installed_paths = inst.get("installed_paths") or []
|
||||||
|
if not isinstance(installed_paths, list):
|
||||||
|
installed_paths = []
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -556,13 +559,17 @@ class BCSRepoDetailView(HomeAssistantView):
|
|||||||
"latest_version": repo.latest_version,
|
"latest_version": repo.latest_version,
|
||||||
"latest_version_source": repo.latest_version_source,
|
"latest_version_source": repo.latest_version_source,
|
||||||
"category": repo.meta_category,
|
"category": repo.meta_category,
|
||||||
|
"category_key": self.core._repo_install_type(repo),
|
||||||
|
"install_target": self.core._repo_install_target(repo),
|
||||||
"meta_author": repo.meta_author,
|
"meta_author": repo.meta_author,
|
||||||
"meta_maintainer": repo.meta_maintainer,
|
"meta_maintainer": repo.meta_maintainer,
|
||||||
"meta_source": repo.meta_source,
|
"meta_source": repo.meta_source,
|
||||||
"installed": installed,
|
"installed": installed,
|
||||||
|
"install_type": inst.get("install_type") or self.core._repo_install_type(repo),
|
||||||
"installed_version": inst.get("installed_version"),
|
"installed_version": inst.get("installed_version"),
|
||||||
"installed_manifest_version": inst.get("installed_manifest_version"),
|
"installed_manifest_version": inst.get("installed_manifest_version"),
|
||||||
"installed_domains": domains,
|
"installed_domains": domains,
|
||||||
|
"installed_paths": installed_paths,
|
||||||
"favorite": self.core.is_favorite_repo(repo.id),
|
"favorite": self.core.is_favorite_repo(repo.id),
|
||||||
}
|
}
|
||||||
}, status=200)
|
}, status=200)
|
||||||
|
|||||||
Reference in New Issue
Block a user