add Kong Konnect gateway support with Settings UI

This commit is contained in:
2026-04-14 11:22:33 -03:00
parent c8a764c868
commit 0d0950d97d
8 changed files with 152 additions and 21 deletions

View File

@@ -9,3 +9,4 @@ data:
GROQ_API_KEY: "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"
GROQ_MODEL: "llama-3.3-70b-versatile"
LANGFUSE_HOST: "http://langfuse:3000"
KONG_PROXY_URL: ""

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { apiFetch } from '../config'
const emit = defineEmits<{ change: [id: string] }>()
@@ -7,15 +8,15 @@ const scenarios = ref<any[]>([])
const active = ref('')
async function loadScenarios() {
const res = await fetch('/scenarios')
const res = await apiFetch('/scenarios')
scenarios.value = await res.json()
const activeRes = await fetch('/scenarios/active')
const activeRes = await apiFetch('/scenarios/active')
const activeData = await activeRes.json()
active.value = activeData.scenario_id
}
async function switchScenario(id: string) {
await fetch('/scenarios/active', {
await apiFetch('/scenarios/active', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scenario_id: id }),

View File

@@ -1,4 +1,5 @@
import { ref, onUnmounted } from 'vue'
import { wsBase } from '../config'
export interface GraphNode {
id: string
@@ -27,8 +28,7 @@ export function useAgentEvents() {
function connect() {
if (ws) return
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
ws = new WebSocket(`${protocol}//${location.host}/ws/agent-events`)
ws = new WebSocket(`${wsBase()}/ws/agent-events`)
ws.onopen = () => { agentStatus.value = 'live' }

34
ui/app/src/config.ts Normal file
View File

@@ -0,0 +1,34 @@
const STORAGE_KEY = 'nova-kong-proxy-url'
function stored(): string {
return localStorage.getItem(STORAGE_KEY) || ''
}
export function getKongProxyUrl(): string {
return stored() || import.meta.env.VITE_KONG_PROXY_URL || ''
}
export function setKongProxyUrl(url: string) {
if (url) {
localStorage.setItem(STORAGE_KEY, url.replace(/\/+$/, ''))
} else {
localStorage.removeItem(STORAGE_KEY)
}
}
export function apiBase(): string {
return getKongProxyUrl()
}
export function wsBase(): string {
const kong = getKongProxyUrl()
if (kong) {
return kong.replace(/^http/, 'ws')
}
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
return `${protocol}//${location.host}`
}
export function apiFetch(path: string, init?: RequestInit): Promise<Response> {
return fetch(`${apiBase()}${path}`, init)
}

View File

@@ -4,6 +4,7 @@ import { Panel, SplitPane, LogRenderer } from 'soleprint-ui'
import NotificationCard from '../components/NotificationCard.vue'
import HandoverBrief from '../components/HandoverBrief.vue'
import { useAgentEvents } from '../composables/useAgentEvents'
import { apiFetch } from '../config'
const flights = ref<any[]>([])
const selectedFlight = ref('')
@@ -29,7 +30,7 @@ watch(scenarioVersion, () => {
})
async function loadFlights() {
const res = await fetch('/scenarios/data/flights')
const res = await apiFetch('/scenarios/data/flights')
const data = await res.json()
flights.value = data.map((f: any) => ({
id: f.flight_id,
@@ -44,7 +45,7 @@ async function runFce() {
notification.value = null
if (!showInternals.value) showInternals.value = true
const res = await fetch('/agents/fce', {
const res = await apiFetch('/agents/fce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ flight_id: selectedFlight.value }),
@@ -52,7 +53,7 @@ async function runFce() {
const { run_id } = await res.json()
const poll = setInterval(async () => {
const r = await fetch(`/agents/runs/${run_id}`)
const r = await apiFetch(`/agents/runs/${run_id}`)
const data = await r.json()
if (data.status === 'completed') {
clearInterval(poll)
@@ -70,7 +71,7 @@ async function runHandover() {
handoverBrief.value = null
if (!showInternals.value) showInternals.value = true
const res = await fetch('/agents/handover', {
const res = await apiFetch('/agents/handover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
@@ -78,7 +79,7 @@ async function runHandover() {
const { run_id } = await res.json()
const poll = setInterval(async () => {
const r = await fetch(`/agents/runs/${run_id}`)
const r = await apiFetch(`/agents/runs/${run_id}`)
const data = await r.json()
if (data.status === 'completed') {
clearInterval(poll)

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { Panel } from 'soleprint-ui'
import { apiFetch } from '../config'
const activeTab = ref<'flights' | 'crew' | 'notes' | 'maintenance' | 'rebookings'>('flights')
const flights = ref<any[]>([])
@@ -16,11 +17,11 @@ const editNotesText = ref('')
async function loadAll() {
const [f, c, n, m, r] = await Promise.all([
fetch('/scenarios/data/flights').then(r => r.json()),
fetch('/scenarios/data/crew').then(r => r.json()),
fetch('/scenarios/data/crew-notes').then(r => r.json()),
fetch('/scenarios/data/maintenance').then(r => r.json()),
fetch('/scenarios/data/rebookings').then(r => r.json()),
apiFetch('/scenarios/data/flights').then(r => r.json()),
apiFetch('/scenarios/data/crew').then(r => r.json()),
apiFetch('/scenarios/data/crew-notes').then(r => r.json()),
apiFetch('/scenarios/data/maintenance').then(r => r.json()),
apiFetch('/scenarios/data/rebookings').then(r => r.json()),
])
flights.value = f
crew.value = c
@@ -30,7 +31,7 @@ async function loadAll() {
}
async function patchFlight(flight: any) {
await fetch(`/scenarios/data/flights/${flight.flight_id}`, {
await apiFetch(`/scenarios/data/flights/${flight.flight_id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -43,7 +44,7 @@ async function patchFlight(flight: any) {
}
async function patchCrew(c: any) {
const res = await fetch(`/scenarios/data/crew/${c.crew_id}`, {
const res = await apiFetch(`/scenarios/data/crew/${c.crew_id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ duty_hours_elapsed: c.duty_hours_elapsed }),
@@ -61,7 +62,7 @@ function startEditNotes(flightId: string) {
async function saveNotes(flightId: string) {
const notes = editNotesText.value.split('\n').filter(l => l.trim())
await fetch(`/scenarios/data/crew-notes/${flightId}`, {
await apiFetch(`/scenarios/data/crew-notes/${flightId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes }),

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Panel } from 'soleprint-ui'
import { apiFetch, getKongProxyUrl, setKongProxyUrl } from '../config'
const config = ref<any>(null)
const selectedProvider = ref('')
@@ -8,9 +9,35 @@ const apiKey = ref('')
const model = ref('')
const baseUrl = ref('')
const saving = ref(false)
const kongUrl = ref(getKongProxyUrl())
const kongStatus = ref<'idle' | 'ok' | 'error'>('idle')
async function checkKong() {
if (!kongUrl.value) {
kongStatus.value = 'idle'
return
}
try {
const res = await fetch(`${kongUrl.value.replace(/\/+$/, '')}/health`, { signal: AbortSignal.timeout(5000) })
kongStatus.value = res.ok ? 'ok' : 'error'
} catch {
kongStatus.value = 'error'
}
}
function saveKong() {
setKongProxyUrl(kongUrl.value)
checkKong()
}
function clearKong() {
kongUrl.value = ''
setKongProxyUrl('')
kongStatus.value = 'idle'
}
async function loadConfig() {
const res = await fetch('/config/llm')
const res = await apiFetch('/config/llm')
config.value = await res.json()
selectedProvider.value = config.value.provider
const p = config.value.providers[selectedProvider.value]
@@ -33,7 +60,7 @@ async function save() {
if (model.value) body.model = model.value
if (baseUrl.value) body.base_url = baseUrl.value
const res = await fetch('/config/llm', {
const res = await apiFetch('/config/llm', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
@@ -50,7 +77,10 @@ const providerLabels: Record<string, string> = {
template: 'Template (no LLM)',
}
onMounted(loadConfig)
onMounted(() => {
loadConfig()
checkKong()
})
</script>
<template>
@@ -111,6 +141,30 @@ onMounted(loadConfig)
</div>
</div>
</Panel>
<Panel title="API Gateway (Kong Konnect)" status="idle">
<div class="config-form">
<div class="field">
<label>Kong Proxy URL</label>
<div class="kong-row">
<input
v-model="kongUrl"
class="input kong-input"
placeholder="https://us.api.konghq.com/..."
@keyup.enter="saveKong"
/>
<span :class="['kong-dot', kongStatus]"></span>
</div>
<span class="kong-hint">
{{ kongUrl ? (kongStatus === 'ok' ? 'Connected' : kongStatus === 'error' ? 'Unreachable' : 'Not checked') : 'Not configured — direct connection to API' }}
</span>
</div>
<div class="actions">
<button class="save-btn" @click="saveKong">Apply</button>
<button class="clear-btn" @click="clearKong" v-if="kongUrl">Clear</button>
</div>
</div>
</Panel>
</div>
</template>
@@ -254,4 +308,41 @@ onMounted(loadConfig)
font-size: 11px;
color: var(--text-dim);
}
.kong-row {
display: flex;
align-items: center;
gap: 8px;
}
.kong-input { flex: 1; }
.kong-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.kong-dot.idle { background: var(--surface-3); }
.kong-dot.ok { background: var(--status-live); }
.kong-dot.error { background: var(--status-error, #e55); }
.kong-hint {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
}
.clear-btn {
background: var(--surface-2);
color: var(--text-secondary);
border: var(--panel-border);
padding: 6px 16px;
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
}
.clear-btn:hover { background: var(--surface-3); }
</style>

View File

@@ -8,6 +8,8 @@ export default defineConfig({
proxy: {
'/agents': 'http://localhost:8000',
'/scenarios': 'http://localhost:8000',
'/config': 'http://localhost:8000',
'/health': 'http://localhost:8000',
'/ws': {
target: 'ws://localhost:8000',
ws: true,