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:
jules
2026-05-02 07:55:46 +10:00
parent 7ba415d78e
commit 0fcb9e40f1
20 changed files with 7472 additions and 28 deletions

View File

@@ -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}