add Kong Konnect gateway support with Settings UI
This commit is contained in:
@@ -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: ""
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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
34
ui/app/src/config.ts
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user