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

84
scripts/mint-search-token.mjs Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
// Mint an HMAC-signed JWT for arcadia-search and (optionally) write it
// into .env.local as VITE_ARCADIA_SEARCH_TOKEN.
//
// arcadia-search expects:
// - HS512 by default when only JWT_HMAC_SECRET is set on the server
// (no PEM). HS256/HS384 work too if JWT_ALGORITHM is overridden.
// - a `tenant_id` claim (configurable server-side via JWT_TENANT_CLAIM).
//
// Usage:
// node scripts/mint-search-token.mjs # uses defaults, writes .env.local
// node scripts/mint-search-token.mjs --print # print only, don't write
// node scripts/mint-search-token.mjs --tenant=foo --days=30
// JWT_HMAC_SECRET=xyz node scripts/mint-search-token.mjs
import { createHmac } from "node:crypto"
import { readFileSync, writeFileSync, existsSync } from "node:fs"
import { fileURLToPath } from "node:url"
import { dirname, resolve } from "node:path"
const HERE = dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = resolve(HERE, "..")
const ENV_LOCAL = resolve(PROJECT_ROOT, ".env.local")
function arg(name, fallback) {
const prefix = `--${name}=`
const hit = process.argv.find((a) => a.startsWith(prefix))
return hit ? hit.slice(prefix.length) : fallback
}
const flag = (name) => process.argv.includes(`--${name}`)
const SECRET = process.env.JWT_HMAC_SECRET ?? "test-secret-change-me"
const TENANT = arg("tenant", process.env.VITE_ARCADIA_TENANT ?? "platform-admin")
const SUBJECT = arg("sub", "arcadia-admin")
const ALGORITHM = (arg("alg", "HS512")).toUpperCase()
const DAYS = Number(arg("days", "365"))
const PRINT_ONLY = flag("print")
const HMAC_ALG = { HS256: "sha256", HS384: "sha384", HS512: "sha512" }[ALGORITHM]
if (!HMAC_ALG) {
console.error(`Unsupported algorithm "${ALGORITHM}". Use HS256, HS384, or HS512.`)
process.exit(1)
}
const b64 = (v) =>
Buffer.from(typeof v === "string" ? v : JSON.stringify(v)).toString("base64url")
const now = Math.floor(Date.now() / 1000)
const header = b64({ alg: ALGORITHM, typ: "JWT" })
const payload = b64({
sub: SUBJECT,
tenant_id: TENANT,
iat: now,
exp: now + DAYS * 86400,
})
const signature = createHmac(HMAC_ALG, SECRET)
.update(`${header}.${payload}`)
.digest("base64url")
const token = `${header}.${payload}.${signature}`
if (PRINT_ONLY) {
console.log(token)
process.exit(0)
}
// Upsert VITE_ARCADIA_SEARCH_TOKEN in .env.local without disturbing other keys.
const KEY = "VITE_ARCADIA_SEARCH_TOKEN"
let existing = ""
if (existsSync(ENV_LOCAL)) existing = readFileSync(ENV_LOCAL, "utf8")
const line = `${KEY}=${token}`
const next = new RegExp(`^${KEY}=.*$`, "m").test(existing)
? existing.replace(new RegExp(`^${KEY}=.*$`, "m"), line)
: (existing.endsWith("\n") || existing === "" ? existing : existing + "\n") + line + "\n"
writeFileSync(ENV_LOCAL, next)
console.log(`Wrote ${KEY} to ${ENV_LOCAL}`)
console.log(` alg: ${ALGORITHM}`)
console.log(` tenant: ${TENANT}`)
console.log(` sub: ${SUBJECT}`)
console.log(` expires: ${new Date((now + DAYS * 86400) * 1000).toISOString()}`)
console.log(` secret: ${SECRET === "test-secret-change-me" ? "(default — override with JWT_HMAC_SECRET=)" : "(from JWT_HMAC_SECRET)"}`)
console.log(``)
console.log(`Restart 'npm run dev' so Vite picks up the new env var.`)