diff --git a/CLAUDE.md b/CLAUDE.md index 1b61278..247734a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,7 +109,7 @@ SaaS admin credentials (`SAAS_ADMIN_USER`/`SAAS_ADMIN_PASS`) work for both the S - `cameleer-saas` — SaaS app (frontend + JAR baked in) - `cameleer-logto` — custom Logto with sign-in UI baked in - Docker builds: `--no-cache`, `--provenance=false` for Gitea compatibility -- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`. No volume mounts — all artifacts come from CI-built images +- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath) - Design system: import from `@cameleer/design-system` (Gitea npm registry) ## Disabled Skills diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index feb22f6..fb3f980 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,8 +12,11 @@ services: cameleer-saas: ports: - "8080:8080" + volumes: + - ./ui/dist:/app/static environment: SPRING_PROFILES_ACTIVE: dev + SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/ cameleer3-server: ports: diff --git a/ui/package-lock.json b/ui/package-lock.json index b5261df..478559c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@cameleer/design-system": "0.1.37", + "@cameleer/design-system": "0.1.38", "@logto/react": "^4.0.13", "@tanstack/react-query": "^5.90.0", "lucide-react": "^1.7.0", @@ -309,9 +309,9 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.37", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.37/design-system-0.1.37.tgz", - "integrity": "sha512-aboFqyADzT7r8PNHkdktumoWmOZpqa7Gn+jjMdmXp5EKZdz5Iva1RJdeTFmRIVbxk/4dJ8fOAC8+q/TvNQcU8A==", + "version": "0.1.38", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.38/design-system-0.1.38.tgz", + "integrity": "sha512-8tsWZTYkLg3JbvA8p+MVP05nsuJnIXZZvgx6d71e7BO3rtoI8bpvQn/ZElag6tQnBEbeqStQqqDnZ5TAWN2pvw==", "dependencies": { "lucide-react": "^1.7.0", "react": "^19.0.0", diff --git a/ui/package.json b/ui/package.json index 82b2d60..7422fdb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,7 @@ "postinstall": "node -e \"const fs=require('fs'),p='node_modules/@cameleer/design-system/assets/';if(fs.existsSync('public')){fs.copyFileSync(p+'cameleer3-logo.svg','public/favicon.svg')}\"" }, "dependencies": { - "@cameleer/design-system": "0.1.37", + "@cameleer/design-system": "0.1.38", "@logto/react": "^4.0.13", "@tanstack/react-query": "^5.90.0", "lucide-react": "^1.7.0", diff --git a/ui/src/auth/OrgResolver.tsx b/ui/src/auth/OrgResolver.tsx index 7fded57..bfe0be2 100644 --- a/ui/src/auth/OrgResolver.tsx +++ b/ui/src/auth/OrgResolver.tsx @@ -13,7 +13,8 @@ import { fetchConfig } from '../config'; export function OrgResolver({ children }: { children: React.ReactNode }) { const { data: me, isLoading, isError } = useMe(); const { getAccessToken } = useLogto(); - const { setOrganizations, setCurrentOrg, setScopes, currentOrgId } = useOrgStore(); + const { getIdTokenClaims } = useLogto(); + const { setOrganizations, setCurrentOrg, setScopes, setUsername, currentOrgId } = useOrgStore(); // Effect 1: Org population — runs when /api/me data loads useEffect(() => { @@ -32,6 +33,16 @@ export function OrgResolver({ children }: { children: React.ReactNode }) { if (orgEntries.length === 1 && !currentOrgId) { setCurrentOrg(orgEntries[0].id); } + + // Extract display name from ID token (local decode, no network call) + // eslint-disable-next-line react-hooks/exhaustive-deps — getIdTokenClaims is unstable + getIdTokenClaims().then((claims) => { + if (claims) { + const c = claims as Record; + const name = (c.username ?? c.name ?? c.email ?? null) as string | null; + setUsername(name); + } + }).catch(() => {}); }, [me]); // Effect 2: Scope fetching — runs when me loads OR when currentOrgId changes diff --git a/ui/src/auth/useOrganization.ts b/ui/src/auth/useOrganization.ts index 303159a..8768e88 100644 --- a/ui/src/auth/useOrganization.ts +++ b/ui/src/auth/useOrganization.ts @@ -12,9 +12,11 @@ interface OrgState { currentTenantId: string | null; // DB UUID — used for API calls like /api/tenants/{id} organizations: OrgInfo[]; scopes: Set; + username: string | null; setCurrentOrg: (orgId: string | null) => void; setOrganizations: (orgs: OrgInfo[]) => void; setScopes: (scopes: Set) => void; + setUsername: (name: string | null) => void; } export const useOrgStore = create((set, get) => ({ @@ -22,6 +24,8 @@ export const useOrgStore = create((set, get) => ({ currentTenantId: null, organizations: [], scopes: new Set(), + username: null, + setUsername: (name) => set({ username: name }), setCurrentOrg: (orgId) => { const org = get().organizations.find((o) => o.id === orgId); set({ currentOrgId: orgId, currentTenantId: org?.tenantId ?? null }); diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 27bf780..06ae5ac 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -7,6 +7,7 @@ import { } from '@cameleer/design-system'; import { useAuth } from '../auth/useAuth'; import { useScopes } from '../auth/useScopes'; +import { useOrgStore } from '../auth/useOrganization'; import { EnvironmentTree } from './EnvironmentTree'; import cameleerLogo from '@cameleer/design-system/assets/cameleer3-logo.svg'; @@ -93,6 +94,7 @@ export function Layout() { const navigate = useNavigate(); const { logout } = useAuth(); const scopes = useScopes(); + const { username } = useOrgStore(); const [envSectionOpen, setEnvSectionOpen] = useState(true); const [collapsed, setCollapsed] = useState(false); @@ -158,7 +160,7 @@ export function Layout() { {/* User info + logout */} } - label="Account" + label={username ?? 'Account'} onClick={logout} /> @@ -169,7 +171,7 @@ export function Layout() { diff --git a/ui/src/pages/LicensePage.tsx b/ui/src/pages/LicensePage.tsx index e6e376d..374c3a1 100644 --- a/ui/src/pages/LicensePage.tsx +++ b/ui/src/pages/LicensePage.tsx @@ -17,9 +17,9 @@ const FEATURE_LABELS: Record = { }; const LIMIT_LABELS: Record = { - maxAgents: 'Max Agents', - retentionDays: 'Retention Days', - maxEnvironments: 'Max Environments', + max_agents: 'Max Agents', + retention_days: 'Retention Days', + max_environments: 'Max Environments', }; function tierColor(tier: string): 'primary' | 'success' | 'warning' | 'error' {