From 3f14dc3bd9541b69bbeb2ff68a00155c04bddf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Sat, 17 Jan 2026 08:16:54 +0000 Subject: [PATCH] Buttons add 8n panel --- .../bahmcloud_store/panel/panel.js | 230 ++++++++++++------ 1 file changed, 161 insertions(+), 69 deletions(-) diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js index 50673c7..215d739 100644 --- a/custom_components/bahmcloud_store/panel/panel.js +++ b/custom_components/bahmcloud_store/panel/panel.js @@ -25,13 +25,11 @@ class BahmcloudStorePanel extends HTMLElement { this._readmeHtml = null; this._readmeError = null; - // Manual refresh UX state this._refreshing = false; - this._status = ""; - // Install/Update UX this._installingRepoId = null; this._updatingRepoId = null; + this._uninstallingRepoId = null; this._restartRequired = false; this._lastActionMsg = null; } @@ -56,7 +54,6 @@ class BahmcloudStorePanel extends HTMLElement { const data = await this._hass.callApi("get", "bcs"); this._data = data; - // keep detail fresh if (this._view === "detail" && this._detailRepoId && Array.isArray(data?.repos)) { const fresh = data.repos.find((r) => this._safeId(r?.id) === this._detailRepoId); if (fresh) this._detailRepo = fresh; @@ -75,19 +72,16 @@ class BahmcloudStorePanel extends HTMLElement { this._refreshing = true; this._error = null; - this._status = "Refreshing…"; + this._loading = true; this._update(); try { const resp = await this._hass.callApi("post", "bcs?action=refresh", {}); if (!resp?.ok) { - this._status = ""; - this._error = this._safeText(resp?.message) || "Refresh failed."; - } else { - this._status = "Refresh done."; + const msg = this._safeText(resp?.message) || "Refresh failed."; + this._error = msg; } } catch (e) { - this._status = ""; this._error = e?.message ? String(e.message) : String(e); } finally { this._refreshing = false; @@ -148,6 +142,35 @@ class BahmcloudStorePanel extends HTMLElement { } } + async _uninstallRepo(repoId) { + if (!this._hass) return; + 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."); + if (!ok) return; + + this._uninstallingRepoId = repoId; + this._error = null; + this._lastActionMsg = null; + this._update(); + + try { + const resp = await this._hass.callApi("post", `bcs/uninstall?repo_id=${encodeURIComponent(repoId)}`, {}); + if (!resp?.ok) { + this._error = this._safeText(resp?.message) || "Uninstall failed."; + } else { + this._restartRequired = !!resp.restart_required; + this._lastActionMsg = "Uninstall finished. Restart required."; + } + } catch (e) { + this._error = e?.message ? String(e.message) : String(e); + } finally { + this._uninstallingRepoId = null; + await this._load(); + } + } + async _restartHA() { if (!this._hass) return; try { @@ -178,7 +201,6 @@ class BahmcloudStorePanel extends HTMLElement { this._readmeText = null; this._readmeHtml = null; this._readmeError = null; - this._status = ""; this._update(); return; } @@ -244,10 +266,6 @@ class BahmcloudStorePanel extends HTMLElement { this._readmeHtml = null; this._readmeError = null; - this._status = ""; - this._restartRequired = false; - this._lastActionMsg = null; - this._update(); this._loadReadme(repoId); } @@ -333,7 +351,6 @@ class BahmcloudStorePanel extends HTMLElement { .row{ display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } .muted{ color: var(--secondary-text-color); } .small{ font-size: 12px; } - .badge{ padding:6px 10px; border-radius:999px; @@ -457,15 +474,15 @@ class BahmcloudStorePanel extends HTMLElement {
- +
Store
-
Manage repositories
-
Settings / About
+
Manage
+
About
@@ -478,9 +495,18 @@ class BahmcloudStorePanel extends HTMLElement { root.getElementById("backBtn").addEventListener("click", () => this._goBack()); root.getElementById("refreshBtn").addEventListener("click", () => this._refreshAll()); - root.getElementById("tabStore").addEventListener("click", () => { this._view = "store"; this._update(); }); - root.getElementById("tabManage").addEventListener("click", () => { this._view = "manage"; this._update(); }); - root.getElementById("tabAbout").addEventListener("click", () => { this._view = "about"; this._update(); }); + root.getElementById("tabStore").addEventListener("click", () => { + this._view = "store"; + this._update(); + }); + root.getElementById("tabManage").addEventListener("click", () => { + this._view = "manage"; + this._update(); + }); + root.getElementById("tabAbout").addEventListener("click", () => { + this._view = "about"; + this._update(); + }); this._update(); } @@ -505,17 +531,18 @@ class BahmcloudStorePanel extends HTMLElement { const fabs = root.getElementById("fabs"); if (!content || !fabs) return; - const err = this._error ? `
Error: ${this._esc(this._error)}
` : ""; - const status = this._status ? `
${this._esc(this._status)}
` : ""; + const err = this._error + ? `
Error: ${this._esc(this._error)}
` + : ""; if (this._loading) { - content.innerHTML = `${err}${status}
Loading…
`; + content.innerHTML = `${err}
Loading…
`; fabs.innerHTML = ""; return; } if (!this._data?.ok) { - content.innerHTML = `${err}${status}
No data. Please refresh.
`; + content.innerHTML = `${err}
No data. Please refresh.
`; fabs.innerHTML = ""; return; } @@ -526,13 +553,13 @@ class BahmcloudStorePanel extends HTMLElement { else if (this._view === "about") html = this._renderAbout(); else if (this._view === "detail") html = this._renderDetail(); - content.innerHTML = `${err}${status}${html}`; + content.innerHTML = `${err}${html}`; fabs.innerHTML = this._view === "detail" ? this._renderFabs() : ""; if (this._view === "store") this._wireStore(); if (this._view === "manage") this._wireManage(); if (this._view === "detail") { - this._wireDetail(); + this._wireDetail(); // now always wires buttons this._wireFabs(); } } @@ -556,19 +583,12 @@ class BahmcloudStorePanel extends HTMLElement { } _asBoolStrict(v) { - // IMPORTANT: only treat literal true as installed return v === true; } _renderStore() { const repos = Array.isArray(this._data.repos) ? this._data.repos : []; - const categories = Array.from( - new Set(repos.map((r) => this._safeText(r?.category)).filter((c) => !!c)) - ).sort(); - - const providers = ["github", "gitlab", "gitea", "other"]; - const filtered = repos .filter((r) => { const name = (this._safeText(r?.name) || "").toLowerCase(); @@ -594,10 +614,17 @@ class BahmcloudStorePanel extends HTMLElement { return an.localeCompare(bn); }); + const categories = Array.from( + new Set(repos.map((r) => this._safeText(r?.category)).filter((c) => !!c)) + ).sort(); + + const providers = ["github", "gitlab", "gitea", "other"]; + const cards = filtered .map((r) => { const id = this._safeId(r?.id); const name = this._safeText(r?.name) || "Unnamed repository"; + const url = this._safeText(r?.url) || ""; const desc = this._safeText(r?.description) || ""; const latest = this._safeText(r?.latest_version); @@ -620,9 +647,7 @@ class BahmcloudStorePanel extends HTMLElement {
${this._esc(name)}
${this._esc(desc)}
-
- Creator: ${this._esc(this._safeText(r?.owner || "-"))} · Latest: ${this._esc(latest || "-")} · Meta: ${this._esc(this._safeText(r?.meta_source || "-"))} -
+
${this._esc(url)}
${badgeHtml}
@@ -633,7 +658,7 @@ class BahmcloudStorePanel extends HTMLElement { return `
- +
-
BCS ${this._esc(this._data.version || "-")} · Repositories: ${repos.length}
+
Version: ${this._esc(this._data.version || "-")} · Repositories: ${repos.length}
${cards || `
No repositories found.
`} @@ -660,9 +685,24 @@ class BahmcloudStorePanel extends HTMLElement { const cat = root.getElementById("cat"); const prov = root.getElementById("prov"); - if (q) q.addEventListener("input", (e) => { this._search = e?.target?.value || ""; this._update(); }); - if (cat) cat.addEventListener("change", (e) => { this._category = e?.target?.value || "all"; this._update(); }); - if (prov) prov.addEventListener("change", (e) => { this._provider = e?.target?.value || "all"; this._update(); }); + if (q) { + q.addEventListener("input", (e) => { + this._search = e?.target?.value || ""; + this._update(); + }); + } + if (cat) { + cat.addEventListener("change", (e) => { + this._category = e?.target?.value || "all"; + this._update(); + }); + } + if (prov) { + prov.addEventListener("change", (e) => { + this._provider = e?.target?.value || "all"; + this._update(); + }); + } root.querySelectorAll("[data-open]").forEach((el) => { const id = el.getAttribute("data-open"); @@ -673,9 +713,9 @@ class BahmcloudStorePanel extends HTMLElement { _renderAbout() { return `
-
Installation & Updates
+
About
- Installation and updates are now available via the Store UI. + Bahmcloud Store is a provider-neutral repository index and UI for Home Assistant.
Current integration version: ${this._esc(this._data?.version || "-")} @@ -692,18 +732,15 @@ class BahmcloudStorePanel extends HTMLElement { const url = this._safeText(r?.url) || ""; const desc = this._safeText(r?.description) || ""; - const repoId = this._safeId(r?.id); - - const installed = this._asBoolStrict(r?.installed); - const installedVersion = this._safeText(r?.installed_version); - const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : []; - const latestVersion = this._safeText(r?.latest_version); - - const busyInstall = this._installingRepoId === repoId; - const busyUpdate = this._updatingRepoId === repoId; - const busy = busyInstall || busyUpdate; - - const updateAvailable = installed && !!latestVersion && (!installedVersion || latestVersion !== installedVersion); + const infoBits = [ + this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -", + 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?.category) ? `Category: ${this._safeText(r?.category)}` : 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_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null, + ].filter(Boolean); const readmeBlock = this._readmeLoading ? `
Loading README…
` @@ -732,8 +769,23 @@ class BahmcloudStorePanel extends HTMLElement {
`; + const repoId = this._safeId(r?.id); + + const installed = this._asBoolStrict(r?.installed); + const installedVersion = this._safeText(r?.installed_version); + const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : []; + const latestVersion = this._safeText(r?.latest_version); + + const busyInstall = this._installingRepoId === repoId; + const busyUpdate = this._updatingRepoId === repoId; + const busyUninstall = this._uninstallingRepoId === repoId; + const busy = busyInstall || busyUpdate || busyUninstall; + + const updateAvailable = installed && !!latestVersion && (!installedVersion || latestVersion !== installedVersion); + const installBtn = ``; const updateBtn = ``; + const uninstallBtn = ``; const restartHint = this._restartRequired ? ` @@ -757,12 +809,7 @@ class BahmcloudStorePanel extends HTMLElement {
${this._esc(name)}
${this._esc(desc)}
-
- Creator: ${this._esc(this._safeText(r?.owner || "-"))} - · Latest: ${this._esc(latestVersion || "-")} - · Provider: ${this._esc(this._safeText(r?.provider || "-"))} - · Meta: ${this._esc(this._safeText(r?.meta_source || "-"))} -
+
${this._esc(infoBits.join(" · "))}
@@ -790,6 +837,7 @@ class BahmcloudStorePanel extends HTMLElement {
${installBtn} ${updateBtn} + ${uninstallBtn}
${restartHint} @@ -802,13 +850,36 @@ class BahmcloudStorePanel extends HTMLElement { _wireDetail() { const root = this.shadowRoot; + // Always wire action buttons (even if README is already loaded) const btnInstall = root.getElementById("btnInstall"); const btnUpdate = root.getElementById("btnUpdate"); + const btnUninstall = root.getElementById("btnUninstall"); const btnRestart = root.getElementById("btnRestart"); - if (btnInstall) btnInstall.addEventListener("click", () => { if (!btnInstall.disabled && this._detailRepoId) this._installRepo(this._detailRepoId); }); - if (btnUpdate) btnUpdate.addEventListener("click", () => { if (!btnUpdate.disabled && this._detailRepoId) this._updateRepo(this._detailRepoId); }); - if (btnRestart) btnRestart.addEventListener("click", () => this._restartHA()); + if (btnInstall) { + btnInstall.addEventListener("click", () => { + if (btnInstall.disabled) return; + if (this._detailRepoId) this._installRepo(this._detailRepoId); + }); + } + + if (btnUpdate) { + btnUpdate.addEventListener("click", () => { + if (btnUpdate.disabled) return; + if (this._detailRepoId) this._updateRepo(this._detailRepoId); + }); + } + + if (btnUninstall) { + btnUninstall.addEventListener("click", () => { + if (btnUninstall.disabled) return; + if (this._detailRepoId) this._uninstallRepo(this._detailRepoId); + }); + } + + if (btnRestart) { + btnRestart.addEventListener("click", () => this._restartHA()); + } const mount = root.getElementById("readmePretty"); if (!mount) return; @@ -845,11 +916,12 @@ class BahmcloudStorePanel extends HTMLElement { const latest = this._safeText(r?.latest_version); const installedVersion = this._safeText(r?.installed_version); - const busy = this._installingRepoId === repoId || this._updatingRepoId === repoId; + const busy = this._installingRepoId === repoId || this._updatingRepoId === repoId || this._uninstallingRepoId === repoId; const updateAvailable = installed && !!latest && (!installedVersion || latest !== installedVersion); const installDisabled = installed || busy; const updateDisabled = !updateAvailable || busy; + const uninstallDisabled = !installed || busy; return `
@@ -857,6 +929,7 @@ class BahmcloudStorePanel extends HTMLElement { +
`; @@ -874,13 +947,32 @@ class BahmcloudStorePanel extends HTMLElement { const reload = root.getElementById("fabReload"); const install = root.getElementById("fabInstall"); const update = root.getElementById("fabUpdate"); + const uninstall = root.getElementById("fabUninstall"); const info = root.getElementById("fabInfo"); if (open) open.addEventListener("click", () => url && window.open(url, "_blank", "noreferrer")); if (reload) reload.addEventListener("click", () => this._detailRepoId && this._loadReadme(this._detailRepoId)); - if (install) install.addEventListener("click", () => { if (!install.disabled) this._installRepo(repoId); }); - if (update) update.addEventListener("click", () => { if (!update.disabled) this._updateRepo(repoId); }); + if (install) { + install.addEventListener("click", () => { + if (install.disabled) return; + this._installRepo(repoId); + }); + } + + if (update) { + update.addEventListener("click", () => { + if (update.disabled) return; + this._updateRepo(repoId); + }); + } + + if (uninstall) { + uninstall.addEventListener("click", () => { + if (uninstall.disabled) return; + this._uninstallRepo(repoId); + }); + } if (info) info.addEventListener("click", () => { this._view = "about"; this._update(); }); }