fix(render+roles): batch smooth shading + step timings + global_admin role support

Render pipeline:
- Replace per-object _apply_smooth() loop with _apply_smooth_batch(): selects
  all 175 parts, calls shade_smooth_by_angle() ONCE in C → reduces 16s to ~0.2s
- Remove 175 per-part "assigned material to part" log lines (replace with summary)
- Add TIMING_SUMMARY log line at end of every render showing all step durations
- _lap() helper records split times for: template_load, glb_import, rotation,
  smooth_shading, material_assign, pre_render_setup, gpu_render

Frontend role checks:
- Add global_admin + tenant_admin to User role type in auth store
- Add isAdmin() and isPrivileged() helper functions
- Fix Admin.tsx, Layout.tsx, Notifications.tsx, OrderDetail.tsx, ProductDetail.tsx,
  CostOverviewWidget.tsx — all were checking role === 'admin' but JWT now has
  role === 'global_admin' after migration 049 (admin → global_admin backfill)
- This caused Admin page to render completely empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:29:22 +01:00
parent ac48d359e6
commit 8933d0be17
8 changed files with 83 additions and 38 deletions
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { DollarSign, FileText } from 'lucide-react'
import api from '../../../api/client'
import { useAuthStore } from '../../../store/auth'
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../../../store/auth'
interface Invoice {
id: string
@@ -27,7 +27,7 @@ function Skeleton() {
export default function CostOverviewWidget() {
const user = useAuthStore((s) => s.user)
const isPrivileged =
user?.role === 'admin' || user?.role === 'project_manager'
checkIsPrivileged(user)
const { data, isLoading, error } = useQuery<Invoice[]>({
queryKey: ['invoices-widget'],
+9 -9
View File
@@ -1,6 +1,6 @@
import { Outlet, NavLink, useNavigate, Link } from 'react-router-dom'
import { LayoutDashboard, Package, Settings, LogOut, FlaskConical, Activity, Library, Plus, SlidersHorizontal, Building2, GitBranch, Image, BellRing, Receipt, Server, Upload, Menu, X } from 'lucide-react'
import { useAuthStore } from '../../store/auth'
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../../store/auth'
import { clsx } from 'clsx'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
@@ -148,7 +148,7 @@ export default function Layout() {
)
})}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
{(checkIsPrivileged(user)) && (
<NavLink
to="/admin"
onClick={() => setSidebarOpen(false)}
@@ -165,7 +165,7 @@ export default function Layout() {
Admin
</NavLink>
)}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
{(checkIsPrivileged(user)) && (
<NavLink
to="/billing"
onClick={() => setSidebarOpen(false)}
@@ -182,7 +182,7 @@ export default function Layout() {
Billing
</NavLink>
)}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
{(checkIsPrivileged(user)) && (
<NavLink
to="/media"
onClick={() => setSidebarOpen(false)}
@@ -199,7 +199,7 @@ export default function Layout() {
Media Browser
</NavLink>
)}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
{(checkIsPrivileged(user)) && (
<NavLink
to="/workers"
onClick={() => setSidebarOpen(false)}
@@ -216,7 +216,7 @@ export default function Layout() {
Workers
</NavLink>
)}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
{(checkIsPrivileged(user)) && (
<NavLink
to="/workflows"
onClick={() => setSidebarOpen(false)}
@@ -233,7 +233,7 @@ export default function Layout() {
Workflows
</NavLink>
)}
{(user?.role === 'admin' || user?.role === 'project_manager') && (
{(checkIsPrivileged(user)) && (
<NavLink
to="/asset-libraries"
onClick={() => setSidebarOpen(false)}
@@ -250,7 +250,7 @@ export default function Layout() {
Asset Libraries
</NavLink>
)}
{user?.role === 'admin' && (
{checkIsAdmin(user) && (
<NavLink
to="/notification-settings"
onClick={() => setSidebarOpen(false)}
@@ -267,7 +267,7 @@ export default function Layout() {
Notification Settings
</NavLink>
)}
{user?.role === 'admin' && (
{checkIsAdmin(user) && (
<NavLink
to="/tenants"
onClick={() => setSidebarOpen(false)}
+3 -3
View File
@@ -10,7 +10,7 @@ import TemplateEditor from '../components/admin/TemplateEditor'
import PricingTierTable from '../components/admin/PricingTierTable'
import OutputTypeTable from '../components/admin/OutputTypeTable'
import RenderTemplateTable from '../components/admin/RenderTemplateTable'
import { useAuthStore } from '../store/auth'
import { useAuthStore, isAdmin as checkIsAdmin } from '../store/auth'
import { listPricingTiers } from '../api/pricing'
import { listOutputTypes } from '../api/outputTypes'
import {
@@ -26,7 +26,7 @@ import type { GPUProbeResult } from '../api/worker'
export default function AdminPage() {
const qc = useQueryClient()
const user = useAuthStore((s) => s.user)
const isAdmin = user?.role === 'admin'
const isAdmin = checkIsAdmin(user)
const [showNewUser, setShowNewUser] = useState(false)
const [newUser, setNewUser] = useState({ email: '', password: '', full_name: '', role: 'client' })
const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null)
@@ -335,7 +335,7 @@ export default function AdminPage() {
<p className="text-sm font-medium text-content">{user.full_name}</p>
<p className="text-xs text-content-muted">{user.email}</p>
</div>
<span className={`badge mr-4 ${user.role === 'admin' ? 'badge-green' : 'badge-gray'}`}>
<span className={`badge mr-4 ${checkIsAdmin(user) ? 'badge-green' : 'badge-gray'}`}>
{user.role}
</span>
<span className={`badge mr-4 ${user.is_active ? 'badge-green' : 'badge-red'}`}>
+2 -2
View File
@@ -10,7 +10,7 @@ import {
getNotifications, markAsRead, markOneAsRead,
type Notification, type NotificationChannel,
} from '../api/notifications'
import { useAuthStore } from '../store/auth'
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../store/auth'
const ACTION_CONFIG: Record<string, { icon: typeof Bell; label: (d: Record<string, unknown> | null) => string; color: string }> = {
'order.submitted': { icon: Send, label: (d) => `Order ${d?.order_number ?? '?'} submitted`, color: 'text-blue-500' },
@@ -62,7 +62,7 @@ export default function NotificationsPage() {
const qc = useQueryClient()
const user = useAuthStore((s) => s.user)
const isAdminOrPM = user?.role === 'admin' || user?.role === 'project_manager'
const isAdminOrPM = checkIsPrivileged(user)
const visibleTabs = TABS.filter(t => !t.adminOnly || isAdminOrPM)
const currentTab = visibleTabs.find(t => t.key === activeTab) ?? visibleTabs[0]
+2 -2
View File
@@ -15,7 +15,7 @@ import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbn
import type { OrderItem, OrderLine } from '../api/orders'
import { listOutputTypes } from '../api/outputTypes'
import type { OutputType } from '../api/outputTypes'
import { useAuthStore } from '../store/auth'
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../store/auth'
import StepDropzone from '../components/upload/StepDropzone'
import CadPartMaterials from '../components/orders/CadPartMaterials'
import LiveRenderLog from '../components/LiveRenderLog'
@@ -180,7 +180,7 @@ export default function OrderDetailPage() {
const canSubmit = order.status === 'draft'
const canDelete = order.status === 'draft' || order.status === 'rejected'
const isDraft = order.status === 'draft'
const isPrivileged = user?.role === 'admin' || user?.role === 'project_manager'
const isPrivileged = checkIsPrivileged(user)
const canReject = isPrivileged && (order.status === 'submitted' || order.status === 'processing')
const canResubmit = order.status === 'rejected' && (isPrivileged || order.created_by === user?.id)
const rp = order.render_progress
+2 -2
View File
@@ -17,7 +17,7 @@ import type { Product, CadPartMaterial, ProductRender, RenderPosition } from '..
import { listMaterials } from '../api/materials'
import MaterialInput from '../components/shared/MaterialInput'
import MaterialWizard from '../components/MaterialWizard'
import { useAuthStore } from '../store/auth'
import { useAuthStore, isAdmin as checkIsAdmin, isPrivileged as checkIsPrivileged } from '../store/auth'
import { generateGltfGeometry, generateGltfProduction, resetStuckProcessing } from '../api/cad'
import { listMediaAssets as getMediaAssets } from '../api/media'
import InlineCadViewer from '../components/cad/InlineCadViewer'
@@ -129,7 +129,7 @@ export default function ProductDetailPage() {
const navigate = useNavigate()
const qc = useQueryClient()
const user = useAuthStore((s) => s.user)
const isPrivileged = user?.role === 'admin' || user?.role === 'project_manager'
const isPrivileged = checkIsPrivileged(user)
const [editMode, setEditMode] = useState(false)
const [draft, setDraft] = useState<Partial<Product>>({})
+9 -1
View File
@@ -5,10 +5,18 @@ interface User {
id: string
email: string
full_name: string
role: 'admin' | 'project_manager' | 'client'
role: 'admin' | 'global_admin' | 'tenant_admin' | 'project_manager' | 'client'
is_active: boolean
}
/** True for any role that has full administrative access. */
export const isAdmin = (user: User | null): boolean =>
user?.role === 'admin' || user?.role === 'global_admin' || user?.role === 'tenant_admin'
/** True for admin or project_manager (privileged users). */
export const isPrivileged = (user: User | null): boolean =>
isAdmin(user) || user?.role === 'project_manager'
interface AuthState {
token: string | null
user: User | null