Phase 5: platform feature-flags CRUD, impersonation, billing catalogue
Three new platform screens on top of the Phase 1-4 work. Feature flags (/feature-flags) — platform-wide flag registry. New route + lib/arcadia/feature-flags.ts, capability platform.feature_flags, nav under Automation. List/create/edit/delete with a per-row default toggle; pairs with the Phase-4 per-tenant override tab. Impersonation — "Impersonate" action on active users. Entirely client-side token swap in session.ts (beginImpersonation parks the operator's session + API token and swaps to the impersonation token; endImpersonation restores it), with a sticky "Viewing as <email> — Stop" banner in the shell driven by the JWT's impersonated_by claim. Stop is client-side because the impersonation token carries the target's roles and can't reach the admin-gated /stop endpoint; impersonation is stateless JWT so restoring the parked token is sufficient. Billing (/billing) — replaced the coming-soon stub with the real plan catalogue from GET /billing/plans (lib/arcadia/billing.ts). Per-tenant plan assignment stays on the tenant detail page; Entitlements + Apps remain honestly marked "Soon". Verified in-browser with real backend; typecheck adds zero errors (36→36). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,8 @@ import {
|
||||
Plug,
|
||||
MessageSquare,
|
||||
CreditCard,
|
||||
Flag,
|
||||
Eye,
|
||||
// CREMA:NAV-ICONS
|
||||
} from "lucide-react"
|
||||
|
||||
@@ -65,7 +67,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "~/components/ui/popover"
|
||||
import { profileInitials, useProfile } from "~/lib/profile"
|
||||
import { signOut, useSession } from "~/lib/session"
|
||||
import { endImpersonation, signOut, useSession } from "~/lib/session"
|
||||
import { capabilityForPath, useCapabilities } from "~/lib/capabilities"
|
||||
import {
|
||||
addNotification,
|
||||
@@ -160,6 +162,7 @@ const navGroups: NavGroup[] = [
|
||||
label: "Automation",
|
||||
icon: Plug,
|
||||
items: [
|
||||
{ to: "/feature-flags", icon: Flag, label: "Feature flags" },
|
||||
{ to: "/webhooks", icon: WebhookIcon, label: "Webhooks" },
|
||||
{ to: "/scheduled-tasks", icon: CalendarClock, label: "Scheduled" },
|
||||
{ to: "/integrations", icon: Plug, label: "Integrations" },
|
||||
@@ -468,6 +471,7 @@ export function AppShell({
|
||||
</aside>
|
||||
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
<ImpersonationBanner />
|
||||
{/* Mobile-only menu trigger, floating top-left of main */}
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
<SheetTrigger
|
||||
@@ -787,6 +791,37 @@ function NotificationDispatcher() {
|
||||
)
|
||||
}
|
||||
|
||||
function ImpersonationBanner() {
|
||||
const session = useSession()
|
||||
const navigate = useNavigate()
|
||||
if (!session?.impersonatedBy) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="impersonation-banner"
|
||||
className="sticky top-0 z-40 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-b border-amber-500/40 bg-amber-500/15 px-4 py-2 text-sm text-amber-900 backdrop-blur-sm dark:text-amber-200"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Eye className="size-4" />
|
||||
Viewing as <span className="font-semibold">{session.email}</span> — actions you take
|
||||
happen as this user.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
data-action="impersonation-stop"
|
||||
onClick={() => {
|
||||
endImpersonation()
|
||||
navigate("/users")
|
||||
}}
|
||||
className="rounded-md border border-amber-600/40 px-2 py-0.5 font-medium transition-colors hover:bg-amber-500/25"
|
||||
>
|
||||
Stop impersonating
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationsBell() {
|
||||
const items = useNotifications()
|
||||
const unread = unreadCount(items)
|
||||
|
||||
Reference in New Issue
Block a user