// Instance settings data layer (#79): admin-only, instance-wide configuration. // // The GET/PATCH return both the stored settings and a read-only "effective" view // — what's actually in force after layering the DB over the environment — so the // form can say "inheriting ollama-cloud/glm-5.2:cloud from the environment" and // whether the API key is present, without the key ever crossing the wire. import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { z } from 'zod' import { ApiError, api } from './api' import { capabilitiesKey } from './agent' export const instanceSettingsSchema = z.object({ // '' means "inherit the PANSY_AGENT_MODEL env var". agentModel: z.string(), // null means "inherit PANSY_AGENT_ENABLED"; true/false is an explicit override. agentEnabled: z.boolean().nullable(), version: z.number(), updatedAt: z.string(), }) export type InstanceSettings = z.infer export const effectiveAgentSchema = z.object({ model: z.string(), enabled: z.boolean(), hasApiKey: z.boolean(), agentLive: z.boolean(), }) export type EffectiveAgent = z.infer export const settingsResponseSchema = z.object({ settings: instanceSettingsSchema, effective: effectiveAgentSchema, }) export type SettingsResponse = z.infer export const settingsKey = ['settings'] as const export const settingsQueryOptions = queryOptions({ queryKey: settingsKey, queryFn: async (): Promise => settingsResponseSchema.parse(await api.get('/settings')), }) export function useSettings() { return useQuery(settingsQueryOptions) } export interface SettingsUpdate { agentModel: string agentEnabled: boolean | null version: number } export function useUpdateSettings() { const qc = useQueryClient() return useMutation({ mutationFn: async (input: SettingsUpdate): Promise => settingsResponseSchema.parse(await api.patch('/settings', input)), onSuccess: (res) => { qc.setQueryData(settingsKey, res) // The save may have turned the assistant on or off; the editor keys its // chat tab off /capabilities, so make it re-read rather than trust its // cached answer. qc.invalidateQueries({ queryKey: capabilitiesKey }) }, }) } /** If err is a 409 version conflict, return the fresh settings it carries so a * form can rebase; otherwise null. */ export function conflictSettings(err: unknown): InstanceSettings | null { if (err instanceof ApiError && err.isConflict && err.body && typeof err.body === 'object') { const current = (err.body as { current?: unknown }).current const parsed = instanceSettingsSchema.safeParse(current) if (parsed.success) return parsed.data } return null }