Compare commits
2 Commits
feat/integ
...
feat/plan-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed12a434c7 | ||
| 6ab9b730f5 |
@@ -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.
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -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)
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
145
app/lib/arcadia/billing.ts
Normal file
145
app/lib/arcadia/billing.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Tenant self-service billing helpers — wraps /api/v1/billing/* on arcadia-core.
|
||||||
|
//
|
||||||
|
// The provider is resolved server-side (mock in dev, a real PSP in prod), so
|
||||||
|
// these helpers are provider-agnostic: `createCheckoutSession` returns a hosted
|
||||||
|
// URL to redirect the browser to, whatever the backend provider is.
|
||||||
|
// Shapes mirror `ArcadiaWeb.API.BillingJSON`.
|
||||||
|
|
||||||
|
import type { ArcadiaClient } from "@crema/arcadia-client"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Wire types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface BillingOverview {
|
||||||
|
billing_model: string | null
|
||||||
|
plan: string
|
||||||
|
subscription_status: string
|
||||||
|
current_period_start: string | null
|
||||||
|
current_period_end: string | null
|
||||||
|
trial_ends_at: string | null
|
||||||
|
billing_email: string | null
|
||||||
|
plan_limits: Record<string, unknown>
|
||||||
|
tenant_limits: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanPricing {
|
||||||
|
period: string
|
||||||
|
price_cents: number
|
||||||
|
currency: string
|
||||||
|
discount_label: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanMeter {
|
||||||
|
meter_key: string
|
||||||
|
included_units: number | null
|
||||||
|
overage_price_cents: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogPlan {
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
description: string | null
|
||||||
|
billing_track: string
|
||||||
|
trial_days: number
|
||||||
|
metadata: Record<string, unknown>
|
||||||
|
pricing: PlanPricing[]
|
||||||
|
meters: PlanMeter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Invoice {
|
||||||
|
id: string | null
|
||||||
|
amount: number | null
|
||||||
|
currency: string | null
|
||||||
|
status: string | null
|
||||||
|
due_date: string | null
|
||||||
|
paid_at: string | null
|
||||||
|
invoice_url: string | null
|
||||||
|
pdf_url: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckoutSession {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PortalSession {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Reads
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function getBillingOverview(
|
||||||
|
arcadia: ArcadiaClient,
|
||||||
|
): Promise<BillingOverview> {
|
||||||
|
const res = await arcadia.GET<{ data: BillingOverview }>(
|
||||||
|
"/api/v1/billing/overview",
|
||||||
|
)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listBillingPlans(
|
||||||
|
arcadia: ArcadiaClient,
|
||||||
|
): Promise<CatalogPlan[]> {
|
||||||
|
const res = await arcadia.GET<{ data: CatalogPlan[] }>(
|
||||||
|
"/api/v1/billing/plans",
|
||||||
|
)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listInvoices(
|
||||||
|
arcadia: ArcadiaClient,
|
||||||
|
opts: { limit?: number } = {},
|
||||||
|
): Promise<Invoice[]> {
|
||||||
|
const qs = opts.limit ? `?limit=${opts.limit}` : ""
|
||||||
|
const res = await arcadia.GET<{ data: Invoice[] }>(
|
||||||
|
`/api/v1/billing/invoices${qs}`,
|
||||||
|
)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a hosted checkout session for `plan` and return the URL to redirect
|
||||||
|
* the browser to. `successUrl`/`cancelUrl` are where the provider returns the
|
||||||
|
* user (default: back to this page).
|
||||||
|
*/
|
||||||
|
export async function createCheckoutSession(
|
||||||
|
arcadia: ArcadiaClient,
|
||||||
|
input: { plan: string; successUrl?: string; cancelUrl?: string },
|
||||||
|
): Promise<CheckoutSession> {
|
||||||
|
const res = await arcadia.POST<{ data: CheckoutSession }>(
|
||||||
|
"/api/v1/billing/checkout",
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
plan: input.plan,
|
||||||
|
success_url: input.successUrl,
|
||||||
|
cancel_url: input.cancelUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a billing-portal session for self-service payment management. */
|
||||||
|
export async function createPortalSession(
|
||||||
|
arcadia: ArcadiaClient,
|
||||||
|
input: { returnUrl?: string } = {},
|
||||||
|
): Promise<PortalSession> {
|
||||||
|
const res = await arcadia.POST<{ data: PortalSession }>(
|
||||||
|
"/api/v1/billing/portal",
|
||||||
|
{ body: { return_url: input.returnUrl } },
|
||||||
|
)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancel the active subscription (at period end). */
|
||||||
|
export async function cancelSubscription(
|
||||||
|
arcadia: ArcadiaClient,
|
||||||
|
): Promise<void> {
|
||||||
|
await arcadia.POST("/api/v1/billing/cancel", { body: {} })
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 ---------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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"
|
||||||
// CREMA:PROVIDERS-IMPORTS
|
// CREMA:PROVIDERS-IMPORTS
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,18 +1,210 @@
|
|||||||
// Tenant subscription + billing — placeholder. Real surface lists the
|
// Tenant subscription + billing — self-service surface.
|
||||||
// active plan, renewal date, invoices, and payment method for the
|
//
|
||||||
// active tenant. Data source not wired yet.
|
// Wired to /api/v1/billing/* on arcadia-core. The payment provider is resolved
|
||||||
|
// server-side (mock in dev), so "Subscribe" creates a hosted checkout session
|
||||||
|
// and redirects the browser to the returned URL; on return the overview
|
||||||
|
// reflects the new plan.
|
||||||
|
|
||||||
import { CreditCard } from "lucide-react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
Check,
|
||||||
|
CreditCard,
|
||||||
|
ExternalLink,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
|
||||||
import { AppShell } from "~/components/layout/app-shell"
|
import { AppShell } from "~/components/layout/app-shell"
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"
|
||||||
|
import { Badge } from "~/components/ui/badge"
|
||||||
|
import { Button } from "~/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card"
|
} from "~/components/ui/card"
|
||||||
|
import { Separator } from "~/components/ui/separator"
|
||||||
|
import { Skeleton } from "~/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "~/components/ui/table"
|
||||||
|
import {
|
||||||
|
type BillingOverview,
|
||||||
|
type CatalogPlan,
|
||||||
|
type Invoice,
|
||||||
|
type PlanPricing,
|
||||||
|
cancelSubscription,
|
||||||
|
createCheckoutSession,
|
||||||
|
createPortalSession,
|
||||||
|
getBillingOverview,
|
||||||
|
listBillingPlans,
|
||||||
|
listInvoices,
|
||||||
|
} from "~/lib/arcadia/billing"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Formatting helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function monthlyPricing(plan: CatalogPlan): PlanPricing | null {
|
||||||
|
if (!plan.pricing.length) return null
|
||||||
|
return plan.pricing.find((p) => p.period === "monthly") ?? plan.pricing[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMoney(cents: number | null, currency: string | null): string {
|
||||||
|
if (cents == null) return "—"
|
||||||
|
const cur = currency || "AUD"
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat(undefined, {
|
||||||
|
style: "currency",
|
||||||
|
currency: cur,
|
||||||
|
}).format(cents / 100)
|
||||||
|
} catch {
|
||||||
|
return `${(cents / 100).toFixed(2)} ${cur}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string | null): string {
|
||||||
|
if (!iso) return "—"
|
||||||
|
const d = new Date(iso)
|
||||||
|
return Number.isNaN(d.getTime())
|
||||||
|
? "—"
|
||||||
|
: d.toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusVariant(
|
||||||
|
status: string,
|
||||||
|
): "default" | "secondary" | "destructive" | "outline" {
|
||||||
|
switch (status) {
|
||||||
|
case "active":
|
||||||
|
return "default"
|
||||||
|
case "trialing":
|
||||||
|
return "secondary"
|
||||||
|
case "past_due":
|
||||||
|
case "unpaid":
|
||||||
|
case "canceled":
|
||||||
|
case "cancelled":
|
||||||
|
return "destructive"
|
||||||
|
default:
|
||||||
|
return "outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Route
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export default function PlanRoute() {
|
export default function PlanRoute() {
|
||||||
|
const arcadia = useArcadiaClient()
|
||||||
|
|
||||||
|
const [overview, setOverview] = useState<BillingOverview | null>(null)
|
||||||
|
const [plans, setPlans] = useState<CatalogPlan[]>([])
|
||||||
|
const [invoices, setInvoices] = useState<Invoice[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
// slug currently being acted on (checkout), or "__portal"/"__cancel"
|
||||||
|
const [busy, setBusy] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const [ov, pls, invs] = await Promise.all([
|
||||||
|
getBillingOverview(arcadia),
|
||||||
|
listBillingPlans(arcadia).catch(() => [] as CatalogPlan[]),
|
||||||
|
listInvoices(arcadia, { limit: 12 }).catch(() => [] as Invoice[]),
|
||||||
|
])
|
||||||
|
setOverview(ov)
|
||||||
|
setPlans(pls)
|
||||||
|
setInvoices(invs)
|
||||||
|
} catch (e) {
|
||||||
|
setError(
|
||||||
|
e instanceof ArcadiaError || e instanceof Error
|
||||||
|
? e.message
|
||||||
|
: "Failed to load billing details.",
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [arcadia])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load()
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
const returnUrl =
|
||||||
|
typeof window !== "undefined" ? window.location.href : undefined
|
||||||
|
|
||||||
|
async function handleSubscribe(plan: CatalogPlan) {
|
||||||
|
setBusy(plan.slug)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const { url } = await createCheckoutSession(arcadia, {
|
||||||
|
plan: plan.slug,
|
||||||
|
successUrl: returnUrl,
|
||||||
|
cancelUrl: returnUrl,
|
||||||
|
})
|
||||||
|
window.location.href = url
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "Could not start checkout.")
|
||||||
|
setBusy(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePortal() {
|
||||||
|
setBusy("__portal")
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const { url } = await createPortalSession(arcadia, { returnUrl })
|
||||||
|
window.location.href = url
|
||||||
|
} catch (e) {
|
||||||
|
setError(
|
||||||
|
e instanceof Error ? e.message : "Could not open the billing portal.",
|
||||||
|
)
|
||||||
|
setBusy(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
if (
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
!window.confirm(
|
||||||
|
"Cancel your subscription at the end of the current period?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBusy("__cancel")
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await cancelSubscription(arcadia)
|
||||||
|
await load()
|
||||||
|
} catch (e) {
|
||||||
|
setError(
|
||||||
|
e instanceof Error ? e.message : "Could not cancel the subscription.",
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setBusy(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSlug = overview?.plan
|
||||||
|
const hasActiveSub =
|
||||||
|
overview != null &&
|
||||||
|
overview.plan !== "free" &&
|
||||||
|
["active", "trialing", "past_due"].includes(overview.subscription_status)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -26,15 +218,237 @@ export default function PlanRoute() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
{error && (
|
||||||
<CardTitle>Coming soon</CardTitle>
|
<Alert variant="destructive">
|
||||||
<CardDescription>
|
<AlertCircle className="size-4" />
|
||||||
Billing is not yet wired to a payment provider on this deployment.
|
<AlertTitle>Something went wrong</AlertTitle>
|
||||||
</CardDescription>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
</CardHeader>
|
</Alert>
|
||||||
<CardContent />
|
)}
|
||||||
</Card>
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-32 w-full" />
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Current plan */}
|
||||||
|
{overview && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CardTitle className="capitalize">
|
||||||
|
{overview.plan} plan
|
||||||
|
</CardTitle>
|
||||||
|
<Badge variant={statusVariant(overview.subscription_status)}>
|
||||||
|
{overview.subscription_status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{hasActiveSub && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handlePortal}
|
||||||
|
disabled={busy != null}
|
||||||
|
>
|
||||||
|
{busy === "__portal" ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ExternalLink className="size-4" />
|
||||||
|
)}
|
||||||
|
Manage billing
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasActiveSub && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={busy != null}
|
||||||
|
>
|
||||||
|
{busy === "__cancel" ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
{overview.current_period_end
|
||||||
|
? `Renews ${formatDate(overview.current_period_end)}`
|
||||||
|
: "No active billing period."}
|
||||||
|
{overview.trial_ends_at
|
||||||
|
? ` · Trial ends ${formatDate(overview.trial_ends_at)}`
|
||||||
|
: ""}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{overview.billing_email && (
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Billing email: {overview.billing_email}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Plan picker */}
|
||||||
|
{plans.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-lg font-medium">Available plans</h2>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{plans.map((plan) => {
|
||||||
|
const price = monthlyPricing(plan)
|
||||||
|
const isCurrent = plan.slug === currentSlug
|
||||||
|
const isAud = (price?.currency || "AUD") === "AUD"
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={plan.slug}
|
||||||
|
className={isCurrent ? "border-primary" : undefined}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<CardTitle>{plan.name}</CardTitle>
|
||||||
|
{isCurrent && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<Check className="size-3" />
|
||||||
|
Current
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CardDescription>{plan.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-2xl font-semibold">
|
||||||
|
{price
|
||||||
|
? formatMoney(price.price_cents, price.currency)
|
||||||
|
: "—"}
|
||||||
|
</span>
|
||||||
|
{price && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
/{price.period}
|
||||||
|
{isAud ? " incl. GST" : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{price?.discount_label && (
|
||||||
|
<Badge variant="outline">{price.discount_label}</Badge>
|
||||||
|
)}
|
||||||
|
{plan.trial_days > 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{plan.trial_days}-day free trial
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{plan.meters.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||||
|
{plan.meters.slice(0, 5).map((m) => (
|
||||||
|
<li
|
||||||
|
key={m.meter_key}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Check className="size-3 text-primary" />
|
||||||
|
{m.included_units != null
|
||||||
|
? `${m.included_units.toLocaleString()} `
|
||||||
|
: ""}
|
||||||
|
{m.meter_key.replace(/_/g, " ")}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant={isCurrent ? "outline" : "default"}
|
||||||
|
disabled={isCurrent || busy != null}
|
||||||
|
onClick={() => handleSubscribe(plan)}
|
||||||
|
>
|
||||||
|
{busy === plan.slug ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
{isCurrent
|
||||||
|
? "Current plan"
|
||||||
|
: hasActiveSub
|
||||||
|
? `Switch to ${plan.name}`
|
||||||
|
: `Subscribe to ${plan.name}`}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Invoices */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Invoices</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Recent billing history for this tenant.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{invoices.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">No invoices yet.</p>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Date</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Amount</TableHead>
|
||||||
|
<TableHead className="text-right">Invoice</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{invoices.map((inv, i) => (
|
||||||
|
<TableRow key={inv.id ?? i}>
|
||||||
|
<TableCell>
|
||||||
|
{formatDate(inv.paid_at ?? inv.due_date)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={statusVariant(inv.status ?? "")}>
|
||||||
|
{inv.status ?? "—"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{formatMoney(inv.amount, inv.currency)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{inv.invoice_url || inv.pdf_url ? (
|
||||||
|
<a
|
||||||
|
href={(inv.invoice_url ?? inv.pdf_url) as string}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-primary hover:underline"
|
||||||
|
>
|
||||||
|
View <ExternalLink className="size-3" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"—"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AppShell>
|
</AppShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|
||||||
|
|||||||
@@ -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, ~30–80MB resident |
|
| Index storage | Static JSON, fetched once | mmap'd disk, ~30–80MB resident |
|
||||||
| Practical corpus size | ~5–10MB / ~50–100k chunks | GB-scale, no hard cap |
|
| Practical corpus size | ~5–10MB / ~50–100k 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 H1–H3,
|
markdown from `../reference/arcadia-app/`, chunks at H1–H3,
|
||||||
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
|
||||||
|
|||||||
@@ -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"] },
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
Reference in New Issue
Block a user