This commit is contained in:
2026-01-15 20:25:34 +00:00
parent 296c816633
commit c20bd4dd07

View File

@@ -53,7 +53,6 @@ class BahmcloudStorePanel extends HTMLElement {
const data = await this._hass.callApi("get", "bcs"); const data = await this._hass.callApi("get", "bcs");
this._data = data; this._data = data;
// keep detail repo fresh (installed state, versions, etc.)
if (this._view === "detail" && this._detailRepoId && Array.isArray(data?.repos)) { if (this._view === "detail" && this._detailRepoId && Array.isArray(data?.repos)) {
const fresh = data.repos.find((r) => this._safeId(r?.id) === this._detailRepoId); const fresh = data.repos.find((r) => this._safeId(r?.id) === this._detailRepoId);
if (fresh) this._detailRepo = fresh; if (fresh) this._detailRepo = fresh;
@@ -72,13 +71,10 @@ class BahmcloudStorePanel extends HTMLElement {
this._refreshing = true; this._refreshing = true;
this._error = null; this._error = null;
// Show a loading state immediately
this._loading = true; this._loading = true;
this._update(); this._update();
try { try {
// IMPORTANT: This hits POST /api/bcs?action=refresh
const resp = await this._hass.callApi("post", "bcs?action=refresh", {}); const resp = await this._hass.callApi("post", "bcs?action=refresh", {});
if (!resp?.ok) { if (!resp?.ok) {
const msg = this._safeText(resp?.message) || "Refresh failed."; const msg = this._safeText(resp?.message) || "Refresh failed.";
@@ -90,7 +86,6 @@ class BahmcloudStorePanel extends HTMLElement {
this._refreshing = false; this._refreshing = false;
} }
// Always reload data after refresh attempt (even on failure)
await this._load(); await this._load();
} }
@@ -298,14 +293,9 @@ class BahmcloudStorePanel extends HTMLElement {
} }
.iconbtn:hover{ filter:brightness(0.98); } .iconbtn:hover{ filter:brightness(0.98); }
.wrap{ .wrap{ max-width:1200px; margin:0 auto; padding:16px; }
max-width:1200px; margin:0 auto; padding:16px;
}
.tabs{ .tabs{ display:flex; gap:10px; flex-wrap:wrap; margin:8px 0 16px; }
display:flex; gap:10px; flex-wrap:wrap;
margin:8px 0 16px;
}
.tab{ .tab{
padding:10px 14px; border-radius:999px; padding:10px 14px; border-radius:999px;
border:1px solid var(--divider-color); border:1px solid var(--divider-color);
@@ -314,21 +304,11 @@ class BahmcloudStorePanel extends HTMLElement {
} }
.tab.active{ border-color: var(--bcs-accent); box-shadow: 0 0 0 2px rgba(30,136,229,.15); } .tab.active{ border-color: var(--bcs-accent); box-shadow: 0 0 0 2px rgba(30,136,229,.15); }
.grid{ .grid{ display:grid; gap:12px; grid-template-columns: repeat(1, minmax(0, 1fr)); }
display:grid; gap:12px; @media (min-width: 900px){ .grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); } }
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 900px){
.grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.grid2{ .grid2{ display:grid; gap:12px; grid-template-columns: 1fr; }
display:grid; gap:12px; @media (min-width: 1024px){ .grid2{ grid-template-columns: 1.2fr .8fr; } }
grid-template-columns: 1fr;
}
@media (min-width: 1024px){
.grid2{ grid-template-columns: 1.2fr .8fr; }
}
.card{ .card{
padding:14px 14px; padding:14px 14px;
@@ -377,10 +357,7 @@ class BahmcloudStorePanel extends HTMLElement {
border-color: rgba(30,136,229,.35); border-color: rgba(30,136,229,.35);
background: rgba(30,136,229,.08); background: rgba(30,136,229,.08);
} }
button:disabled{ button:disabled{ opacity: .55; cursor: not-allowed; }
opacity: .55;
cursor: not-allowed;
}
.err{ .err{
margin:12px 0; margin:12px 0;
@@ -399,7 +376,7 @@ class BahmcloudStorePanel extends HTMLElement {
gap:10px; gap:10px;
z-index: 60; z-index: 60;
} }
.fab{ .fabbtn{
width:54px; height:54px; width:54px; height:54px;
border-radius:18px; border-radius:18px;
border:1px solid var(--divider-color); border:1px solid var(--divider-color);
@@ -409,12 +386,16 @@ class BahmcloudStorePanel extends HTMLElement {
box-shadow: 0 8px 18px rgba(0,0,0,.12); box-shadow: 0 8px 18px rgba(0,0,0,.12);
user-select:none; user-select:none;
font-size: 18px; font-size: 18px;
padding: 0;
} }
.fab.primary{ .fabbtn.primary{
border-color: rgba(30,136,229,.35); border-color: rgba(30,136,229,.35);
background: rgba(30,136,229,.10); background: rgba(30,136,229,.10);
} }
.fab[disabled]{ opacity: .55; cursor: not-allowed; } .fabbtn:disabled{
opacity: .55;
cursor: not-allowed;
}
pre.readme{ pre.readme{
padding: 12px; padding: 12px;
@@ -507,7 +488,6 @@ class BahmcloudStorePanel extends HTMLElement {
const subtitle = root.getElementById("subtitle"); const subtitle = root.getElementById("subtitle");
if (subtitle) subtitle.textContent = this._view === "detail" ? "Details" : this._view[0].toUpperCase() + this._view.slice(1); if (subtitle) subtitle.textContent = this._view === "detail" ? "Details" : this._view[0].toUpperCase() + this._view.slice(1);
// tabs
const setActive = (id, on) => { const setActive = (id, on) => {
const el = root.getElementById(id); const el = root.getElementById(id);
if (!el) return; if (!el) return;
@@ -521,7 +501,6 @@ class BahmcloudStorePanel extends HTMLElement {
const fabs = root.getElementById("fabs"); const fabs = root.getElementById("fabs");
if (!content || !fabs) return; if (!content || !fabs) return;
// error block
const err = this._error const err = this._error
? `<div class="err"><strong>Error:</strong> ${this._esc(this._error)}</div>` ? `<div class="err"><strong>Error:</strong> ${this._esc(this._error)}</div>`
: ""; : "";
@@ -538,7 +517,6 @@ class BahmcloudStorePanel extends HTMLElement {
return; return;
} }
// render view
let html = ""; let html = "";
if (this._view === "store") html = this._renderStore(); if (this._view === "store") html = this._renderStore();
else if (this._view === "manage") html = this._renderManage(); else if (this._view === "manage") html = this._renderManage();
@@ -548,11 +526,10 @@ class BahmcloudStorePanel extends HTMLElement {
content.innerHTML = `${err}${html}`; content.innerHTML = `${err}${html}`;
fabs.innerHTML = this._view === "detail" ? this._renderFabs() : ""; fabs.innerHTML = this._view === "detail" ? this._renderFabs() : "";
// wire view interactions
if (this._view === "store") this._wireStore(); if (this._view === "store") this._wireStore();
if (this._view === "manage") this._wireManage(); if (this._view === "manage") this._wireManage();
if (this._view === "detail") { if (this._view === "detail") {
this._wireDetail(); this._wireDetail(); // now always wires buttons
this._wireFabs(); this._wireFabs();
} }
} }
@@ -563,8 +540,7 @@ class BahmcloudStorePanel extends HTMLElement {
} }
_safeId(v) { _safeId(v) {
const s = this._safeText(v).trim(); return this._safeText(v).trim();
return s;
} }
_esc(s) { _esc(s) {
@@ -576,6 +552,10 @@ class BahmcloudStorePanel extends HTMLElement {
.replaceAll("'", "&#039;"); .replaceAll("'", "&#039;");
} }
_asBoolStrict(v) {
return v === true;
}
_renderStore() { _renderStore() {
const repos = Array.isArray(this._data.repos) ? this._data.repos : []; const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
@@ -618,7 +598,7 @@ class BahmcloudStorePanel extends HTMLElement {
const desc = this._safeText(r?.description) || ""; const desc = this._safeText(r?.description) || "";
const latest = this._safeText(r?.latest_version); const latest = this._safeText(r?.latest_version);
const installed = !!r?.installed; const installed = this._asBoolStrict(r?.installed);
const installedVersion = this._safeText(r?.installed_version); const installedVersion = this._safeText(r?.installed_version);
const updateAvailable = installed && !!latest && (!installedVersion || latest !== installedVersion); const updateAvailable = installed && !!latest && (!installedVersion || latest !== installedVersion);
@@ -694,7 +674,6 @@ class BahmcloudStorePanel extends HTMLElement {
}); });
} }
// card clicks
root.querySelectorAll("[data-open]").forEach((el) => { root.querySelectorAll("[data-open]").forEach((el) => {
const id = el.getAttribute("data-open"); const id = el.getAttribute("data-open");
el.addEventListener("click", () => this._openRepoDetail(id)); el.addEventListener("click", () => this._openRepoDetail(id));
@@ -723,12 +702,9 @@ class BahmcloudStorePanel extends HTMLElement {
const url = this._safeText(r?.url) || ""; const url = this._safeText(r?.url) || "";
const desc = this._safeText(r?.description) || ""; const desc = this._safeText(r?.description) || "";
const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: -";
const badge = `<div class="badge">${this._esc(this._safeText(r?.provider || "repo"))}</div>`;
const infoBits = [ const infoBits = [
this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -", this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -",
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?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null, this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
@@ -765,7 +741,7 @@ class BahmcloudStorePanel extends HTMLElement {
const repoId = this._safeId(r?.id); const repoId = this._safeId(r?.id);
const installed = !!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 latestVersion = this._safeText(r?.latest_version); const latestVersion = this._safeText(r?.latest_version);
@@ -806,7 +782,7 @@ class BahmcloudStorePanel extends HTMLElement {
<a href="${this._esc(url)}" target="_blank" rel="noreferrer">Open repository</a> <a href="${this._esc(url)}" target="_blank" rel="noreferrer">Open repository</a>
</div> </div>
</div> </div>
${badge} <div class="badge">${this._esc(this._safeText(r?.provider || "repo"))}</div>
</div> </div>
</div> </div>
@@ -840,6 +816,30 @@ class BahmcloudStorePanel extends HTMLElement {
_wireDetail() { _wireDetail() {
const root = this.shadowRoot; 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 btnRestart = root.getElementById("btnRestart");
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 (btnRestart) {
btnRestart.addEventListener("click", () => this._restartHA());
}
const mount = root.getElementById("readmePretty"); const mount = root.getElementById("readmePretty");
if (!mount) return; if (!mount) return;
@@ -847,36 +847,11 @@ class BahmcloudStorePanel extends HTMLElement {
if (this._readmeHtml) { if (this._readmeHtml) {
mount.innerHTML = this._readmeHtml; mount.innerHTML = this._readmeHtml;
this._postprocessRenderedMarkdown(mount); this._postprocessRenderedMarkdown(mount);
return; } else {
mount.innerHTML = `<div class="muted">Rendered HTML not available. Use “Show raw Markdown”.</div>`;
} }
mount.innerHTML = `<div class="muted">Rendered HTML not available. Use “Show raw Markdown”.</div>`; } else {
return; mount.innerHTML = "";
}
mount.innerHTML = "";
const btnInstall = root.getElementById("btnInstall");
const btnUpdate = root.getElementById("btnUpdate");
const btnRestart = root.getElementById("btnRestart");
if (btnInstall) {
btnInstall.addEventListener("click", () => {
if (btnInstall.hasAttribute("disabled")) return;
if (this._detailRepoId) this._installRepo(this._detailRepoId);
});
}
if (btnUpdate) {
btnUpdate.addEventListener("click", () => {
if (btnUpdate.hasAttribute("disabled")) return;
if (this._detailRepoId) this._updateRepo(this._detailRepoId);
});
}
if (btnRestart) {
btnRestart.addEventListener("click", () => {
this._restartHA();
});
} }
} }
@@ -896,7 +871,7 @@ class BahmcloudStorePanel extends HTMLElement {
if (!r) return ""; if (!r) return "";
const repoId = this._safeId(r?.id); const repoId = this._safeId(r?.id);
const installed = !!r?.installed; const installed = this._asBoolStrict(r?.installed);
const latest = this._safeText(r?.latest_version); const latest = this._safeText(r?.latest_version);
const installedVersion = this._safeText(r?.installed_version); const installedVersion = this._safeText(r?.installed_version);
@@ -906,16 +881,13 @@ class BahmcloudStorePanel extends HTMLElement {
const installDisabled = installed || busy; const installDisabled = installed || busy;
const updateDisabled = !updateAvailable || busy; const updateDisabled = !updateAvailable || busy;
const installTitle = installed ? "Already installed" : busy ? "Installing…" : "Install";
const updateTitle = !installed ? "Not installed" : !updateAvailable ? "No update available" : busy ? "Updating…" : "Update";
return ` return `
<div class="fabs"> <div class="fabs">
<div class="fab primary" id="fabOpen" title="Open repository">↗</div> <button class="fabbtn primary" id="fabOpen" title="Open repository">↗</button>
<div class="fab" id="fabReload" title="Reload README">⟳</div> <button class="fabbtn" id="fabReload" title="Reload README">⟳</button>
<div class="fab" id="fabInstall" title="${installTitle}" ${installDisabled ? "disabled" : ""}></div> <button class="fabbtn" id="fabInstall" title="${installDisabled ? (installed ? "Already installed" : "Installing…") : "Install"}" ${installDisabled ? "disabled" : ""}></button>
<div class="fab" id="fabUpdate" title="${updateTitle}" ${updateDisabled ? "disabled" : ""}>↑</div> <button class="fabbtn" id="fabUpdate" title="${updateDisabled ? (!installed ? "Not installed" : "No update available") : "Update"}" ${updateDisabled ? "disabled" : ""}>↑</button>
<div class="fab" id="fabInfo" title="About">i</div> <button class="fabbtn" id="fabInfo" title="About">i</button>
</div> </div>
`; `;
} }
@@ -926,6 +898,7 @@ class BahmcloudStorePanel extends HTMLElement {
if (!r) return; if (!r) return;
const url = this._safeText(r?.url); const url = this._safeText(r?.url);
const repoId = this._safeId(r?.id);
const open = root.getElementById("fabOpen"); const open = root.getElementById("fabOpen");
const reload = root.getElementById("fabReload"); const reload = root.getElementById("fabReload");
@@ -933,35 +906,24 @@ class BahmcloudStorePanel extends HTMLElement {
const update = root.getElementById("fabUpdate"); const update = root.getElementById("fabUpdate");
const info = root.getElementById("fabInfo"); const info = root.getElementById("fabInfo");
const repoId = this._safeId(r?.id);
if (open) open.addEventListener("click", () => url && window.open(url, "_blank", "noreferrer")); if (open) open.addEventListener("click", () => url && window.open(url, "_blank", "noreferrer"));
if (reload) { if (reload) reload.addEventListener("click", () => this._detailRepoId && this._loadReadme(this._detailRepoId));
reload.addEventListener("click", () => {
if (this._detailRepoId) this._loadReadme(this._detailRepoId);
});
}
if (install) { if (install) {
install.addEventListener("click", () => { install.addEventListener("click", () => {
if (install.hasAttribute("disabled")) return; if (install.disabled) return;
this._installRepo(repoId); this._installRepo(repoId);
}); });
} }
if (update) { if (update) {
update.addEventListener("click", () => { update.addEventListener("click", () => {
if (update.hasAttribute("disabled")) return; if (update.disabled) return;
this._updateRepo(repoId); this._updateRepo(repoId);
}); });
} }
if (info) { if (info) info.addEventListener("click", () => { this._view = "about"; this._update(); });
info.addEventListener("click", () => {
this._view = "about";
this._update();
});
}
} }
_renderManage() { _renderManage() {
@@ -1016,16 +978,8 @@ class BahmcloudStorePanel extends HTMLElement {
const name = root.getElementById("customName"); const name = root.getElementById("customName");
const add = root.getElementById("addCustom"); const add = root.getElementById("addCustom");
if (url) { if (url) url.addEventListener("input", (e) => { this._customAddUrl = e?.target?.value || ""; });
url.addEventListener("input", (e) => { if (name) name.addEventListener("input", (e) => { this._customAddName = e?.target?.value || ""; });
this._customAddUrl = e?.target?.value || "";
});
}
if (name) {
name.addEventListener("input", (e) => {
this._customAddName = e?.target?.value || "";
});
}
if (add) add.addEventListener("click", () => this._addCustomRepo()); if (add) add.addEventListener("click", () => this._addCustomRepo());
root.querySelectorAll("[data-remove]").forEach((el) => { root.querySelectorAll("[data-remove]").forEach((el) => {