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:
113
app/lib/search-admin.ts
Normal file
113
app/lib/search-admin.ts
Normal 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"),
|
||||
}
|
||||
Reference in New Issue
Block a user