Spaces:
Sleeping
feat: admin LLM control panel as in-app tab (D-022)
Browse filesPreviously the LLM control surface lived at /admin/llm-control.html β a
separate URL that had to be remembered + opened in a fresh tab. This
surfaces it as a header tab matching the existing UI pattern, with the
panel iframe-embedded inside the right-side aside drawer.
- New showAdmin state + slate-gradient button ("Admin Β· Access panel")
positioned next to the language toggle.
- Toggling Admin closes any other panel (marketplace/premium/profile).
- Toggling any other panel closes Admin (mutual exclusion via existing pattern).
- iframe sandbox: allow-scripts + allow-same-origin + allow-forms so the
control HTML can call /api/admin/* on the same origin.
- Backend admin API stays IP-gated (ADMIN_IP_ALLOWLIST), so non-allowlisted
visitors see the control HTML but its API calls return 404 β the panel
silently shows "not authorized" state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- frontend/src/app/page.tsx +53 -4
|
@@ -62,6 +62,10 @@ export default function Page() {
|
|
| 62 |
const [showPremium, setShowPremium] = useState(false);
|
| 63 |
const [showMarketplace, setShowMarketplace] = useState(false);
|
| 64 |
const [showProfile, setShowProfile] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
const [marketplace, setMarketplace] = useState<MarketplaceResponse | null>(null);
|
| 66 |
const [openPolicy, setOpenPolicy] = useState<MarketplacePolicy | null>(null);
|
| 67 |
const [sessionId, setSessionId] = useState<string | undefined>();
|
|
@@ -370,7 +374,7 @@ export default function Page() {
|
|
| 370 |
</div>
|
| 371 |
<div className="flex items-center gap-2 sm:gap-3">
|
| 372 |
<button
|
| 373 |
-
onClick={() => { setShowMarketplace(!showMarketplace); setShowPremium(false); setShowCoverage(false); }}
|
| 374 |
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 375 |
showMarketplace
|
| 376 |
? "ring-2 ring-[var(--primary)]"
|
|
@@ -402,7 +406,7 @@ export default function Page() {
|
|
| 402 |
</div>
|
| 403 |
</button>
|
| 404 |
<button
|
| 405 |
-
onClick={() => { setShowPremium(!showPremium); setShowMarketplace(false); setShowCoverage(false); setShowProfile(false); }}
|
| 406 |
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 407 |
showPremium ? "ring-2 ring-[var(--primary)]" : ""
|
| 408 |
}`}
|
|
@@ -420,7 +424,7 @@ export default function Page() {
|
|
| 420 |
</div>
|
| 421 |
</button>
|
| 422 |
<button
|
| 423 |
-
onClick={() => { setShowProfile(!showProfile); setShowMarketplace(false); setShowPremium(false); setShowCoverage(false); }}
|
| 424 |
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 425 |
showProfile ? "ring-2 ring-[var(--primary)]" : ""
|
| 426 |
}`}
|
|
@@ -443,6 +447,27 @@ export default function Page() {
|
|
| 443 |
)}
|
| 444 |
</div>
|
| 445 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
{/* UI language toggle β flips visual chrome + voice TTS together */}
|
| 447 |
<button
|
| 448 |
onClick={() => setTtsLang(ttsLang === "en-IN" ? "hi-IN" : "en-IN")}
|
|
@@ -552,7 +577,7 @@ export default function Page() {
|
|
| 552 |
{/* Panel column β sits beside the chat on desktop, takes over on
|
| 553 |
mobile. Stays mounted as long as a panel is open; chat in the
|
| 554 |
other column remains fully interactive (real-time copilot). */}
|
| 555 |
-
{(showMarketplace || showPremium || showProfile) && (
|
| 556 |
<aside className="lg:w-3/5 w-full overflow-y-auto bg-[var(--background)]">
|
| 557 |
{showMarketplace && marketplace && (
|
| 558 |
<MarketplacePanel
|
|
@@ -574,6 +599,30 @@ export default function Page() {
|
|
| 574 |
uiLang={uiLang}
|
| 575 |
/>
|
| 576 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
</aside>
|
| 578 |
)}
|
| 579 |
</div>
|
|
|
|
| 62 |
const [showPremium, setShowPremium] = useState(false);
|
| 63 |
const [showMarketplace, setShowMarketplace] = useState(false);
|
| 64 |
const [showProfile, setShowProfile] = useState(false);
|
| 65 |
+
// Admin panel: iframe-embedded LLM control surface. Backend admin API is
|
| 66 |
+
// IP-gated (ADMIN_IP_ALLOWLIST), so the panel itself silently renders the
|
| 67 |
+
// dashboard's "not authorized" view for non-allowlisted IPs.
|
| 68 |
+
const [showAdmin, setShowAdmin] = useState(false);
|
| 69 |
const [marketplace, setMarketplace] = useState<MarketplaceResponse | null>(null);
|
| 70 |
const [openPolicy, setOpenPolicy] = useState<MarketplacePolicy | null>(null);
|
| 71 |
const [sessionId, setSessionId] = useState<string | undefined>();
|
|
|
|
| 374 |
</div>
|
| 375 |
<div className="flex items-center gap-2 sm:gap-3">
|
| 376 |
<button
|
| 377 |
+
onClick={() => { setShowMarketplace(!showMarketplace); setShowPremium(false); setShowCoverage(false); setShowAdmin(false); }}
|
| 378 |
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 379 |
showMarketplace
|
| 380 |
? "ring-2 ring-[var(--primary)]"
|
|
|
|
| 406 |
</div>
|
| 407 |
</button>
|
| 408 |
<button
|
| 409 |
+
onClick={() => { setShowPremium(!showPremium); setShowMarketplace(false); setShowCoverage(false); setShowProfile(false); setShowAdmin(false); }}
|
| 410 |
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 411 |
showPremium ? "ring-2 ring-[var(--primary)]" : ""
|
| 412 |
}`}
|
|
|
|
| 424 |
</div>
|
| 425 |
</button>
|
| 426 |
<button
|
| 427 |
+
onClick={() => { setShowProfile(!showProfile); setShowMarketplace(false); setShowPremium(false); setShowCoverage(false); setShowAdmin(false); }}
|
| 428 |
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 429 |
showProfile ? "ring-2 ring-[var(--primary)]" : ""
|
| 430 |
}`}
|
|
|
|
| 447 |
)}
|
| 448 |
</div>
|
| 449 |
</button>
|
| 450 |
+
{/* Admin access β opens the LLM control panel in an embedded view.
|
| 451 |
+
Backend admin API is IP-gated, so the panel works only from the
|
| 452 |
+
allowlisted home IP; from other networks it shows "not authorized". */}
|
| 453 |
+
<button
|
| 454 |
+
onClick={() => { setShowAdmin(!showAdmin); setShowMarketplace(false); setShowPremium(false); setShowProfile(false); setShowCoverage(false); }}
|
| 455 |
+
className={`group relative overflow-hidden rounded-xl transition-all shadow-sm hover:shadow-md ${
|
| 456 |
+
showAdmin ? "ring-2 ring-[var(--primary)]" : ""
|
| 457 |
+
}`}
|
| 458 |
+
title="LLM control panel β health, chain order, usage (admin-only, IP-gated)"
|
| 459 |
+
>
|
| 460 |
+
<div className="absolute inset-0 bg-gradient-to-br from-slate-700 via-slate-600 to-zinc-700" />
|
| 461 |
+
<div className="relative flex items-stretch text-white">
|
| 462 |
+
<div className="flex items-center justify-center px-3 py-2 bg-black/15">
|
| 463 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2 4 6v6c0 5 3.5 9 8 10 4.5-1 8-5 8-10V6l-8-4z" /><path d="M9 12l2 2 4-4" /></svg>
|
| 464 |
+
</div>
|
| 465 |
+
<div className="px-3 py-2 text-left">
|
| 466 |
+
<div className="text-[10px] uppercase tracking-wider opacity-85 leading-none">Admin</div>
|
| 467 |
+
<div className="text-xs font-bold leading-tight whitespace-nowrap">Access panel</div>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
</button>
|
| 471 |
{/* UI language toggle β flips visual chrome + voice TTS together */}
|
| 472 |
<button
|
| 473 |
onClick={() => setTtsLang(ttsLang === "en-IN" ? "hi-IN" : "en-IN")}
|
|
|
|
| 577 |
{/* Panel column β sits beside the chat on desktop, takes over on
|
| 578 |
mobile. Stays mounted as long as a panel is open; chat in the
|
| 579 |
other column remains fully interactive (real-time copilot). */}
|
| 580 |
+
{(showMarketplace || showPremium || showProfile || showAdmin) && (
|
| 581 |
<aside className="lg:w-3/5 w-full overflow-y-auto bg-[var(--background)]">
|
| 582 |
{showMarketplace && marketplace && (
|
| 583 |
<MarketplacePanel
|
|
|
|
| 599 |
uiLang={uiLang}
|
| 600 |
/>
|
| 601 |
)}
|
| 602 |
+
{showAdmin && (
|
| 603 |
+
<div className="flex flex-col h-full">
|
| 604 |
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)] bg-[var(--card)]">
|
| 605 |
+
<div>
|
| 606 |
+
<h2 className="text-sm font-semibold">Admin Β· LLM Control Panel</h2>
|
| 607 |
+
<p className="text-xs text-[var(--muted-foreground)]">
|
| 608 |
+
IP-gated. Only the home network IP can interact with chain reordering / probes.
|
| 609 |
+
</p>
|
| 610 |
+
</div>
|
| 611 |
+
<button
|
| 612 |
+
onClick={() => setShowAdmin(false)}
|
| 613 |
+
className="text-xs text-[var(--muted-foreground)] hover:underline"
|
| 614 |
+
>
|
| 615 |
+
close
|
| 616 |
+
</button>
|
| 617 |
+
</div>
|
| 618 |
+
<iframe
|
| 619 |
+
src="/admin/llm-control.html"
|
| 620 |
+
title="LLM Control Panel"
|
| 621 |
+
className="flex-1 w-full border-0 bg-white"
|
| 622 |
+
sandbox="allow-scripts allow-same-origin allow-forms"
|
| 623 |
+
/>
|
| 624 |
+
</div>
|
| 625 |
+
)}
|
| 626 |
</aside>
|
| 627 |
)}
|
| 628 |
</div>
|