diff --git a/client/src/components/Admin/AdminPluginsPanel.tsx b/client/src/components/Admin/AdminPluginsPanel.tsx index 198b353f8..df3343244 100644 --- a/client/src/components/Admin/AdminPluginsPanel.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.tsx @@ -1,14 +1,16 @@ import { useEffect, useState } from 'react' -import { Blocks, AlertTriangle, PackageOpen, RefreshCw, Power, Trash2, Download, Bug, X } from 'lucide-react' +import { Blocks, AlertTriangle, PackageOpen, RefreshCw, Trash2, Download, Bug, X, ShieldCheck, ArrowUpCircle, Puzzle } from 'lucide-react' import { adminApi } from '../../api/client' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import ConfirmDialog from '../shared/ConfirmDialog' +import ToggleSwitch from '../Settings/ToggleSwitch' /** - * Admin → Plugins (#plugins). Lists installed plugins with lifecycle actions - * (activate / deactivate / uninstall), a registry browser to install new ones, - * and each plugin's error log. Gated by the runtime-enabled flag. + * Admin → Plugins (#plugins). Separates the admin's ON/OFF intent (the toggle, + * backed by `enabled`) from runtime health (`status`), surfaces per-plugin errors + * and available updates, and hosts the registry browser. Gated by the + * runtime-enabled flag. */ interface PluginRow { @@ -18,6 +20,8 @@ interface PluginRow { type: string version: string | null status: string + enabled: number + last_error: string | null reviewed_at: string | null } interface RegistryItem { @@ -30,30 +34,57 @@ interface RegistryItem { reviewedAt: string | null } -const STATUS_CLASS: Record = { - active: 'bg-emerald-500/15 text-emerald-600', - inactive: 'bg-surface-tertiary text-content-muted', - disabled: 'bg-amber-500/15 text-amber-600', - error: 'bg-rose-500/15 text-rose-600', - incompatible: 'bg-orange-500/15 text-orange-600', +// Runtime health → colour + dot. `enabled` is shown separately by the toggle. +const HEALTH: Record = { + active: { cls: 'text-emerald-600', dot: 'bg-emerald-500' }, + starting: { cls: 'text-sky-600', dot: 'bg-sky-500 animate-pulse' }, + error: { cls: 'text-rose-600', dot: 'bg-rose-500' }, + inactive: { cls: 'text-content-faint', dot: 'bg-content-faint/50' }, + disabled: { cls: 'text-amber-600', dot: 'bg-amber-500' }, + incompatible: { cls: 'text-orange-600', dot: 'bg-orange-500' }, +} + +function isNewer(a: string, b: string): boolean { + const pa = a.split('.').map(Number) + const pb = b.split('.').map(Number) + for (let i = 0; i < 3; i++) { + const x = pa[i] || 0 + const y = pb[i] || 0 + if (x !== y) return x > y + } + return false } export default function AdminPluginsPanel() { const { t } = useTranslation() const toast = useToast() - const [enabled, setEnabled] = useState(false) + const [runtimeOn, setRuntimeOn] = useState(false) const [plugins, setPlugins] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) const [busy, setBusy] = useState(null) const [view, setView] = useState<'installed' | 'browse'>('installed') const [registry, setRegistry] = useState(null) + const [latest, setLatest] = useState>({}) const [errorsFor, setErrorsFor] = useState<{ id: string; rows: Array<{ ts: string; level: string; message: string }> } | null>(null) - const [confirmUninstall, setConfirmUninstall] = useState(null) + const [confirmUninstall, setConfirmUninstall] = useState(null) const refresh = () => { adminApi.plugins() - .then((d: { enabled: boolean; plugins: PluginRow[] }) => { setEnabled(!!d.enabled); setPlugins(d.plugins || []) }) + .then((d: { enabled: boolean; plugins: PluginRow[] }) => { + setRuntimeOn(!!d.enabled) + setPlugins(d.plugins || []) + // Learn the latest registry versions in the background for update badges. + if ((d.plugins || []).length) { + adminApi.pluginBrowse() + .then((items: RegistryItem[]) => { + const map: Record = {} + items.forEach((i) => { if (i.latest) map[i.id] = i.latest }) + setLatest(map) + }) + .catch(() => {}) + } + }) .catch(() => setError(true)) .finally(() => setLoading(false)) } @@ -71,35 +102,40 @@ export default function AdminPluginsPanel() { if (!registry) adminApi.pluginBrowse().then(setRegistry).catch(() => setRegistry([])) } const openErrors = (id: string) => - adminApi.pluginErrors(id).then((d: { errors: typeof errorsFor extends null ? never : Array<{ ts: string; level: string; message: string }> }) => - setErrorsFor({ id, rows: d.errors as Array<{ ts: string; level: string; message: string }> })).catch(() => setErrorsFor({ id, rows: [] })) + adminApi.pluginErrors(id) + .then((d: { errors: Array<{ ts: string; level: string; message: string }> }) => setErrorsFor({ id, rows: d.errors })) + .catch(() => setErrorsFor({ id, rows: [] })) + + const updateAvailable = (p: PluginRow) => !!(p.version && latest[p.id] && isNewer(latest[p.id], p.version)) return (
-
-
- + {/* Header */} +
+
+
-
+

{t('admin.plugins.title')}

{t('admin.plugins.subtitle')}

- {enabled && ( + {runtimeOn && (
)}
- {!enabled && !loading && !error && ( -
+ {/* Runtime-disabled notice */} + {!runtimeOn && !loading && !error && ( +

{t('admin.plugins.disabledTitle')}

@@ -108,72 +144,107 @@ export default function AdminPluginsPanel() {
)} -
+
{loading ? ( -
{t('common.loading')}
+
{t('common.loading')}
) : error ? ( -
{t('admin.plugins.loadError')}
+
{t('admin.plugins.loadError')}
) : view === 'browse' ? ( - p.id))} onInstall={(id) => act(id, () => adminApi.pluginInstall(id), t('admin.plugins.installed'))} /> ) : plugins.length === 0 ? ( -
- -

{t('admin.plugins.empty')}

+
+
+ +
+

{t('admin.plugins.empty')}

+ {runtimeOn && ( + + )}
) : ( -
- {plugins.map(p => ( -
-
- -
-
-
- {p.name} - {p.version && v{p.version}} - · {p.type} +
+ {plugins.map(p => { + const health = HEALTH[p.status] || HEALTH.inactive + const hasUpdate = updateAvailable(p) + return ( +
+
+ +
+ +
+
+ {p.name} + {p.version && v{p.version}} + {p.reviewed_at && ( + + {t('admin.plugins.reviewed')} + + )} + {hasUpdate && ( + + )} +
+
+ + + {t(`admin.plugins.status.${p.status}` as never)} + + · {t(`admin.plugins.type.${p.type}` as never)} +
+ {p.status === 'error' && p.last_error && ( +

{p.last_error}

+ )} +
+ +
+ openErrors(p.id)}> + setConfirmUninstall(p)}> +
+ busy !== p.id && act( + p.id, + () => p.enabled === 1 ? adminApi.pluginDeactivate(p.id) : adminApi.pluginActivate(p.id), + p.enabled === 1 ? t('admin.plugins.deactivated') : t('admin.plugins.activated'), + )} + /> +
- {p.description &&

{p.description}

}
- - {t(`admin.plugins.status.${p.status}` as never)} - -
- {p.status === 'active' ? ( - act(p.id, () => adminApi.pluginDeactivate(p.id), t('admin.plugins.deactivated'))}> - ) : ( - act(p.id, () => adminApi.pluginActivate(p.id), t('admin.plugins.activated'))}> - )} - openErrors(p.id)}> - setConfirmUninstall(p.id)}> -
-
- ))} + ) + })}
)}
-
+
+

{t('admin.plugins.trustNote')}

+ {/* Error-log modal */} {errorsFor && ( -
setErrorsFor(null)}> -
e.stopPropagation()}> -
- {errorsFor.id} — {t('admin.plugins.errorLog')} +
setErrorsFor(null)}> +
e.stopPropagation()}> +
+ {errorsFor.id} — {t('admin.plugins.errorLog')}
- {errorsFor.rows.length === 0 ?

{t('admin.plugins.noErrors')}

: + {errorsFor.rows.length === 0 ?

{t('admin.plugins.noErrors')}

: errorsFor.rows.map((r, i) => ( -
- {r.level} - {r.ts} - {r.message} +
+ {r.level} + {r.ts} + {r.message}
))}
@@ -185,8 +256,8 @@ export default function AdminPluginsPanel() { isOpen={!!confirmUninstall} onClose={() => setConfirmUninstall(null)} onConfirm={async () => { - const id = confirmUninstall!; setConfirmUninstall(null) - await act(id, () => adminApi.pluginUninstall(id, true), t('admin.plugins.uninstalled')) + const p = confirmUninstall!; setConfirmUninstall(null) + await act(p.id, () => adminApi.pluginUninstall(p.id, true), t('admin.plugins.uninstalled')) }} title={t('admin.plugins.uninstallTitle')} message={t('admin.plugins.uninstallBody')} @@ -195,43 +266,58 @@ export default function AdminPluginsPanel() { ) } -function IconBtn({ children, title, onClick, disabled, accent, danger }: { - children: React.ReactNode; title: string; onClick: () => void; disabled?: boolean; accent?: boolean; danger?: boolean +function IconBtn({ children, title, onClick, disabled, danger }: { + children: React.ReactNode; title: string; onClick: () => void; disabled?: boolean; danger?: boolean }) { return ( ) } -function RegistryGrid({ items, onInstall, busy, t }: { +function RegistryGrid({ items, onInstall, busy, t, installedIds }: { items: RegistryItem[] | null onInstall: (id: string) => void busy: string | null - t: (k: string) => string + t: (k: string, p?: Record) => string + installedIds: Set }) { - if (!items) return
{t('common.loading')}
- if (items.length === 0) return
{t('admin.plugins.registryEmpty')}
+ if (!items) return
{t('common.loading')}
+ if (items.length === 0) return
{t('admin.plugins.registryEmpty')}
return (
- {items.map(item => ( -
-
- {item.name} - {item.latest ? `v${item.latest}` : ''} - {item.reviewedAt && ✓ reviewed} + {items.map(item => { + const installed = installedIds.has(item.id) + return ( +
+
+
+ +
+
+
+ {item.name} + {item.latest && v{item.latest}} +
+ {item.author} · {t(`admin.plugins.type.${item.type}` as never)} +
+
+

{item.description}

+
+ {item.reviewedAt + ? {t('admin.plugins.reviewed')} + : {t('admin.plugins.unreviewed')}} + +
-

{item.description}

-
- {item.author} · {item.type} - -
-
- ))} + ) + })}
) } diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index a098e97a7..8eb97be44 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -3352,6 +3352,14 @@ function runMigrations(db: Database.Database): void { ); `); }, + // Migration 156: separate the admin's ON/OFF intent (`enabled`) from the + // runtime health (`status`). A crash used to flip status to 'error', which + // erased the "keep it on" intent, so the plugin never rebooted after a deploy. + // Boot now retries every enabled plugin regardless of last status. + () => { + db.exec("ALTER TABLE plugins ADD COLUMN enabled INTEGER NOT NULL DEFAULT 0;"); + db.exec("UPDATE plugins SET enabled = 1 WHERE status = 'active';"); + }, ]; if (currentVersion < migrations.length) { diff --git a/server/src/nest/plugins/plugin-runtime.service.ts b/server/src/nest/plugins/plugin-runtime.service.ts index a21442c4a..8ae176f47 100644 --- a/server/src/nest/plugins/plugin-runtime.service.ts +++ b/server/src/nest/plugins/plugin-runtime.service.ts @@ -52,8 +52,11 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy { } catch { /* discovery must never block boot */ } - const active = db.prepare("SELECT id FROM plugins WHERE status = 'active'").all() as Array<{ id: string }>; - for (const { id } of active) { + // Boot everything the admin left ENABLED, regardless of the last runtime + // status — a crash or a bad deploy set status='error' but must not silently + // keep the plugin down forever. + const enabled = db.prepare('SELECT id FROM plugins WHERE enabled = 1').all() as Array<{ id: string }>; + for (const { id } of enabled) { this.activate(id).catch(() => { /* status is persisted as error by the supervisor hook */ }); @@ -79,7 +82,8 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy { // Activating IS the consent gate: grant the plugin's DECLARED permissions // (the admin reviewed them on the consent screen), persist, then spawn. const declared = parseArray(row.permissions).filter(isKnownPermission); - db.prepare('UPDATE plugins SET granted_permissions = ? WHERE id = ?').run(JSON.stringify(declared), id); + // Mark it enabled (admin intent) so it reboots after restarts/crashes. + db.prepare('UPDATE plugins SET granted_permissions = ?, enabled = 1 WHERE id = ?').run(JSON.stringify(declared), id); const config = decryptConfig(parseObject(row.config)); const egress = declared.filter((p) => p.startsWith('http:outbound:')).map((p) => p.slice('http:outbound:'.length)); await this.supervisor.activate(id, new Set(declared), config, egress); @@ -88,7 +92,7 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy { async deactivate(id: string): Promise { await this.supervisor.disable(id); closePluginDataDb(id); - db.prepare("UPDATE plugins SET status = 'inactive', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(id); + db.prepare("UPDATE plugins SET status = 'inactive', enabled = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(id); } /** Stop the plugin, remove its code, and optionally delete all its data. */ diff --git a/server/src/nest/plugins/plugins.service.ts b/server/src/nest/plugins/plugins.service.ts index c71036fea..689f8ba9d 100644 --- a/server/src/nest/plugins/plugins.service.ts +++ b/server/src/nest/plugins/plugins.service.ts @@ -20,6 +20,8 @@ export interface PluginListItem { icon: string | null; version: string | null; status: string; + enabled: number; + last_error: string | null; reviewed_at: string | null; source_repo: string | null; } @@ -29,7 +31,7 @@ export class PluginsService { list(): { enabled: boolean; plugins: PluginListItem[] } { const plugins = db .prepare( - `SELECT id, name, description, type, icon, version, status, reviewed_at, source_repo + `SELECT id, name, description, type, icon, version, status, enabled, last_error, reviewed_at, source_repo FROM plugins ORDER BY sort_order, name`, ) diff --git a/server/tests/integration/plugins/plugin-runtime.test.ts b/server/tests/integration/plugins/plugin-runtime.test.ts index 32ac3cbff..179fa1874 100644 --- a/server/tests/integration/plugins/plugin-runtime.test.ts +++ b/server/tests/integration/plugins/plugin-runtime.test.ts @@ -12,7 +12,7 @@ const { testDb } = vi.hoisted(() => { const Database = require('better-sqlite3'); const db = new Database(':memory:'); db.exec(`CREATE TABLE plugins ( - id TEXT PRIMARY KEY, status TEXT, permissions TEXT DEFAULT '[]', granted_permissions TEXT DEFAULT '[]', + id TEXT PRIMARY KEY, status TEXT, enabled INTEGER DEFAULT 0, permissions TEXT DEFAULT '[]', granted_permissions TEXT DEFAULT '[]', config TEXT DEFAULT '{}', last_error TEXT, updated_at TEXT); CREATE TABLE plugin_error_log (id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_id TEXT, level TEXT, message TEXT, ts TEXT); CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, scope TEXT, secret INTEGER); @@ -115,11 +115,19 @@ describe('PluginRuntimeService (M2 end-to-end)', () => { process.env.TREK_PLUGINS_ENABLED = 'true'; }); - it('deactivate stops the plugin and marks it inactive', async () => { + it('deactivate stops the plugin and clears the enabled intent', async () => { await runtime.deactivate('counter'); expect(runtime.isActive('counter')).toBe(false); - const row = testDb.prepare("SELECT status FROM plugins WHERE id = 'counter'").get() as { status: string }; + const row = testDb.prepare("SELECT status, enabled FROM plugins WHERE id = 'counter'").get() as { status: string; enabled: number }; expect(row.status).toBe('inactive'); + expect(row.enabled).toBe(0); + }); + + it('activate sets the enabled intent so it survives a reboot', async () => { + await runtime.deactivate('counter'); + await runtime.activate('counter'); + const row = testDb.prepare("SELECT enabled FROM plugins WHERE id = 'counter'").get() as { enabled: number }; + expect(row.enabled).toBe(1); }); it('tolerates malformed granted_permissions / config JSON on activate', async () => { @@ -132,10 +140,11 @@ describe('PluginRuntimeService (M2 end-to-end)', () => { await rt.deactivate('messy'); }); - it('onModuleInit boots plugins that are active in the DB', async () => { + it('onModuleInit boots every ENABLED plugin — even one left in error state', async () => { fs.mkdirSync(path.join(codeRoot, 'booter', 'server'), { recursive: true }); fs.writeFileSync(path.join(codeRoot, 'booter', 'server', 'index.js'), 'module.exports = { async onLoad() {} };'); - testDb.prepare("INSERT INTO plugins (id, status, granted_permissions, config) VALUES ('booter','active','[]','{}')").run(); + // status='error' from a previous crash, but enabled=1 → must still boot + testDb.prepare("INSERT INTO plugins (id, status, enabled, granted_permissions, config) VALUES ('booter','error',1,'[]','{}')").run(); const rt = new PluginRuntimeService(); rt.onModuleInit(); // fire-and-forget spawn @@ -144,6 +153,17 @@ describe('PluginRuntimeService (M2 end-to-end)', () => { await rt.deactivate('booter'); }); + it('onModuleInit does NOT boot a disabled plugin', async () => { + fs.mkdirSync(path.join(codeRoot, 'sleeper', 'server'), { recursive: true }); + fs.writeFileSync(path.join(codeRoot, 'sleeper', 'server', 'index.js'), 'module.exports = { async onLoad() {} };'); + testDb.prepare("INSERT INTO plugins (id, status, enabled, granted_permissions, config) VALUES ('sleeper','inactive',0,'[]','{}')").run(); + + const rt = new PluginRuntimeService(); + rt.onModuleInit(); + await new Promise((r) => setTimeout(r, 300)); + expect(rt.isActive('sleeper')).toBe(false); + }); + it('outboundHostsOf extracts declared http:outbound hosts', () => { testDb.prepare("INSERT INTO plugins (id, status, granted_permissions, config) VALUES ('net','inactive','[\"db:own\",\"http:outbound:api.x.com\",\"http:outbound:*.y.com\"]','{}')").run(); const rt = new PluginRuntimeService(); diff --git a/server/tests/unit/plugins/plugins-service.test.ts b/server/tests/unit/plugins/plugins-service.test.ts index 9721c06a9..6a3cfb43f 100644 --- a/server/tests/unit/plugins/plugins-service.test.ts +++ b/server/tests/unit/plugins/plugins-service.test.ts @@ -9,7 +9,7 @@ const { testDb } = vi.hoisted(() => { const db = new Database(':memory:'); db.exec(`CREATE TABLE plugins ( id TEXT PRIMARY KEY, name TEXT, description TEXT, type TEXT, icon TEXT, version TEXT, - status TEXT, reviewed_at TEXT, source_repo TEXT, config TEXT DEFAULT '{}', updated_at TEXT, + status TEXT, enabled INTEGER DEFAULT 0, last_error TEXT, reviewed_at TEXT, source_repo TEXT, config TEXT DEFAULT '{}', updated_at TEXT, sort_order INTEGER DEFAULT 0); CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, scope TEXT, secret INTEGER); CREATE TABLE plugin_error_log (id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_id TEXT, level TEXT, message TEXT, ts TEXT DEFAULT '2026-01-01');`); diff --git a/shared/src/i18n/ar/admin.ts b/shared/src/i18n/ar/admin.ts index d0ce98c31..797738d6b 100644 --- a/shared/src/i18n/ar/admin.ts +++ b/shared/src/i18n/ar/admin.ts @@ -196,6 +196,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/br/admin.ts b/shared/src/i18n/br/admin.ts index ece0b1bac..fc2e43ed7 100644 --- a/shared/src/i18n/br/admin.ts +++ b/shared/src/i18n/br/admin.ts @@ -175,6 +175,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/cs/admin.ts b/shared/src/i18n/cs/admin.ts index c514afbbd..10f58c985 100644 --- a/shared/src/i18n/cs/admin.ts +++ b/shared/src/i18n/cs/admin.ts @@ -176,6 +176,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/de/admin.ts b/shared/src/i18n/de/admin.ts index 706ba1ab0..86c1e5260 100644 --- a/shared/src/i18n/de/admin.ts +++ b/shared/src/i18n/de/admin.ts @@ -178,6 +178,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Plugin deinstallieren?', 'admin.plugins.uninstallBody': 'Das stoppt das Plugin, entfernt seinen Code und löscht alle seine Daten. Das kann nicht rückgängig gemacht werden.', 'admin.plugins.trustNote': 'Plugins laufen isoliert mit nur den von dir freigegebenen Berechtigungen. Installiere nur Code von Autoren, denen du vertraust.', + 'admin.plugins.status.starting': 'Startet…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Seite', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Geprüft', + 'admin.plugins.unreviewed': 'Ungeprüft', + 'admin.plugins.updated': 'Plugin aktualisiert', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Plugin aktivieren', 'plugins.notFound': 'Plugin nicht gefunden', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Plugins von Drittanbietern auf deiner Instanz installieren und verwalten.', diff --git a/shared/src/i18n/en/admin.ts b/shared/src/i18n/en/admin.ts index 52da7fc33..5b87145de 100644 --- a/shared/src/i18n/en/admin.ts +++ b/shared/src/i18n/en/admin.ts @@ -230,6 +230,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/es/admin.ts b/shared/src/i18n/es/admin.ts index 98b4d7dde..85e1e09fb 100644 --- a/shared/src/i18n/es/admin.ts +++ b/shared/src/i18n/es/admin.ts @@ -177,6 +177,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/fr/admin.ts b/shared/src/i18n/fr/admin.ts index 0bbb42b17..85a0ef6e0 100644 --- a/shared/src/i18n/fr/admin.ts +++ b/shared/src/i18n/fr/admin.ts @@ -179,6 +179,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/gr/admin.ts b/shared/src/i18n/gr/admin.ts index f8d62ba76..22c328b75 100644 --- a/shared/src/i18n/gr/admin.ts +++ b/shared/src/i18n/gr/admin.ts @@ -224,6 +224,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/hu/admin.ts b/shared/src/i18n/hu/admin.ts index ea6deb0d9..21a1c34a8 100644 --- a/shared/src/i18n/hu/admin.ts +++ b/shared/src/i18n/hu/admin.ts @@ -178,6 +178,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/id/admin.ts b/shared/src/i18n/id/admin.ts index 0f5566250..02f5fb9fe 100644 --- a/shared/src/i18n/id/admin.ts +++ b/shared/src/i18n/id/admin.ts @@ -220,6 +220,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/it/admin.ts b/shared/src/i18n/it/admin.ts index b7ed119b3..c6549a6d2 100644 --- a/shared/src/i18n/it/admin.ts +++ b/shared/src/i18n/it/admin.ts @@ -177,6 +177,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/ja/admin.ts b/shared/src/i18n/ja/admin.ts index ac1b64148..a87931bac 100644 --- a/shared/src/i18n/ja/admin.ts +++ b/shared/src/i18n/ja/admin.ts @@ -209,6 +209,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/ko/admin.ts b/shared/src/i18n/ko/admin.ts index 64a6d1592..8f75c4d3d 100644 --- a/shared/src/i18n/ko/admin.ts +++ b/shared/src/i18n/ko/admin.ts @@ -215,6 +215,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/nl/admin.ts b/shared/src/i18n/nl/admin.ts index 2d7b484f7..aa627499b 100644 --- a/shared/src/i18n/nl/admin.ts +++ b/shared/src/i18n/nl/admin.ts @@ -178,6 +178,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/pl/admin.ts b/shared/src/i18n/pl/admin.ts index a666524e7..7856c4cc1 100644 --- a/shared/src/i18n/pl/admin.ts +++ b/shared/src/i18n/pl/admin.ts @@ -171,6 +171,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/ru/admin.ts b/shared/src/i18n/ru/admin.ts index 97fdc4e42..d2865b01e 100644 --- a/shared/src/i18n/ru/admin.ts +++ b/shared/src/i18n/ru/admin.ts @@ -178,6 +178,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/sv/admin.ts b/shared/src/i18n/sv/admin.ts index 836f29f86..b967dba11 100644 --- a/shared/src/i18n/sv/admin.ts +++ b/shared/src/i18n/sv/admin.ts @@ -230,6 +230,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/tr/admin.ts b/shared/src/i18n/tr/admin.ts index c266f930d..b71ab4007 100644 --- a/shared/src/i18n/tr/admin.ts +++ b/shared/src/i18n/tr/admin.ts @@ -222,6 +222,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/uk/admin.ts b/shared/src/i18n/uk/admin.ts index d6c3fd752..5cf2a3ce2 100644 --- a/shared/src/i18n/uk/admin.ts +++ b/shared/src/i18n/uk/admin.ts @@ -178,6 +178,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/vi/admin.ts b/shared/src/i18n/vi/admin.ts index c8e23099d..e14d8f639 100644 --- a/shared/src/i18n/vi/admin.ts +++ b/shared/src/i18n/vi/admin.ts @@ -230,6 +230,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/zh-TW/admin.ts b/shared/src/i18n/zh-TW/admin.ts index a09ec917a..c6c7c6c38 100644 --- a/shared/src/i18n/zh-TW/admin.ts +++ b/shared/src/i18n/zh-TW/admin.ts @@ -212,6 +212,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.', diff --git a/shared/src/i18n/zh/admin.ts b/shared/src/i18n/zh/admin.ts index 269f5a82f..065878a27 100644 --- a/shared/src/i18n/zh/admin.ts +++ b/shared/src/i18n/zh/admin.ts @@ -173,6 +173,15 @@ const admin: TranslationStrings = { 'admin.plugins.uninstallTitle': 'Uninstall plugin?', 'admin.plugins.uninstallBody': 'This stops the plugin, removes its code, and deletes all of its data. This cannot be undone.', 'admin.plugins.trustNote': 'Plugins run isolated with only the permissions you approve. Only install code from authors you trust.', + 'admin.plugins.status.starting': 'Starting…', + 'admin.plugins.type.widget': 'Widget', + 'admin.plugins.type.page': 'Page', + 'admin.plugins.type.integration': 'Integration', + 'admin.plugins.reviewed': 'Reviewed', + 'admin.plugins.unreviewed': 'Not reviewed', + 'admin.plugins.updated': 'Plugin updated', + 'admin.plugins.updateTo': 'Update → v{version}', + 'admin.plugins.enabledToggle': 'Enable plugin', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.',