diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js index b9e5e07..c2100ab 100644 --- a/custom_components/bahmcloud_store/panel/panel.js +++ b/custom_components/bahmcloud_store/panel/panel.js @@ -15,14 +15,14 @@ class BahmcloudStorePanel extends HTMLElement { this._search = ""; this._category = "all"; - this._provider = "all"; // NEW: provider filter (all|github|gitea|gitlab|other|custom) + this._provider = "all"; // all|github|gitea|gitlab|other|custom this._detailRepoId = null; this._detailRepo = null; this._readmeLoading = false; this._readmeText = null; - this._readmeHtml = null; // backend may provide; client renderer does not rely on it + this._readmeHtml = null; this._readmeError = null; } @@ -127,7 +127,7 @@ class BahmcloudStorePanel extends HTMLElement { _openRepoDetail(repoId) { const repos = Array.isArray(this._data?.repos) ? this._data.repos : []; - const repo = repos.find((r) => r.id === repoId); + const repo = repos.find((r) => this._safeId(r?.id) === repoId); if (!repo) return; this._view = "detail"; @@ -160,7 +160,7 @@ class BahmcloudStorePanel extends HTMLElement { } else { this._readmeText = null; this._readmeHtml = null; - this._readmeError = resp?.message || "README not found."; + this._readmeError = this._safeText(resp?.message) || "README not found."; } } catch (e) { this._readmeText = null; @@ -340,7 +340,6 @@ class BahmcloudStorePanel extends HTMLElement { details{ margin-top: 10px; } summary{ cursor:pointer; color: var(--bcs-accent); font-weight: 900; } - /* Pretty Markdown container */ .md { line-height: 1.65; font-size: 14px; } .md :is(h1,h2,h3){ margin: 18px 0 10px; } .md :is(p,ul,ol,pre,blockquote,table){ margin: 10px 0; } @@ -392,7 +391,7 @@ class BahmcloudStorePanel extends HTMLElement { }); } - // Prevent HA global shortcuts while typing inside the panel + // prevent HA global shortcuts while typing const stopIfFormField = (e) => { const t = e.composedPath ? e.composedPath()[0] : e.target; if (!t) return; @@ -401,7 +400,6 @@ class BahmcloudStorePanel extends HTMLElement { tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable; if (isEditable) e.stopPropagation(); }; - root.addEventListener("keydown", stopIfFormField, true); root.addEventListener("keyup", stopIfFormField, true); root.addEventListener("keypress", stopIfFormField, true); @@ -450,7 +448,7 @@ class BahmcloudStorePanel extends HTMLElement { const subtitle = root.getElementById("subtitle"); const fabs = root.getElementById("fabs"); - const v = this._data?.version ? String(this._data.version) : null; + const v = this._safeText(this._data?.version); subtitle.textContent = v ? `BCS ${v}` : "BCS — loading…"; for (const tab of root.querySelectorAll(".tab")) { @@ -502,26 +500,23 @@ class BahmcloudStorePanel extends HTMLElement { _renderStore() { const repos = Array.isArray(this._data.repos) ? this._data.repos : []; const categories = this._computeCategories(repos); - - // Provider stats (for overview) const stats = this._computeProviderStats(repos); const filtered = repos .filter((r) => { const q = (this._search || "").trim().toLowerCase(); if (!q) return true; - const hay = `${r.name || ""} ${r.description || ""} ${r.url || ""} ${r.owner || ""}`.toLowerCase(); + const hay = `${this._safeText(r?.name)} ${this._safeText(r?.description)} ${this._safeText(r?.url)} ${this._safeText(r?.owner)}`.toLowerCase(); return hay.includes(q); }) .filter((r) => { if (this._category === "all") return true; - return (r.category || "").toLowerCase() === this._category; + return this._safeLower(r?.category) === this._category; }) .filter((r) => { if (this._provider === "all") return true; - if (this._provider === "custom") return r.source === "custom"; - const pv = (r.provider || "other").toLowerCase(); - return pv === this._provider; + if (this._provider === "custom") return r?.source === "custom"; + return this._safeLower(r?.provider) === this._provider; }); const options = [ @@ -538,24 +533,27 @@ class BahmcloudStorePanel extends HTMLElement { const rows = filtered .map((r) => { + const id = this._safeId(r?.id); + const name = this._safeText(r?.name) || "Unnamed repository"; + const desc = this._safeText(r?.description) || "No description available."; + const badge = - r.source === "custom" + r?.source === "custom" ? `Custom` : `Index`; - const desc = r.description || "No description available."; - const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -"; - const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown"; - const cat = r.category ? `Category: ${r.category}` : null; - const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null; + const creator = this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -"; + const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: unknown"; + const cat = this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null; + const metaSrc = this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null; const lineBits = [creator, latest, cat, metaSrc].filter(Boolean); return ` -