New /search route manages tenants and corpora on the arcadia-search box via its privileged /admin/* surface (default :7801) — KPI tiles, flat tenant×corpus table with Rebuild / Edit config / Delete actions, New tenant / New corpus dialogs, and a Restart service button. New app/lib/search-admin.ts wraps the bearer-token fetch. Configured by VITE_ARCADIA_SEARCH_ADMIN_URL + VITE_ARCADIA_SEARCH_ADMIN_TOKEN; the route renders a warning banner when the token is unset. Token ships in the client bundle — fine for this internal tool, called out in CLAUDE.md and the source comments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
// Client for the arcadia-search admin sidecar (`/admin/*` on the
|
|
// search box, default :7801). Used by the Search route to manage
|
|
// tenants, corpora, and trigger rebuilds.
|
|
//
|
|
// Auth: static bearer token from VITE_ARCADIA_SEARCH_ADMIN_TOKEN,
|
|
// matched constant-time against ADMIN_TOKEN on the sidecar. The token
|
|
// ships in the client bundle — fine for an internal admin tool on a
|
|
// trusted network; in production, proxy through arcadia-core.
|
|
|
|
const BASE_URL =
|
|
import.meta.env.VITE_ARCADIA_SEARCH_ADMIN_URL ?? "http://127.0.0.1:7801"
|
|
const TOKEN = import.meta.env.VITE_ARCADIA_SEARCH_ADMIN_TOKEN ?? ""
|
|
|
|
export class SearchAdminError extends Error {
|
|
status: number
|
|
constructor(message: string, status: number) {
|
|
super(message)
|
|
this.name = "SearchAdminError"
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
async function call<T>(
|
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<T> {
|
|
const headers: Record<string, string> = {}
|
|
if (TOKEN) headers["Authorization"] = `Bearer ${TOKEN}`
|
|
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
const res = await fetch(`${BASE_URL}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
})
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "")
|
|
throw new SearchAdminError(
|
|
text || `${res.status} ${res.statusText}`,
|
|
res.status,
|
|
)
|
|
}
|
|
if (res.status === 204) return undefined as T
|
|
return (await res.json()) as T
|
|
}
|
|
|
|
export type TenantSummary = { id: string; corpus_count: number }
|
|
export type CorpusSummary = {
|
|
tenant: string
|
|
corpus: string
|
|
indexed: boolean
|
|
num_docs: number | null
|
|
live_path: string | null
|
|
}
|
|
export type CorpusDetail = {
|
|
config: Record<string, unknown>
|
|
status: CorpusSummary
|
|
}
|
|
export type RebuildResult = {
|
|
tenant: string
|
|
corpus: string
|
|
chunk_count: number
|
|
live_path: string
|
|
built_at: string
|
|
}
|
|
|
|
export const searchAdmin = {
|
|
baseUrl: BASE_URL,
|
|
hasToken: !!TOKEN,
|
|
listTenants: () =>
|
|
call<{ tenants: TenantSummary[] }>("GET", "/admin/tenants"),
|
|
createTenant: (id: string) =>
|
|
call<TenantSummary>("POST", "/admin/tenants", { id }),
|
|
deleteTenant: (id: string) =>
|
|
call<void>("DELETE", `/admin/tenants/${encodeURIComponent(id)}`),
|
|
listCorpora: (tenant: string) =>
|
|
call<{ corpora: CorpusSummary[] }>(
|
|
"GET",
|
|
`/admin/tenants/${encodeURIComponent(tenant)}/corpora`,
|
|
),
|
|
createCorpus: (tenant: string, body: Record<string, unknown>) =>
|
|
call<CorpusSummary>(
|
|
"POST",
|
|
`/admin/tenants/${encodeURIComponent(tenant)}/corpora`,
|
|
body,
|
|
),
|
|
getCorpus: (tenant: string, corpus: string) =>
|
|
call<CorpusDetail>(
|
|
"GET",
|
|
`/admin/tenants/${encodeURIComponent(tenant)}/corpora/${encodeURIComponent(corpus)}`,
|
|
),
|
|
updateCorpus: (
|
|
tenant: string,
|
|
corpus: string,
|
|
body: Record<string, unknown>,
|
|
) =>
|
|
call<CorpusSummary>(
|
|
"PUT",
|
|
`/admin/tenants/${encodeURIComponent(tenant)}/corpora/${encodeURIComponent(corpus)}`,
|
|
body,
|
|
),
|
|
deleteCorpus: (tenant: string, corpus: string) =>
|
|
call<void>(
|
|
"DELETE",
|
|
`/admin/tenants/${encodeURIComponent(tenant)}/corpora/${encodeURIComponent(corpus)}`,
|
|
),
|
|
rebuild: (tenant: string, corpus: string) =>
|
|
call<RebuildResult>(
|
|
"POST",
|
|
`/admin/tenants/${encodeURIComponent(tenant)}/corpora/${encodeURIComponent(corpus)}/rebuild`,
|
|
),
|
|
restart: () => call<void>("POST", "/admin/restart"),
|
|
}
|