admin: completeness + UI consistency pass

Arcadia wiring:
- home: real Overview dashboard (tenants/users/audit/health probe) replacing the inherited Vibespace welcome tiles; skeleton loaders, refresh button, registers admin context
- profile: split into Account (synced via getUser/updateUser of session user) and local Preferences; updateSessionUser keeps the appbar in sync after edits
- session: drop unused signIn mock, add updateSessionUser, refresh tests
- profile schema: drop redundant Profile.name/email (session is the source of truth)
- routes: delete orphaned resources route + lib

Auth flows that previously 404'd:
- /signup, /login/forgot, /login/reset, /login/2fa wired via @crema/arcadia-auth-ui
- shared AuthShell + AuthBrand wrapper

Assistant tools (admin-tools.ts):
- +10 tools: deactivate_tenant, set_user_status, delete_user, list_memberships, list_roles, revoke_api_key, create_user, update_user, assign_role, remove_role
- list_memberships gains user_id filter for "tenants this user belongs to" queries
- search_kb / read_chunk: new token resolution (window override → VITE_ARCADIA_SEARCH_TOKEN service token → operator session JWT → "dev"); on 401/403 emit a tailored hint based on which token was used

UI consistency:
- new PageHeader component
- AppShell.title was unrendered — dropped; first-child padding on #main-content keeps the floating actions pill from colliding with header content
- removed dead "Sign in required" fallback cards from 14 routes (AppShell already redirects)
- stripped p-6 from outer wrappers across 14 routes (was double-padding under AppShell's own p-6)
- migrated home + tenants to PageHeader

arcadia-search ergonomics:
- scripts/mint-search-token.mjs + `npm run mint:search-token` mints HS512 JWT with required tenant_id claim, upserts VITE_ARCADIA_SEARCH_TOKEN into .env.local
- README/.env document the new VITE_ARCADIA_SEARCH_URL / VITE_ARCADIA_SEARCH_TOKEN knobs
- .env.local now gitignored

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-04 15:37:31 +10:00
parent 444516e900
commit 20c592dfa7
44 changed files with 1594 additions and 984 deletions

View File

@@ -1,6 +1,7 @@
// Session — minimal auth scaffold backed by localStorage.
// Swap loadSession/signIn/signOut for real calls (cookies + server) when you
// wire a backend. The shape here matches what AppShell + useUser expect.
// Sign-in is owned by `persistFromArcadiaLogin`, which is called by the auth
// routes after a successful arcadia API exchange. The shape here matches what
// AppShell + useUser expect.
import { useEffect, useSyncExternalStore } from "react"
@@ -50,35 +51,6 @@ export function loadSession(): Session | null {
return readFromStorage()
}
/**
* Mock sign-in. Validates only that email + password are non-empty; returns
* a fake session. Replace with a real fetch to your auth endpoint.
*/
export async function signIn(
email: string,
password: string,
): Promise<Session> {
await new Promise((r) => setTimeout(r, 250))
if (!email.trim() || !password.trim()) {
throw new Error("Email and password are required.")
}
if (!email.includes("@")) {
throw new Error("Enter a valid email address.")
}
const session: Session = {
userId: `u-${Date.now().toString(36)}`,
name: email.split("@")[0].replace(/\W/g, " ").trim() || email,
email,
token: `dev-${Math.random().toString(36).slice(2, 14)}`,
issuedAt: Date.now(),
}
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, JSON.stringify(session))
window.dispatchEvent(new CustomEvent(CHANGE_EVENT))
}
return session
}
export function signOut() {
if (typeof window === "undefined") return
localStorage.removeItem(STORAGE_KEY)
@@ -116,6 +88,26 @@ export function persistFromArcadiaLogin(
return session
}
/** Patch the stored session's identity fields without changing the token.
* Use after the operator edits their profile so the appbar avatar and
* protected-shell greeting reflect the new name/email immediately. */
export function updateSessionUser(patch: {
name?: string
email?: string
}): Session | null {
if (typeof window === "undefined") return null
const current = readFromStorage()
if (!current) return null
const next: Session = {
...current,
name: patch.name?.trim() ? patch.name : current.name,
email: patch.email?.trim() ? patch.email : current.email,
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
window.dispatchEvent(new CustomEvent(CHANGE_EVENT))
return next
}
/** True if a non-expired session is in storage. */
export function hasSession(): boolean {
return !!readFromStorage()