diff --git a/client/src/api/client.ts b/client/src/api/client.ts index d1f5de501..d1245eccf 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -461,6 +461,13 @@ export const adminApi = { addons: () => apiClient.get('/admin/addons').then(r => r.data), updateAddon: (id: number | string, data: Record) => apiClient.put(`/admin/addons/${id}`, data).then(r => r.data), plugins: () => apiClient.get('/admin/plugins').then(r => r.data), + pluginBrowse: () => apiClient.get('/admin/plugins/registry').then(r => r.data), + pluginInstall: (id: string, version?: string) => apiClient.post('/admin/plugins/install', { id, version }).then(r => r.data), + pluginActivate: (id: string) => apiClient.post(`/admin/plugins/${id}/activate`).then(r => r.data), + pluginDeactivate: (id: string) => apiClient.post(`/admin/plugins/${id}/deactivate`).then(r => r.data), + pluginUninstall: (id: string, deleteData: boolean) => apiClient.post(`/admin/plugins/${id}/uninstall`, { deleteData }).then(r => r.data), + pluginRescan: () => apiClient.post('/admin/plugins/rescan').then(r => r.data), + pluginErrors: (id: string) => apiClient.get(`/admin/plugins/${id}/errors`).then(r => r.data), // Local LLM (Ollama) management for the AI-parsing addon. llmLocalModels: (baseUrl: string): Promise<{ models: { name: string; size: number }[] }> => apiClient.get('/admin/llm/local/models', { params: { baseUrl } }).then(r => r.data), diff --git a/client/src/components/Admin/AdminPluginsPanel.tsx b/client/src/components/Admin/AdminPluginsPanel.tsx index b80519509..198b353f8 100644 --- a/client/src/components/Admin/AdminPluginsPanel.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.tsx @@ -1,13 +1,14 @@ import { useEffect, useState } from 'react' -import { Blocks, AlertTriangle, PackageOpen } from 'lucide-react' +import { Blocks, AlertTriangle, PackageOpen, RefreshCw, Power, Trash2, Download, Bug, X } from 'lucide-react' import { adminApi } from '../../api/client' import { useTranslation } from '../../i18n' +import { useToast } from '../shared/Toast' +import ConfirmDialog from '../shared/ConfirmDialog' /** - * Admin → Plugins (#plugins), M0 read-only scaffold. Lists installed plugins - * from the registry table and shows whether the runtime is enabled - * (TREK_PLUGINS_ENABLED). Install, activation and the registry browser arrive in - * later milestones; this panel is intentionally inert. + * 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. */ interface PluginRow { @@ -15,11 +16,18 @@ interface PluginRow { name: string description: string | null type: string - icon: string | null version: string | null status: string reviewed_at: string | null - source_repo: string | null +} +interface RegistryItem { + id: string + name: string + author: string + description: string + type: string + latest: string | null + reviewedAt: string | null } const STATUS_CLASS: Record = { @@ -32,23 +40,39 @@ const STATUS_CLASS: Record = { export default function AdminPluginsPanel() { const { t } = useTranslation() + const toast = useToast() const [enabled, setEnabled] = 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 [errorsFor, setErrorsFor] = useState<{ id: string; rows: Array<{ ts: string; level: string; message: string }> } | null>(null) + const [confirmUninstall, setConfirmUninstall] = useState(null) - useEffect(() => { - let alive = true + const refresh = () => { adminApi.plugins() - .then((d: { enabled: boolean; plugins: PluginRow[] }) => { - if (!alive) return - setEnabled(!!d.enabled) - setPlugins(d.plugins || []) - }) - .catch(() => { if (alive) setError(true) }) - .finally(() => { if (alive) setLoading(false) }) - return () => { alive = false } - }, []) + .then((d: { enabled: boolean; plugins: PluginRow[] }) => { setEnabled(!!d.enabled); setPlugins(d.plugins || []) }) + .catch(() => setError(true)) + .finally(() => setLoading(false)) + } + useEffect(refresh, []) + + const act = async (id: string, fn: () => Promise, ok: string) => { + setBusy(id) + try { await fn(); toast.success(ok); refresh() } + catch (e) { toast.error((e as { response?: { data?: { error?: string } } })?.response?.data?.error || t('admin.plugins.actionError')) } + finally { setBusy(null) } + } + + const openBrowse = () => { + setView('browse') + 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: [] })) return (
@@ -56,10 +80,22 @@ export default function AdminPluginsPanel() {
-
+

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

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

+ {enabled && ( +
+ + +
+ )}
{!enabled && !loading && !error && ( @@ -77,6 +113,9 @@ export default function AdminPluginsPanel() {
{t('common.loading')}
) : error ? (
{t('admin.plugins.loadError')}
+ ) : view === 'browse' ? ( + act(id, () => adminApi.pluginInstall(id), t('admin.plugins.installed'))} /> ) : plugins.length === 0 ? (
@@ -93,12 +132,24 @@ export default function AdminPluginsPanel() {
{p.name} {p.version && v{p.version}} + · {p.type}
{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)}> +
))} @@ -106,8 +157,81 @@ export default function AdminPluginsPanel() {
-

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

+

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

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

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

: + errorsFor.rows.map((r, i) => ( +
+ {r.level} + {r.ts} + {r.message} +
+ ))} +
+
+
+ )} + + setConfirmUninstall(null)} + onConfirm={async () => { + const id = confirmUninstall!; setConfirmUninstall(null) + await act(id, () => adminApi.pluginUninstall(id, true), t('admin.plugins.uninstalled')) + }} + title={t('admin.plugins.uninstallTitle')} + message={t('admin.plugins.uninstallBody')} + /> + + ) +} + +function IconBtn({ children, title, onClick, disabled, accent, danger }: { + children: React.ReactNode; title: string; onClick: () => void; disabled?: boolean; accent?: boolean; danger?: boolean +}) { + return ( + + ) +} + +function RegistryGrid({ items, onInstall, busy, t }: { + items: RegistryItem[] | null + onInstall: (id: string) => void + busy: string | null + t: (k: string) => string +}) { + 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} +
+

{item.description}

+
+ {item.author} · {item.type} + +
+
+ ))}
) } diff --git a/client/src/components/Plugins/PluginWidgets.tsx b/client/src/components/Plugins/PluginWidgets.tsx new file mode 100644 index 000000000..b7308fca3 --- /dev/null +++ b/client/src/components/Plugins/PluginWidgets.tsx @@ -0,0 +1,24 @@ +import PluginFrame from './PluginFrame' +import type { ActivePlugin } from '../../store/pluginStore' + +/** + * Renders active `widget` plugins as dashboard cards (#plugins, M8). Each is a + * sandboxed PluginFrame; the widget talks to TREK only over the bridge. + */ +export default function PluginWidgets({ plugins, tripId = null }: { plugins: ActivePlugin[]; tripId?: string | null }) { + if (plugins.length === 0) return null + return ( + <> + {plugins.map((p) => ( +
+
+ {p.name} +
+
+ +
+
+ ))} + + ) +} diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index c3c1d1f1d..2968e59aa 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -20,6 +20,8 @@ import { } from 'lucide-react' import { IcsSubscribeModal } from '../components/Planner/IcsSubscribeModal' import CollectionsWidget from '../components/Dashboard/CollectionsWidget' +import PluginWidgets from '../components/Plugins/PluginWidgets' +import { usePluginStore } from '../store/pluginStore' import { formatTime, splitReservationDateTime } from '../utils/formatters' import { convertDistance, getDistanceUnitLabel } from '../utils/units' import { useSettingsStore } from '../store/settingsStore' @@ -125,7 +127,8 @@ export default function DashboardPage(): React.ReactElement { const isAddonEnabled = useAddonStore(s => s.isEnabled) const showCollections = isAddonEnabled('collections') && sideWidgets.collections // Desktop has a master toggle for the whole right column; off → centered layout. - const sidebarVisible = (isMobile || dashCfg.desktop.sidebar) && (showCurrency || showCollections || showTimezones || showUpcoming) + const widgetPlugins = usePluginStore(s => s.plugins).filter(p => p.type === 'widget') + const sidebarVisible = (isMobile || dashCfg.desktop.sidebar) && (showCurrency || showCollections || showTimezones || showUpcoming || widgetPlugins.length > 0) return ( <> @@ -234,6 +237,7 @@ export default function DashboardPage(): React.ReactElement { {showCollections && navigate('/collections')} />} {showTimezones && } {showUpcoming && navigate(`/trips/${tripId}`)} />} + )} diff --git a/plugin-sdk/test/sdk.test.ts b/plugin-sdk/test/sdk.test.ts index 944076200..eb0b13ec7 100644 --- a/plugin-sdk/test/sdk.test.ts +++ b/plugin-sdk/test/sdk.test.ts @@ -85,3 +85,20 @@ describe('scaffold + validate CLIs', () => { expect(validatePluginDir(tmp).ok).toBe(false); }); }); + +describe('reference plugin (examples/trip-countdown)', () => { + const dir = path.resolve(import.meta.dirname, '..', 'examples', 'trip-countdown'); + + it('passes the same validation authors run', () => { + const r = validatePluginDir(dir); + expect(r.errors).toEqual([]); + expect(r.ok).toBe(true); + }); + + it('has a valid, minimal-permission manifest', () => { + const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'trek-plugin.json'), 'utf8')); + const res = validateManifest(manifest); + expect(res.ok).toBe(true); + expect(res.manifest?.permissions).toEqual(['db:read:trips']); + }); +}); diff --git a/server/src/nest/plugins/plugin-runtime.service.ts b/server/src/nest/plugins/plugin-runtime.service.ts index d954ce10b..a21442c4a 100644 --- a/server/src/nest/plugins/plugin-runtime.service.ts +++ b/server/src/nest/plugins/plugin-runtime.service.ts @@ -3,9 +3,12 @@ import { db } from '../../db/database'; import { pluginsEnabled } from './kill-switch'; import { decrypt_api_key } from '../../services/apiKeyCrypto'; import { PluginSupervisor, type PluginRouteInfo } from './supervisor/plugin-supervisor'; +import fs from 'node:fs'; import { createRealRpcHost, closePluginDataDb } from './host/create-rpc-host'; +import { removePluginData } from './host/plugin-data.service'; import { isKnownPermission } from './protocol/envelope'; import { discoverPlugins } from './install/discovery'; +import { pluginCodeDir } from './paths'; /** * Owns the isolated-plugin runtime lifecycle inside NestJS (#plugins, M2). @@ -78,7 +81,8 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy { const declared = parseArray(row.permissions).filter(isKnownPermission); db.prepare('UPDATE plugins SET granted_permissions = ? WHERE id = ?').run(JSON.stringify(declared), id); const config = decryptConfig(parseObject(row.config)); - await this.supervisor.activate(id, new Set(declared), 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); } async deactivate(id: string): Promise { @@ -87,6 +91,21 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy { db.prepare("UPDATE plugins SET status = 'inactive', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(id); } + /** Stop the plugin, remove its code, and optionally delete all its data. */ + async uninstall(id: string, deleteData: boolean): Promise { + await this.supervisor.disable(id); + closePluginDataDb(id); + // Code always goes; the DB metadata + fields go so it disappears from the UI. + fs.rmSync(pluginCodeDir(id), { recursive: true, force: true }); + db.prepare('DELETE FROM plugins WHERE id = ?').run(id); + db.prepare('DELETE FROM plugin_settings_fields WHERE plugin_id = ?').run(id); + if (deleteData) { + removePluginData(id); + db.prepare('DELETE FROM plugin_error_log WHERE plugin_id = ?').run(id); + db.prepare("DELETE FROM settings WHERE key LIKE ?").run(`plugin:${id}:%`); + } + } + isActive(id: string): boolean { return this.supervisor.isActive(id); } diff --git a/server/src/nest/plugins/plugins.controller.ts b/server/src/nest/plugins/plugins.controller.ts index d18200934..4ee32d968 100644 --- a/server/src/nest/plugins/plugins.controller.ts +++ b/server/src/nest/plugins/plugins.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpCode, HttpException, Param, Post, Put, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpException, Param, Post, Put, UseGuards } from '@nestjs/common'; import { PluginsService } from './plugins.service'; import { PluginRuntimeService } from './plugin-runtime.service'; import { PluginRegistryService } from './registry/registry.service'; @@ -74,6 +74,24 @@ export class PluginsController { return { status: 'inactive' }; } + @Post(':id/uninstall') + @HttpCode(200) + async uninstall(@Param('id') id: string, @Body() body: { deleteData?: boolean }) { + await this.runtime.uninstall(id, !!body?.deleteData); + return { status: 'uninstalled' }; + } + + @Get(':id/errors') + errors(@Param('id') id: string) { + return { errors: this.plugins.errors(id) }; + } + + @Delete(':id/errors') + clearErrors(@Param('id') id: string) { + this.plugins.clearErrors(id); + return { ok: true }; + } + @Post('rescan') @HttpCode(200) rescan() { diff --git a/server/src/nest/plugins/plugins.service.ts b/server/src/nest/plugins/plugins.service.ts index 20ea074b0..c71036fea 100644 --- a/server/src/nest/plugins/plugins.service.ts +++ b/server/src/nest/plugins/plugins.service.ts @@ -67,6 +67,17 @@ export class PluginsService { return maskSecrets(config, secretKeys); } + /** A plugin's error log, newest first. */ + errors(id: string): Array<{ ts: string; level: string; message: string }> { + return db + .prepare('SELECT ts, level, message FROM plugin_error_log WHERE plugin_id = ? ORDER BY ts DESC, id DESC LIMIT 200') + .all(id) as Array<{ ts: string; level: string; message: string }>; + } + + clearErrors(id: string): void { + db.prepare('DELETE FROM plugin_error_log WHERE plugin_id = ?').run(id); + } + /** Read the instance config with secret fields masked. */ getInstanceConfig(id: string): Record { const row = db.prepare('SELECT config FROM plugins WHERE id = ?').get(id) as { config: string } | undefined; diff --git a/server/src/nest/plugins/runtime/plugin-host-entry.ts b/server/src/nest/plugins/runtime/plugin-host-entry.ts index 3f60e715c..d467248cd 100644 --- a/server/src/nest/plugins/runtime/plugin-host-entry.ts +++ b/server/src/nest/plugins/runtime/plugin-host-entry.ts @@ -125,11 +125,41 @@ process.on('message', (raw: unknown) => { return; } if (msg.k === 'evt') { - if (msg.topic === 'init') void boot(((msg.data as { config?: Record })?.config) ?? {}); - else if (msg.topic === 'shutdown') void shutdown(); + if (msg.topic === 'init') { + const d = msg.data as { config?: Record; egress?: string[] }; + installEgressGuard(d.egress ?? []); + void boot(d.config ?? {}); + } else if (msg.topic === 'shutdown') void shutdown(); } }); +/** + * Restrict the plugin's outbound network to its declared egress hosts. With no + * declared egress, ALL outbound fetch is blocked. Wildcards like `*.host` match + * any subdomain. This is process-level defense in depth; the container runtime + * (v2) enforces it at the network layer. + */ +function installEgressGuard(egress: string[]): void { + const patterns = egress.map((h) => h.trim().toLowerCase()).filter(Boolean); + const allowed = (hostname: string): boolean => { + const h = hostname.toLowerCase(); + return patterns.some((p) => (p.startsWith('*.') ? h === p.slice(2) || h.endsWith(p.slice(1)) : h === p)); + }; + const realFetch = globalThis.fetch; + if (typeof realFetch !== 'function') return; + globalThis.fetch = ((input: unknown, init?: unknown) => { + const url = typeof input === 'string' ? input : (input as { url?: string })?.url ?? String(input); + let host: string; + try { + host = new URL(url).hostname; + } catch { + return Promise.reject(new Error('egress: invalid url')); + } + if (!allowed(host)) return Promise.reject(new Error(`egress: ${host} is not in the plugin's declared hosts`)); + return (realFetch as (i: unknown, n: unknown) => Promise)(input, init); + }) as typeof fetch; +} + // Ask the host for the init payload (instance config), then wait for it. send({ k: 'evt', topic: 'hello', data: {} }); diff --git a/server/src/nest/plugins/supervisor/plugin-supervisor.ts b/server/src/nest/plugins/supervisor/plugin-supervisor.ts index 4470e4b47..6bfba6e74 100644 --- a/server/src/nest/plugins/supervisor/plugin-supervisor.ts +++ b/server/src/nest/plugins/supervisor/plugin-supervisor.ts @@ -39,6 +39,7 @@ interface Supervised { id: string; granted: ReadonlySet; config: Record; + egress: string[]; rpcHost: PluginRpcHost; child: ChildProcess | null; status: PluginStatus; @@ -80,12 +81,13 @@ export class PluginSupervisor { } /** Spawn a plugin and resolve once it reports `loaded` (or reject on load error). */ - activate(id: string, granted: ReadonlySet, config: Record = {}): Promise { + activate(id: string, granted: ReadonlySet, config: Record = {}, egress: string[] = []): Promise { if (this.running.has(id)) return Promise.resolve(); const sup: Supervised = { id, granted, config, + egress, rpcHost: this.createRpcHost(id, granted), child: null, status: 'starting', @@ -201,7 +203,7 @@ export class PluginSupervisor { if (msg.k === 'evt') { switch (msg.topic) { case 'hello': - sup.child?.send({ k: 'evt', topic: 'init', data: { config: sup.config } } satisfies Envelope); + sup.child?.send({ k: 'evt', topic: 'init', data: { config: sup.config, egress: sup.egress } } satisfies Envelope); break; case 'heartbeat': sup.lastBeat = Date.now(); diff --git a/server/tests/integration/plugins/plugin-runtime.test.ts b/server/tests/integration/plugins/plugin-runtime.test.ts index 001b7034a..32ac3cbff 100644 --- a/server/tests/integration/plugins/plugin-runtime.test.ts +++ b/server/tests/integration/plugins/plugin-runtime.test.ts @@ -15,7 +15,8 @@ const { testDb } = vi.hoisted(() => { id TEXT PRIMARY KEY, status TEXT, 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);`); + CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, scope TEXT, secret INTEGER); + CREATE TABLE settings (user_id INTEGER, key TEXT, value TEXT);`); return { testDb: db }; }); vi.mock('../../../src/db/database', () => ({ db: testDb, canAccessTrip: () => undefined })); @@ -150,6 +151,31 @@ describe('PluginRuntimeService (M2 end-to-end)', () => { expect(rt.outboundHostsOf('missing')).toEqual([]); }); + it('uninstall removes the code, DB rows, settings and (with deleteData) data', async () => { + fs.mkdirSync(path.join(codeRoot, 'gone', 'server'), { recursive: true }); + fs.writeFileSync(path.join(codeRoot, 'gone', 'server', 'index.js'), 'module.exports={}'); + testDb.prepare("INSERT INTO plugins (id, status, permissions, config) VALUES ('gone','inactive','[]','{}')").run(); + testDb.prepare("INSERT INTO plugin_settings_fields (plugin_id, field_key, scope, secret) VALUES ('gone','k','instance',0)").run(); + testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (1, 'plugin:gone:units', 'metric')").run(); + + await new PluginRuntimeService().uninstall('gone', true); + + expect(fs.existsSync(path.join(codeRoot, 'gone'))).toBe(false); + expect(testDb.prepare("SELECT COUNT(*) c FROM plugins WHERE id='gone'").get()).toMatchObject({ c: 0 }); + expect(testDb.prepare("SELECT COUNT(*) c FROM plugin_settings_fields WHERE plugin_id='gone'").get()).toMatchObject({ c: 0 }); + expect(testDb.prepare("SELECT COUNT(*) c FROM settings WHERE key LIKE 'plugin:gone:%'").get()).toMatchObject({ c: 0 }); + }); + + it('the egress guard blocks a fetch to an undeclared host', async () => { + fs.mkdirSync(path.join(codeRoot, 'nettry', 'server'), { recursive: true }); + fs.writeFileSync(path.join(codeRoot, 'nettry', 'server', 'index.js'), + "module.exports = { async onLoad() { await fetch('https://blocked.example/x'); } };"); + testDb.prepare("INSERT INTO plugins (id, status, permissions, config) VALUES ('nettry','inactive','[]','{}')").run(); + // no http:outbound granted -> egress guard blocks all outbound -> onLoad throws + await expect(new PluginRuntimeService().activate('nettry')).rejects.toThrow(/egress/); + await new PluginRuntimeService().deactivate('nettry').catch(() => {}); + }); + it('onModuleDestroy tears down cleanly', async () => { await expect(new PluginRuntimeService().onModuleDestroy()).resolves.toBeUndefined(); }); diff --git a/server/tests/unit/plugins/plugins-service.test.ts b/server/tests/unit/plugins/plugins-service.test.ts index e8a3ef274..9721c06a9 100644 --- a/server/tests/unit/plugins/plugins-service.test.ts +++ b/server/tests/unit/plugins/plugins-service.test.ts @@ -11,7 +11,8 @@ const { testDb } = vi.hoisted(() => { 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, sort_order INTEGER DEFAULT 0); - CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, scope TEXT, secret INTEGER);`); + 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');`); return { testDb: db }; }); vi.mock('../../../src/db/database', () => ({ db: testDb })); @@ -144,3 +145,14 @@ describe('PluginsService instance config', () => { expect(() => new PluginsService().getInstanceConfig('nope')).toThrow(/not found/); }); }); + +describe('PluginsService error log', () => { + beforeEach(() => testDb.exec('DELETE FROM plugin_error_log')); + it('lists and clears a plugin error log', () => { + testDb.prepare("INSERT INTO plugin_error_log (plugin_id, level, message) VALUES ('p','error','boom')").run(); + const svc = new PluginsService(); + expect(svc.errors('p')).toEqual([{ ts: '2026-01-01', level: 'error', message: 'boom' }]); + svc.clearErrors('p'); + expect(svc.errors('p')).toEqual([]); + }); +}); diff --git a/shared/src/i18n/ar/admin.ts b/shared/src/i18n/ar/admin.ts index a135894fa..d0ce98c31 100644 --- a/shared/src/i18n/ar/admin.ts +++ b/shared/src/i18n/ar/admin.ts @@ -178,6 +178,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'فشل الحفظ', 'admin.tabs.addons': 'الإضافات', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 f710c4137..ece0b1bac 100644 --- a/shared/src/i18n/br/admin.ts +++ b/shared/src/i18n/br/admin.ts @@ -157,6 +157,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Falha ao salvar', 'admin.tabs.addons': 'Complementos', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 6ae099377..c514afbbd 100644 --- a/shared/src/i18n/cs/admin.ts +++ b/shared/src/i18n/cs/admin.ts @@ -158,6 +158,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Uložení se nezdařilo', 'admin.tabs.addons': 'Doplňky', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 4f140f764..706ba1ab0 100644 --- a/shared/src/i18n/de/admin.ts +++ b/shared/src/i18n/de/admin.ts @@ -160,6 +160,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Fehler beim Speichern', 'admin.tabs.addons': 'Addons', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Neu scannen', + 'admin.plugins.rescanned': 'Plugin-Ordner neu gescannt', + 'admin.plugins.browse': 'Plugins durchsuchen', + 'admin.plugins.installed': 'Installiert', + 'admin.plugins.install': 'Installieren', + 'admin.plugins.registryEmpty': 'Noch keine Plugins in der Registry verfügbar.', + 'admin.plugins.actionError': 'Aktion fehlgeschlagen', + 'admin.plugins.activate': 'Aktivieren', + 'admin.plugins.activated': 'Plugin aktiviert', + 'admin.plugins.deactivate': 'Deaktivieren', + 'admin.plugins.deactivated': 'Plugin deaktiviert', + 'admin.plugins.viewErrors': 'Fehlerprotokoll ansehen', + 'admin.plugins.errorLog': 'Fehlerprotokoll', + 'admin.plugins.noErrors': 'Keine Fehler protokolliert.', + 'admin.plugins.uninstalled': 'Plugin deinstalliert', + '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.', '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 11bf31734..52da7fc33 100644 --- a/shared/src/i18n/en/admin.ts +++ b/shared/src/i18n/en/admin.ts @@ -212,6 +212,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Failed to save', 'admin.tabs.addons': 'Addons', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 5ff2fe965..98b4d7dde 100644 --- a/shared/src/i18n/es/admin.ts +++ b/shared/src/i18n/es/admin.ts @@ -159,6 +159,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Error al guardar', 'admin.tabs.addons': 'Complementos', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 eba6ca83a..0bbb42b17 100644 --- a/shared/src/i18n/fr/admin.ts +++ b/shared/src/i18n/fr/admin.ts @@ -161,6 +161,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Erreur de sauvegarde', 'admin.tabs.addons': 'Extensions', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 ee7fb52f4..f8d62ba76 100644 --- a/shared/src/i18n/gr/admin.ts +++ b/shared/src/i18n/gr/admin.ts @@ -206,6 +206,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Αποτυχία αποθήκευσης', 'admin.tabs.addons': 'Πρόσθετα', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 3c525d90e..ea6deb0d9 100644 --- a/shared/src/i18n/hu/admin.ts +++ b/shared/src/i18n/hu/admin.ts @@ -160,6 +160,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Nem sikerült menteni', 'admin.tabs.addons': 'Bővítmények', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 efed83e38..0f5566250 100644 --- a/shared/src/i18n/id/admin.ts +++ b/shared/src/i18n/id/admin.ts @@ -202,6 +202,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Gagal menyimpan', 'admin.tabs.addons': 'Addon', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 b96a5cfa8..b7ed119b3 100644 --- a/shared/src/i18n/it/admin.ts +++ b/shared/src/i18n/it/admin.ts @@ -159,6 +159,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Impossibile salvare', 'admin.tabs.addons': 'Moduli', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 3aa991710..ac1b64148 100644 --- a/shared/src/i18n/ja/admin.ts +++ b/shared/src/i18n/ja/admin.ts @@ -191,6 +191,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': '保存に失敗しました', 'admin.tabs.addons': 'アドオン', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 41d635dac..64a6d1592 100644 --- a/shared/src/i18n/ko/admin.ts +++ b/shared/src/i18n/ko/admin.ts @@ -197,6 +197,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': '저장 실패', 'admin.tabs.addons': '애드온', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 0f9a7a3cb..2d7b484f7 100644 --- a/shared/src/i18n/nl/admin.ts +++ b/shared/src/i18n/nl/admin.ts @@ -160,6 +160,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Fout bij opslaan', 'admin.tabs.addons': 'Add-ons', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 92df20e5e..a666524e7 100644 --- a/shared/src/i18n/pl/admin.ts +++ b/shared/src/i18n/pl/admin.ts @@ -153,6 +153,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Nie udało się zapisać szablonu', 'admin.tabs.addons': 'Dodatki', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 0714c02b5..97fdc4e42 100644 --- a/shared/src/i18n/ru/admin.ts +++ b/shared/src/i18n/ru/admin.ts @@ -160,6 +160,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Ошибка сохранения', 'admin.tabs.addons': 'Дополнения', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 b7ede99f6..836f29f86 100644 --- a/shared/src/i18n/sv/admin.ts +++ b/shared/src/i18n/sv/admin.ts @@ -212,6 +212,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Det gick inte att spara', 'admin.tabs.addons': 'Tillägg', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 aa4482be1..c266f930d 100644 --- a/shared/src/i18n/tr/admin.ts +++ b/shared/src/i18n/tr/admin.ts @@ -204,6 +204,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Kaydedilemedi', 'admin.tabs.addons': 'Eklentiler', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 f67cdfc54..d6c3fd752 100644 --- a/shared/src/i18n/uk/admin.ts +++ b/shared/src/i18n/uk/admin.ts @@ -160,6 +160,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Помилка збереження', 'admin.tabs.addons': 'Дополнения', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 b11394b36..c8e23099d 100644 --- a/shared/src/i18n/vi/admin.ts +++ b/shared/src/i18n/vi/admin.ts @@ -212,6 +212,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': 'Không lưu được', 'admin.tabs.addons': 'Tiện ích bổ sung', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 fdd63af91..a09ec917a 100644 --- a/shared/src/i18n/zh-TW/admin.ts +++ b/shared/src/i18n/zh-TW/admin.ts @@ -194,6 +194,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': '儲存失敗', 'admin.tabs.addons': '擴充套件', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', '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 d9a82beaf..269f5a82f 100644 --- a/shared/src/i18n/zh/admin.ts +++ b/shared/src/i18n/zh/admin.ts @@ -155,6 +155,24 @@ const admin: TranslationStrings = { 'admin.packingTemplates.saveError': '保存失败', 'admin.tabs.addons': '扩展', 'admin.tabs.plugins': 'Plugins', + 'admin.plugins.rescan': 'Rescan', + 'admin.plugins.rescanned': 'Rescanned the plugins folder', + 'admin.plugins.browse': 'Browse plugins', + 'admin.plugins.installed': 'Installed', + 'admin.plugins.install': 'Install', + 'admin.plugins.registryEmpty': 'No plugins available in the registry yet.', + 'admin.plugins.actionError': 'Action failed', + 'admin.plugins.activate': 'Activate', + 'admin.plugins.activated': 'Plugin activated', + 'admin.plugins.deactivate': 'Deactivate', + 'admin.plugins.deactivated': 'Plugin deactivated', + 'admin.plugins.viewErrors': 'View error log', + 'admin.plugins.errorLog': 'Error log', + 'admin.plugins.noErrors': 'No errors logged.', + 'admin.plugins.uninstalled': 'Plugin uninstalled', + '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.', 'plugins.notFound': 'Plugin not found', 'admin.plugins.title': 'Plugins', 'admin.plugins.subtitle': 'Install and manage third-party plugins on your instance.',