mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat(ui): add security shield icon in sidepanel header (3 states)
Small "SEC" badge in the top-right of the sidepanel that reflects the
security module's current state. Three states drive color:
protected green — all layers ok (TestSavantAI + transcript + canary)
degraded amber — one+ ML layer offline but canary + arch controls active
inactive red — security module crashed, arch controls only
Consumes /health.security (surfaced in commit 7e9600ff). Updated once on
connection bootstrap. Shield stays hidden until /health arrives so the user
never sees a flickering "unknown" state.
Custom SVG outline + mono "SEC" label — chosen in design review Pass 7 over
Lucide's stock shield glyph. Matches the industrial/CLI brand voice in
DESIGN.md ("monospace as personality font").
Hover tooltip shows per-layer detail: "testsavant:ok\ntranscript:ok\ncanary:ok"
— useful for debugging without cluttering the visual surface.
Known v1 limitation: only updates at connection bootstrap. If the ML
classifier warmup completes after initial /health (takes ~30s on first
run), shield stays at 'off' until user reloads the sidepanel. Follow-up
TODO: extend /sidebar-chat polling to refresh security state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,39 @@
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
/* ─── Security Shield ───────────────────────────────────────────── */
|
||||
/* 3 states — green=protected, amber=degraded, red=inactive.
|
||||
Custom SVG outline + "SEC" label in JetBrains Mono to match the
|
||||
industrial/CLI aesthetic (design review Pass 7 decision). */
|
||||
|
||||
.security-shield {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transition: color 200ms ease-out, background 200ms ease-out;
|
||||
cursor: default;
|
||||
}
|
||||
.security-shield[data-status="protected"] {
|
||||
color: var(--success, #22C55E);
|
||||
}
|
||||
.security-shield[data-status="degraded"] {
|
||||
color: var(--amber-400, #FBBF24);
|
||||
}
|
||||
.security-shield[data-status="inactive"] {
|
||||
color: var(--error, #EF4444);
|
||||
}
|
||||
|
||||
/* ─── Connection Banner ─────────────────────────────────────────── */
|
||||
|
||||
.conn-banner {
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
<link rel="stylesheet" href="sidepanel.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Security shield — reflects ~/.gstack/security/session-state.json status.
|
||||
Hidden until the sidebar knows its state (avoids flicker on first load).
|
||||
Consumes /health.security — see browse/src/security.ts getStatus(). -->
|
||||
<div class="security-shield" id="security-shield" role="status" aria-label="Security status: unknown" style="display:none" title="Security">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
<span class="security-shield-label" id="security-shield-label">SEC</span>
|
||||
</div>
|
||||
|
||||
<!-- Connection status banner -->
|
||||
<div class="conn-banner" id="conn-banner" style="display:none">
|
||||
<span class="conn-banner-text" id="conn-banner-text">Reconnecting...</span>
|
||||
|
||||
@@ -176,6 +176,35 @@ function hideSecurityBanner() {
|
||||
if (banner) banner.style.display = 'none';
|
||||
}
|
||||
|
||||
// Shield icon state update — consumes /health.security.status.
|
||||
// status ∈ { 'protected', 'degraded', 'inactive' }.
|
||||
// 'protected' = all layers ok. 'degraded' = at least one ML layer off or failed
|
||||
// (sidebar still defended by canary + architectural controls).
|
||||
// 'inactive' = security module crashed — only architectural controls active.
|
||||
const SHIELD_LABELS = {
|
||||
protected: { label: 'SEC', aria: 'Security status: protected' },
|
||||
degraded: { label: 'SEC', aria: 'Security status: degraded (some layers offline)' },
|
||||
inactive: { label: 'SEC', aria: 'Security status: inactive (architectural controls only)' },
|
||||
};
|
||||
function updateSecurityShield(securityState) {
|
||||
const shield = document.getElementById('security-shield');
|
||||
const labelEl = document.getElementById('security-shield-label');
|
||||
if (!shield || !securityState) return;
|
||||
const status = securityState.status || 'inactive';
|
||||
const info = SHIELD_LABELS[status] || SHIELD_LABELS.inactive;
|
||||
shield.setAttribute('data-status', status);
|
||||
shield.setAttribute('aria-label', info.aria);
|
||||
shield.style.display = 'inline-flex';
|
||||
if (labelEl) labelEl.textContent = info.label;
|
||||
// Hover tooltip gives layer-level detail for debugging.
|
||||
if (securityState.layers) {
|
||||
const parts = Object.entries(securityState.layers).map(([k, v]) => `${k}:${v}`);
|
||||
shield.setAttribute('title', `Security — ${status}\n${parts.join('\n')}`);
|
||||
} else {
|
||||
shield.setAttribute('title', `Security — ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up banner interactivity once on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const closeBtn = document.getElementById('security-banner-close');
|
||||
@@ -1662,6 +1691,8 @@ async function tryConnect() {
|
||||
`token: yes (from /health)\nStarting SSE + chat polling...`
|
||||
);
|
||||
updateConnection(`http://127.0.0.1:${port}`, data.token);
|
||||
// Shield state arrives on /health alongside the auth token.
|
||||
if (data.security) updateSecurityShield(data.security);
|
||||
return;
|
||||
}
|
||||
setLoadingStatus(
|
||||
|
||||
Reference in New Issue
Block a user