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