Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 313fcf94e7 | |||
| d976ab56e3 | |||
| 80c1c2966f | |||
| de3fbf1a12 |
5
.idea/changes.md
generated
5
.idea/changes.md
generated
@@ -19,6 +19,11 @@
|
||||
- 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/`.
|
||||
- Bumped the integration version from `0.7.4` to `0.7.5` and added the `0.7.5` release entry to `CHANGELOG.md` for blueprint support and the documentation refresh.
|
||||
- Fixed blueprint update backups: blueprint file updates now create content backups before overwrite and can roll back copied blueprint files if installation fails.
|
||||
- Kept the no-restart behavior for blueprints, because blueprint deployment does not normally require a Home Assistant restart.
|
||||
- Restored blueprint backup/restore availability in the UI and backend: the restore button is visible again for blueprint installs, blueprint backups can be listed, and blueprint content can now be restored from backup without forcing a restart.
|
||||
- Fixed category-specific uninstall and restore messaging in the active panel: blueprints now reference `/config/blueprints` and explicitly state that no restart is required, while integrations keep the `/config/custom_components` restart warning.
|
||||
- Updated the public documentation set (`README.md`, `README_DEVELOPER.md`, `README_FULL.md`, and `CHANGELOG.md`) for the completed blueprint backup, restore, uninstall, and restart-behavior work, and bumped the integration version to `0.7.6` for the next release.
|
||||
|
||||
### 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.
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -11,6 +11,20 @@ Sections:
|
||||
|
||||
---
|
||||
|
||||
## 0.7.6 - 2026-03-23
|
||||
|
||||
### Added
|
||||
- Blueprint backup listing and restore support in the repository detail view.
|
||||
|
||||
### Changed
|
||||
- Install, update, uninstall, and restore feedback now reflects whether a restart is actually required for the selected content type.
|
||||
- Blueprint restore dialogs and action messages now reference `/config/blueprints` instead of integration-only paths.
|
||||
|
||||
### Fixed
|
||||
- Blueprint updates now create usable content backups before overwriting files.
|
||||
- Blueprint backups can be restored again from the active store UI after the previous restore-button regression.
|
||||
- Blueprint uninstall confirmation text now matches the real uninstall behavior and no longer incorrectly warns about `/config/custom_components` or mandatory restarts.
|
||||
|
||||
## 0.7.5 - 2026-03-23
|
||||
|
||||
### Added
|
||||
|
||||
@@ -27,6 +27,7 @@ This README is for end users.
|
||||
- Pin repositories so important items stay easy to find
|
||||
- Use native Home Assistant update entities for installed integrations
|
||||
- Create and restore backups for integration installs and updates
|
||||
- Create and restore blueprint content backups during blueprint updates and restore actions
|
||||
|
||||
---
|
||||
|
||||
@@ -74,7 +75,7 @@ Installed to:
|
||||
|
||||
- Expected repository content: `blueprints/...`
|
||||
- Install target: `/config/blueprints/...`
|
||||
- Supports install and uninstall through the store
|
||||
- Supports install, update, uninstall, backup, and restore through the store
|
||||
- No restart is normally required for blueprint deployment
|
||||
|
||||
---
|
||||
@@ -83,4 +84,3 @@ Installed to:
|
||||
|
||||
- Developer documentation: `README_DEVELOPER.md`
|
||||
- Full user guide: `README_FULL.md`
|
||||
|
||||
|
||||
@@ -86,7 +86,8 @@ Category currently matters operationally:
|
||||
|
||||
- Expected source layout: `blueprints/...`
|
||||
- Install target: `/config/blueprints/...`
|
||||
- Initial support is focused on direct install and uninstall
|
||||
- Supports install, update, uninstall, content backup, and content restore
|
||||
- Restores and uninstalls are path-based and operate on the recorded installed blueprint files
|
||||
- Category-aware groundwork is in place so future content types can use their own install strategies
|
||||
|
||||
## HTTP API
|
||||
@@ -117,7 +118,7 @@ Base path: `/api/bcs`
|
||||
- README rendering
|
||||
- Release notes rendering
|
||||
- Version selection
|
||||
- Backup restore UI for integrations
|
||||
- Backup restore UI for integrations and blueprints
|
||||
|
||||
## Contributing to the Official Store Index
|
||||
|
||||
@@ -156,4 +157,3 @@ The long-term architecture should remain category-aware:
|
||||
- category -> UI affordances
|
||||
|
||||
This is especially important before Templates and Lovelace support are added, because those should stay compatible with established HACS expectations where possible.
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ That makes update checks more reliable when a repository uses tags or releases t
|
||||
|
||||
- Source layout: `blueprints/...`
|
||||
- Install target: `/config/blueprints/...`
|
||||
- Supports install and uninstall through the store
|
||||
- Supports install, update, uninstall, backup, and restore through the store
|
||||
- Intended for blueprint repositories without integration-specific folder structures
|
||||
|
||||
---
|
||||
@@ -150,7 +150,8 @@ Blueprint installs normally do not require a Home Assistant restart.
|
||||
|
||||
### Blueprints
|
||||
|
||||
- Blueprint repositories can also be reinstalled from another selected version.
|
||||
- Blueprint repositories can also be updated or reinstalled from another selected version.
|
||||
- Blueprint updates create content backups before overwriting files.
|
||||
- The current blueprint path handling is focused on direct deployment to the blueprints folder.
|
||||
|
||||
---
|
||||
@@ -167,6 +168,7 @@ Blueprint installs normally do not require a Home Assistant restart.
|
||||
|
||||
- Removes the installed blueprint files recorded by BCS
|
||||
- Cleans up empty directories below `/config/blueprints` when possible
|
||||
- Does not require a restart under normal conditions
|
||||
|
||||
---
|
||||
|
||||
@@ -198,6 +200,7 @@ Notes:
|
||||
## Backups and Restore
|
||||
|
||||
Integration installs and updates create backups before overwriting existing files.
|
||||
Blueprint updates also create content backups before overwriting deployed blueprint files.
|
||||
|
||||
Backup path:
|
||||
|
||||
@@ -211,7 +214,16 @@ Restore flow:
|
||||
4. Confirm restore
|
||||
5. Restart Home Assistant if prompted
|
||||
|
||||
Restore is currently intended for integrations.
|
||||
Restore is available for integrations and blueprints.
|
||||
|
||||
Blueprint restore flow:
|
||||
|
||||
1. Open the blueprint repository detail
|
||||
2. Click **Restore**
|
||||
3. Select a backup
|
||||
4. Confirm restore
|
||||
|
||||
Blueprint restores overwrite the recorded installed files under `/config/blueprints/...` and normally do not require a restart.
|
||||
|
||||
---
|
||||
|
||||
@@ -273,7 +285,7 @@ Opening a repository detail view can force immediate enrichment for that reposit
|
||||
Restart usually matters only for integration changes.
|
||||
|
||||
- Integration install/update/uninstall/restore: restart expected
|
||||
- Blueprint install/uninstall: restart usually not needed
|
||||
- Blueprint install/update/uninstall/restore: restart usually not needed
|
||||
|
||||
BCS uses a Home Assistant repair flow to surface restart requirements for integration changes.
|
||||
|
||||
|
||||
@@ -1335,6 +1335,11 @@ class BCSCore:
|
||||
|
||||
await self.hass.async_add_executor_job(_mkdir)
|
||||
|
||||
@staticmethod
|
||||
def _backup_repo_key(repo_id: str) -> str:
|
||||
safe = "".join(ch if ch.isalnum() or ch in ("-", "_") else "_" for ch in str(repo_id or "").strip())
|
||||
return safe or "repo"
|
||||
|
||||
def _build_backup_meta(self, repo_id: str, domain: str) -> dict[str, object]:
|
||||
"""Build metadata for backup folders so restores can recover the stored version."""
|
||||
inst = self.get_installed(repo_id) or {}
|
||||
@@ -1391,6 +1396,68 @@ class BCSCore:
|
||||
_LOGGER.info("BCS backup created: domain=%s path=%s", domain, backup_path)
|
||||
return backup_path
|
||||
|
||||
async def _backup_paths(
|
||||
self,
|
||||
repo_id: str,
|
||||
relative_paths: list[str],
|
||||
*,
|
||||
meta: dict[str, object] | None = None,
|
||||
) -> Path | None:
|
||||
"""Backup arbitrary files under the Home Assistant config root."""
|
||||
|
||||
clean_paths = [str(p).strip().replace("\\", "/") for p in (relative_paths or []) if str(p).strip()]
|
||||
if not clean_paths:
|
||||
return None
|
||||
|
||||
cfg_root = Path(self.hass.config.path(""))
|
||||
existing: list[str] = []
|
||||
for rel in clean_paths:
|
||||
target = cfg_root / rel
|
||||
if target.exists():
|
||||
existing.append(rel)
|
||||
if not existing:
|
||||
return None
|
||||
|
||||
await self._ensure_backup_root()
|
||||
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
repo_root = self._backup_root / "_content" / self._backup_repo_key(repo_id)
|
||||
backup_path = repo_root / ts
|
||||
|
||||
def _do_backup() -> None:
|
||||
repo_root.mkdir(parents=True, exist_ok=True)
|
||||
if backup_path.exists():
|
||||
shutil.rmtree(backup_path, ignore_errors=True)
|
||||
backup_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for rel in existing:
|
||||
src = cfg_root / rel
|
||||
dest = backup_path / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if src.is_file():
|
||||
shutil.copy2(src, dest)
|
||||
elif src.is_dir():
|
||||
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||
|
||||
if meta:
|
||||
try:
|
||||
meta_path = backup_path / BACKUP_META_FILENAME
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
backups = [p for p in repo_root.iterdir() if p.is_dir()]
|
||||
backups.sort(key=lambda p: p.name, reverse=True)
|
||||
for old in backups[self._backup_keep_per_domain :]:
|
||||
shutil.rmtree(old, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await self.hass.async_add_executor_job(_do_backup)
|
||||
_LOGGER.info("BCS content backup created: repo_id=%s path=%s files=%s", repo_id, backup_path, len(existing))
|
||||
return backup_path
|
||||
|
||||
async def _restore_domain_from_backup(self, domain: str, backup_path: Path) -> None:
|
||||
"""Restore a domain folder from a backup."""
|
||||
dest_root = Path(self.hass.config.path("custom_components"))
|
||||
@@ -1428,6 +1495,37 @@ class BCSCore:
|
||||
await self.hass.async_add_executor_job(_restore)
|
||||
_LOGGER.info("BCS rollback applied: domain=%s from=%s", domain, backup_path)
|
||||
|
||||
async def _restore_paths_from_backup(self, backup_path: Path, *, remove_targets: list[str] | None = None) -> None:
|
||||
"""Restore arbitrary backed up files under the Home Assistant config root."""
|
||||
|
||||
cfg_root = Path(self.hass.config.path(""))
|
||||
remove_list = [str(p).strip().replace("\\", "/") for p in (remove_targets or []) if str(p).strip()]
|
||||
|
||||
def _restore() -> None:
|
||||
if not backup_path.exists() or not backup_path.is_dir():
|
||||
return
|
||||
|
||||
for rel in remove_list:
|
||||
target = cfg_root / rel
|
||||
if target.exists():
|
||||
if target.is_dir():
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
elif target.is_file():
|
||||
target.unlink(missing_ok=True)
|
||||
|
||||
for src in backup_path.rglob("*"):
|
||||
if not src.is_file():
|
||||
continue
|
||||
if src.name == BACKUP_META_FILENAME:
|
||||
continue
|
||||
rel = src.relative_to(backup_path)
|
||||
dest = cfg_root / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
|
||||
await self.hass.async_add_executor_job(_restore)
|
||||
_LOGGER.info("BCS content rollback applied: from=%s", backup_path)
|
||||
|
||||
async def list_repo_backups(self, repo_id: str) -> list[dict[str, Any]]:
|
||||
"""List available backup sets for an installed repository.
|
||||
|
||||
@@ -1438,6 +1536,30 @@ class BCSCore:
|
||||
domains of the repository.
|
||||
"""
|
||||
inst = self.get_installed(repo_id) or {}
|
||||
install_type = str(inst.get("install_type") or "integration").strip() or "integration"
|
||||
if install_type == "blueprint":
|
||||
repo_root = self._backup_root / "_content" / self._backup_repo_key(repo_id)
|
||||
|
||||
def _list_content() -> list[str]:
|
||||
if not repo_root.exists() or not repo_root.is_dir():
|
||||
return []
|
||||
ids = [p.name for p in repo_root.iterdir() if p.is_dir()]
|
||||
ids.sort(reverse=True)
|
||||
return ids
|
||||
|
||||
ids = await self.hass.async_add_executor_job(_list_content)
|
||||
items: list[dict[str, Any]] = []
|
||||
for bid in ids[: self._backup_keep_per_domain]:
|
||||
label = self._format_backup_id(bid)
|
||||
meta = await self._read_content_backup_meta(repo_id, bid)
|
||||
ver = None
|
||||
if isinstance(meta, dict):
|
||||
ver = meta.get("installed_version") or meta.get("ref")
|
||||
if ver:
|
||||
label = f"{label} ({ver})"
|
||||
items.append({"id": bid, "label": label, "complete": True, "domains": [], "installed_version": str(ver) if ver else None})
|
||||
return items
|
||||
|
||||
domains = inst.get("domains") or []
|
||||
if not isinstance(domains, list) or not domains:
|
||||
return []
|
||||
@@ -1489,6 +1611,53 @@ class BCSCore:
|
||||
if not inst:
|
||||
raise BCSInstallError("Repository is not installed")
|
||||
|
||||
install_type = str(getattr(inst, "install_type", "integration") or "integration").strip() or "integration"
|
||||
if install_type == "blueprint":
|
||||
backup_path = self._backup_root / "_content" / self._backup_repo_key(repo_id) / backup_id
|
||||
if not backup_path.exists() or not backup_path.is_dir():
|
||||
raise BCSInstallError("Selected backup is not available")
|
||||
|
||||
installed_paths = [str(p).strip() for p in (getattr(inst, "installed_paths", None) or []) if str(p).strip()]
|
||||
|
||||
async with self._install_lock:
|
||||
_LOGGER.info("BCS restore started: repo_id=%s backup_id=%s type=blueprint", repo_id, backup_id)
|
||||
|
||||
try:
|
||||
await self._backup_paths(repo_id, installed_paths)
|
||||
except Exception:
|
||||
_LOGGER.debug("BCS pre-restore content backup failed for repo_id=%s", repo_id, exc_info=True)
|
||||
|
||||
await self._restore_paths_from_backup(backup_path, remove_targets=installed_paths)
|
||||
|
||||
restored_meta = await self._read_content_backup_meta(repo_id, backup_id)
|
||||
restored_version: str | None = None
|
||||
if isinstance(restored_meta, dict):
|
||||
rv = restored_meta.get("installed_version") or restored_meta.get("ref")
|
||||
if rv is not None and str(rv).strip():
|
||||
restored_version = str(rv).strip()
|
||||
if not restored_version:
|
||||
restored_version = f"restored:{backup_id}"
|
||||
|
||||
repo = self.get_repo(repo_id)
|
||||
repo_url = getattr(repo, "url", None) or ""
|
||||
|
||||
await self.storage.set_installed_repo(
|
||||
repo_id=repo_id,
|
||||
url=repo_url,
|
||||
domains=[],
|
||||
installed_version=restored_version,
|
||||
installed_manifest_version=None,
|
||||
ref=restored_version,
|
||||
install_type="blueprint",
|
||||
installed_paths=installed_paths,
|
||||
)
|
||||
|
||||
await self._refresh_installed_cache()
|
||||
self.signal_updated()
|
||||
|
||||
_LOGGER.info("BCS restore complete: repo_id=%s backup_id=%s type=blueprint", repo_id, backup_id)
|
||||
return {"ok": True, "repo_id": repo_id, "backup_id": backup_id, "domains": [], "installed_paths": installed_paths, "restored_version": restored_version, "restart_required": False}
|
||||
|
||||
domains = inst.get("domains") or []
|
||||
if not isinstance(domains, list) or not domains:
|
||||
raise BCSInstallError("No installed domains found")
|
||||
@@ -1585,6 +1754,17 @@ class BCSCore:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def _read_content_backup_meta(self, repo_id: str, backup_id: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
p = self._backup_root / "_content" / self._backup_repo_key(repo_id) / backup_id / BACKUP_META_FILENAME
|
||||
if not p.exists():
|
||||
return None
|
||||
txt = await self.hass.async_add_executor_job(p.read_text, 'utf-8')
|
||||
data = json.loads(txt)
|
||||
return data if isinstance(data, dict) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def _read_backup_manifest_version(self, domain: str, backup_id: str) -> str | None:
|
||||
"""Best-effort: read manifest.json version from a legacy backup (no metadata)."""
|
||||
@@ -1806,6 +1986,7 @@ class BCSCore:
|
||||
installed_domains: list[str] = []
|
||||
installed_paths: list[str] = []
|
||||
backups: dict[str, Path] = {}
|
||||
content_backup: Path | None = None
|
||||
install_type = self._repo_install_type(repo)
|
||||
|
||||
inst_before = self.get_installed(repo_id) or {}
|
||||
@@ -1832,8 +2013,22 @@ class BCSCore:
|
||||
if not blueprints_root:
|
||||
raise BCSInstallError("blueprints folder not found in repository ZIP")
|
||||
|
||||
config_root = Path(self.hass.config.path(""))
|
||||
target_root = Path(self.hass.config.path("blueprints"))
|
||||
planned_paths: list[str] = []
|
||||
|
||||
for src in blueprints_root.rglob("*"):
|
||||
if not src.is_file():
|
||||
continue
|
||||
rel = src.relative_to(blueprints_root)
|
||||
planned_paths.append(str(Path("blueprints") / rel).replace("\\", "/"))
|
||||
|
||||
if not planned_paths:
|
||||
raise BCSInstallError("No blueprint files found under blueprints/")
|
||||
|
||||
m = dict(backup_meta)
|
||||
m["install_type"] = "blueprint"
|
||||
m["installed_paths"] = planned_paths
|
||||
content_backup = await self._backup_paths(repo_id, planned_paths, meta=m)
|
||||
|
||||
def _copy_blueprints() -> list[str]:
|
||||
copied: list[str] = []
|
||||
@@ -1849,8 +2044,6 @@ class BCSCore:
|
||||
return copied
|
||||
|
||||
installed_paths = await self.hass.async_add_executor_job(_copy_blueprints)
|
||||
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:
|
||||
@@ -1934,6 +2127,12 @@ class BCSCore:
|
||||
except Exception:
|
||||
_LOGGER.debug("BCS rollback failed for domain=%s", domain, exc_info=True)
|
||||
|
||||
if content_backup is not None:
|
||||
try:
|
||||
await self._restore_paths_from_backup(content_backup, remove_targets=installed_paths)
|
||||
except Exception:
|
||||
_LOGGER.debug("BCS content rollback failed for repo_id=%s", repo_id, exc_info=True)
|
||||
|
||||
# Remove newly created domains if the install did not complete.
|
||||
for domain in created_new:
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "bahmcloud_store",
|
||||
"name": "Bahmcloud Store",
|
||||
"version": "0.7.5",
|
||||
"version": "0.7.6",
|
||||
"documentation": "https://git.bahmcloud.de/bahmcloud/bahmcloud_store",
|
||||
"config_flow": true,
|
||||
"platforms": ["update"],
|
||||
|
||||
@@ -206,7 +206,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._error = this._safeText(resp?.message) || "Install failed.";
|
||||
} else {
|
||||
this._restartRequired = !!resp.restart_required;
|
||||
this._lastActionMsg = "Installation finished. Restart required.";
|
||||
this._lastActionMsg = resp?.restart_required
|
||||
? "Installation finished. Restart required."
|
||||
: "Installation finished. No restart required.";
|
||||
}
|
||||
} catch (e) {
|
||||
this._error = e?.message ? String(e.message) : String(e);
|
||||
@@ -238,7 +240,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._error = this._safeText(resp?.message) || "Update failed.";
|
||||
} else {
|
||||
this._restartRequired = !!resp.restart_required;
|
||||
this._lastActionMsg = "Update finished. Restart required.";
|
||||
this._lastActionMsg = resp?.restart_required
|
||||
? "Update finished. Restart required."
|
||||
: "Update finished. No restart required.";
|
||||
}
|
||||
} catch (e) {
|
||||
this._error = e?.message ? String(e.message) : String(e);
|
||||
@@ -253,7 +257,11 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
if (!repoId) return;
|
||||
if (this._installingRepoId || this._updatingRepoId || this._uninstallingRepoId) return;
|
||||
|
||||
const ok = window.confirm("Really uninstall this repository? This will remove its files from /config/custom_components and requires a restart.");
|
||||
const details = this._getInstallTypeDetails(repoId);
|
||||
const uninstallText = details.restartRequired
|
||||
? `Really uninstall this repository? This will remove its files from ${details.targetPath} and requires a restart.`
|
||||
: `Really uninstall this repository? This will remove its files from ${details.targetPath}. No restart is required.`;
|
||||
const ok = window.confirm(uninstallText);
|
||||
if (!ok) return;
|
||||
|
||||
this._uninstallingRepoId = repoId;
|
||||
@@ -267,7 +275,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._error = this._safeText(resp?.message) || "Uninstall failed.";
|
||||
} else {
|
||||
this._restartRequired = !!resp.restart_required;
|
||||
this._lastActionMsg = "Uninstall finished. Restart required.";
|
||||
this._lastActionMsg = resp?.restart_required
|
||||
? "Uninstall finished. Restart required."
|
||||
: "Uninstall finished. No restart required.";
|
||||
}
|
||||
} catch (e) {
|
||||
this._error = e?.message ? String(e.message) : String(e);
|
||||
@@ -337,7 +347,11 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = window.confirm("Restore selected backup? This will overwrite the installed files under /config/custom_components and requires a restart.");
|
||||
const details = this._getInstallTypeDetails(this._restoreRepoId);
|
||||
const restoreText = details.restartRequired
|
||||
? `Restore selected backup? This will overwrite the installed files under ${details.targetPath} and requires a restart.`
|
||||
: `Restore selected backup? This will overwrite the installed files under ${details.targetPath}. No restart is required.`;
|
||||
const ok = window.confirm(restoreText);
|
||||
if (!ok) return;
|
||||
|
||||
this._restoring = true;
|
||||
@@ -350,7 +364,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
this._restoreError = this._safeText(resp?.message) || "Restore failed.";
|
||||
} else {
|
||||
this._restartRequired = !!resp.restart_required;
|
||||
this._lastActionMsg = "Restore finished. Restart required.";
|
||||
this._lastActionMsg = resp?.restart_required
|
||||
? "Restore finished. Restart required."
|
||||
: "Restore finished. No restart required.";
|
||||
this._closeRestore();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -981,6 +997,27 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
return !!id && Array.isArray(this._favoriteRepoIds) && this._favoriteRepoIds.includes(id);
|
||||
}
|
||||
|
||||
_getRepoById(repoId) {
|
||||
const id = this._safeId(repoId);
|
||||
if (!id || !Array.isArray(this._data?.repos)) return null;
|
||||
return this._data.repos.find((r) => this._safeId(r?.id) === id) || null;
|
||||
}
|
||||
|
||||
_getInstallTypeDetails(repoOrId) {
|
||||
const repo = typeof repoOrId === "string" ? this._getRepoById(repoOrId) : repoOrId;
|
||||
const installType = this._safeText(repo?.install_type) || "integration";
|
||||
if (installType === "blueprint") {
|
||||
return { installType, targetPath: "/config/blueprints", restartRequired: false };
|
||||
}
|
||||
if (installType === "template") {
|
||||
return { installType, targetPath: "/config", restartRequired: false };
|
||||
}
|
||||
if (installType === "lovelace") {
|
||||
return { installType, targetPath: "/config/www", restartRequired: false };
|
||||
}
|
||||
return { installType: "integration", targetPath: "/config/custom_components", restartRequired: true };
|
||||
}
|
||||
|
||||
async _toggleFavorite(repoId) {
|
||||
if (!this._hass || !repoId) return;
|
||||
|
||||
@@ -1379,9 +1416,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
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>`;
|
||||
const restoreBtn = installType === "integration"
|
||||
? `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`
|
||||
: ``;
|
||||
const restoreBtn = `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`;
|
||||
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
|
||||
|
||||
const restartHint = this._restartRequired
|
||||
@@ -1628,12 +1663,16 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
})
|
||||
.join("");
|
||||
|
||||
const restoreDetails = this._getInstallTypeDetails(this._restoreRepoId);
|
||||
const actionHint = restoreDetails.restartRequired
|
||||
? `Select a backup to restore. This will overwrite files under ${restoreDetails.targetPath} and requires a restart.`
|
||||
: `Select a backup to restore. This will overwrite files under ${restoreDetails.targetPath}. No restart is required.`;
|
||||
const msg = this._restoreLoading
|
||||
? "Loading backups…"
|
||||
: this._restoreError
|
||||
? this._safeText(this._restoreError)
|
||||
: opts.length
|
||||
? "Select a backup to restore. This will overwrite files under /config/custom_components and requires a restart."
|
||||
? actionHint
|
||||
: "No backups found.";
|
||||
|
||||
return `
|
||||
|
||||
Reference in New Issue
Block a user