fix: restore General settings (name/log-level/retention), remove OIDC section, wire save

This commit is contained in:
2026-06-29 08:52:11 +02:00
parent 02041751e3
commit 5febc03412
4 changed files with 113 additions and 30 deletions

View File

@@ -104,7 +104,8 @@ func run() error {
go syncer.Run(ctx, cfg.Catalog.SyncInterval, refresh)
// Purge request logs older than 1 year on a daily tick.
// Purge request logs on a daily tick. Retention is controlled by the
// log.retention_days setting (0 = keep forever). Defaults to 365 days.
go func() {
tick := time.NewTicker(24 * time.Hour)
defer tick.Stop()
@@ -113,8 +114,16 @@ func run() error {
case <-ctx.Done():
return
case <-tick.C:
var days int
purgeCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
_, _ = pool.Exec(purgeCtx, `DELETE FROM request_logs WHERE created_at < NOW() - INTERVAL '1 year'`)
if ok, _ := st.GetSetting(purgeCtx, "log.retention_days", &days); !ok {
days = 365
}
if days > 0 {
_, _ = pool.Exec(purgeCtx,
`DELETE FROM request_logs WHERE created_at < NOW() - ($1 || ' days')::interval`,
days)
}
cancel()
}
}

View File

@@ -47,8 +47,8 @@ export const api = {
req<void>(`/models/${id}`, { method: 'PATCH', body: JSON.stringify(d) }),
},
settings: {
get: () => req<Record<string, string>>('/settings'),
put: (key: string, value: string) =>
get: () => req<Record<string, any>>('/settings'),
put: (key: string, value: any) =>
req<void>(`/settings/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(value) }),
},
catalog: {

View File

@@ -3,42 +3,84 @@ import { api } from '../lib/api'
const D = 'rgba(237,237,240,'
function Section({ title, badge, children }: { title: string; badge?: React.ReactNode; children: React.ReactNode }) {
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div style={{ background: '#17171c', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 10, overflow: 'hidden', marginBottom: 14 }}>
<div style={{ padding: '13px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)', display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ padding: '13px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<span style={{ fontSize: 13, fontWeight: 600, color: '#f0eeff' }}>{title}</span>
{badge}
</div>
<div style={{ padding: 20 }}>{children}</div>
</div>
)
}
function ReadonlyField({ label, value }: { label: string; value: string }) {
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<label style={{ display: 'block', fontSize: 12, fontWeight: 500, color: `${D}0.55)`, marginBottom: 6 }}>{label}</label>
<div style={{
background: 'rgba(0,0,0,0.18)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 6,
padding: '8px 12px', fontSize: 13, color: `${D}0.4)`, fontFamily: "'DM Mono',monospace",
userSelect: 'all',
}}>{value || '—'}</div>
{children}
</div>
)
}
interface AuthConfig { client_id: string; auth_url: string; token_url: string }
const inputStyle: React.CSSProperties = {
width: '100%', background: 'rgba(0,0,0,0.28)',
border: '1px solid rgba(255,255,255,0.09)', borderRadius: 6,
padding: '8px 12px', fontSize: 13, color: '#ededf0', outline: 'none',
boxSizing: 'border-box',
}
const RETENTION_OPTIONS = [
{ label: '7 days', value: 7 },
{ label: '30 days', value: 30 },
{ label: '90 days', value: 90 },
{ label: '180 days', value: 180 },
{ label: '1 year', value: 365 },
{ label: '2 years', value: 730 },
{ label: 'Indefinite',value: 0 },
]
const LOG_LEVELS = ['debug', 'info', 'warn', 'error']
export default function Settings() {
const [name, setName] = useState('Local Model Routex')
const [logLevel, setLogLevel] = useState('info')
const [retentionDays, setRetentionDays] = useState(365)
const [dirty, setDirty] = useState(false)
const [saving, setSaving] = useState(false)
const [saveMsg, setSaveMsg] = useState('')
const [syncLoading, setSyncLoading] = useState(false)
const [syncMsg, setSyncMsg] = useState('')
const [auth, setAuth] = useState<AuthConfig | null>(null)
useEffect(() => {
fetch('/api/auth/config').then(r => r.json()).then(setAuth).catch(() => {})
api.settings.get().then(s => {
if (s['app.name']) setName(s['app.name'])
if (s['app.log_level']) setLogLevel(s['app.log_level'])
if (s['log.retention_days'] !== undefined) setRetentionDays(Number(s['log.retention_days']))
}).catch(() => {})
}, [])
function markDirty() { setDirty(true); setSaveMsg('') }
async function save() {
setSaving(true)
setSaveMsg('')
try {
await Promise.all([
api.settings.put('app.name', name),
api.settings.put('app.log_level', logLevel),
api.settings.put('log.retention_days', retentionDays),
])
setDirty(false)
setSaveMsg('Saved')
} catch (e: any) {
setSaveMsg('Error: ' + e.message)
} finally {
setSaving(false)
}
}
async function forceSync() {
setSyncLoading(true)
setSyncMsg('')
@@ -52,20 +94,25 @@ export default function Settings() {
}
}
const issuer = auth?.auth_url
? new URL(auth.auth_url).origin + new URL(auth.auth_url).pathname.replace(/\/authorize$/, '')
: ''
return (
<div className="animate-fade-up" style={{ maxWidth: 620 }}>
<Section title="Authentication"
badge={<span style={{ fontSize: 10, fontWeight: 600, padding: '2px 7px', borderRadius: 3, background: 'rgba(34,197,94,0.12)', color: '#4ade80', border: '1px solid rgba(34,197,94,0.2)' }}>OIDC</span>}>
<div style={{ display: 'grid', gap: 14 }}>
<ReadonlyField label="OIDC Issuer URL" value={issuer} />
<ReadonlyField label="Client ID" value={auth?.client_id ?? ''} />
<p style={{ fontSize: 12, color: `${D}0.3)`, lineHeight: 1.5, margin: 0 }}>
Configured via environment variables (<code style={{ fontFamily: 'monospace' }}>GATEWAY_OIDC_*</code>).
</p>
<Section title="General">
<div style={{ display: 'grid', gap: 15 }}>
<Field label="Router name">
<input type="text" value={name} onChange={e => { setName(e.target.value); markDirty() }} style={inputStyle} />
</Field>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<Field label="Log level">
<select value={logLevel} onChange={e => { setLogLevel(e.target.value); markDirty() }} style={{ ...inputStyle, cursor: 'pointer' }}>
{LOG_LEVELS.map(l => <option key={l} value={l}>{l}</option>)}
</select>
</Field>
<Field label="Log retention">
<select value={retentionDays} onChange={e => { setRetentionDays(Number(e.target.value)); markDirty() }} style={{ ...inputStyle, cursor: 'pointer' }}>
{RETENTION_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</Field>
</div>
</div>
</Section>
@@ -86,6 +133,18 @@ export default function Settings() {
</button>
</div>
</Section>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 10 }}>
{saveMsg && <span style={{ fontSize: 12, color: saveMsg.startsWith('Error') ? '#f87171' : '#4ade80' }}>{saveMsg}</span>}
<button onClick={() => { setDirty(false); setSaveMsg('') }} disabled={!dirty || saving}
style={{ background: 'transparent', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 7, padding: '8px 16px', fontSize: 13, color: `${D}0.5)`, cursor: 'pointer', opacity: !dirty ? 0.4 : 1 }}>
Discard
</button>
<button onClick={save} disabled={!dirty || saving}
style={{ background: '#7c3aed', border: 'none', borderRadius: 7, padding: '8px 20px', fontSize: 13, fontWeight: 500, color: 'white', cursor: 'pointer', opacity: !dirty || saving ? 0.5 : 1 }}>
{saving ? 'Saving…' : 'Save changes'}
</button>
</div>
</div>
)
}

View File

@@ -243,9 +243,24 @@ func (a *API) adminUpdateModel(w http.ResponseWriter, r *http.Request) {
// --- settings ---
func (a *API) adminGetSettings(w http.ResponseWriter, r *http.Request) {
out := map[string]any{}
var strategy string
_, _ = a.Store.GetSetting(r.Context(), "routing.strategy", &strategy)
writeJSON(w, http.StatusOK, map[string]any{"routing.strategy": strategy})
if ok, _ := a.Store.GetSetting(r.Context(), "routing.strategy", &strategy); ok {
out["routing.strategy"] = strategy
}
var appName string
if ok, _ := a.Store.GetSetting(r.Context(), "app.name", &appName); ok {
out["app.name"] = appName
}
var logLevel string
if ok, _ := a.Store.GetSetting(r.Context(), "app.log_level", &logLevel); ok {
out["app.log_level"] = logLevel
}
var retentionDays int
if ok, _ := a.Store.GetSetting(r.Context(), "log.retention_days", &retentionDays); ok {
out["log.retention_days"] = retentionDays
}
writeJSON(w, http.StatusOK, out)
}
func (a *API) adminPutSetting(w http.ResponseWriter, r *http.Request) {