From e46cd6e4889c5bc08bca5b76cfe8b56c328afc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Sun, 18 Jan 2026 08:34:44 +0000 Subject: [PATCH] 0.6.0 --- .../bahmcloud_store/panel/panel.js | 211 +++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js index 092ef3f..17dd9da 100644 --- a/custom_components/bahmcloud_store/panel/panel.js +++ b/custom_components/bahmcloud_store/panel/panel.js @@ -36,6 +36,15 @@ class BahmcloudStorePanel extends HTMLElement { this._uninstallingRepoId = null; this._restartRequired = false; this._lastActionMsg = null; + + // Phase F2.1: restore from backups + this._restoreOpen = false; + this._restoreRepoId = null; + this._restoreLoading = false; + this._restoreOptions = []; + this._restoreSelected = ""; + this._restoring = false; + this._restoreError = null; } set hass(hass) { @@ -175,6 +184,90 @@ class BahmcloudStorePanel extends HTMLElement { } } + + + async _openRestore(repoId) { + if (!this._hass) return; + if (!repoId) return; + if (this._installingRepoId || this._updatingRepoId || this._uninstallingRepoId) return; + + this._restoreRepoId = repoId; + this._restoreOpen = true; + this._restoreLoading = true; + this._restoreError = null; + this._restoreOptions = []; + this._restoreSelected = ""; + this._update(); + + try { + const resp = await this._hass.callApi("get", `bcs/backups?repo_id=${encodeURIComponent(repoId)}`); + const list = Array.isArray(resp?.backups) ? resp.backups : []; + this._restoreOptions = list; + const firstComplete = list.find((x) => x && x.complete); + const firstAny = list[0]; + const pick = firstComplete || firstAny; + if (pick && pick.id) this._restoreSelected = String(pick.id); + + if (!list.length) { + this._restoreError = "No backups found for this repository."; + } + } catch (e) { + this._restoreError = e?.message ? String(e.message) : String(e); + } finally { + this._restoreLoading = false; + this._update(); + } + } + + _closeRestore() { + this._restoreOpen = false; + this._restoreRepoId = null; + this._restoreLoading = false; + this._restoreError = null; + this._restoreOptions = []; + this._restoreSelected = ""; + this._restoring = false; + this._update(); + } + + async _restoreSelectedBackup() { + if (!this._hass) return; + if (!this._restoreRepoId) return; + const bid = String(this._restoreSelected || "").trim(); + if (!bid) return; + if (this._restoring) return; + + const chosen = (this._restoreOptions || []).find((x) => String(x?.id) === bid); + if (chosen && chosen.complete === false) { + this._restoreError = "Selected backup is not available for all domains of this repository."; + this._update(); + return; + } + + const ok = window.confirm("Restore selected backup? This will overwrite the installed files under /config/custom_components and requires a restart."); + if (!ok) return; + + this._restoring = true; + this._restoreError = null; + this._update(); + + try { + const resp = await this._hass.callApi("post", `bcs/restore?repo_id=${encodeURIComponent(this._restoreRepoId)}&backup_id=${encodeURIComponent(bid)}`, {}); + if (!resp?.ok) { + this._restoreError = this._safeText(resp?.message) || "Restore failed."; + } else { + this._restartRequired = !!resp.restart_required; + this._lastActionMsg = "Restore finished. Restart required."; + this._closeRestore(); + } + } catch (e) { + this._restoreError = e?.message ? String(e.message) : String(e); + } finally { + this._restoring = false; + await this._load(); + } + } + async _restartHA() { if (!this._hass) return; try { @@ -413,6 +506,22 @@ class BahmcloudStorePanel extends HTMLElement { } button:disabled{ opacity: .55; cursor: not-allowed; } + .modalOverlay{ + position:fixed; inset:0; z-index:999; + background: rgba(0,0,0,0.45); + display:flex; align-items:center; justify-content:center; + padding:16px; + } + .modal{ + width: min(520px, 100%); + background: var(--card-background-color); + border:1px solid var(--divider-color); + border-radius:18px; + padding:16px; + box-shadow: 0 10px 30px rgba(0,0,0,0.25); + } + .modalTitle{ font-size:16px; font-weight:700; } + .err{ margin:12px 0; padding:12px 14px; @@ -617,7 +726,8 @@ class BahmcloudStorePanel extends HTMLElement { else if (this._view === "about") html = this._renderAbout(); else if (this._view === "detail") html = this._renderDetail(); - content.innerHTML = `${err}${html}`; + const modal = this._renderRestoreModal(); + content.innerHTML = `${err}${html}${modal}`; if (this._view === "store") this._wireStore(); if (this._view === "manage") this._wireManage(); @@ -625,6 +735,8 @@ class BahmcloudStorePanel extends HTMLElement { this._wireDetail(); // now always wires buttons } + this._wireRestoreModal(); + // Restore focus and cursor for the search field after re-render. if (restore.id && this._view === "store") { const el = root.getElementById(restore.id); @@ -906,6 +1018,7 @@ class BahmcloudStorePanel extends HTMLElement { const installBtn = ``; const updateBtn = ``; const uninstallBtn = ``; + const restoreBtn = ``; const restartHint = this._restartRequired ? ` @@ -958,6 +1071,7 @@ class BahmcloudStorePanel extends HTMLElement { ${installBtn} ${updateBtn} ${uninstallBtn} + ${restoreBtn} ${restartHint} @@ -974,6 +1088,7 @@ class BahmcloudStorePanel extends HTMLElement { const btnInstall = root.getElementById("btnInstall"); const btnUpdate = root.getElementById("btnUpdate"); const btnUninstall = root.getElementById("btnUninstall"); + const btnRestore = root.getElementById("btnRestore"); const btnRestart = root.getElementById("btnRestart"); const btnReadmeToggle = root.getElementById("btnReadmeToggle"); @@ -998,6 +1113,15 @@ class BahmcloudStorePanel extends HTMLElement { }); } + if (btnRestore) { + btnRestore.addEventListener("click", () => { + if (btnRestore.disabled) return; + if (this._detailRepoId) this._openRestore(this._detailRepoId); + }); + } + + + if (btnRestart) { btnRestart.addEventListener("click", () => this._restartHA()); } @@ -1024,6 +1148,45 @@ class BahmcloudStorePanel extends HTMLElement { } } + _wireRestoreModal() { + const root = this.shadowRoot; + if (!root) return; + + const overlay = root.getElementById("restoreOverlay"); + if (!overlay) return; + + // Click outside modal closes it + overlay.addEventListener("click", (ev) => { + if (ev.target === overlay) this._closeRestore(); + }); + + const sel = root.getElementById("restoreSelect"); + if (sel) { + try { + if (this._restoreSelected) sel.value = String(this._restoreSelected); + } catch (_) {} + sel.addEventListener("change", () => { + this._restoreSelected = String(sel.value || ""); + }); + } + + const btnCancel = root.getElementById("btnRestoreCancel"); + if (btnCancel) { + btnCancel.addEventListener("click", () => this._closeRestore()); + } + + const btnApply = root.getElementById("btnRestoreApply"); + if (btnApply) { + btnApply.addEventListener("click", () => { + if (btnApply.disabled) return; + const v = root.getElementById("restoreSelect")?.value; + if (v) this._restoreSelected = String(v); + this._restoreSelectedBackup(); + }); + } + } + + _postprocessRenderedMarkdown(container) { if (!container) return; try { @@ -1036,6 +1199,52 @@ class BahmcloudStorePanel extends HTMLElement { } + _renderRestoreModal() { + if (!this._restoreOpen) return ""; + + const opts = Array.isArray(this._restoreOptions) ? this._restoreOptions : []; + const disabled = this._restoreLoading || this._restoring || !opts.length; + + const optionsHtml = opts + .map((o) => { + const id = this._safeText(o?.id) || ""; + const label = this._safeText(o?.label) || id; + return ``; + }) + .join(""); + + 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." + : "No backups found."; + + return ` +
+ +
+ `; + } + + + _renderManage() { const repos = Array.isArray(this._data.repos) ? this._data.repos : []; const custom = repos.filter((r) => r?.source === "custom");