search: add Search section calling arcadia-search admin sidecar

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>
This commit is contained in:
jules
2026-05-04 16:26:34 +10:00
parent 20c592dfa7
commit eb7bc62d14
5 changed files with 991 additions and 0 deletions

113
app/lib/search-admin.ts Normal file
View File

@@ -0,0 +1,113 @@
// 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"),
}