stage-27 polish: admin presence announces itself in the topbar

Once you're signed in, the topbar gains:
- An "Admin" nav link (cyan-accented, with a small shield-lock SVG)
  appearing only when an admin session exists.
- A glowing chip on the right showing "● ADMIN · <who>" with a
  separated lock button (⏻) for sign-out. Pops in with a scale-bounce
  entrance, then a slow box-shadow pulse so it stays noticeable without
  being annoying. Respects prefers-reduced-motion.
- Both rendered via request.session in base.html, so every page gets
  them automatically with no per-route plumbing.

Removed the now-redundant in-panel "lock ⏻" from admin.html — the
topbar owns logout, the admin page header shows enrolled count instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m17hr1l
2026-05-23 01:14:58 +02:00
parent cb7bef4e40
commit eaca27be26
3 changed files with 79 additions and 1 deletions

View File

@@ -471,3 +471,61 @@ tr.sev-low .sev-badge { color: var(--muted); }
.enroll-card .gate-qr { width: 130px; height: 130px; }
.enroll-body h3 { margin: 0 0 6px; font-size: 15px; }
.enroll-body p { margin: 0; font-size: 13px; color: var(--muted); }
/* ── admin in the topbar (announced, not hidden) ────────────── */
.nav-admin {
display: inline-flex; align-items: center; gap: 6px;
margin-left: 18px; padding: 4px 10px;
color: var(--accent) !important;
background: rgba(30, 200, 255, 0.08);
border: 1px solid color-mix(in oklab, var(--accent) 35%, var(--border));
border-radius: 6px;
text-shadow: 0 0 8px var(--accent-glow);
}
.nav-admin:hover { background: rgba(30, 200, 255, 0.16); border-color: var(--accent); text-decoration: none; }
.admin-chip {
display: inline-flex; align-items: stretch; margin-left: 14px;
font-family: var(--font-display); font-size: 11px; letter-spacing: 0.12em;
border: 1px solid var(--accent);
border-radius: 6px; overflow: hidden;
background: linear-gradient(180deg, rgba(30,200,255,0.12), rgba(30,200,255,0.02));
box-shadow: 0 0 0 0 var(--accent-glow);
animation: admin-chip-in 0.55s cubic-bezier(.2,1.4,.4,1), admin-chip-glow 3.2s ease-in-out 0.6s infinite;
}
.admin-chip-body, .admin-chip-lock {
display: inline-flex; align-items: center; gap: 7px;
padding: 4px 10px; color: var(--accent) !important;
text-decoration: none;
}
.admin-chip-body { padding-right: 8px; }
.admin-chip-body:hover { background: rgba(30,200,255,0.15); text-decoration: none; }
.admin-chip-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--green); box-shadow: 0 0 10px var(--green);
animation: admin-dot-blink 1.6s ease-in-out infinite;
}
.admin-chip-label { text-transform: uppercase; font-weight: 600; }
.admin-chip-lock {
border-left: 1px solid color-mix(in oklab, var(--accent) 35%, var(--border));
font-size: 13px; padding: 4px 10px;
background: rgba(0,0,0,0.18);
}
.admin-chip-lock:hover { background: rgba(248,113,113,0.18); color: var(--red) !important; }
@keyframes admin-chip-in {
0% { opacity: 0; transform: translateY(-8px) scale(0.92); box-shadow: 0 0 0 0 var(--accent); }
60% { opacity: 1; transform: translateY(0) scale(1.05); box-shadow: 0 0 24px var(--accent); }
100% { opacity: 1; transform: scale(1); box-shadow: 0 0 12px var(--accent-glow); }
}
@keyframes admin-chip-glow {
0%,100% { box-shadow: 0 0 8px rgba(30,200,255,0.25); }
50% { box-shadow: 0 0 18px rgba(30,200,255,0.55); }
}
@keyframes admin-dot-blink {
0%,80%,100% { opacity: 1; }
90% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
.admin-chip, .admin-chip-dot { animation: none; }
}

View File

@@ -4,7 +4,7 @@
<section class="panel">
<div class="panel-head">
<h1>Admin Control Center</h1>
<span class="count">{% if who %}signed in as <strong>{{ who }}</strong> · {% endif %}<a href="/admin/logout" class="lg-sub">lock ⏻</a></span>
<span class="count">{{ members|length }} member{{ '' if members|length == 1 else 's' }} enrolled</span>
</div>
<p class="page-intro">The secured zone — TOTP-gated, hidden from the nav. Manage who can get in here, and (next) watch the live infrastructure.</p>
<div class="verdict verdict-clean">✓ Admin session active — expires after 60 min idle.</div>

View File

@@ -23,7 +23,27 @@
<a href="/queue">Queue</a>
<a href="/ledger">Ledger</a>
<a href="/train">Trainline</a>
{% if request.session.get('admin_who') %}
<a href="/admin" class="nav-admin" title="Admin control center">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" style="vertical-align:-2px;">
<path d="M12 2 L20 5 V12 C20 17 16.5 20.5 12 22 C7.5 20.5 4 17 4 12 V5 Z"
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
<circle cx="12" cy="11" r="2.4" fill="none" stroke="currentColor" stroke-width="1.8"/>
<rect x="11" y="12.5" width="2" height="4" rx="1" fill="currentColor"/>
</svg>
Admin
</a>
{% endif %}
</nav>
{% if request.session.get('admin_who') %}
<span class="admin-chip" title="Admin session · click to manage">
<a href="/admin" class="admin-chip-body">
<span class="admin-chip-dot"></span>
<span class="admin-chip-label">ADMIN · {{ request.session.get('admin_who') }}</span>
</a>
<a href="/admin/logout" class="admin-chip-lock" title="Lock the admin zone (sign out)" aria-label="lock"></a>
</span>
{% endif %}
<span class="model-status" id="model-status" data-state="checking" title="checking…">
<span class="model-status-dot"></span><span class="model-status-text">model</span>
</span>