diff --git a/frontend/e2e/plugin-manager-layout.spec.js b/frontend/e2e/plugin-manager-layout.spec.js index 69106b3..99b238d 100644 --- a/frontend/e2e/plugin-manager-layout.spec.js +++ b/frontend/e2e/plugin-manager-layout.spec.js @@ -73,6 +73,21 @@ test.describe('E: Plugin Manager layout', () => { expect(buttonStyle.borderRadius).toBe('6px'); }); + test('plugin manager summarizes plugin health and elevated permissions before the list', async ({ page }) => { + await openPluginManager(page); + + const health = page.locator('[data-plugin-manager-summary="health"]'); + await expect(health).toBeVisible(); + await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('7'); + await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0'); + await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0'); + + const risk = page.locator('[data-plugin-manager-summary="risk"]'); + await expect(risk).toBeVisible(); + await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('4'); + await expect(risk).toContainText('elevated permissions'); + }); + test('workspace selection keeps exactly one active node', async ({ page }) => { const selected = page.locator('.wt-node.selected .wt-label'); await expect(selected).toHaveCount(1); diff --git a/frontend/src/lib/plugin-manager/PluginManager.svelte b/frontend/src/lib/plugin-manager/PluginManager.svelte index 8aba0db..2651d85 100644 --- a/frontend/src/lib/plugin-manager/PluginManager.svelte +++ b/frontend/src/lib/plugin-manager/PluginManager.svelte @@ -227,6 +227,25 @@ $: totalPlugins = plugins.length; $: totalCaps = capabilities.length; $: totalPerms = permissions.length; + $: statusSummary = computeStatusSummary(plugins); + $: elevatedPermissionPluginCount = computeElevatedPermissionPluginCount(plugins, permissions); + + function computeStatusSummary(pluginRows) { + return pluginRows.reduce((summary, plugin) => { + const status = plugin?.status || 'unknown'; + if (status === 'loaded') summary.loaded += 1; + else if (status === 'degraded') summary.degraded += 1; + else if (status === 'failed' || status === 'incompatible' || status === 'missing-required-capability') summary.failed += 1; + else if (status === 'disabled' || plugin?.enabled === false) summary.disabled += 1; + else summary.other += 1; + return summary; + }, { loaded: 0, degraded: 0, failed: 0, disabled: 0, other: 0 }); + } + + function computeElevatedPermissionPluginCount(pluginRows, permissionRows) { + const dangerous = new Set((permissionRows || []).filter(permission => permission.dangerous).map(permission => permission.name)); + return (pluginRows || []).filter(plugin => (plugin?.manifest?.permissions || []).some(permission => dangerous.has(permission))).length; + } function closeSettings() { settingsPanel = null; @@ -280,6 +299,25 @@ {totalPerms} permissions known +
+
+
Plugin Health
+
+ {statusSummary.loaded} loaded + {statusSummary.degraded} degraded + {statusSummary.failed} failed + {statusSummary.disabled} disabled +
+
+ +
+
Permission Risk
+
+ {elevatedPermissionPluginCount} plugins request elevated permissions +
+
+
+ {#if plugins.length === 0 && missingInstalled.length === 0}
@@ -471,6 +509,47 @@ background: #16213e; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; color: #a0a0b8; border: 1px solid #0f3460; } + .scan-summary { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + gap: 0.75rem; + margin-bottom: 1rem; + } + .scan-card { + min-width: 0; + padding: 0.75rem; + border: 1px solid #0f3460; + border-radius: 8px; + background: #121a2c; + } + .scan-card-title { + color: #e0e0f0; + font-size: 0.82rem; + font-weight: 700; + margin-bottom: 0.5rem; + } + .scan-metrics { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.45rem; + } + .scan-metrics span { + min-height: 1.55rem; + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.18rem 0.5rem; + border: 1px solid rgba(78, 204, 163, 0.16); + border-radius: 5px; + color: #a0a0b8; + background: #101626; + font-size: 0.76rem; + } + .scan-metrics strong { + color: #f4f7fb; + font-size: 0.88rem; + } .empty { padding: 2rem; text-align: center; color: #a0a0b8; background: #16213e; border-radius: 8px; border: 1px dashed #0f3460; @@ -542,6 +621,10 @@ width: 100%; } + .scan-summary { + grid-template-columns: 1fr; + } + .modal { width: min(880px, calc(100vw - 2rem)); height: min(680px, calc(100vh - 2rem));