import { html, nothing } from 'lit'; import { LightElement } from '../lib/base.js'; import { t } from '../lib/i18n.js'; import { jf, hasSchema, pluginHealth } from './shared/plugin-common.js'; // Plugins page (`#plugins`) — the admin board of every registered plugin, // and the single plugin management surface (the old per-user `#plugins` // page is gone: a plugin with per-user settings — Telegram's pairing, // Honcho's opt-in — hosts them in its own sidebar page via `web_pages()`). // // One card per plugin: an enable/disable toggle, a health dot (green = // enabled, running and fully configured; red = enabled but broken; grey = // off) and a Configure button opening the plugin's own detail page // (`#plugin-detail?id=…`). Instance config and user-access grants live on the // detail page, not here — the catalog stays a quick status board. // // Styling reuses the connectors card grid (`web/css/connectors.css`). const PAGE_ID = 'plugins'; export class PluginCatalogPage extends LightElement { static get properties() { return { _open: { state: true }, _all: { state: true }, // PluginInfo[] _error: { state: true }, _status: { state: true }, // { [pluginId]: { err?: string } } — toggle feedback }; } constructor() { super(); this._open = false; this._reset(); } _reset() { this._all = null; this._error = null; this._status = {}; } connectedCallback() { super.connectedCallback(); this.__onLocaleChanged = () => this.requestUpdate(); window.addEventListener('locale-changed', this.__onLocaleChanged); window.addEventListener('llm-page-change', (e) => { this._open = e.detail.page === PAGE_ID; this.style.display = this._open ? 'flex' : 'none'; if (this._open) this._load(); }); } disconnectedCallback() { window.removeEventListener('locale-changed', this.__onLocaleChanged); super.disconnectedCallback(); } async _load() { this._error = null; try { this._all = await jf('/api/plugins'); } catch (e) { this._error = e.message; } } /// The toggle flips `enabled` only — the persisted config travels back /// unchanged so a flip never clobbers what the detail page saved. async _toggle(p, enabled) { this._status = { ...this._status, [p.id]: {} }; try { await jf(`/api/plugins/${encodeURIComponent(p.id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, config: p.config || {} }), }); this._all = await jf('/api/plugins'); window.dispatchEvent(new CustomEvent('plugins-changed')); } catch (e) { this._status = { ...this._status, [p.id]: { err: e.message } }; } } _configure(p) { history.pushState({ page: 'plugin-detail' }, '', `#plugin-detail?id=${encodeURIComponent(p.id)}`); window.dispatchEvent(new CustomEvent('llm-page-change', { detail: { page: 'plugin-detail' } })); } // ── Render ───────────────────────────────────────────────────────────────── render() { if (!this._open) return nothing; const loading = this._all === null && !this._error; return html`
${this._error ? html`
${this._error}
` : nothing} ${loading ? html`
${t('plugins.loading')}
` : html`
${(this._all ?? []).length === 0 ? html`

${t('plugins.empty.manage')}

` : html`
${this._all.map(p => this._renderCard(p))}
`}
`}
`; } _renderHealth(p) { const h = pluginHealth(p); const cls = h === 'ok' ? 'ok' : (h === 'off' ? 'off' : 'err'); return html` ${t(`plugins.health.${h}`)} `; } _renderCard(p) { const status = this._status[p.id] || {}; return html`
${p.name}
${p.id}
${this._renderHealth(p)}
${p.description ? html`
${p.description}
` : nothing}
${hasSchema(p.config_schema) ? html` ${t('plugins.badge.instance_config')}` : nothing} ${p.has_user_page ? html` ${t('plugins.badge.user_page')}` : nothing}
this._toggle(p, e.target.checked)} />
${status.err ? html`
${status.err}
` : nothing}
`; } }