// — the dashboard's list of discovered repositories. // // A self-contained control in the ActiveX spirit (AGENT.md §1.1): it lives in a // shadow root, fetches its own data from /api/repos on connect, renders itself, // and cleans up on disconnect. It talks to the rest of the app only via a // bubbling/composed `repo:select` CustomEvent — no shared globals. class RepoList extends HTMLElement { #refreshMs = 15000; #timer = null; #controller = null; constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.#renderShell(); this.#load(); this.#timer = setInterval(() => this.#load(), this.#refreshMs); } disconnectedCallback() { clearInterval(this.#timer); this.#controller?.abort(); } async #load() { this.#controller?.abort(); this.#controller = new AbortController(); try { const res = await fetch('/api/repos', { signal: this.#controller.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); this.#renderRepos(await res.json()); } catch (err) { if (err.name !== 'AbortError') this.#renderError(err); } } #select(repo) { // Cross-component communication is via events only (AGENT.md §1.1). this.dispatchEvent(new CustomEvent('repo:select', { detail: repo, bubbles: true, composed: true, })); } #renderShell() { this.shadowRoot.innerHTML = `

Loading repositories…

`; } #renderError(err) { this.shadowRoot.getElementById('body').innerHTML = `

Could not load repositories: ${this.#esc(err.message)}

`; } #renderRepos(repos) { const body = this.shadowRoot.getElementById('body'); if (!repos || repos.length === 0) { body.innerHTML = `

No repositories found. Check GIT_REPO_ROOTS.

`; return; } const ul = document.createElement('ul'); for (const r of repos) { const li = document.createElement('li'); li.innerHTML = ` ${this.#esc(r.name)} ${this.#esc(r.branch || '—')} ${r.ahead ? `↑${r.ahead}` : ''} ${r.behind ? `↓${r.behind}` : ''} ${r.dirty ? 'dirty' : 'clean'} `; li.addEventListener('click', () => this.#select(r)); ul.appendChild(li); } body.replaceChildren(ul); } #esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } } customElements.define('repo-list', RepoList);