Add Buckets, Monitoring, Memberships, Networking, SSO, Announcements, Status page
Full set of admin surfaces on top of /platform/* and /admin/* endpoints,
plus a migration of /assistant onto @crema/llm-providers-ui.
Buckets (/buckets):
S3-level CRUD over /platform/buckets — list, create, delete (with the
6-digit confirmation flow the backend enforces), per-bucket configure
for versioning / CORS rules / policy JSON, plus an object browser
with FileGrid/FileList from @crema/file-ui and presigned-URL reveal.
Storage-config picker scopes the view to one credential at a time.
Monitoring (/monitoring):
Live dashboard. Service health board derived from indirect signals
(status-ui OverallStatus + ComponentRow). KPI tiles for sessions,
jobs, audit. Tabs: background jobs (Donut + BarChart + retry recent),
sessions (Sparkline of last 24h sign-ins), audit activity (BarChart
of severity / top resource types), infrastructure (DO summary +
WorldMapSvg coloured by droplet region + droplet list + Spaces),
rate limits. 30s auto-refresh.
Memberships (/memberships):
M:N glue between users and tenants over /admin/memberships. Add /
edit / suspend / activate / remove with role multi-select.
Networking (/networking):
Tabs over /platform/{firewalls,vpcs,domains,floating_ips}.
Read/delete on firewalls, read on VPCs, full DNS-record CRUD, and
inline assign/unassign for floating IPs.
SSO (/sso):
/sso/identity-providers CRUD with PEM cert as write-only field, plus
/sso/sessions list with destroy.
Announcements (/announcements):
/admin/announcements CRUD. Platform-wide vs per-tenant audience,
schedule windows, dismissible + active toggles.
Status page (/status-page):
/admin/status-page/{components,incidents,subscribers}. Components
CRUD, incidents with timeline + post-update + resolve flow,
subscriber list. Public preview at the top using StatusBoard +
IncidentTimeline from @crema/status-ui.
Assistant migration:
/assistant now uses @crema/llm-providers-ui (provider catalog +
vault key resolution) instead of ~/lib/llm-settings. Same async
buildAdapter() flow used by /ai. The legacy lib file is now
unreferenced and can be removed when ready.
New sibling libs wired (cloned from CremaUIStudio):
lib-file-ui, lib-card-ui, lib-dashboard-ui, lib-chart-ui,
lib-map-ui, lib-status-ui.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,6 @@ function withTimeout<T>(p: Promise<T>, ms: number, signal: AbortSignal): Promise
|
||||
import {
|
||||
LLMProvider,
|
||||
MockLLM,
|
||||
OpenAICompatibleAdapter,
|
||||
listModels,
|
||||
useChat,
|
||||
useCompletion,
|
||||
@@ -154,7 +153,11 @@ import {
|
||||
runActionBlocks,
|
||||
trimMessages,
|
||||
} from "@crema/action-bus"
|
||||
import { useLLMSettings } from "~/lib/llm-settings"
|
||||
import {
|
||||
buildAdapter,
|
||||
getProvider,
|
||||
useSettings as useProviderSettings,
|
||||
} from "@crema/llm-providers-ui"
|
||||
import {
|
||||
composeSystemPrompt,
|
||||
loadActiveAgentId,
|
||||
@@ -233,7 +236,9 @@ const mockAdapter = new MockLLM({
|
||||
})
|
||||
|
||||
export default function AssistantRoute() {
|
||||
const settings = useLLMSettings()
|
||||
const settings = useProviderSettings()
|
||||
const arcadia = useArcadiaClient()
|
||||
const provider = getProvider(settings.providerId)
|
||||
const agents = useAgents()
|
||||
const threads = useThreads()
|
||||
const [status, setStatus] = useState<Status>({ kind: "probing" })
|
||||
@@ -282,32 +287,108 @@ export default function AssistantRoute() {
|
||||
updateThread(id, { title })
|
||||
}, [])
|
||||
|
||||
const [adapter, setAdapter] = useState<LLMAdapter>(mockAdapter)
|
||||
|
||||
// When the user switches providers in /settings, follow.
|
||||
useEffect(() => {
|
||||
if (settings.model) setModel(settings.model)
|
||||
}, [settings.providerId, settings.model])
|
||||
|
||||
const probe = useCallback(() => {
|
||||
const ac = new AbortController()
|
||||
setStatus({ kind: "probing" })
|
||||
withTimeout(
|
||||
listModels({ baseURL: settings.baseURL, signal: ac.signal }),
|
||||
PROBE_TIMEOUT_MS,
|
||||
ac.signal,
|
||||
)
|
||||
.then((rows) => {
|
||||
|
||||
const resolveSecret = async (name: string): Promise<string> => {
|
||||
const res = await arcadia.GET<{ data: { value: string } }>(
|
||||
`/api/v1/secrets/${encodeURIComponent(name)}`,
|
||||
)
|
||||
return res.data.value
|
||||
}
|
||||
const arcadiaBaseURL =
|
||||
(import.meta.env.VITE_ARCADIA_URL as string | undefined) ?? "http://localhost:4000"
|
||||
const arcadiaTenantId =
|
||||
(import.meta.env.VITE_ARCADIA_TENANT as string | undefined) ?? "default"
|
||||
const arcadiaAuthToken =
|
||||
typeof window !== "undefined"
|
||||
? sessionStorage.getItem("arcadia_access_token") ?? undefined
|
||||
: undefined
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
const a = await buildAdapter({
|
||||
settings,
|
||||
resolveSecret,
|
||||
arcadiaBaseURL,
|
||||
arcadiaAuthToken,
|
||||
arcadiaTenantId,
|
||||
})
|
||||
setAdapter(a)
|
||||
} catch {
|
||||
setAdapter(mockAdapter)
|
||||
}
|
||||
|
||||
// Anthropic has no /v1/models endpoint — use the catalog defaults.
|
||||
if (provider.transport === "anthropic") {
|
||||
const ids = provider.defaultModels.length
|
||||
? provider.defaultModels
|
||||
: ["claude-opus-4-7"]
|
||||
setStatus({ kind: "live", models: ids })
|
||||
setModel((cur) => (cur && ids.includes(cur) ? cur : settings.model || ids[0]))
|
||||
return
|
||||
}
|
||||
|
||||
const baseURL = settings.baseURL || provider.baseURL
|
||||
let apiKey: string | undefined
|
||||
if (provider.requiresKey && settings.secretName) {
|
||||
try {
|
||||
apiKey = await resolveSecret(settings.secretName)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await withTimeout(
|
||||
listModels({ baseURL, apiKey, signal: ac.signal }),
|
||||
PROBE_TIMEOUT_MS,
|
||||
ac.signal,
|
||||
)
|
||||
const ids = rows.map((m) => m.id)
|
||||
if (ids.length === 0) {
|
||||
setStatus({ kind: "mock", reason: "LM Studio returned no models" })
|
||||
setStatus({ kind: "mock", reason: "endpoint returned no models" })
|
||||
return
|
||||
}
|
||||
setStatus({ kind: "live", models: ids })
|
||||
setModel((cur) => (cur && ids.includes(cur) ? cur : ids[0]))
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setModel((cur) => (cur && ids.includes(cur) ? cur : settings.model || ids[0]))
|
||||
} catch (err: unknown) {
|
||||
if ((err as DOMException)?.name === "AbortError") return
|
||||
setStatus({
|
||||
kind: "mock",
|
||||
reason: err instanceof Error ? err.message : "LM Studio unreachable",
|
||||
})
|
||||
})
|
||||
if (provider.defaultModels.length) {
|
||||
setStatus({ kind: "live", models: provider.defaultModels })
|
||||
setModel((cur) =>
|
||||
cur && provider.defaultModels.includes(cur)
|
||||
? cur
|
||||
: settings.model || provider.defaultModels[0],
|
||||
)
|
||||
} else {
|
||||
setStatus({
|
||||
kind: "mock",
|
||||
reason: err instanceof Error ? err.message : "endpoint unreachable",
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => ac.abort()
|
||||
}, [settings.baseURL])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
arcadia,
|
||||
settings.providerId,
|
||||
settings.baseURL,
|
||||
settings.secretName,
|
||||
settings.mode,
|
||||
settings.model,
|
||||
provider.transport,
|
||||
provider.baseURL,
|
||||
provider.requiresKey,
|
||||
])
|
||||
|
||||
useEffect(() => probe(), [probe])
|
||||
|
||||
@@ -315,14 +396,6 @@ export default function AssistantRoute() {
|
||||
if (model && model !== "mock") localStorage.setItem(STORAGE_KEY, model)
|
||||
}, [model])
|
||||
|
||||
const adapter: LLMAdapter = useMemo(
|
||||
() =>
|
||||
status.kind === "live"
|
||||
? new OpenAICompatibleAdapter({ baseURL: settings.baseURL })
|
||||
: mockAdapter,
|
||||
[status.kind, settings.baseURL],
|
||||
)
|
||||
|
||||
const activeModel =
|
||||
status.kind === "live" ? model || status.models[0] : "mock"
|
||||
|
||||
@@ -339,7 +412,7 @@ export default function AssistantRoute() {
|
||||
onModelChange={setModel}
|
||||
contextTokens={settings.contextTokens}
|
||||
responseBudget={settings.responseBudget}
|
||||
baseURL={settings.baseURL}
|
||||
baseURL={settings.baseURL || provider.baseURL}
|
||||
basePrompt={settings.systemPrompt}
|
||||
onRetryProbe={probe}
|
||||
onRemount={remount}
|
||||
|
||||
Reference in New Issue
Block a user