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_API_KEY: "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"
|
||||||
GROQ_MODEL: "llama-3.3-70b-versatile"
|
GROQ_MODEL: "llama-3.3-70b-versatile"
|
||||||
LANGFUSE_HOST: "http://langfuse:3000"
|
LANGFUSE_HOST: "http://langfuse:3000"
|
||||||
|
KONG_PROXY_URL: ""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { apiFetch } from '../config'
|
||||||
|
|
||||||
const emit = defineEmits<{ change: [id: string] }>()
|
const emit = defineEmits<{ change: [id: string] }>()
|
||||||
|
|
||||||
@@ -7,15 +8,15 @@ const scenarios = ref<any[]>([])
|
|||||||
const active = ref('')
|
const active = ref('')
|
||||||
|
|
||||||
async function loadScenarios() {
|
async function loadScenarios() {
|
||||||
const res = await fetch('/scenarios')
|
const res = await apiFetch('/scenarios')
|
||||||
scenarios.value = await res.json()
|
scenarios.value = await res.json()
|
||||||
const activeRes = await fetch('/scenarios/active')
|
const activeRes = await apiFetch('/scenarios/active')
|
||||||
const activeData = await activeRes.json()
|
const activeData = await activeRes.json()
|
||||||
active.value = activeData.scenario_id
|
active.value = activeData.scenario_id
|
||||||
}
|
}
|
||||||
|
|
||||||
async function switchScenario(id: string) {
|
async function switchScenario(id: string) {
|
||||||
await fetch('/scenarios/active', {
|
await apiFetch('/scenarios/active', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ scenario_id: id }),
|
body: JSON.stringify({ scenario_id: id }),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref, onUnmounted } from 'vue'
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { wsBase } from '../config'
|
||||||
|
|
||||||
export interface GraphNode {
|
export interface GraphNode {
|
||||||
id: string
|
id: string
|
||||||
@@ -27,8 +28,7 @@ export function useAgentEvents() {
|
|||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
if (ws) return
|
if (ws) return
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
ws = new WebSocket(`${wsBase()}/ws/agent-events`)
|
||||||
ws = new WebSocket(`${protocol}//${location.host}/ws/agent-events`)
|
|
||||||
|
|
||||||
ws.onopen = () => { agentStatus.value = 'live' }
|
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 NotificationCard from '../components/NotificationCard.vue'
|
||||||
import HandoverBrief from '../components/HandoverBrief.vue'
|
import HandoverBrief from '../components/HandoverBrief.vue'
|
||||||
import { useAgentEvents } from '../composables/useAgentEvents'
|
import { useAgentEvents } from '../composables/useAgentEvents'
|
||||||
|
import { apiFetch } from '../config'
|
||||||
|
|
||||||
const flights = ref<any[]>([])
|
const flights = ref<any[]>([])
|
||||||
const selectedFlight = ref('')
|
const selectedFlight = ref('')
|
||||||
@@ -29,7 +30,7 @@ watch(scenarioVersion, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function loadFlights() {
|
async function loadFlights() {
|
||||||
const res = await fetch('/scenarios/data/flights')
|
const res = await apiFetch('/scenarios/data/flights')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
flights.value = data.map((f: any) => ({
|
flights.value = data.map((f: any) => ({
|
||||||
id: f.flight_id,
|
id: f.flight_id,
|
||||||
@@ -44,7 +45,7 @@ async function runFce() {
|
|||||||
notification.value = null
|
notification.value = null
|
||||||
if (!showInternals.value) showInternals.value = true
|
if (!showInternals.value) showInternals.value = true
|
||||||
|
|
||||||
const res = await fetch('/agents/fce', {
|
const res = await apiFetch('/agents/fce', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ flight_id: selectedFlight.value }),
|
body: JSON.stringify({ flight_id: selectedFlight.value }),
|
||||||
@@ -52,7 +53,7 @@ async function runFce() {
|
|||||||
const { run_id } = await res.json()
|
const { run_id } = await res.json()
|
||||||
|
|
||||||
const poll = setInterval(async () => {
|
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()
|
const data = await r.json()
|
||||||
if (data.status === 'completed') {
|
if (data.status === 'completed') {
|
||||||
clearInterval(poll)
|
clearInterval(poll)
|
||||||
@@ -70,7 +71,7 @@ async function runHandover() {
|
|||||||
handoverBrief.value = null
|
handoverBrief.value = null
|
||||||
if (!showInternals.value) showInternals.value = true
|
if (!showInternals.value) showInternals.value = true
|
||||||
|
|
||||||
const res = await fetch('/agents/handover', {
|
const res = await apiFetch('/agents/handover', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
@@ -78,7 +79,7 @@ async function runHandover() {
|
|||||||
const { run_id } = await res.json()
|
const { run_id } = await res.json()
|
||||||
|
|
||||||
const poll = setInterval(async () => {
|
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()
|
const data = await r.json()
|
||||||
if (data.status === 'completed') {
|
if (data.status === 'completed') {
|
||||||
clearInterval(poll)
|
clearInterval(poll)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { Panel } from 'soleprint-ui'
|
import { Panel } from 'soleprint-ui'
|
||||||
|
import { apiFetch } from '../config'
|
||||||
|
|
||||||
const activeTab = ref<'flights' | 'crew' | 'notes' | 'maintenance' | 'rebookings'>('flights')
|
const activeTab = ref<'flights' | 'crew' | 'notes' | 'maintenance' | 'rebookings'>('flights')
|
||||||
const flights = ref<any[]>([])
|
const flights = ref<any[]>([])
|
||||||
@@ -16,11 +17,11 @@ const editNotesText = ref('')
|
|||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
const [f, c, n, m, r] = await Promise.all([
|
const [f, c, n, m, r] = await Promise.all([
|
||||||
fetch('/scenarios/data/flights').then(r => r.json()),
|
apiFetch('/scenarios/data/flights').then(r => r.json()),
|
||||||
fetch('/scenarios/data/crew').then(r => r.json()),
|
apiFetch('/scenarios/data/crew').then(r => r.json()),
|
||||||
fetch('/scenarios/data/crew-notes').then(r => r.json()),
|
apiFetch('/scenarios/data/crew-notes').then(r => r.json()),
|
||||||
fetch('/scenarios/data/maintenance').then(r => r.json()),
|
apiFetch('/scenarios/data/maintenance').then(r => r.json()),
|
||||||
fetch('/scenarios/data/rebookings').then(r => r.json()),
|
apiFetch('/scenarios/data/rebookings').then(r => r.json()),
|
||||||
])
|
])
|
||||||
flights.value = f
|
flights.value = f
|
||||||
crew.value = c
|
crew.value = c
|
||||||
@@ -30,7 +31,7 @@ async function loadAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function patchFlight(flight: any) {
|
async function patchFlight(flight: any) {
|
||||||
await fetch(`/scenarios/data/flights/${flight.flight_id}`, {
|
await apiFetch(`/scenarios/data/flights/${flight.flight_id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -43,7 +44,7 @@ async function patchFlight(flight: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function patchCrew(c: 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',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ duty_hours_elapsed: c.duty_hours_elapsed }),
|
body: JSON.stringify({ duty_hours_elapsed: c.duty_hours_elapsed }),
|
||||||
@@ -61,7 +62,7 @@ function startEditNotes(flightId: string) {
|
|||||||
|
|
||||||
async function saveNotes(flightId: string) {
|
async function saveNotes(flightId: string) {
|
||||||
const notes = editNotesText.value.split('\n').filter(l => l.trim())
|
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',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ notes }),
|
body: JSON.stringify({ notes }),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { Panel } from 'soleprint-ui'
|
import { Panel } from 'soleprint-ui'
|
||||||
|
import { apiFetch, getKongProxyUrl, setKongProxyUrl } from '../config'
|
||||||
|
|
||||||
const config = ref<any>(null)
|
const config = ref<any>(null)
|
||||||
const selectedProvider = ref('')
|
const selectedProvider = ref('')
|
||||||
@@ -8,9 +9,35 @@ const apiKey = ref('')
|
|||||||
const model = ref('')
|
const model = ref('')
|
||||||
const baseUrl = ref('')
|
const baseUrl = ref('')
|
||||||
const saving = ref(false)
|
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() {
|
async function loadConfig() {
|
||||||
const res = await fetch('/config/llm')
|
const res = await apiFetch('/config/llm')
|
||||||
config.value = await res.json()
|
config.value = await res.json()
|
||||||
selectedProvider.value = config.value.provider
|
selectedProvider.value = config.value.provider
|
||||||
const p = config.value.providers[selectedProvider.value]
|
const p = config.value.providers[selectedProvider.value]
|
||||||
@@ -33,7 +60,7 @@ async function save() {
|
|||||||
if (model.value) body.model = model.value
|
if (model.value) body.model = model.value
|
||||||
if (baseUrl.value) body.base_url = baseUrl.value
|
if (baseUrl.value) body.base_url = baseUrl.value
|
||||||
|
|
||||||
const res = await fetch('/config/llm', {
|
const res = await apiFetch('/config/llm', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -50,7 +77,10 @@ const providerLabels: Record<string, string> = {
|
|||||||
template: 'Template (no LLM)',
|
template: 'Template (no LLM)',
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadConfig)
|
onMounted(() => {
|
||||||
|
loadConfig()
|
||||||
|
checkKong()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -111,6 +141,30 @@ onMounted(loadConfig)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -254,4 +308,41 @@ onMounted(loadConfig)
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-dim);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/agents': 'http://localhost:8000',
|
'/agents': 'http://localhost:8000',
|
||||||
'/scenarios': 'http://localhost:8000',
|
'/scenarios': 'http://localhost:8000',
|
||||||
|
'/config': 'http://localhost:8000',
|
||||||
|
'/health': 'http://localhost:8000',
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://localhost:8000',
|
target: 'ws://localhost:8000',
|
||||||
ws: true,
|
ws: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user