2 Commits

Author SHA1 Message Date
jules
ea34bcd886 fix(auth): reject expired JWT on session read (silent-401 shell)
readFromStorage validated token shape but never checked exp, so an expired
token mounted the full authed shell and every API call 401d silently. Decode
the JWT and treat an expired token as no session. Pattern backported from
skyai-finance. Frontend audit 2026-06-20, rank 1.

Also clears the localStorage Session in onUnauthorized (root.tsx) so a 401
fully logs out instead of leaving a dead session behind getToken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:24:20 +10:00
6ab9b730f5 Merge pull request 'Operator Integrations page + capability framework (P3 UI)' (#1) from feat/integration-registry into main 2026-06-09 13:15:15 +00:00
63 changed files with 123 additions and 85 deletions

View File

@@ -7,9 +7,9 @@ This file is a quick map, not a duplication of upstream docs.
## What Arcadia Admin is ## What Arcadia Admin is
- **Arcadia Admin** is the operator/admin UI for [arcadia-core](../reference/arcadia-core), a multi-tenant Phoenix backend. Surfaces tenant management, user/role admin, billing, audit logs, storage configs, scheduled tasks, feature flags, and platform monitoring on top of arcadia's `/api/v1` and `/admin/*` endpoints. - **Arcadia Admin** is the operator/admin UI for [arcadia-core](../reference/arcadia-app), a multi-tenant Phoenix backend. Surfaces tenant management, user/role admin, billing, audit logs, storage configs, scheduled tasks, feature flags, and platform monitoring on top of arcadia's `/api/v1` and `/admin/*` endpoints.
- **Cloned from** [Vibespace](../vibespace) — the starter for webapps in this style. Vibespace and Skyrise are the upstream sources of truth for the shell and the theme; don't backport arcadia-admin-specific changes into Vibespace unless they're broadly applicable. - **Cloned from** [Vibespace](../vibespace) — the starter for webapps in this style. Vibespace and Skyrise are the upstream sources of truth for the shell and the theme; don't backport arcadia-admin-specific changes into Vibespace unless they're broadly applicable.
- **Backend reference** lives at `../reference/arcadia-core/`. Treat it as read-only documentation — it's the Phoenix umbrella app that owns the OpenAPI spec, controllers, schemas, and seed data. Spec is regenerated from a running arcadia at `http://localhost:4000/api/openapi` via `node ../lib-arcadia-core-client/scripts/sync-spec.mjs` (run from this directory). - **Backend reference** lives at `../reference/arcadia-app/`. Treat it as read-only documentation — it's the Phoenix umbrella app that owns the OpenAPI spec, controllers, schemas, and seed data. Spec is regenerated from a running arcadia at `http://localhost:4000/api/openapi` via `node ../lib-arcadia-client/scripts/sync-spec.mjs` (run from this directory).
- **Skyrise** (`lib-theme-skyrise`) is the canonical theme — premium AI-first glass, iridescent body, vivid text, Apple-spring motion. Theme tweaks belong upstream in Vibespace + Skyrise, not here. - **Skyrise** (`lib-theme-skyrise`) is the canonical theme — premium AI-first glass, iridescent body, vivid text, Apple-spring motion. Theme tweaks belong upstream in Vibespace + Skyrise, not here.
- The brand string lives in **one place**: `app/lib/identity.ts` (`useBrand()` / `getBrand()`). Don't hardcode "Arcadia Admin" in components, page titles, or copy. - The brand string lives in **one place**: `app/lib/identity.ts` (`useBrand()` / `getBrand()`). Don't hardcode "Arcadia Admin" in components, page titles, or copy.

View File

@@ -1,6 +1,6 @@
# Arcadia Admin # Arcadia Admin
Admin webapp for [arcadia-core](../reference/arcadia-core) — the multi-tenant Phoenix backend. Built on the [Crema design system](https://git.sky-ai.com/CremaUIStudio) with the **Skyrise** theme and started from the [Vibespace](../vibespace) starter. Admin webapp for [arcadia-core](../reference/arcadia-app) — the multi-tenant Phoenix backend. Built on the [Crema design system](https://git.sky-ai.com/CremaUIStudio) with the **Skyrise** theme and started from the [Vibespace](../vibespace) starter.
Surfaces tenant management, user/role administration, billing, audit logs, storage configs, scheduled tasks, feature flags, and platform monitoring on top of arcadia's `/api/v1` and `/admin/*` endpoints. Surfaces tenant management, user/role administration, billing, audit logs, storage configs, scheduled tasks, feature flags, and platform monitoring on top of arcadia's `/api/v1` and `/admin/*` endpoints.
@@ -15,7 +15,7 @@ Open [http://localhost:5173](http://localhost:5173). The app talks to arcadia at
To use it for real: To use it for real:
1. Have arcadia running locally (see `../reference/arcadia-core/DEV_SETUP.md`). 1. Have arcadia running locally (see `../reference/arcadia-app/DEV_SETUP.md`).
2. Visit `/login` and sign in with admin credentials. In dev seeds: `admin@example.com` / `AdminP@ssw0rd` (tenant `default`). 2. Visit `/login` and sign in with admin credentials. In dev seeds: `admin@example.com` / `AdminP@ssw0rd` (tenant `default`).
## Configuration ## Configuration
@@ -33,7 +33,7 @@ To use it for real:
`app/components/layout/app-shell.tsx` — left rail + appbar + avatar dropdown. Brand identity in `app/lib/identity.ts` (`name: "Arcadia Admin"`, icon: `Shield`). The shell is **template code, not a lib** — fork it freely as admin features are added. `app/components/layout/app-shell.tsx` — left rail + appbar + avatar dropdown. Brand identity in `app/lib/identity.ts` (`name: "Arcadia Admin"`, icon: `Shield`). The shell is **template code, not a lib** — fork it freely as admin features are added.
### Arcadia client + auth UI ### Arcadia client + auth UI
- [`@crema/arcadia-core-client`](../lib-arcadia-core-client) — typed HTTP client (generic + openapi-fetch-backed `client.typed`), Phoenix Channels realtime, error normalization. Mounted at the root via `<ArcadiaProvider>`. - [`@crema/arcadia-client`](../lib-arcadia-client) — typed HTTP client (generic + openapi-fetch-backed `client.typed`), Phoenix Channels realtime, error normalization. Mounted at the root via `<ArcadiaProvider>`.
- [`@crema/arcadia-auth-ui`](../lib-arcadia-auth-ui) — login / signup / password reset / 2FA forms, themed via Skyrise tokens. The `/login` route renders `<LoginForm>`. - [`@crema/arcadia-auth-ui`](../lib-arcadia-auth-ui) — login / signup / password reset / 2FA forms, themed via Skyrise tokens. The `/login` route renders `<LoginForm>`.
### Skyrise theme ### Skyrise theme
@@ -50,8 +50,8 @@ Surface tints (`body[data-surface="snow|stone|sage|slate"]`) and dark mode (`htm
your-workspace/ your-workspace/
arcadia-admin/ ← this repo arcadia-admin/ ← this repo
vibespace/ ← starter that this was cloned from vibespace/ ← starter that this was cloned from
reference/arcadia-core/ ← Phoenix backend (read-only reference) reference/arcadia-app/ ← Phoenix backend (read-only reference)
lib-arcadia-core-client/ lib-arcadia-client/
lib-arcadia-auth-ui/ lib-arcadia-auth-ui/
lib-action-bus/ lib-action-bus/
lib-aifirst-ui/ lib-aifirst-ui/
@@ -84,4 +84,4 @@ your-workspace/
- [`docs/AI_FIRST.md`](docs/AI_FIRST.md) — command-bus / DSL system tour - [`docs/AI_FIRST.md`](docs/AI_FIRST.md) — command-bus / DSL system tour
- [`app/components/layout/THEME_CONTRACT.md`](app/components/layout/THEME_CONTRACT.md) — token contract every theme must satisfy - [`app/components/layout/THEME_CONTRACT.md`](app/components/layout/THEME_CONTRACT.md) — token contract every theme must satisfy
- `CLAUDE.md` — orientation for an LLM working in this repo - `CLAUDE.md` — orientation for an LLM working in this repo
- `../reference/arcadia-core/` — backend (DEV_SETUP, controllers, OpenAPI source-of-truth) - `../reference/arcadia-app/` — backend (DEV_SETUP, controllers, OpenAPI source-of-truth)

View File

@@ -15,7 +15,7 @@
@source "../../lib-aifirst-ui/src"; @source "../../lib-aifirst-ui/src";
@source "../../lib-llm-ui/src"; @source "../../lib-llm-ui/src";
@source "../../lib-action-bus/src"; @source "../../lib-action-bus/src";
@source "../../lib-arcadia-core-client/src"; @source "../../lib-arcadia-client/src";
@source "../../lib-arcadia-auth-ui/src"; @source "../../lib-arcadia-auth-ui/src";
@source "../../lib-table-ui/src"; @source "../../lib-table-ui/src";
@source "../../lib-search-ui/src"; @source "../../lib-search-ui/src";

View File

@@ -9,7 +9,7 @@
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { Pencil, Plus, Sparkles, Star, Trash2, Upload } from "lucide-react" import { Pencil, Plus, Sparkles, Star, Trash2, Upload } from "lucide-react"
import { useArcadiaClient } from "@crema/arcadia-core-client" import { useArcadiaClient } from "@crema/arcadia-client"
import { import {
loadSettings as loadActiveSettings, loadSettings as loadActiveSettings,
saveSettings as saveActiveSettings, saveSettings as saveActiveSettings,

View File

@@ -15,7 +15,7 @@ import {
X, X,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { AlertBanner, ConfirmDialog } from "@crema/feedback-ui" import { AlertBanner, ConfirmDialog } from "@crema/feedback-ui"
import { Badge } from "~/components/ui/badge" import { Badge } from "~/components/ui/badge"

View File

@@ -5,7 +5,7 @@
// Each tool is a named function with documented args. The LLM never sees // Each tool is a named function with documented args. The LLM never sees
// raw HTTP — only the menu below. // raw HTTP — only the menu below.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
import { import {
createToolRuntime, createToolRuntime,
type ToolDef, type ToolDef,
@@ -53,7 +53,7 @@ const docsClient = createRAGClient("/docs-index.json")
// tooling and wire it through `.env.local`. // tooling and wire it through `.env.local`.
// 3. operator session JWT — works only when arcadia-search shares the // 3. operator session JWT — works only when arcadia-search shares the
// JWT signing secret with the arcadia issuing the operator's session // JWT signing secret with the arcadia issuing the operator's session
// (i.e. local arcadia-core + local arcadia-search with matching keys). // (i.e. local arcadia-app + local arcadia-search with matching keys).
// 4. "dev" literal — only accepted by AUTH_MODE=dev backends. // 4. "dev" literal — only accepted by AUTH_MODE=dev backends.
function readEnv(key: string): string | undefined { function readEnv(key: string): string | undefined {
if (typeof import.meta === "undefined") return undefined if (typeof import.meta === "undefined") return undefined
@@ -667,7 +667,7 @@ const TOOLS: ToolDef<ToolCtx>[] = [
{ {
name: "search_docs", name: "search_docs",
description: description:
"Search the arcadia-core documentation (architecture, API surface, deploy/setup guides) for passages relevant to a question. Use this for conceptual or procedural questions where the live API tools won't help — e.g. 'how does multi-tenant isolation work', 'how do I deploy to production', 'what is the modular monolith pattern'. Returns up to `limit` ranked passages with title, sourcePath, and excerpt. Cite results by sourcePath in your reply.", "Search the arcadia-app documentation (architecture, API surface, deploy/setup guides) for passages relevant to a question. Use this for conceptual or procedural questions where the live API tools won't help — e.g. 'how does multi-tenant isolation work', 'how do I deploy to production', 'what is the modular monolith pattern'. Returns up to `limit` ranked passages with title, sourcePath, and excerpt. Cite results by sourcePath in your reply.",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
@@ -715,7 +715,7 @@ const TOOLS: ToolDef<ToolCtx>[] = [
{ {
name: "search_kb", name: "search_kb",
description: description:
"Lexical (BM25) search over the arcadia-search Tantivy backend. Returns chunks with snippets + chunk_ids that can be passed to `read_chunk` to expand. Prefer this over `search_docs` (browser) when you need richer hits or when the content wouldn't be in the bundled docs.\n\nKnown corpora on the platform-admin tenant:\n- `docs` — arcadia-core architecture/ops docs (same as the browser RAG, server-hosted for parity).\n- `operator-tools` — arcadia-search + arcadia-admin documentation (admin sidecar, deploy script, search admin UI, MULTI_TENANT, RAG, AI_FIRST, LIBS, LLM_PROXY_CONTRACT).\n- `files` — markdown/text files uploaded by tenant users via arcadia-core.\n\nIf you're not sure what's available, call `list_search_corpora` first. Operators can add new corpora via the `/search` route.", "Lexical (BM25) search over the arcadia-search Tantivy backend. Returns chunks with snippets + chunk_ids that can be passed to `read_chunk` to expand. Prefer this over `search_docs` (browser) when you need richer hits or when the content wouldn't be in the bundled docs.\n\nKnown corpora on the platform-admin tenant:\n- `docs` — arcadia-app architecture/ops docs (same as the browser RAG, server-hosted for parity).\n- `operator-tools` — arcadia-search + arcadia-admin documentation (admin sidecar, deploy script, search admin UI, MULTI_TENANT, RAG, AI_FIRST, LIBS, LLM_PROXY_CONTRACT).\n- `files` — markdown/text files uploaded by tenant users via arcadia-app.\n\nIf you're not sure what's available, call `list_search_corpora` first. Operators can add new corpora via the `/search` route.",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {

View File

@@ -1,5 +1,5 @@
// Domain primer baked into the assistant's system prompt so it understands // Domain primer baked into the assistant's system prompt so it understands
// what arcadia-core is, what platform admins do, and how the data model fits // what arcadia-app is, what platform admins do, and how the data model fits
// together. Keep this tight — it costs context tokens on every turn. // together. Keep this tight — it costs context tokens on every turn.
export const ARCADIA_KNOWLEDGE = `Arcadia (the backend you administer): export const ARCADIA_KNOWLEDGE = `Arcadia (the backend you administer):
@@ -31,7 +31,7 @@ Things to keep in mind when assisting:
- Writes are auditable. Suggest the user double-check tenant slug and impact before suspend/deactivate. Deactivate is harsher than suspend — only use when clearly intended. - Writes are auditable. Suggest the user double-check tenant slug and impact before suspend/deactivate. Deactivate is harsher than suspend — only use when clearly intended.
- The operator can impersonate tenant users for debugging (POST /api/v1/admin/impersonate/:user_id) — surface this when they ask "why can't user X log in". - The operator can impersonate tenant users for debugging (POST /api/v1/admin/impersonate/:user_id) — surface this when they ask "why can't user X log in".
- Quotas / rate cards / billing config errors usually surface as 402/403 from /api/v1 endpoints — diagnose by checking the tenant's billing-config and api-metering quotas. - Quotas / rate cards / billing config errors usually surface as 402/403 from /api/v1 endpoints — diagnose by checking the tenant's billing-config and api-metering quotas.
- The reference Phoenix app lives at \`reference/arcadia-core/\` in the workspace; its OpenAPI spec is at /api/openapi (sync via \`node ../lib-arcadia-core-client/scripts/sync-spec.mjs\`). - The reference Phoenix app lives at \`reference/arcadia-app/\` in the workspace; its OpenAPI spec is at /api/openapi (sync via \`node ../lib-arcadia-client/scripts/sync-spec.mjs\`).
- Search admin (arcadia-search) is a separate service. Manage tenants/corpora at \`/search\`. Use \`list_search_corpora\` if you don't know what's indexed; \`rebuild_search_corpus\` after uploads or when results look stale; \`search_kb\` / \`read_chunk\` to query. - Search admin (arcadia-search) is a separate service. Manage tenants/corpora at \`/search\`. Use \`list_search_corpora\` if you don't know what's indexed; \`rebuild_search_corpus\` after uploads or when results look stale; \`search_kb\` / \`read_chunk\` to query.
When the user asks something that maps to a tool, call it. When they ask about a concept, explain it from this primer in plain language. Write tools (suspend_tenant, activate_tenant) prompt the operator with an inline confirm card before they actually run — you do not need to ask in prose first; just call the tool and the user will see the confirmation UI. If the user denies a write, do not retry it; ask what they'd like to do differently. When the user asks something that maps to a tool, call it. When they ask about a concept, explain it from this primer in plain language. Write tools (suspend_tenant, activate_tenant) prompt the operator with an inline confirm card before they actually run — you do not need to ask in prose first; just call the tool and the user will see the confirmation UI. If the user denies a write, do not retry it; ask what they'd like to do differently.

View File

@@ -1,7 +1,7 @@
// Platform announcements helpers. // Platform announcements helpers.
// Backend: /api/v1/admin/announcements (admin CRUD). // Backend: /api/v1/admin/announcements (admin CRUD).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type AnnouncementType = export type AnnouncementType =
| "info" | "info"

View File

@@ -4,7 +4,7 @@
// once — list/show endpoints only return the prefix. Callers must surface // once — list/show endpoints only return the prefix. Callers must surface
// the value to the user immediately on create. // the value to the user immediately on create.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface ApiKey { export interface ApiKey {
id: string id: string

View File

@@ -1,7 +1,7 @@
// Audit log + observability helpers. // Audit log + observability helpers.
// All endpoints are read-only; the backend writes audit events itself. // All endpoints are read-only; the backend writes audit events itself.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type AuditSeverity = "info" | "warning" | "error" | "critical" | string export type AuditSeverity = "info" | "warning" | "error" | "critical" | string

View File

@@ -2,7 +2,7 @@
// Backend: /api/v1/platform/buckets/*. All operations require a // Backend: /api/v1/platform/buckets/*. All operations require a
// storage_config_id pointing at a credential row in /api/v1/storage_configs. // storage_config_id pointing at a credential row in /api/v1/storage_configs.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface Bucket { export interface Bucket {
name: string name: string

View File

@@ -2,7 +2,7 @@
// used by the avatar uploader. The full digital-objects API is much // used by the avatar uploader. The full digital-objects API is much
// larger; add endpoints here as we wire more features. // larger; add endpoints here as we wire more features.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface DigitalObject { export interface DigitalObject {
id: string id: string

View File

@@ -2,9 +2,9 @@
// //
// Backed by /api/v1/health* (public — no auth). Each subsystem is probed // Backed by /api/v1/health* (public — no auth). Each subsystem is probed
// independently; the overall endpoint aggregates and returns 503 if any // independently; the overall endpoint aggregates and returns 503 if any
// subsystem is not "ok". See arcadia-core commit f427892. // subsystem is not "ok". See arcadia-app commit f427892.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type HealthSubsystem = "api" | "db" | "workers" | "storage" export type HealthSubsystem = "api" | "db" | "workers" | "storage"

View File

@@ -4,7 +4,7 @@
// arcadia-console's tenant surface); this file just exposes operator-idiomatic // arcadia-console's tenant surface); this file just exposes operator-idiomatic
// names so the page reads naturally. // names so the page reads naturally.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
import { import {
createIntegrationsApi, createIntegrationsApi,
type CredentialInput, type CredentialInput,

View File

@@ -1,7 +1,7 @@
// Arcadia invitations API helpers. // Arcadia invitations API helpers.
// Backed by /api/v1/invitations. // Backed by /api/v1/invitations.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface InvitationRole { export interface InvitationRole {
id: string id: string

View File

@@ -8,7 +8,7 @@
// `tenant_id: null` configurations are platform-defaults visible to // `tenant_id: null` configurations are platform-defaults visible to
// every tenant. Names are unique within (tenant, name). // every tenant. Names are unique within (tenant, name).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type LlmProvider = "openai" | "anthropic" | "deepseek" | "qwen" | "lmstudio" export type LlmProvider = "openai" | "anthropic" | "deepseek" | "qwen" | "lmstudio"
@@ -124,7 +124,7 @@ export async function getCatalog(arcadia: ArcadiaClient): Promise<CatalogEntry[]
/** /**
* Compute cost in cents for a given input/output token count using a * Compute cost in cents for a given input/output token count using a
* configuration's published rates. Mirrors `LlmConfiguration.compute_cost_cents/3` * configuration's published rates. Mirrors `LlmConfiguration.compute_cost_cents/3`
* in arcadia-core — keep in sync. * in arcadia-app — keep in sync.
*/ */
export function computeCostCents( export function computeCostCents(
config: Pick<LlmConfiguration, "input_cost_per_million" | "output_cost_per_million">, config: Pick<LlmConfiguration, "input_cost_per_million" | "output_cost_per_million">,

View File

@@ -1,13 +1,13 @@
// Arcadia LLM proxy client. // Arcadia LLM proxy client.
// //
// Implements the spec in docs/LLM_PROXY_CONTRACT.md against arcadia-core's // Implements the spec in docs/LLM_PROXY_CONTRACT.md against arcadia-app's
// POST /api/v1/ai/llm/chat. The lib (@crema/llm-providers-ui buildAdapter) // POST /api/v1/ai/llm/chat. The lib (@crema/llm-providers-ui buildAdapter)
// owns the streaming chat path itself; this module exposes a lightweight // owns the streaming chat path itself; this module exposes a lightweight
// non-streaming probe so the Settings "Test connection" button can verify // non-streaming probe so the Settings "Test connection" button can verify
// the proxy round-trips end-to-end (auth → secret resolution → upstream // the proxy round-trips end-to-end (auth → secret resolution → upstream
// dispatch → response shape). // dispatch → response shape).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type LLMProxyProvider = export type LLMProxyProvider =
| "openai" | "openai"

View File

@@ -1,7 +1,7 @@
// Tenant memberships — the M:N glue between users and tenants. // Tenant memberships — the M:N glue between users and tenants.
// Backend: /api/v1/admin/memberships (admin) + /api/v1/me/tenants (self). // Backend: /api/v1/admin/memberships (admin) + /api/v1/me/tenants (self).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type MembershipStatus = "active" | "suspended" | "deactivated" | string export type MembershipStatus = "active" | "suspended" | "deactivated" | string

View File

@@ -2,7 +2,7 @@
// Wraps /api/v1/admin/monitoring/* + /api/v1/platform/* + a few observability // Wraps /api/v1/admin/monitoring/* + /api/v1/platform/* + a few observability
// endpoints used by the monitoring dashboard. // endpoints used by the monitoring dashboard.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
// --- Rate limits --------------------------------------------------------- // --- Rate limits ---------------------------------------------------------

View File

@@ -1,7 +1,7 @@
// Networking helpers: firewalls, VPCs, domains + DNS records, floating IPs. // Networking helpers: firewalls, VPCs, domains + DNS records, floating IPs.
// Backend: /api/v1/platform/{firewalls,vpcs,domains,floating_ips,...}. // Backend: /api/v1/platform/{firewalls,vpcs,domains,floating_ips,...}.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
const BASE = "/api/v1/platform" const BASE = "/api/v1/platform"

View File

@@ -5,7 +5,7 @@
// `OrganizationContext` plug, so the same per-org routes used by end-users // `OrganizationContext` plug, so the same per-org routes used by end-users
// are used here to mutate any org in the tenant. // are used here to mutate any org in the tenant.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type OrgStatus = "active" | "frozen" | "pending_deletion" | string export type OrgStatus = "active" | "frozen" | "pending_deletion" | string
export type OnOwnerRemoval = export type OnOwnerRemoval =

View File

@@ -3,7 +3,7 @@
// profile fields. The "profile" here is the per-tenant profile row, not // profile fields. The "profile" here is the per-tenant profile row, not
// the auth account. // the auth account.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface Profile { export interface Profile {
id: string id: string

View File

@@ -1,7 +1,7 @@
// Arcadia roles API helpers. // Arcadia roles API helpers.
// Backed by /api/v1/roles (resources route, except :new and :edit). // Backed by /api/v1/roles (resources route, except :new and :edit).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface Role { export interface Role {
id: string id: string

View File

@@ -1,7 +1,7 @@
// Scheduled tasks (cron) helpers. // Scheduled tasks (cron) helpers.
// Backend: /api/v1/admin/scheduled-tasks (CRUD + runs/enable/disable/trigger). // Backend: /api/v1/admin/scheduled-tasks (CRUD + runs/enable/disable/trigger).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type ScheduledTaskAction = "webhook" | "event" export type ScheduledTaskAction = "webhook" | "event"

View File

@@ -5,7 +5,7 @@
// exposed by these endpoints. Tenant-side resolution (returning the value) // exposed by these endpoints. Tenant-side resolution (returning the value)
// goes through a separate runtime endpoint that's not used by the admin UI. // goes through a separate runtime endpoint that's not used by the admin UI.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type SecretCategory = export type SecretCategory =
| "api_key" | "api_key"

View File

@@ -2,7 +2,7 @@
// Backend: /api/v1/sso/identity-providers (tenant CRUD) + /sessions. // Backend: /api/v1/sso/identity-providers (tenant CRUD) + /sessions.
// Note: certificates are large and write-only. // Note: certificates are large and write-only.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface IdentityProvider { export interface IdentityProvider {
id: string id: string

View File

@@ -1,7 +1,7 @@
// Status page helpers — components, incidents, subscribers. // Status page helpers — components, incidents, subscribers.
// Backend: /api/v1/admin/status-page/* (admin CRUD). // Backend: /api/v1/admin/status-page/* (admin CRUD).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type ComponentStatus = export type ComponentStatus =
| "operational" | "operational"

View File

@@ -8,7 +8,7 @@
// generic `arcadia.GET<T>` / `arcadia.POST<T>` / etc. — same pattern as // generic `arcadia.GET<T>` / `arcadia.POST<T>` / etc. — same pattern as
// `tenants.ts`. Switch to `arcadia.typed.*` when the spec gains coverage. // `tenants.ts`. Switch to `arcadia.typed.*` when the spec gains coverage.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type StorageBackend = "s3" | "local" | "gcs" export type StorageBackend = "s3" | "local" | "gcs"
export type StorageStatus = "active" | "inactive" | "degraded" | "maintenance" export type StorageStatus = "active" | "inactive" | "degraded" | "maintenance"

View File

@@ -6,7 +6,7 @@
// gains coverage, switch to `arcadia.typed.GET("/api/v1/admin/tenants", ...)` // gains coverage, switch to `arcadia.typed.GET("/api/v1/admin/tenants", ...)`
// and drop these manual types. // and drop these manual types.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type TenantStatus = "active" | "suspended" | "deactivated" | string export type TenantStatus = "active" | "suspended" | "deactivated" | string

View File

@@ -1,6 +1,6 @@
// Per-user usage + quota helpers. // Per-user usage + quota helpers.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export interface UserUsage { export interface UserUsage {
storage_used_bytes: number storage_used_bytes: number

View File

@@ -4,7 +4,7 @@
// describe these operations as typed paths, so we hand-roll types and use // describe these operations as typed paths, so we hand-roll types and use
// the generic verb methods on the client. Same pattern as tenants.ts. // the generic verb methods on the client. Same pattern as tenants.ts.
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type UserStatus = "active" | "inactive" | "suspended" export type UserStatus = "active" | "inactive" | "suspended"

View File

@@ -1,7 +1,7 @@
// Outbound webhook helpers. // Outbound webhook helpers.
// Backend: /api/v1/webhooks (CRUD + pause/resume/regenerate-secret/deliveries/stats/test). // Backend: /api/v1/webhooks (CRUD + pause/resume/regenerate-secret/deliveries/stats/test).
import type { ArcadiaClient } from "@crema/arcadia-core-client" import type { ArcadiaClient } from "@crema/arcadia-client"
export type WebhookStatus = "active" | "paused" | "disabled" export type WebhookStatus = "active" | "paused" | "disabled"
export type WebhookRetryStrategy = "linear" | "exponential" export type WebhookRetryStrategy = "linear" | "exponential"

View File

@@ -1,13 +1,13 @@
// Arcadia LLM-gateway client. // Arcadia LLM-gateway client.
// //
// The integration registry lives on arcadia-llm-gateway, not arcadia-core, so // The integration registry lives on arcadia-llm-gateway, not arcadia-app, so
// it needs its own ArcadiaClient pointed at a different base URL. Everything // it needs its own ArcadiaClient pointed at a different base URL. Everything
// else is identical to the arcadia-core client: the same access token (the // else is identical to the arcadia-app client: the same access token (the
// gateway validates arcadia-core JWTs via the shared Guardian secret) and the // gateway validates arcadia-app JWTs via the shared Guardian secret) and the
// same 401 cleanup. The gateway's CORS already allows localhost + any // same 401 cleanup. The gateway's CORS already allows localhost + any
// *.sky-ai.com origin, so the browser calls it directly. // *.sky-ai.com origin, so the browser calls it directly.
import { createArcadiaClient, type ArcadiaClient } from "@crema/arcadia-core-client" import { createArcadiaClient, type ArcadiaClient } from "@crema/arcadia-client"
const GATEWAY_URL = import.meta.env.VITE_LLM_GATEWAY_URL ?? "http://localhost:4015" const GATEWAY_URL = import.meta.env.VITE_LLM_GATEWAY_URL ?? "http://localhost:4015"

View File

@@ -11,7 +11,7 @@
// panel remains the place to switch between configs. // panel remains the place to switch between configs.
import { useEffect } from "react" import { useEffect } from "react"
import { useArcadiaClient } from "@crema/arcadia-core-client" import { useArcadiaClient } from "@crema/arcadia-client"
import { import {
loadSettings, loadSettings,
saveSettings, saveSettings,

View File

@@ -3,7 +3,7 @@
// immediately, without waiting for the user to navigate to /profile. // immediately, without waiting for the user to navigate to /profile.
import { useEffect } from "react" import { useEffect } from "react"
import { useArcadiaClient } from "@crema/arcadia-core-client" import { useArcadiaClient } from "@crema/arcadia-client"
import { fetchDigitalObjectAsBlobUrl } from "~/lib/arcadia/digital-objects" import { fetchDigitalObjectAsBlobUrl } from "~/lib/arcadia/digital-objects"
import { getProfile, pickAvatarUrl } from "~/lib/arcadia/profiles" import { getProfile, pickAvatarUrl } from "~/lib/arcadia/profiles"

View File

@@ -44,6 +44,10 @@ function readFromStorage(): Session | null {
typeof parsed.token !== "string" typeof parsed.token !== "string"
) )
return null return null
// An expired JWT is not a session: without this the shell renders as
// "logged in" and every API call 401s silently. Treat it as null so the
// app bounces to /login. (Frontend audit 2026-06-20.)
if (isTokenExpired(parsed.token)) return null
return { return {
userId: parsed.userId, userId: parsed.userId,
name: name:
@@ -76,6 +80,35 @@ export function loadSession(): Session | null {
return readFromStorage() return readFromStorage()
} }
// A token counts as expired only if it's a JWT carrying an `exp` in the past
// (minus a small clock-skew grace). Non-JWT dev/mock tokens (no decodable
// `exp`) are treated as non-expiring so offline/test flows keep working.
const TOKEN_EXPIRY_SKEW_S = 30
export function isTokenExpired(token: string | undefined | null): boolean {
if (!token) return true
const claims = decodeJwtPayload(token)
const exp =
claims && typeof claims.exp === "number" ? (claims.exp as number) : null
if (exp === null) return false
return Date.now() / 1000 >= exp - TOKEN_EXPIRY_SKEW_S
}
function decodeJwtPayload(token: string): Record<string, unknown> | null {
const parts = token.split(".")
if (parts.length !== 3) return null
try {
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/")
const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - (b64.length % 4))
const json =
typeof atob === "function"
? atob(b64 + pad)
: Buffer.from(b64 + pad, "base64").toString("utf-8")
return JSON.parse(json) as Record<string, unknown>
} catch {
return null
}
}
export function signOut() { export function signOut() {
if (typeof window === "undefined") return if (typeof window === "undefined") return
localStorage.removeItem(STORAGE_KEY) localStorage.removeItem(STORAGE_KEY)

View File

@@ -12,9 +12,10 @@ import "./app.css"
import { ToastProvider, Toaster } from "@crema/notification-ui" import { ToastProvider, Toaster } from "@crema/notification-ui"
import { CommandBusProvider } from "@crema/action-bus" import { CommandBusProvider } from "@crema/action-bus"
import { ArcadiaProvider } from "@crema/arcadia-core-client" import { ArcadiaProvider } from "@crema/arcadia-client"
import { LlmConfigBootstrap } from "~/lib/llm-config-bootstrap" import { LlmConfigBootstrap } from "~/lib/llm-config-bootstrap"
import { ProfileBootstrap } from "~/lib/profile-bootstrap" import { ProfileBootstrap } from "~/lib/profile-bootstrap"
import { signOut } from "~/lib/session"
// CREMA:PROVIDERS-IMPORTS // CREMA:PROVIDERS-IMPORTS
const ARCADIA_URL = import.meta.env.VITE_ARCADIA_URL ?? "http://localhost:4000" const ARCADIA_URL = import.meta.env.VITE_ARCADIA_URL ?? "http://localhost:4000"
@@ -59,6 +60,10 @@ export default function App() {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
sessionStorage.removeItem("arcadia_access_token") sessionStorage.removeItem("arcadia_access_token")
sessionStorage.removeItem("arcadia_refresh_token") sessionStorage.removeItem("arcadia_refresh_token")
// Also clear the localStorage Session (crema.session); otherwise
// useSession() still reports "logged in" after a 401 and the shell
// keeps mounting with a dead token. (Frontend audit 2026-06-20.)
signOut()
} }
}} }}
> >

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { Activity, Eye, RefreshCw } from "lucide-react" import { Activity, Eye, RefreshCw } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -67,7 +67,7 @@ import {
import { addLibraryItem } from "~/lib/library" import { addLibraryItem } from "~/lib/library"
import { Avatar, AvatarFallback } from "~/components/ui/avatar" import { Avatar, AvatarFallback } from "~/components/ui/avatar"
import { pageTitle } from "~/lib/page-meta" import { pageTitle } from "~/lib/page-meta"
import { useArcadiaClient } from "@crema/arcadia-core-client" import { useArcadiaClient } from "@crema/arcadia-client"
import type { Message as LLMMessage, ToolCall } from "@crema/llm-ui" import type { Message as LLMMessage, ToolCall } from "@crema/llm-ui"
// Shape of a single hit returned by the `search_docs` tool. Defined here // Shape of a single hit returned by the `search_docs` tool. Defined here
// rather than imported from the lib because the tool wrapper in // rather than imported from the lib because the tool wrapper in

View File

@@ -7,7 +7,7 @@ import {
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -98,7 +98,7 @@ import {
runLLMToolCalls, runLLMToolCalls,
} from "~/lib/admin-tools" } from "~/lib/admin-tools"
import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge" import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge"
import { useArcadiaClient } from "@crema/arcadia-core-client" import { useArcadiaClient } from "@crema/arcadia-client"
import { ConfirmCard } from "~/components/assistant/confirm-card" import { ConfirmCard } from "~/components/assistant/confirm-card"
import type { ToolCall } from "@crema/llm-ui" import type { ToolCall } from "@crema/llm-ui"

View File

@@ -17,7 +17,7 @@ import {
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
DataTable, DataTable,

View File

@@ -11,7 +11,7 @@ import {
Users as UsersIcon, Users as UsersIcon,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { AlertBanner } from "@crema/feedback-ui" import { AlertBanner } from "@crema/feedback-ui"
import { AppShell } from "~/components/layout/app-shell" import { AppShell } from "~/components/layout/app-shell"

View File

@@ -14,7 +14,7 @@ import {
Plus, Plus,
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError } from "@crema/arcadia-core-client" import { ArcadiaError } from "@crema/arcadia-client"
import { AppShell } from "~/components/layout/app-shell" import { AppShell } from "~/components/layout/app-shell"
import { Badge } from "~/components/ui/badge" import { Badge } from "~/components/ui/badge"

View File

@@ -9,7 +9,7 @@ import {
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -14,7 +14,7 @@ import {
Zap, Zap,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { AlertBanner } from "@crema/feedback-ui" import { AlertBanner } from "@crema/feedback-ui"
import { import {
BarChart, BarChart,

View File

@@ -10,7 +10,7 @@ import {
Wifi, Wifi,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { AlertBanner, ConfirmDialog, EmptyState, LoadingOverlay } from "@crema/feedback-ui" import { AlertBanner, ConfirmDialog, EmptyState, LoadingOverlay } from "@crema/feedback-ui"
import { AppShell } from "~/components/layout/app-shell" import { AppShell } from "~/components/layout/app-shell"

View File

@@ -11,7 +11,7 @@ import {
Users as UsersIcon, Users as UsersIcon,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { Check, RefreshCw, Trash2 } from "lucide-react" import { Check, RefreshCw, Trash2 } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { AlertBanner } from "@crema/feedback-ui" import { AlertBanner } from "@crema/feedback-ui"
import { AppShell } from "~/components/layout/app-shell" import { AppShell } from "~/components/layout/app-shell"

View File

@@ -12,7 +12,7 @@ import {
Zap, Zap,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -12,7 +12,7 @@ import {
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -16,7 +16,7 @@ import {
useSettings as useProviderSettings, useSettings as useProviderSettings,
type LLMProvidersSettings, type LLMProvidersSettings,
} from "@crema/llm-providers-ui" } from "@crema/llm-providers-ui"
import { useArcadiaClient } from "@crema/arcadia-core-client" import { useArcadiaClient } from "@crema/arcadia-client"
import { probeProxy, type LLMProxyProvider } from "~/lib/arcadia/llm-proxy" import { probeProxy, type LLMProxyProvider } from "~/lib/arcadia/llm-proxy"
import { LlmConfigurationsPanel } from "~/components/settings/llm-configurations-panel" import { LlmConfigurationsPanel } from "~/components/settings/llm-configurations-panel"

View File

@@ -9,7 +9,7 @@ import {
X, X,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,
@@ -580,7 +580,7 @@ function IdpEditorDialog({
id="idp-callback" id="idp-callback"
value={callbackUrl} value={callbackUrl}
onChange={(e) => setCallbackUrl(e.target.value)} onChange={(e) => setCallbackUrl(e.target.value)}
placeholder="https://your-arcadia-core/api/v1/auth/saml/callback" placeholder="https://your-arcadia-app/api/v1/auth/saml/callback"
className="font-mono text-xs" className="font-mono text-xs"
data-action="idp-form-callback" data-action="idp-form-callback"
/> />

View File

@@ -8,7 +8,7 @@ import {
Trash2, Trash2,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { AlertBanner, ConfirmDialog, EmptyState, LoadingOverlay } from "@crema/feedback-ui" import { AlertBanner, ConfirmDialog, EmptyState, LoadingOverlay } from "@crema/feedback-ui"
import { import {
IncidentTimeline, IncidentTimeline,

View File

@@ -12,7 +12,7 @@ import {
Wrench, Wrench,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react" import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react"
import { Pause, Play, Plus, RefreshCw } from "lucide-react" import { Pause, Play, Plus, RefreshCw } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -14,7 +14,7 @@ import {
X, X,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -15,7 +15,7 @@ import {
Webhook as WebhookIcon, Webhook as WebhookIcon,
} from "lucide-react" } from "lucide-react"
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client" import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import { import {
ActionsCell, ActionsCell,
BadgeCell, BadgeCell,

View File

@@ -1,6 +1,6 @@
# LLM Proxy Contract # LLM Proxy Contract
> **Status: implemented.** Backend lives in `arcadia-core` at `apps/arcadia_core/lib/arcadia/ai/llm_proxy*` (see commit `75669f1`). This document remains the contract that `lib-llm-providers-ui` and `app/lib/arcadia/llm-proxy.ts` expect from arcadia — keep it in sync if either side changes. > **Status: implemented.** Backend lives in `arcadia-app` at `apps/arcadia_core/lib/arcadia/ai/llm_proxy*` (see commit `75669f1`). This document remains the contract that `lib-llm-providers-ui` and `app/lib/arcadia/llm-proxy.ts` expect from arcadia — keep it in sync if either side changes.
## Why a proxy? ## Why a proxy?

View File

@@ -14,7 +14,7 @@ shape.
|---|---|---| |---|---|---|
| Lib / service | `@crema/lexical-rag-ui` | `arcadia-search` (Rust) | | Lib / service | `@crema/lexical-rag-ui` | `arcadia-search` (Rust) |
| Engine | MiniSearch (BM25, JS) | Tantivy (BM25, Rust) | | Engine | MiniSearch (BM25, JS) | Tantivy (BM25, Rust) |
| Where it runs | In the user's browser | Sibling of arcadia-core | | Where it runs | In the user's browser | Sibling of arcadia-app |
| Index storage | Static JSON, fetched once | mmap'd disk, ~3080MB resident | | Index storage | Static JSON, fetched once | mmap'd disk, ~3080MB resident |
| Practical corpus size | ~510MB / ~50100k chunks | GB-scale, no hard cap | | Practical corpus size | ~510MB / ~50100k chunks | GB-scale, no hard cap |
| Update cadence | Static — rebuilt at app build time | Live — cron, webhook, or admin trigger | | Update cadence | Static — rebuilt at app build time | Live — cron, webhook, or admin trigger |
@@ -58,7 +58,7 @@ The index is a single JSON file built offline by
**How it's wired here.** **How it's wired here.**
- Build script: `arcadia-admin/scripts/build-docs-index.mjs` reads - Build script: `arcadia-admin/scripts/build-docs-index.mjs` reads
markdown from `../reference/arcadia-core/`, chunks at H1H3, markdown from `../reference/arcadia-app/`, chunks at H1H3,
produces `public/docs-index.json`. Runs on `npm run build:docs` produces `public/docs-index.json`. Runs on `npm run build:docs`
(and as the `prebuild` step before `npm run build`). (and as the `prebuild` step before `npm run build`).
- Tool wrapper: `app/lib/admin-tools.ts` constructs a singleton - Tool wrapper: `app/lib/admin-tools.ts` constructs a singleton
@@ -130,7 +130,7 @@ text extraction, webhook signature, service tokens) see
The default deploy runs **both**: The default deploy runs **both**:
- `search_docs` indexes the same arcadia-core docs the parity corpus - `search_docs` indexes the same arcadia-app docs the parity corpus
on `arcadia-search` indexes. Same content, two engines. on `arcadia-search` indexes. Same content, two engines.
- This is intentional — it means the assistant always has *something* - This is intentional — it means the assistant always has *something*
to search, even if `arcadia-search` is down or unreachable. The to search, even if `arcadia-search` is down or unreachable. The

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
// Build the /docs-index.json bundle consumed by @crema/lexical-rag-ui at // Build the /docs-index.json bundle consumed by @crema/lexical-rag-ui at
// runtime. Thin wrapper — all engine logic lives in the lib's builder.ts; // runtime. Thin wrapper — all engine logic lives in the lib's builder.ts;
// this file owns the per-app config (which arcadia-core docs to index and // this file owns the per-app config (which arcadia-app docs to index and
// how to tag them). // how to tag them).
// //
// Run: npm run build:docs // Run: npm run build:docs
@@ -18,11 +18,11 @@ import MiniSearch from "minisearch"
import { buildIndex } from "../../lib-lexical-rag-ui/src/builder.mjs" import { buildIndex } from "../../lib-lexical-rag-ui/src/builder.mjs"
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..") const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const ARCADIA = resolve(ROOT, "../reference/arcadia-core") const ARCADIA = resolve(ROOT, "../reference/arcadia-app")
const OUT = resolve(ROOT, "public/docs-index.json") const OUT = resolve(ROOT, "public/docs-index.json")
const SOURCES = [ const SOURCES = [
// Arcadia platform docs (resolved against ARCADIA = ../reference/arcadia-core). // Arcadia platform docs (resolved against ARCADIA = ../reference/arcadia-app).
{ path: "README.md", tags: ["core"] }, { path: "README.md", tags: ["core"] },
{ path: "docs/ARCADIA.md", tags: ["core"] }, { path: "docs/ARCADIA.md", tags: ["core"] },
{ path: "docs/MODULAR_MONOLITH.md", tags: ["core"] }, { path: "docs/MODULAR_MONOLITH.md", tags: ["core"] },

View File

@@ -26,8 +26,8 @@
"@crema/llm-ui/*": ["../lib-llm-ui/src/*"], "@crema/llm-ui/*": ["../lib-llm-ui/src/*"],
"@crema/action-bus": ["../lib-action-bus/src/index.tsx"], "@crema/action-bus": ["../lib-action-bus/src/index.tsx"],
"@crema/action-bus/*": ["../lib-action-bus/src/*"], "@crema/action-bus/*": ["../lib-action-bus/src/*"],
"@crema/arcadia-core-client": ["../lib-arcadia-core-client/src/index.tsx"], "@crema/arcadia-client": ["../lib-arcadia-client/src/index.tsx"],
"@crema/arcadia-core-client/*": ["../lib-arcadia-core-client/src/*"], "@crema/arcadia-client/*": ["../lib-arcadia-client/src/*"],
"@crema/integration-registry-client": ["../lib-integration-registry-client/src/index.tsx"], "@crema/integration-registry-client": ["../lib-integration-registry-client/src/index.tsx"],
"@crema/integration-registry-client/*": ["../lib-integration-registry-client/src/*"], "@crema/integration-registry-client/*": ["../lib-integration-registry-client/src/*"],
"@crema/arcadia-auth-ui": ["../lib-arcadia-auth-ui/src/index.tsx"], "@crema/arcadia-auth-ui": ["../lib-arcadia-auth-ui/src/index.tsx"],

View File

@@ -60,7 +60,7 @@ const searchUiSrc = fileURLToPath(
new URL("../lib-search-ui/src", import.meta.url), new URL("../lib-search-ui/src", import.meta.url),
) )
const arcadiaClientSrc = fileURLToPath( const arcadiaClientSrc = fileURLToPath(
new URL("../lib-arcadia-core-client/src", import.meta.url), new URL("../lib-arcadia-client/src", import.meta.url),
) )
const integrationRegistryClientSrc = fileURLToPath( const integrationRegistryClientSrc = fileURLToPath(
new URL("../lib-integration-registry-client/src", import.meta.url), new URL("../lib-integration-registry-client/src", import.meta.url),
@@ -175,7 +175,7 @@ export default defineConfig({
{ find: "@crema/auth-ui", replacement: `${authUiSrc}/index.tsx` }, { find: "@crema/auth-ui", replacement: `${authUiSrc}/index.tsx` },
{ find: "@crema/table-ui", replacement: `${tableUiSrc}/index.tsx` }, { find: "@crema/table-ui", replacement: `${tableUiSrc}/index.tsx` },
{ find: "@crema/search-ui", replacement: `${searchUiSrc}/index.tsx` }, { find: "@crema/search-ui", replacement: `${searchUiSrc}/index.tsx` },
{ find: "@crema/arcadia-core-client", replacement: `${arcadiaClientSrc}/index.tsx` }, { find: "@crema/arcadia-client", replacement: `${arcadiaClientSrc}/index.tsx` },
{ {
find: "@crema/integration-registry-client", find: "@crema/integration-registry-client",
replacement: `${integrationRegistryClientSrc}/index.tsx`, replacement: `${integrationRegistryClientSrc}/index.tsx`,