feat(guide): Testing role-tab (fleet pilot) + sidebar identity indicator

- New Testing tab (order 35, audience internal, 🧪): test accounts, manual flow,
  copy-paste API smoke tests, role/scoping checks, regression watch. Fleet pilot.
- Reader shows 'Signed in as {email} · {role}' above Sign out (decoded from JWT),
  so you always know which identity you're testing as.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:26:29 -04:00
parent 3b80409718
commit 3976251b90
5 changed files with 355 additions and 3 deletions
+12 -2
View File
@@ -11,6 +11,14 @@
let showPassword = false;
let loginError = '';
let loggingIn = false;
let userInfo: { email: string; role: string } | null = null;
function decodeJwt(t: string): { email: string; role: string } | null {
try {
const p = JSON.parse(atob(t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
return { email: p.email ?? '', role: p.role ?? '' };
} catch { return null; }
}
async function checkAuth() {
const t = getToken();
@@ -18,6 +26,7 @@
try {
await authApi.get('/api/users/me', { headers: { Authorization: `Bearer ${t}` } });
authenticated = true;
userInfo = decodeJwt(t);
} catch {
clearToken();
} finally {
@@ -33,6 +42,7 @@
const token = res.data.token;
if (!token) throw new Error('No token in response');
setToken(token);
userInfo = decodeJwt(token);
authenticated = true;
} catch (e: any) {
loginError = e?.response?.data?.message ?? 'Invalid email or password.';
@@ -41,7 +51,7 @@
}
}
function logout() { clearToken(); authenticated = false; }
function logout() { clearToken(); authenticated = false; userInfo = null; }
onMount(checkAuth);
</script>
@@ -80,7 +90,7 @@
</div>
</div>
{:else}
<GuideReader on:logout={logout} />
<GuideReader on:logout={logout} userEmail={userInfo?.email ?? ''} userRole={userInfo?.role ?? ''} />
{/if}
<!-- Toast notifications -->
+23 -1
View File
@@ -6,6 +6,8 @@
import { addToast } from '../lib/stores';
const dispatch = createEventDispatcher();
export let userEmail = '';
export let userRole = '';
let nav: NavApp[] = [];
let loadingNav = true;
@@ -33,7 +35,7 @@
// role-tab icons (Customer / Internal / Architect / Claude)
const ROLE_ICON: Record<string, string> = {
Customer: '👤', Internal: '🔧', Architect: '📐', Claude: '✨', Overview: '📄', Data: '🗄️', Technical: '📐',
Customer: '👤', Internal: '🔧', Architect: '📐', Testing: '🧪', Claude: '✨', Overview: '📄', Data: '🗄️', Technical: '📐',
};
$: filtered = !search.trim()
@@ -161,6 +163,13 @@
{/if}
</nav>
{#if userEmail}
<div class="side-user">
<span class="side-user-label">Signed in as</span>
<span class="side-user-email" title={userEmail}>{userEmail}</span>
{#if userRole}<span class="side-user-role">{userRole}</span>{/if}
</div>
{/if}
<button class="side-logout" on:click={() => dispatch('logout')}>Sign out</button>
</aside>
@@ -258,6 +267,19 @@
.nav-item:hover { background: rgba(255,255,255,0.08); color: white; }
.nav-item.active { background: rgba(255,255,255,0.14); color: white; border-left-color: white; font-weight: 600; }
.side-user {
margin: 0.4rem 1rem 0.2rem; padding: 0.5rem 0.15rem 0.4rem;
border-top: 1px solid rgba(255,255,255,0.12);
display: flex; flex-direction: column; gap: 1px;
}
.side-user-label { color: rgba(255,255,255,0.5); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; }
.side-user-email { color: rgba(255,255,255,0.92); font-size: 0.78rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-user-role {
align-self: flex-start; margin-top: 3px;
color: #93c5fd; background: rgba(59,130,246,0.18); border: 1px solid rgba(59,130,246,0.35);
font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
border-radius: 4px; padding: 1px 6px;
}
.side-logout {
margin: 0.5rem 1rem 1rem; padding: 0.5rem; border-radius: 6px;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);