Add plugin manager scan summary

This commit is contained in:
mirivlad 2026-07-01 03:08:18 +08:00
parent 67468e92f7
commit d3dfa2d652
2 changed files with 98 additions and 0 deletions

View File

@ -73,6 +73,21 @@ test.describe('E: Plugin Manager layout', () => {
expect(buttonStyle.borderRadius).toBe('6px'); 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 }) => { test('workspace selection keeps exactly one active node', async ({ page }) => {
const selected = page.locator('.wt-node.selected .wt-label'); const selected = page.locator('.wt-node.selected .wt-label');
await expect(selected).toHaveCount(1); await expect(selected).toHaveCount(1);

View File

@ -227,6 +227,25 @@
$: totalPlugins = plugins.length; $: totalPlugins = plugins.length;
$: totalCaps = capabilities.length; $: totalCaps = capabilities.length;
$: totalPerms = permissions.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() { function closeSettings() {
settingsPanel = null; settingsPanel = null;
@ -280,6 +299,25 @@
<span class="badge">{totalPerms} permissions known</span> <span class="badge">{totalPerms} permissions known</span>
</div> </div>
<div class="scan-summary" aria-label="Plugin scan summary">
<section class="scan-card" data-plugin-manager-summary="health">
<div class="scan-card-title">Plugin Health</div>
<div class="scan-metrics">
<span data-plugin-status-summary="loaded"><strong>{statusSummary.loaded}</strong> loaded</span>
<span data-plugin-status-summary="degraded"><strong>{statusSummary.degraded}</strong> degraded</span>
<span data-plugin-status-summary="failed"><strong>{statusSummary.failed}</strong> failed</span>
<span data-plugin-status-summary="disabled"><strong>{statusSummary.disabled}</strong> disabled</span>
</div>
</section>
<section class="scan-card" data-plugin-manager-summary="risk">
<div class="scan-card-title">Permission Risk</div>
<div class="scan-metrics">
<span data-plugin-risk-summary="elevated-permissions"><strong>{elevatedPermissionPluginCount}</strong> plugins request elevated permissions</span>
</div>
</section>
</div>
{#if plugins.length === 0 && missingInstalled.length === 0} {#if plugins.length === 0 && missingInstalled.length === 0}
<div class="empty"> <div class="empty">
<div class="empty-icon"> <div class="empty-icon">
@ -471,6 +509,47 @@
background: #16213e; padding: 0.25rem 0.75rem; border-radius: 12px; background: #16213e; padding: 0.25rem 0.75rem; border-radius: 12px;
font-size: 0.8rem; color: #a0a0b8; border: 1px solid #0f3460; 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 { .empty {
padding: 2rem; text-align: center; color: #a0a0b8; padding: 2rem; text-align: center; color: #a0a0b8;
background: #16213e; border-radius: 8px; border: 1px dashed #0f3460; background: #16213e; border-radius: 8px; border: 1px dashed #0f3460;
@ -542,6 +621,10 @@
width: 100%; width: 100%;
} }
.scan-summary {
grid-template-columns: 1fr;
}
.modal { .modal {
width: min(880px, calc(100vw - 2rem)); width: min(880px, calc(100vw - 2rem));
height: min(680px, calc(100vh - 2rem)); height: min(680px, calc(100vh - 2rem));