diff --git a/CLAUDE.md b/CLAUDE.md index 8b63aa2..5139451 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -395,7 +395,7 @@ The role editor (`roles-page.js`) sets the default group + an allowed-groups che | `system-agents.js` | `` | `#system-agents` — the caller's own run history for the background system agents (TIC): agent, start, status, duration, counters; row → the run's session | | `shared-folders.js` | `` | `#shared-folders` — admin-only CRUD for on-disk shared folders (§6): create/describe/delete + per-member read-only/read-write grants; description feeds the assistant's `__SHARED_FOLDERS__` context | | `projects/` | `` | `#projects` — host + list + board; the board is tabbed (**Files** explorer with live watcher + write actions, **Sharing** members), deep-linked `#projects/{id}[/sharing]`. See the Projects section | -| `connector-detail.js` | `` | A connector's own page (`#connector?name=X`): env/secret form + Test, the **OAuth login panel** (sign in → paste code → complete, §15), global enable + per-user access grants | +| `connector-detail.js` | `` | A connector's own page (`#connector?name=X`): env/secret form + Test, the **OAuth login panel** (sign in → paste code → complete, §15), global enable. Access grants live **only** on the Users page (`users-page.js` connectors modal), so "who has what" has a single surface | | `shared/connector-common.js` | (helpers) | Shared Connectors vocabulary: `statusOf` (incl. `needs_login` for a pending OAuth row), `STATUS_LABEL`, schema normalization, `jf` fetch | | `llm-providers.js` | `` | LLM provider management | | `models-hub.js` | `` | Models hub landing (LLM / Transcription / Image) | diff --git a/web/components/connector-detail.js b/web/components/connector-detail.js index 2795c1d..62a8e07 100644 --- a/web/components/connector-detail.js +++ b/web/components/connector-detail.js @@ -2,7 +2,7 @@ import { html, nothing } from 'lit'; import { LightElement } from '../lib/base.js'; import { t } from '../lib/i18n.js'; import { - announceChange, connectorIconUrl, jf, normalizeSchema, parseJson, seedEnv, statusOf, + announceChange, authLabel, connectorIconUrl, jf, normalizeSchema, parseJson, seedEnv, statusOf, } from './shared/connector-common.js'; // One connector's own page — `#connector?name=`. @@ -12,9 +12,9 @@ import { // for a dozen fields, and a fixed-size modal simply could not hold them — it grew // taller than the viewport and the buttons went off-screen. A page scrolls. // -// It is also the natural home for everything else that is per-connector and was -// scattered before: the Test button, the global enable, and the per-user access -// grants — which used to be a second modal reached from a third place. +// It is also the natural home for the other per-connector actions: the Test button +// and the global enable. Access grants are **not** here on purpose: they live only +// on the Users page, so "who has what" has a single surface. // // Deliberately not a `name` field: the list is one row per connector (§7 template), // so the runtime name is the catalog name. The backend still defends against @@ -44,8 +44,6 @@ export class ConnectorDetailPage extends LightElement { _test: { state: true }, // null | 'running' | report _busy: { state: true }, _error: { state: true }, - _users: { state: true }, // admin: for the access panel - _access: { state: true }, // admin: Set of granted user ids _noIcon: { state: true }, _oauth: { state: true }, // in-flight OAuth login: { state, auth_url, code } _qr: { state: true }, // in-flight QR/device login: { state, qr, message } @@ -70,8 +68,6 @@ export class ConnectorDetailPage extends LightElement { this._test = null; this._busy = false; this._error = null; - this._users = null; - this._access = null; this._oauth = null; this._qr = null; this._qrServerId = null; @@ -142,23 +138,11 @@ export class ConnectorDetailPage extends LightElement { this._schema = schema; // Keep whatever the user has already typed across a reload triggered by a save. this._form = { api_key: this._form.api_key || '', env: { ...seedEnv(schema), ...this._form.env } }; - - if (this._isAdmin && this._isGlobal) await this._loadAccess(); } catch (e) { this._error = e.message; } } - async _loadAccess() { - try { - this._users = await jf('/api/users'); - if (this._glob) { - const granted = await jf(`/api/mcp/global/${this._glob.id}/access`); - this._access = new Set(granted || []); - } - } catch (e) { this._error = e.message; } - } - _back() { // Prefer real history so the browser's own Back stays consistent; fall back to // the list when this page was opened straight from a pasted URL. @@ -384,26 +368,6 @@ export class ConnectorDetailPage extends LightElement { finally { this._busy = false; } } - _toggleAccess(userId) { - const next = new Set(this._access); - next.has(userId) ? next.delete(userId) : next.add(userId); - this._access = next; - } - - async _saveAccess() { - this._busy = true; - try { - await jf(`/api/mcp/global/${this._glob.id}/access`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ user_ids: [...this._access] }), - }); - announceChange(); - await this._load(); - } catch (e) { this._error = e.message; } - finally { this._busy = false; } - } - // ── Render ───────────────────────────────────────────────────────────────── render() { @@ -428,7 +392,6 @@ export class ConnectorDetailPage extends LightElement {
${this._error}
` : nothing} ${this._renderSummary()} ${this._renderConfig()} - ${this._renderAccess()} `; } @@ -471,7 +434,7 @@ export class ConnectorDetailPage extends LightElement { ${desc ? html`
${desc}
` : nothing}
- + ${this._isGlobal ? t('connectors.detail.detail_scope_global') : t('connectors.chip.per_user')} ${isScript ? html` @@ -479,7 +442,7 @@ export class ConnectorDetailPage extends LightElement { ${t('connectors.detail.scope_local')} ` : nothing} ${e?.auth_kind && e.auth_kind !== 'none' ? html` - ${e.auth_kind}` : nothing} + ${authLabel(e.auth_kind)}` : nothing} ${status === 'active' ? html` ${t('connectors.detail.status.active')}` : nothing} ${status === 'pending' ? html` @@ -736,37 +699,4 @@ export class ConnectorDetailPage extends LightElement { style="font-size:.7rem;white-space:pre-wrap">${JSON.stringify(t.details, null, 2)}` : nothing}
`; } - - /// Who may use this global connector. Only meaningful once it is enabled — there - /// is no instance to grant access to before that. - _renderAccess() { - if (!this._isGlobal || !this._isAdmin || !this._glob) return nothing; - const users = this._users ?? []; - return html` -
-
-

${t('connectors.detail.access.title')}

-
-
${t('connectors.detail.access.desc')}
- ${users.length === 0 - ? html`

${t('connectors.detail.access.empty')}

` - : html` -
- ${users.map(u => html` -
- this._toggleAccess(u.id)} /> - -
`)} -
`} - -
`; - } } diff --git a/web/components/connectors.js b/web/components/connectors.js index c8fb18e..6769451 100644 --- a/web/components/connectors.js +++ b/web/components/connectors.js @@ -2,7 +2,7 @@ import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { LightElement } from '../lib/base.js'; import { t } from '../lib/i18n.js'; -import { connectorIconUrl, statusOf, STATUS_LABEL, statusText } from './shared/connector-common.js'; +import { authLabel, connectorIconUrl, statusOf, STATUS_LABEL, statusText } from './shared/connector-common.js'; // Connectors (MCP) — blueprint §7/§14/§15. // @@ -445,7 +445,7 @@ export class ConnectorsPage extends LightElement { ${r.description ? html`
${r.description}
` : nothing}
- + ${isGlobal ? t('connectors.chip.global') : t('connectors.chip.per_user')} ${isScript ? html` @@ -453,7 +453,7 @@ export class ConnectorsPage extends LightElement { ${t('connectors.chip.local_script')} ` : nothing} ${r.auth_kind && r.auth_kind !== 'none' ? html` - ${r.auth_kind}` : nothing} + ${authLabel(r.auth_kind)}` : nothing}
${statusText(status)} diff --git a/web/components/shared/connector-common.js b/web/components/shared/connector-common.js index d873cdd..7d6ffb0 100644 --- a/web/components/shared/connector-common.js +++ b/web/components/shared/connector-common.js @@ -34,6 +34,17 @@ export function statusText(status) { }[status] ?? status; } +/// The auth requirement as a human phrase ("Requires an API key"), never the raw +/// enum — `api_key` on a chip told the reader nothing. +export function authLabel(kind) { + return { + api_key: t('connectors.chip.auth.api_key'), + oauth: t('connectors.chip.auth.oauth'), + qr: t('connectors.chip.auth.qr'), + ssh_key: t('connectors.chip.auth.ssh_key'), + }[kind] ?? kind; +} + /// The one place that decides what a connector's state *is*, from whichever runtime /// rows exist for it. /// diff --git a/web/css/connectors.css b/web/css/connectors.css index 06c27ab..1a1e00a 100644 --- a/web/css/connectors.css +++ b/web/css/connectors.css @@ -79,10 +79,14 @@ .connector-row-desc { font-size: 0.74rem; + line-height: 1.35; color: var(--placeholder-color); + /* Up to three lines: long descriptions stay readable without blowing the row + up — the clamp, not the text, decides the height. */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .connector-row-chips { diff --git a/web/i18n/en.js b/web/i18n/en.js index c0e9f5d..678e201 100644 --- a/web/i18n/en.js +++ b/web/i18n/en.js @@ -776,6 +776,10 @@ export default { 'connectors.chip.global': 'global', 'connectors.chip.per_user': 'per-user', 'connectors.chip.local_script': 'local script', + 'connectors.chip.auth.api_key': 'Requires an API key', + 'connectors.chip.auth.oauth': 'Requires sign-in (OAuth)', + 'connectors.chip.auth.qr': 'Requires QR pairing', + 'connectors.chip.auth.ssh_key': 'Requires an SSH key', 'connectors.status.active': 'active', 'connectors.status.needs_fix': 'needs fix', @@ -866,10 +870,6 @@ export default { 'connectors.detail.test.error_saved': 'Saved, but the credentials did not check out — fix them and test again.', 'connectors.detail.test.error_verify': 'Verification failed — the connector stays disabled until the credentials are fixed.', - 'connectors.detail.access.title': 'Who can use it', - 'connectors.detail.access.desc': 'Ticking a box grants this connector\'s tools to that person\'s agent. Saving replaces the whole list.', - 'connectors.detail.access.empty': 'No users.', - 'connectors.detail.access.save': 'Save access', 'connectors.error.no_connector': 'No connector named "{name}" is available to you.', diff --git a/web/i18n/fr.js b/web/i18n/fr.js index 831b938..2c69379 100644 --- a/web/i18n/fr.js +++ b/web/i18n/fr.js @@ -776,6 +776,10 @@ export default { 'connectors.chip.global': 'global', 'connectors.chip.per_user': 'par utilisateur', 'connectors.chip.local_script': 'script local', + 'connectors.chip.auth.api_key': 'Nécessite une clé API', + 'connectors.chip.auth.oauth': 'Connexion requise (OAuth)', + 'connectors.chip.auth.qr': 'Appairage QR requis', + 'connectors.chip.auth.ssh_key': 'Nécessite une clé SSH', 'connectors.status.active': 'actif', 'connectors.status.needs_fix': 'nécessite une correction', @@ -856,10 +860,6 @@ export default { 'connectors.detail.test.error_saved': 'Enregistré, mais les informations n\'ont pas été vérifiées — corrigez-les et testez à nouveau.', 'connectors.detail.test.error_verify': 'La vérification a échoué — le connecteur reste désactivé jusqu\'à ce que les informations soient corrigées.', - 'connectors.detail.access.title': 'Qui peut l\'utiliser', - 'connectors.detail.access.desc': 'Cocher une case accorde les outils de ce connecteur à l\'agent de cette personne. L\'enregistrement remplace toute la liste.', - 'connectors.detail.access.empty': 'Aucun utilisateur.', - 'connectors.detail.access.save': 'Enregistrer les accès', 'connectors.error.no_connector': 'Aucun connecteur nommé "{name}" ne vous est disponible.', diff --git a/web/i18n/it.js b/web/i18n/it.js index b7df042..647c0c7 100644 --- a/web/i18n/it.js +++ b/web/i18n/it.js @@ -776,6 +776,10 @@ export default { 'connectors.chip.global': 'globale', 'connectors.chip.per_user': 'per utente', 'connectors.chip.local_script': 'script locale', + 'connectors.chip.auth.api_key': 'Richiede una API key', + 'connectors.chip.auth.oauth': 'Richiede accesso (OAuth)', + 'connectors.chip.auth.qr': 'Richiede abbinamento QR', + 'connectors.chip.auth.ssh_key': 'Richiede una chiave SSH', 'connectors.status.active': 'attivo', 'connectors.status.needs_fix': 'da sistemare', @@ -856,10 +860,6 @@ export default { 'connectors.detail.test.error_saved': 'Salvato, ma le credenziali non hanno superato la verifica — correggile e prova di nuovo.', 'connectors.detail.test.error_verify': 'Verifica fallita — il connettore rimane disabilitato finché le credenziali non vengono corrette.', - 'connectors.detail.access.title': 'Chi può usarlo', - 'connectors.detail.access.desc': 'Selezionando un utente gli vengono concessi gli strumenti di questo connettore. Salvando si sostituisce l\'intera lista.', - 'connectors.detail.access.empty': 'Nessun utente.', - 'connectors.detail.access.save': 'Salva accesso', 'connectors.error.no_connector': 'Nessun connettore chiamato "{name}" è disponibile per te.',