feat(settings): DB-backed settings store + full Settings page wiring
DB:
- Add system_settings table (key, namespace, value JSONB, description)
- Seed 27 default settings across runtime/database/security/notifications/developer namespaces
Backend:
- New settings router: GET /settings, GET /settings/namespace/:ns,
GET /settings/:key, PUT /settings/:key, PUT /settings (bulk update)
- Register settings router in main.py with /api/v1 prefix
Frontend (client.ts):
- Add put() HTTP helper
- Add ApiSetting interface
- Add settingsApi (list, namespace, get, update, bulkUpdate)
Settings.tsx:
- useQuery to load all settings on mount
- Local draft state — edits accumulate before save
- bulkUpdate mutation on Save Changes (spinner + success banner)
- Reset clears draft back to DB values
- Unsaved changes indicator in header
- All TextInput/SelectInput/Toggle now controlled via settingKey + onChange
This commit is contained in:
@@ -265,6 +265,24 @@ CREATE TABLE IF NOT EXISTS secrets (
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_name ON secrets(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_created_by ON secrets(created_by);
|
||||
|
||||
-- ============================================================================
|
||||
-- SYSTEM SETTINGS TABLE
|
||||
-- ============================================================================
|
||||
-- Key-value store for runtime configuration. Each setting has a namespace
|
||||
-- (e.g. 'runtime', 'database', 'security') for grouping, a typed value,
|
||||
-- and a human-readable description for the UI.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
key VARCHAR(255) PRIMARY KEY,
|
||||
namespace VARCHAR(100) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_system_settings_namespace
|
||||
ON system_settings(namespace);
|
||||
|
||||
-- ============================================================================
|
||||
-- SECRET ACCESS LOG
|
||||
-- ============================================================================
|
||||
|
||||
@@ -22,7 +22,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from libs.db.connection import init_db, close_db, get_db
|
||||
from libs.logging import get_logger
|
||||
from src.api.dependencies import set_controller
|
||||
from src.api.routers import agents, tasks, plugins, policies, security, logs
|
||||
from src.api.routers import agents, tasks, plugins, policies, security, logs, settings
|
||||
from src.kernel_runtime.scheduler.execution_controller import (
|
||||
ExecutionController,
|
||||
)
|
||||
@@ -102,7 +102,8 @@ app.include_router(tasks.router, prefix=API_PREFIX)
|
||||
app.include_router(plugins.router, prefix=API_PREFIX)
|
||||
app.include_router(policies.router, prefix=API_PREFIX)
|
||||
app.include_router(security.router, prefix=API_PREFIX)
|
||||
app.include_router(logs.router, prefix=API_PREFIX)
|
||||
app.include_router(logs.router, prefix=API_PREFIX)
|
||||
app.include_router(settings.router, prefix=API_PREFIX)
|
||||
|
||||
# ── Health ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
165
src/api/routers/settings.py
Normal file
165
src/api/routers/settings.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Settings router — read/write system_settings key-value store."""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from src.common.utils import generate_correlation_id
|
||||
from src.api.dependencies import get_db_conn
|
||||
from libs.db.connection import DatabaseConnection
|
||||
from libs.logging import get_logger
|
||||
|
||||
log = get_logger("api.settings")
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
|
||||
# ── Models ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class SettingResponse(BaseModel):
|
||||
key: str
|
||||
namespace: str
|
||||
value: Any
|
||||
description: Optional[str]
|
||||
updated_at: str
|
||||
|
||||
|
||||
class SettingUpdateRequest(BaseModel):
|
||||
value: Any
|
||||
|
||||
|
||||
class BulkUpdateRequest(BaseModel):
|
||||
settings: Dict[str, Any]
|
||||
|
||||
|
||||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _row_to_setting(row) -> SettingResponse:
|
||||
value = row["value"]
|
||||
if isinstance(value, str):
|
||||
value = json.loads(value)
|
||||
return SettingResponse(
|
||||
key=row["key"],
|
||||
namespace=row["namespace"],
|
||||
value=value,
|
||||
description=row.get("description"),
|
||||
updated_at=row["updated_at"].isoformat(),
|
||||
)
|
||||
|
||||
|
||||
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("", response_model=List[SettingResponse])
|
||||
async def list_settings(
|
||||
db: DatabaseConnection = Depends(get_db_conn),
|
||||
):
|
||||
"""Return all settings, ordered by namespace then key."""
|
||||
correlation_id = generate_correlation_id()
|
||||
rows = await db.fetch(
|
||||
"SELECT * FROM system_settings ORDER BY namespace, key"
|
||||
)
|
||||
log.info("api_list_settings", {
|
||||
"count": len(rows),
|
||||
"correlation_id": correlation_id,
|
||||
})
|
||||
return [_row_to_setting(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/namespace/{namespace}",
|
||||
response_model=List[SettingResponse])
|
||||
async def list_settings_by_namespace(
|
||||
namespace: str,
|
||||
db: DatabaseConnection = Depends(get_db_conn),
|
||||
):
|
||||
"""Return all settings for a given namespace."""
|
||||
correlation_id = generate_correlation_id()
|
||||
rows = await db.fetch(
|
||||
"SELECT * FROM system_settings WHERE namespace = $1"
|
||||
" ORDER BY key",
|
||||
namespace,
|
||||
)
|
||||
log.info("api_list_settings_namespace", {
|
||||
"namespace": namespace,
|
||||
"count": len(rows),
|
||||
"correlation_id": correlation_id,
|
||||
})
|
||||
return [_row_to_setting(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=SettingResponse)
|
||||
async def get_setting(
|
||||
key: str,
|
||||
db: DatabaseConnection = Depends(get_db_conn),
|
||||
):
|
||||
"""Get a single setting by key."""
|
||||
row = await db.fetchrow(
|
||||
"SELECT * FROM system_settings WHERE key = $1", key
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="setting not found")
|
||||
return _row_to_setting(row)
|
||||
|
||||
|
||||
@router.put("/{key}", response_model=SettingResponse)
|
||||
async def update_setting(
|
||||
key: str,
|
||||
body: SettingUpdateRequest,
|
||||
db: DatabaseConnection = Depends(get_db_conn),
|
||||
):
|
||||
"""Update the value of a single setting."""
|
||||
correlation_id = generate_correlation_id()
|
||||
result = await db.execute(
|
||||
"""
|
||||
UPDATE system_settings
|
||||
SET value = $1, updated_at = now()
|
||||
WHERE key = $2
|
||||
""",
|
||||
json.dumps(body.value),
|
||||
key,
|
||||
)
|
||||
if result == "UPDATE 0":
|
||||
raise HTTPException(status_code=404, detail="setting not found")
|
||||
|
||||
row = await db.fetchrow(
|
||||
"SELECT * FROM system_settings WHERE key = $1", key
|
||||
)
|
||||
log.info("api_update_setting", {
|
||||
"key": key,
|
||||
"correlation_id": correlation_id,
|
||||
})
|
||||
return _row_to_setting(row)
|
||||
|
||||
|
||||
@router.put("", response_model=List[SettingResponse])
|
||||
async def bulk_update_settings(
|
||||
body: BulkUpdateRequest,
|
||||
db: DatabaseConnection = Depends(get_db_conn),
|
||||
):
|
||||
"""Update multiple settings in a single call.
|
||||
|
||||
Body: { "settings": { "runtime.log_level": "DEBUG", ... } }
|
||||
Unknown keys are silently ignored (no upsert).
|
||||
"""
|
||||
correlation_id = generate_correlation_id()
|
||||
updated = []
|
||||
for key, value in body.settings.items():
|
||||
result = await db.execute(
|
||||
"""
|
||||
UPDATE system_settings
|
||||
SET value = $1, updated_at = now()
|
||||
WHERE key = $2
|
||||
""",
|
||||
json.dumps(value),
|
||||
key,
|
||||
)
|
||||
if result != "UPDATE 0":
|
||||
updated.append(key)
|
||||
|
||||
rows = await db.fetch(
|
||||
"SELECT * FROM system_settings ORDER BY namespace, key"
|
||||
)
|
||||
log.info("api_bulk_update_settings", {
|
||||
"updated_count": len(updated),
|
||||
"keys": updated,
|
||||
"correlation_id": correlation_id,
|
||||
})
|
||||
return [_row_to_setting(r) for r in rows]
|
||||
@@ -36,10 +36,11 @@ async function request<T>(
|
||||
return json as T
|
||||
}
|
||||
|
||||
const get = <T>(path: string) => request<T>('GET', path)
|
||||
const post = <T>(path: string, body?: unknown) => request<T>('POST', path, body)
|
||||
const patch = <T>(path: string, body?: unknown) => request<T>('PATCH', path, body)
|
||||
const del = <T>(path: string) => request<T>('DELETE', path)
|
||||
const get = <T>(path: string) => request<T>('GET', path)
|
||||
const post = <T>(path: string, body?: unknown) => request<T>('POST', path, body)
|
||||
const patch = <T>(path: string, body?: unknown) => request<T>('PATCH', path, body)
|
||||
const put = <T>(path: string, body?: unknown) => request<T>('PUT', path, body)
|
||||
const del = <T>(path: string) => request<T>('DELETE', path)
|
||||
|
||||
// ── Types matching FastAPI response models ─────────────────────────────────
|
||||
|
||||
@@ -194,6 +195,14 @@ export interface ApiLogEntry {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ApiSetting {
|
||||
key: string
|
||||
namespace: string
|
||||
value: unknown
|
||||
description: string | null
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ApiLogList {
|
||||
items: ApiLogEntry[]
|
||||
total: number
|
||||
@@ -326,3 +335,11 @@ export const logsApi = {
|
||||
},
|
||||
stats: () => get<ApiLogStats>('/logs/stats'),
|
||||
}
|
||||
|
||||
export const settingsApi = {
|
||||
list: () => get<ApiSetting[]>('/settings'),
|
||||
namespace: (ns: string) => get<ApiSetting[]>(`/settings/namespace/${ns}`),
|
||||
get: (key: string) => get<ApiSetting>(`/settings/${key}`),
|
||||
update: (key: string, value: unknown) => put<ApiSetting>(`/settings/${key}`, { value }),
|
||||
bulkUpdate: (settings: Record<string, unknown>) => put<ApiSetting[]>('/settings', { settings }),
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Settings as SettingsIcon, Database, Server, Shield,
|
||||
Bell, Code2, Save, RefreshCw, AlertTriangle,
|
||||
CheckCircle2, ExternalLink,
|
||||
CheckCircle2, ExternalLink, Loader2,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { settingsApi, type ApiSetting } from '@/api/client'
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -42,17 +44,20 @@ function Field({ label, hint, children }: {
|
||||
)
|
||||
}
|
||||
|
||||
function TextInput({ defaultValue, placeholder, mono = true, disabled }: {
|
||||
defaultValue?: string
|
||||
function TextInput({ settingKey, value, onChange, placeholder, mono = true, disabled }: {
|
||||
settingKey?: string
|
||||
value?: string
|
||||
onChange?: (key: string, val: string) => void
|
||||
placeholder?: string
|
||||
mono?: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
defaultValue={defaultValue}
|
||||
value={value ?? ''}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
onChange={e => settingKey && onChange?.(settingKey, e.target.value)}
|
||||
className={cn(
|
||||
'w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm text-slate-200',
|
||||
'focus:outline-none focus:border-[#6470f1]/50 placeholder:text-slate-600 transition-colors',
|
||||
@@ -63,17 +68,30 @@ function TextInput({ defaultValue, placeholder, mono = true, disabled }: {
|
||||
)
|
||||
}
|
||||
|
||||
function SelectInput({ options, defaultValue }: { options: string[]; defaultValue?: string }) {
|
||||
function SelectInput({ settingKey, value, options, onChange }: {
|
||||
settingKey?: string
|
||||
value?: string
|
||||
options: string[]
|
||||
onChange?: (key: string, val: string) => void
|
||||
}) {
|
||||
return (
|
||||
<select defaultValue={defaultValue}
|
||||
<select
|
||||
value={value ?? ''}
|
||||
onChange={e => settingKey && onChange?.(settingKey, e.target.value)}
|
||||
className="w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm font-mono text-slate-200 focus:outline-none focus:border-[#6470f1]/50">
|
||||
{options.map(o => <option key={o} value={o}>{o}</option>)}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
function Toggle({ label, defaultChecked, desc }: { label: string; defaultChecked?: boolean; desc?: string }) {
|
||||
const [checked, setChecked] = useState(defaultChecked ?? false)
|
||||
function Toggle({ settingKey, label, checked, onChange, desc }: {
|
||||
settingKey?: string
|
||||
label: string
|
||||
checked?: boolean
|
||||
onChange?: (key: string, val: boolean) => void
|
||||
desc?: string
|
||||
}) {
|
||||
const isOn = checked ?? false
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2.5 border-b border-white/[0.04] last:border-0">
|
||||
<div>
|
||||
@@ -81,25 +99,29 @@ function Toggle({ label, defaultChecked, desc }: { label: string; defaultChecked
|
||||
{desc && <div className="text-[10px] font-mono text-slate-600 mt-0.5">{desc}</div>}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setChecked(!checked)}
|
||||
className={cn('w-10 h-5 rounded-full transition-all relative', checked ? 'bg-[#6470f1]' : 'bg-white/[0.08]')}
|
||||
onClick={() => settingKey && onChange?.(settingKey, !isOn)}
|
||||
className={cn('w-10 h-5 rounded-full transition-all relative', isOn ? 'bg-[#6470f1]' : 'bg-white/[0.08]')}
|
||||
>
|
||||
<span className={cn('absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all',
|
||||
checked ? 'left-5' : 'left-0.5')} />
|
||||
isOn ? 'left-5' : 'left-0.5')} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SaveBar({ onSave }: { onSave: () => void }) {
|
||||
function SaveBar({ onSave, onReset, saving }: {
|
||||
onSave: () => void
|
||||
onReset: () => void
|
||||
saving?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-3 mt-5 pt-5 border-t border-white/[0.06]">
|
||||
<button className="text-sm font-mono text-slate-500 hover:text-slate-300 px-4 py-2 transition-colors">
|
||||
<button onClick={onReset} className="text-sm font-mono text-slate-500 hover:text-slate-300 px-4 py-2 transition-colors">
|
||||
Reset
|
||||
</button>
|
||||
<button onClick={onSave}
|
||||
className="flex items-center gap-2 bg-[#6470f1] hover:bg-[#4f52e5] text-white font-mono text-sm rounded-xl px-5 py-2.5 transition-all shadow-[0_0_20px_rgba(100,112,241,0.25)]">
|
||||
<Save size={13} />
|
||||
<button onClick={onSave} disabled={saving}
|
||||
className="flex items-center gap-2 bg-[#6470f1] hover:bg-[#4f52e5] text-white font-mono text-sm rounded-xl px-5 py-2.5 transition-all shadow-[0_0_20px_rgba(100,112,241,0.25)] disabled:opacity-50">
|
||||
{saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
@@ -113,20 +135,62 @@ type SettingsTab = 'runtime' | 'database' | 'security' | 'notifications' | 'deve
|
||||
export function Settings() {
|
||||
const [tab, setTab] = useState<SettingsTab>('runtime')
|
||||
const [saved, setSaved] = useState(false)
|
||||
const qc = useQueryClient()
|
||||
|
||||
const handleSave = () => {
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2500)
|
||||
}
|
||||
// Local draft — keyed by setting key, value is whatever type the setting is
|
||||
const [draft, setDraft] = useState<Record<string, unknown>>({})
|
||||
|
||||
const { data: allSettings, isLoading } = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: () => settingsApi.list(),
|
||||
})
|
||||
|
||||
// Build a lookup map for easy access in JSX
|
||||
const settingMap: Record<string, ApiSetting> = {}
|
||||
for (const s of allSettings ?? []) settingMap[s.key] = s
|
||||
|
||||
// Helpers to read current value (draft takes priority over DB)
|
||||
const str = (key: string, fallback = '') =>
|
||||
String(draft[key] ?? settingMap[key]?.value ?? fallback)
|
||||
const bool = (key: string, fallback = false) =>
|
||||
Boolean(draft[key] ?? settingMap[key]?.value ?? fallback)
|
||||
|
||||
// Update draft on field change
|
||||
const onChange = (key: string, val: unknown) =>
|
||||
setDraft(d => ({ ...d, [key]: val }))
|
||||
|
||||
// Reset draft to current DB values
|
||||
const handleReset = () => setDraft({})
|
||||
|
||||
const saveMut = useMutation({
|
||||
mutationFn: () => settingsApi.bulkUpdate(draft),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['settings'] })
|
||||
setDraft({})
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2500)
|
||||
},
|
||||
})
|
||||
|
||||
const TABS: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
||||
{ id: 'runtime', label: 'Runtime', icon: Server },
|
||||
{ id: 'database', label: 'Database', icon: Database },
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
{ id: 'notifications', label: 'Notifications', icon: Bell },
|
||||
{ id: 'developer', label: 'Developer', icon: Code2 },
|
||||
{ id: 'runtime', label: 'Runtime', icon: Server },
|
||||
{ id: 'database', label: 'Database', icon: Database },
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
{ id: 'notifications', label: 'Notifications', icon: Bell },
|
||||
{ id: 'developer', label: 'Developer', icon: Code2 },
|
||||
]
|
||||
|
||||
const hasDraft = Object.keys(draft).length > 0
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0f] flex items-center justify-center gap-2">
|
||||
<Loader2 size={16} className="animate-spin text-slate-600" />
|
||||
<span className="text-sm font-mono text-slate-600">Loading settings...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0f] text-slate-200">
|
||||
<div className="max-w-screen-xl mx-auto px-6 py-8">
|
||||
@@ -139,12 +203,19 @@ export function Settings() {
|
||||
</div>
|
||||
<p className="text-xs font-mono text-slate-600">Runtime configuration · environment · integrations</p>
|
||||
</div>
|
||||
{saved && (
|
||||
<div className="flex items-center gap-2 text-[11px] font-mono text-[#00ff88] border border-[#00ff88]/20 rounded-xl px-3 py-2 bg-[#00ff88]/5">
|
||||
<CheckCircle2 size={12} />
|
||||
Changes saved
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{hasDraft && (
|
||||
<span className="text-[10px] font-mono text-[#ffb800] border border-[#ffb800]/20 rounded-xl px-3 py-2 bg-[#ffb800]/5">
|
||||
Unsaved changes
|
||||
</span>
|
||||
)}
|
||||
{saved && (
|
||||
<div className="flex items-center gap-2 text-[11px] font-mono text-[#00ff88] border border-[#00ff88]/20 rounded-xl px-3 py-2 bg-[#00ff88]/5">
|
||||
<CheckCircle2 size={12} />
|
||||
Changes saved
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6">
|
||||
@@ -169,44 +240,51 @@ export function Settings() {
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
{tab === 'runtime' && (
|
||||
<>
|
||||
<Section title="Runtime Configuration" icon={Server} accent="#6470f1">
|
||||
<Field label="Environment">
|
||||
<SelectInput options={['development', 'staging', 'production']} defaultValue="development" />
|
||||
</Field>
|
||||
<Field label="Runtime Version" hint="Read-only — set at deploy time">
|
||||
<TextInput defaultValue="0.1.0" disabled />
|
||||
</Field>
|
||||
<Field label="Log Level">
|
||||
<SelectInput options={['DEBUG', 'INFO', 'WARN', 'ERROR']} defaultValue="INFO" />
|
||||
</Field>
|
||||
<Field label="Max Concurrent Agents" hint="Agents beyond this limit are queued">
|
||||
<TextInput defaultValue="20" />
|
||||
</Field>
|
||||
<Field label="Default Task Timeout (seconds)">
|
||||
<TextInput defaultValue="300" />
|
||||
</Field>
|
||||
<div className="mt-4 space-y-0">
|
||||
<Toggle label="Enable execution event log" defaultChecked={true} desc="Append-only event log for all task graph transitions" />
|
||||
<Toggle label="Enable step checkpointing" defaultChecked={true} desc="Write checkpoint after each completed node for crash recovery" />
|
||||
<Toggle label="Auto-retry failed tasks" defaultChecked={true} desc="Apply retry policy automatically on node failure" />
|
||||
</div>
|
||||
<SaveBar onSave={handleSave} />
|
||||
</Section>
|
||||
</>
|
||||
<Section title="Runtime Configuration" icon={Server} accent="#6470f1">
|
||||
<Field label="Environment">
|
||||
<SelectInput settingKey="runtime.environment" value={str('runtime.environment', 'development')}
|
||||
options={['development', 'staging', 'production']} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Runtime Version" hint="Read-only — set at deploy time">
|
||||
<TextInput value="0.1.0" disabled />
|
||||
</Field>
|
||||
<Field label="Log Level">
|
||||
<SelectInput settingKey="runtime.log_level" value={str('runtime.log_level', 'INFO')}
|
||||
options={['DEBUG', 'INFO', 'WARN', 'ERROR']} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Max Concurrent Agents" hint="Agents beyond this limit are queued">
|
||||
<TextInput settingKey="runtime.max_concurrent_agents"
|
||||
value={str('runtime.max_concurrent_agents', '20')} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Default Task Timeout (seconds)">
|
||||
<TextInput settingKey="runtime.default_task_timeout"
|
||||
value={str('runtime.default_task_timeout', '300')} onChange={onChange} />
|
||||
</Field>
|
||||
<div className="mt-4 space-y-0">
|
||||
<Toggle settingKey="runtime.execution_event_log" label="Enable execution event log"
|
||||
checked={bool('runtime.execution_event_log', true)} onChange={onChange}
|
||||
desc="Append-only event log for all task graph transitions" />
|
||||
<Toggle settingKey="runtime.step_checkpointing" label="Enable step checkpointing"
|
||||
checked={bool('runtime.step_checkpointing', true)} onChange={onChange}
|
||||
desc="Write checkpoint after each completed node for crash recovery" />
|
||||
<Toggle settingKey="runtime.auto_retry_failed" label="Auto-retry failed tasks"
|
||||
checked={bool('runtime.auto_retry_failed', true)} onChange={onChange}
|
||||
desc="Apply retry policy automatically on node failure" />
|
||||
</div>
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{tab === 'database' && (
|
||||
<>
|
||||
<Section title="PostgreSQL" icon={Database} accent="#00d4ff">
|
||||
<Field label="Connection String" hint="Used for agent state, task graph, event log, audit trail">
|
||||
<TextInput defaultValue="postgresql://nebula:***@localhost:5432/nebula_db" />
|
||||
</Field>
|
||||
<Field label="Pool Size">
|
||||
<TextInput defaultValue="20" />
|
||||
<TextInput settingKey="database.pool_size"
|
||||
value={str('database.pool_size', '20')} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Max Overflow">
|
||||
<TextInput defaultValue="40" />
|
||||
<TextInput settingKey="database.max_overflow"
|
||||
value={str('database.max_overflow', '40')} onChange={onChange} />
|
||||
</Field>
|
||||
<div className="flex items-center gap-3 mt-4">
|
||||
<button className="flex items-center gap-2 text-sm font-mono text-[#00d4ff] border border-[#00d4ff]/25 rounded-xl px-4 py-2 hover:bg-[#00d4ff]/8 transition-all">
|
||||
@@ -214,28 +292,35 @@ export function Settings() {
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
<SaveBar onSave={handleSave} />
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
<Section title="Redis (Working Memory)" icon={Database} accent="#a855f7">
|
||||
<Field label="URL">
|
||||
<TextInput defaultValue="redis://localhost:6379/0" />
|
||||
<TextInput settingKey="database.redis_url"
|
||||
value={str('database.redis_url', 'redis://localhost:6379/0')} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Default TTL (seconds)" hint="Default TTL for working memory entries">
|
||||
<TextInput defaultValue="3600" />
|
||||
<TextInput settingKey="database.redis_ttl"
|
||||
value={str('database.redis_ttl', '3600')} onChange={onChange} />
|
||||
</Field>
|
||||
<SaveBar onSave={handleSave} />
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
<Section title="Vector Database" icon={Database} accent="#00ff88">
|
||||
<Field label="Provider">
|
||||
<SelectInput options={['pgvector (PostgreSQL)', 'Qdrant', 'Weaviate', 'Pinecone']} defaultValue="pgvector (PostgreSQL)" />
|
||||
<SelectInput settingKey="database.vector_provider"
|
||||
value={str('database.vector_provider', 'pgvector (PostgreSQL)')}
|
||||
options={['pgvector (PostgreSQL)', 'Qdrant', 'Weaviate', 'Pinecone']} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Embedding Model">
|
||||
<SelectInput options={['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002']} defaultValue="text-embedding-3-small" />
|
||||
<SelectInput settingKey="database.embedding_model"
|
||||
value={str('database.embedding_model', 'text-embedding-3-small')}
|
||||
options={['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002']} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label="Embedding Dimensions" hint="Must match the selected model">
|
||||
<TextInput defaultValue="1536" />
|
||||
<TextInput settingKey="database.embedding_dimensions"
|
||||
value={str('database.embedding_dimensions', '1536')} onChange={onChange} />
|
||||
</Field>
|
||||
<SaveBar onSave={handleSave} />
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
@@ -245,7 +330,7 @@ export function Settings() {
|
||||
<Section title="API Keys" icon={Shield} accent="#ffb800">
|
||||
<Field label="Runtime API Key" hint="Used by webapp and CLI to authenticate with the runtime API">
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput defaultValue="nebula_sk_••••••••••••••••••••" />
|
||||
<TextInput value="nebula_sk_••••••••••••••••••••" disabled />
|
||||
<button className="shrink-0 text-[11px] font-mono text-slate-500 hover:text-slate-300 border border-white/[0.08] rounded-lg px-3 py-2.5 hover:bg-white/5 transition-all whitespace-nowrap">
|
||||
Rotate
|
||||
</button>
|
||||
@@ -254,12 +339,20 @@ export function Settings() {
|
||||
</Section>
|
||||
<Section title="Policy Engine" icon={Shield} accent="#ff4757">
|
||||
<div className="space-y-0">
|
||||
<Toggle label="Enforce zero-trust by default" defaultChecked={true} desc="All actions require explicit policy allow; deny is default" />
|
||||
<Toggle label="Require approval for high-risk actions" defaultChecked={true} desc="filesystem.write, db.write to sensitive paths" />
|
||||
<Toggle label="Auto-expire capability tokens" defaultChecked={true} desc="Tokens expire after 24h unless explicitly renewed" />
|
||||
<Toggle label="Log all policy evaluations" defaultChecked={true} desc="Write every decision to the immutable audit trail" />
|
||||
<Toggle settingKey="security.zero_trust_default" label="Enforce zero-trust by default"
|
||||
checked={bool('security.zero_trust_default', true)} onChange={onChange}
|
||||
desc="All actions require explicit policy allow; deny is default" />
|
||||
<Toggle settingKey="security.require_approval_high_risk" label="Require approval for high-risk actions"
|
||||
checked={bool('security.require_approval_high_risk', true)} onChange={onChange}
|
||||
desc="filesystem.write, db.write to sensitive paths" />
|
||||
<Toggle settingKey="security.auto_expire_tokens" label="Auto-expire capability tokens"
|
||||
checked={bool('security.auto_expire_tokens', true)} onChange={onChange}
|
||||
desc="Tokens expire after 24h unless explicitly renewed" />
|
||||
<Toggle settingKey="security.log_policy_evaluations" label="Log all policy evaluations"
|
||||
checked={bool('security.log_policy_evaluations', true)} onChange={onChange}
|
||||
desc="Write every decision to the immutable audit trail" />
|
||||
</div>
|
||||
<SaveBar onSave={handleSave} />
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
<Section title="Compliance" icon={Shield} accent="#a855f7">
|
||||
<div className="flex items-center gap-2 p-3 rounded-xl bg-[#a855f7]/5 border border-[#a855f7]/15 mb-4">
|
||||
@@ -278,18 +371,27 @@ export function Settings() {
|
||||
{tab === 'notifications' && (
|
||||
<Section title="Notification Channels" icon={Bell} accent="#ffb800">
|
||||
<Field label="Slack Webhook URL" hint="Receives alerts for failed tasks, approval requests, and security events">
|
||||
<TextInput placeholder="https://hooks.slack.com/services/..." />
|
||||
<TextInput settingKey="notifications.slack_webhook"
|
||||
value={str('notifications.slack_webhook', '')} onChange={onChange}
|
||||
placeholder="https://hooks.slack.com/services/..." />
|
||||
</Field>
|
||||
<Field label="Alert Email">
|
||||
<TextInput placeholder="alerts@your-org.com" mono={false} />
|
||||
<TextInput settingKey="notifications.alert_email"
|
||||
value={str('notifications.alert_email', '')} onChange={onChange}
|
||||
placeholder="alerts@your-org.com" mono={false} />
|
||||
</Field>
|
||||
<div className="mt-4 space-y-0">
|
||||
<Toggle label="Notify on task failure" defaultChecked={true} />
|
||||
<Toggle label="Notify on approval requests" defaultChecked={true} />
|
||||
<Toggle label="Notify on policy violations" defaultChecked={false} />
|
||||
<Toggle label="Daily digest" defaultChecked={false} desc="Summary of completed tasks, agent activity, policy events" />
|
||||
<Toggle settingKey="notifications.notify_task_failure" label="Notify on task failure"
|
||||
checked={bool('notifications.notify_task_failure', true)} onChange={onChange} />
|
||||
<Toggle settingKey="notifications.notify_approvals" label="Notify on approval requests"
|
||||
checked={bool('notifications.notify_approvals', true)} onChange={onChange} />
|
||||
<Toggle settingKey="notifications.notify_policy_violations" label="Notify on policy violations"
|
||||
checked={bool('notifications.notify_policy_violations', false)} onChange={onChange} />
|
||||
<Toggle settingKey="notifications.daily_digest" label="Daily digest"
|
||||
checked={bool('notifications.daily_digest', false)} onChange={onChange}
|
||||
desc="Summary of completed tasks, agent activity, policy events" />
|
||||
</div>
|
||||
<SaveBar onSave={handleSave} />
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
@@ -322,9 +424,15 @@ export function Settings() {
|
||||
</Section>
|
||||
<Section title="Debug Options" icon={Code2} accent="#a855f7">
|
||||
<div className="space-y-0">
|
||||
<Toggle label="Verbose execution logging" defaultChecked={false} desc="Log DEBUG level for all execution engine operations" />
|
||||
<Toggle label="Log tool invocation payloads" defaultChecked={false} desc="Warning: may log sensitive data. Disable in production." />
|
||||
<Toggle label="Disable policy enforcement" defaultChecked={false} desc="DANGER: bypasses all security checks. Development only." />
|
||||
<Toggle settingKey="developer.verbose_execution_log" label="Verbose execution logging"
|
||||
checked={bool('developer.verbose_execution_log', false)} onChange={onChange}
|
||||
desc="Log DEBUG level for all execution engine operations" />
|
||||
<Toggle settingKey="developer.log_tool_payloads" label="Log tool invocation payloads"
|
||||
checked={bool('developer.log_tool_payloads', false)} onChange={onChange}
|
||||
desc="Warning: may log sensitive data. Disable in production." />
|
||||
<Toggle settingKey="developer.disable_policy_enforcement" label="Disable policy enforcement"
|
||||
checked={bool('developer.disable_policy_enforcement', false)} onChange={onChange}
|
||||
desc="DANGER: bypasses all security checks. Development only." />
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2 rounded-xl bg-[#ff4757]/5 border border-[#ff4757]/15 px-4 py-3">
|
||||
<AlertTriangle size={13} className="text-[#ff4757] shrink-0" />
|
||||
@@ -332,7 +440,7 @@ export function Settings() {
|
||||
Debug options should never be enabled in production environments.
|
||||
</p>
|
||||
</div>
|
||||
<SaveBar onSave={handleSave} />
|
||||
<SaveBar onSave={() => saveMut.mutate()} onReset={handleReset} saving={saveMut.isPending} />
|
||||
</Section>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user