diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js
deleted file mode 100644
index 8193dc2..0000000
--- a/custom_components/bahmcloud_store/panel/panel.js
+++ /dev/null
@@ -1,900 +0,0 @@
-class BahmcloudStorePanel extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({ mode: "open" });
-
- this._hass = null;
-
- this._view = "store"; // store | manage | about | detail
- this._data = null;
- this._loading = true;
- this._error = null;
-
- this._customAddUrl = "";
- this._customAddName = "";
-
- this._search = "";
- this._category = "all";
- this._provider = "all"; // all|github|gitea|gitlab|other|custom
-
- this._detailRepoId = null;
- this._detailRepo = null;
-
- this._readmeLoading = false;
- this._readmeText = null;
- this._readmeHtml = null;
- this._readmeError = null;
- }
-
- set hass(hass) {
- this._hass = hass;
- if (!this._rendered) {
- this._rendered = true;
- this._render();
- this._load();
- }
- }
-
- async _load(refresh = false) {
- if (!this._hass) return;
-
- this._loading = true;
- this._error = null;
- this._update();
-
- try {
- const path = refresh ? "bcs?refresh=1" : "bcs";
- const data = await this._hass.callApi("get", path);
- this._data = data;
- } catch (e) {
- this._error = this._toErrString(e);
- } finally {
- this._loading = false;
- this._update();
- }
- }
-
- async _refreshNow() {
- // “old-school” refresh that actually forces backend reload
- await this._load(true);
- }
-
- _isDesktop() {
- return window.matchMedia && window.matchMedia("(min-width: 1024px)").matches;
- }
-
- _toggleMenu() {
- if (this._isDesktop()) return;
- try {
- const ev = new Event("hass-toggle-menu", { bubbles: true, composed: true });
- this.dispatchEvent(ev);
- } catch (_) {}
- }
-
- _goBack() {
- if (this._view === "detail") {
- this._view = "store";
- this._detailRepoId = null;
- this._detailRepo = null;
- this._readmeText = null;
- this._readmeHtml = null;
- this._readmeError = null;
- this._update();
- return;
- }
- try {
- history.back();
- } catch (_) {
- window.location.href = "/";
- }
- }
-
- async _addCustomRepo() {
- if (!this._hass) return;
-
- const url = (this._customAddUrl || "").trim();
- const name = (this._customAddName || "").trim() || null;
-
- if (!url) {
- this._error = "Please enter a repository URL.";
- this._update();
- return;
- }
-
- this._error = null;
- this._update();
-
- try {
- await this._hass.callApi("post", "bcs", { op: "add_custom_repo", url, name });
- this._customAddUrl = "";
- this._customAddName = "";
- await this._load();
- this._view = "manage";
- this._update();
- } catch (e) {
- this._error = this._toErrString(e);
- this._update();
- }
- }
-
- async _removeCustomRepo(id) {
- if (!this._hass) return;
-
- try {
- await this._hass.callApi("delete", `bcs/custom_repo?id=${encodeURIComponent(id)}`);
- await this._load();
- this._view = "manage";
- this._update();
- } catch (e) {
- this._error = this._toErrString(e);
- this._update();
- }
- }
-
- _openRepoDetail(repoId) {
- const repos = Array.isArray(this._data?.repos) ? this._data.repos : [];
- const repo = repos.find((r) => this._safeId(r?.id) === repoId);
- if (!repo) return;
-
- this._view = "detail";
- this._detailRepoId = repoId;
- this._detailRepo = repo;
-
- this._readmeText = null;
- this._readmeHtml = null;
- this._readmeError = null;
-
- this._update();
- this._loadReadme(repoId);
- }
-
- async _loadReadme(repoId) {
- if (!this._hass) return;
-
- this._readmeLoading = true;
- this._readmeText = null;
- this._readmeHtml = null;
- this._readmeError = null;
- this._update();
-
- try {
- const resp = await this._hass.callApi("get", `bcs/readme?repo_id=${encodeURIComponent(repoId)}`);
-
- const ok = resp && resp.ok === true;
- const readme = resp && typeof resp.readme === "string" ? resp.readme : null;
- const html = resp && typeof resp.html === "string" ? resp.html : null;
-
- if (ok && readme && readme.trim()) {
- this._readmeText = readme;
- this._readmeHtml = html && html.trim() ? html : null;
- this._readmeError = null;
- } else {
- const msg =
- resp && typeof resp.message === "string" && resp.message.trim()
- ? resp.message.trim()
- : "README not found.";
- this._readmeError = msg;
- }
- } catch (e) {
- this._readmeError = this._toErrString(e) || "README not found.";
- } finally {
- this._readmeLoading = false;
- this._update();
- }
- }
-
- _render() {
- const root = this.shadowRoot;
-
- root.innerHTML = `
-
-
-
-
-
-
←
-
-
Bahmcloud Store
-
BCS — loading…
-
-
-
- Refresh
-
-
-
-
-
-
Store
-
Manage repositories
-
Settings / About
-
-
-
-
-
-
-
- `;
-
- this.shadowRoot.getElementById("refreshBtn").addEventListener("click", () => this._refreshNow());
- this.shadowRoot.getElementById("menuBtn").addEventListener("click", () => this._toggleMenu());
- this.shadowRoot.getElementById("backBtn").addEventListener("click", () => this._goBack());
-
- for (const tab of this.shadowRoot.querySelectorAll(".tab")) {
- tab.addEventListener("click", () => {
- this._view = tab.getAttribute("data-view");
- this._update();
- });
- }
-
- // prevent HA global shortcuts while typing
- const stopIfFormField = (e) => {
- const t = e.composedPath ? e.composedPath()[0] : e.target;
- if (!t) return;
- const tag = (t.tagName || "").toLowerCase();
- const isEditable = tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable;
- if (isEditable) e.stopPropagation();
- };
- this.shadowRoot.addEventListener("keydown", stopIfFormField, true);
- this.shadowRoot.addEventListener("keyup", stopIfFormField, true);
- this.shadowRoot.addEventListener("keypress", stopIfFormField, true);
- }
-
- _captureFocusState() {
- const root = this.shadowRoot;
- const ae = root.activeElement;
- if (!ae || !ae.id) return null;
- const supported = new Set(["searchInput", "categorySelect", "addUrl", "addName"]);
- if (!supported.has(ae.id)) return null;
-
- return {
- id: ae.id,
- selectionStart: typeof ae.selectionStart === "number" ? ae.selectionStart : null,
- selectionEnd: typeof ae.selectionEnd === "number" ? ae.selectionEnd : null,
- };
- }
-
- _restoreFocusState(state) {
- if (!state) return;
- const root = this.shadowRoot;
- const el = root.getElementById(state.id);
- if (!el) return;
- try {
- el.focus({ preventScroll: true });
- if (state.selectionStart !== null && state.selectionEnd !== null && typeof el.setSelectionRange === "function") {
- el.setSelectionRange(state.selectionStart, state.selectionEnd);
- }
- } catch (_) {}
- }
-
- _update() {
- const root = this.shadowRoot;
- const focusState = this._captureFocusState();
-
- const content = root.getElementById("content");
- const err = root.getElementById("error");
- const subtitle = root.getElementById("subtitle");
- const fabs = root.getElementById("fabs");
-
- const v = this._safeText(this._data?.version);
- subtitle.textContent = v ? `BCS ${v}` : "BCS — loading…";
-
- for (const tab of root.querySelectorAll(".tab")) {
- tab.classList.toggle("active", tab.getAttribute("data-view") === this._view);
- }
-
- err.textContent = this._error ? `Error: ${this._error}` : "";
-
- fabs.innerHTML = this._view === "detail" ? this._renderFabs() : "";
- this._wireFabs();
-
- if (this._loading) {
- content.innerHTML = `Loading…
`;
- this._restoreFocusState(focusState);
- return;
- }
-
- if (!this._data) {
- content.innerHTML = `No data.
`;
- this._restoreFocusState(focusState);
- return;
- }
-
- if (this._view === "store") {
- content.innerHTML = this._renderStore();
- this._wireStore();
- this._restoreFocusState(focusState);
- return;
- }
-
- if (this._view === "manage") {
- content.innerHTML = this._renderManage();
- this._wireManage();
- this._restoreFocusState(focusState);
- return;
- }
-
- if (this._view === "about") {
- content.innerHTML = this._renderAbout();
- this._restoreFocusState(focusState);
- return;
- }
-
- content.innerHTML = this._renderDetail();
- this._wireDetail();
- this._restoreFocusState(focusState);
- }
-
- _renderStore() {
- const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
- const categories = this._computeCategories(repos);
- const stats = this._computeProviderStats(repos);
-
- const filtered = repos
- .filter((r) => {
- const q = (this._search || "").trim().toLowerCase();
- if (!q) return true;
- 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) => (this._category === "all" ? true : this._safeLower(r?.category) === this._category))
- .filter((r) => {
- if (this._provider === "all") return true;
- if (this._provider === "custom") return r?.source === "custom";
- return this._safeLower(r?.provider) === this._provider;
- });
-
- const options = [
- `All categories `,
- ...categories.map((c) => `${this._esc(c)} `),
- ].join("");
-
- const providerChips = this._renderProviderChips(stats);
-
- 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" ? `Custom ` : `Index `;
-
- 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 prov = this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null;
- const metaSrc = this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null;
-
- const lineBits = [creator, latest, prov, metaSrc].filter(Boolean);
-
- return `
-
-
-
-
${this._esc(name)}
-
${this._esc(desc)}
-
${this._esc(lineBits.join(" · "))}
-
- ${badge}
-
-
- `;
- })
- .join("");
-
- return `
-
-
Providers
-
${providerChips}
-
-
-
-
- ${options}
-
-
- ${rows || `No repositories match this filter.
`}
- `;
- }
-
- _wireStore() {
- const root = this.shadowRoot;
-
- const search = root.getElementById("searchInput");
- const cat = root.getElementById("categorySelect");
- const chips = root.getElementById("providerChips");
-
- if (search) search.addEventListener("input", (e) => { this._search = e.target.value; this._update(); });
- if (cat) cat.addEventListener("change", (e) => { this._category = String(e.target.value || "all"); this._update(); });
-
- if (chips) {
- for (const c of chips.querySelectorAll("[data-prov]")) {
- c.addEventListener("click", () => {
- const key = c.getAttribute("data-prov");
- if (!key) return;
- this._provider = key;
- this._update();
- });
- }
- }
-
- for (const card of root.querySelectorAll("[data-repo]")) {
- card.addEventListener("click", () => {
- const id = card.getAttribute("data-repo");
- if (id) this._openRepoDetail(id);
- });
- }
- }
-
- _renderProviderChips(stats) {
- const order = ["all", "github", "gitea", "gitlab", "other", "custom"];
- const labels = { all: "All", github: "GitHub", gitea: "Gitea", gitlab: "GitLab", other: "Other", custom: "Custom" };
- const values = { all: stats.total, github: stats.github, gitea: stats.gitea, gitlab: stats.gitlab, other: stats.other, custom: stats.custom };
-
- return order
- .map((key) => {
- const count = values[key] ?? 0;
- const active = this._provider === key ? " active" : "";
- return `${labels[key]} ${count}
`;
- })
- .join("");
- }
-
- _computeProviderStats(repos) {
- const s = { total: 0, github: 0, gitea: 0, gitlab: 0, other: 0, custom: 0 };
- if (!Array.isArray(repos)) return s;
-
- for (const r of repos) {
- s.total += 1;
- if (r?.source === "custom") s.custom += 1;
-
- const p = this._safeLower(r?.provider) || "other";
- if (p === "github") s.github += 1;
- else if (p === "gitea") s.gitea += 1;
- else if (p === "gitlab") s.gitlab += 1;
- else s.other += 1;
- }
- return s;
- }
-
- _renderDetail() {
- const r = this._detailRepo;
- if (!r) return `Repository not found.
`;
-
- const name = this._safeText(r?.name) || "Unnamed repository";
- const desc = this._safeText(r?.description) || "No description available.";
- const url = this._safeText(r?.url) || "#";
-
- const badge = r?.source === "custom" ? `Custom ` : `Index `;
- const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: unknown";
-
- const infoBits = [
- this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -",
- latest,
- this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
- this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
- ].filter(Boolean);
-
- const readmeBlock = this._readmeLoading
- ? `Loading README…
`
- : this._readmeText
- ? `
-
-
-
README
-
Rendered Markdown
-
-
-
-
-
- Show raw Markdown
-
-
${this._esc(this._readmeText)}
-
-
-
- `
- : `
-
-
README
-
${this._esc(this._readmeError || "README not found.")}
-
- `;
-
- return `
-
-
-
-
-
-
${this._esc(name)}
-
${this._esc(desc)}
-
${this._esc(infoBits.join(" · "))}
-
-
- ${badge}
-
-
-
- ${readmeBlock}
-
-
-
-
-
Installation & Updates
-
- Installation and updates are performed manually via Settings → System → Updates .
- The Store UI is used to browse repositories and trigger installation/update actions.
-
-
- Updates remain manual (like HACS).
-
-
-
-
- `;
- }
-
- _wireDetail() {
- const root = this.shadowRoot;
- const mount = root.getElementById("readmePretty");
- if (!mount) return;
-
- if (this._readmeText) {
- if (typeof this._readmeHtml === "string" && this._readmeHtml.trim()) {
- mount.innerHTML = this._readmeHtml;
- this._postprocessRenderedMarkdown(mount);
- return;
- }
- mount.innerHTML = `Rendered HTML not available. Use “Show raw Markdown”.
`;
- return;
- }
-
- mount.innerHTML = "";
- }
-
- _postprocessRenderedMarkdown(container) {
- try {
- const links = container.querySelectorAll("a[href]");
- links.forEach((a) => {
- a.setAttribute("target", "_blank");
- a.setAttribute("rel", "noreferrer noopener");
- });
- } catch (_) {}
- }
-
- _renderFabs() {
- const r = this._detailRepo;
- if (!r) return "";
-
- return `
-
- `;
- }
-
- _wireFabs() {
- const root = this.shadowRoot;
- const r = this._detailRepo;
- if (!r) return;
-
- const url = this._safeText(r?.url);
-
- const open = root.getElementById("fabOpen");
- const reload = root.getElementById("fabReload");
- const info = root.getElementById("fabInfo");
-
- if (open) open.addEventListener("click", () => url && window.open(url, "_blank", "noreferrer"));
- if (reload) reload.addEventListener("click", () => { if (this._detailRepoId) this._loadReadme(this._detailRepoId); });
- if (info) info.addEventListener("click", () => { this._view = "about"; this._update(); });
- }
-
- _renderManage() {
- const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
- const custom = repos.filter((r) => r?.source === "custom");
-
- const list = custom
- .map((r) => {
- const id = this._safeId(r?.id);
- const name = this._safeText(r?.name) || "Unnamed repository";
- const url = this._safeText(r?.url) || "";
- return `
-
-
-
-
${this._esc(name)}
-
${this._esc(url)}
-
-
- Remove
-
-
-
- `;
- })
- .join("");
-
- return `
-
-
Manage repositories
-
Add public repositories from any git provider.
-
-
-
-
-
-
Display name (optional)
-
-
-
-
- Add repository
-
-
-
-
- ${list || `No custom repositories added yet.
`}
- `;
- }
-
- _wireManage() {
- const root = this.shadowRoot;
-
- const addUrl = root.getElementById("addUrl");
- const addName = root.getElementById("addName");
- const addBtn = root.getElementById("addBtn");
-
- if (addUrl) addUrl.addEventListener("input", (e) => (this._customAddUrl = e.target.value));
- if (addName) addName.addEventListener("input", (e) => (this._customAddName = e.target.value));
- if (addBtn) addBtn.addEventListener("click", () => this._addCustomRepo());
-
- for (const btn of root.querySelectorAll("[data-remove]")) {
- btn.addEventListener("click", () => {
- const id = btn.getAttribute("data-remove");
- if (id) this._removeCustomRepo(id);
- });
- }
- }
-
- _renderAbout() {
- const v = this._safeText(this._data?.version) || "-";
- return `
-
-
Settings / About
-
Language: English (v1). i18n will be added later.
-
Theme: follows Home Assistant light/dark automatically.
-
Accent: Bahmcloud Blue.
-
BCS version: ${this._esc(v)}
-
- `;
- }
-
- _computeCategories(repos) {
- const set = new Set();
- for (const r of repos) {
- const c = this._safeLower(r?.category);
- if (c) set.add(c);
- }
- return Array.from(set).sort();
- }
-
- _safeText(v) {
- if (v === null || v === undefined) return "";
- const t = typeof v;
- if (t === "string") return v;
- if (t === "number" || t === "boolean") return String(v);
- return "";
- }
-
- _safeLower(v) {
- const s = this._safeText(v);
- return s ? s.trim().toLowerCase() : "";
- }
-
- _safeId(v) {
- return this._safeText(v) || "";
- }
-
- _toErrString(e) {
- if (!e) return "Unknown error";
- if (typeof e === "string") return e;
- if (typeof e?.message === "string") return e.message;
- try { return JSON.stringify(e); } catch (_) { return "Unknown error"; }
- }
-
- _esc(s) {
- return String(s ?? "")
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
- }
-}
-
-customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);