The overview background graph and the Knowledge Base graph both rendered empty because the SPA's OIDC access token expired (~5 min TTL) and was never refreshed. fetchWithAuth called getToken() synchronously (no refresh); ensureToken returned the stale token without refreshing; storeTokens discarded expires_in; the resulting 401 made fetchGraph return null and both graphs drew nothing, with no error surfaced. - oidc.ts: track expiresAt from expires_in; getToken() returns null within 30s of expiry; ensureToken/initOIDC refresh instead of returning stale tokens; isOIDCConfigured no longer claims configured on expired-only state - config.ts: fetchWithAuth awaits ensureToken (refresh on demand), falls back to static token if OIDC can't yield one, flushes OIDC session on 401; sseUrl is async + refreshes before constructing the EventSource - stores/events.ts: connect() awaits the now-async sseUrl
60 lines
1.5 KiB
TypeScript
60 lines
1.5 KiB
TypeScript
import { writable } from 'svelte/store'
|
|
import { sseUrl } from '$lib/config'
|
|
|
|
export interface OikosEvent {
|
|
id: number
|
|
ts: string
|
|
type: string
|
|
entity_id?: string | null
|
|
severity: 'info' | 'warning' | 'critical'
|
|
source: string
|
|
data?: unknown
|
|
correlation_id?: string | null
|
|
}
|
|
|
|
const MAX_BUFFERED = 200
|
|
|
|
export const liveEvents = writable<OikosEvent[]>([])
|
|
export const connectionState = writable<'connecting' | 'open' | 'closed'>('connecting')
|
|
|
|
let source: EventSource | null = null
|
|
let subscriberCount = 0
|
|
|
|
async function connect() {
|
|
if (source) return
|
|
connectionState.set('connecting')
|
|
// The browser's EventSource sends Last-event-ID automatically on reconnect.
|
|
// sseUrl is async so the OIDC access token is refreshed if expired.
|
|
source = new EventSource(await sseUrl('/api/v1/events/stream'))
|
|
|
|
source.onopen = () => connectionState.set('open')
|
|
|
|
source.onmessage = (ev) => {
|
|
try {
|
|
const parsed: OikosEvent = JSON.parse(ev.data)
|
|
liveEvents.update((events) => [parsed, ...events].slice(0, MAX_BUFFERED))
|
|
} catch {
|
|
// skip malformed
|
|
}
|
|
}
|
|
|
|
source.onerror = () => {
|
|
connectionState.set('closed')
|
|
}
|
|
}
|
|
|
|
function disconnect() {
|
|
source?.close()
|
|
source = null
|
|
}
|
|
|
|
// Reference-counted: the stream stays open as long as at least one page subscribes.
|
|
export function subscribeEvents(): () => void {
|
|
subscriberCount++
|
|
if (subscriberCount === 1) connect()
|
|
return () => {
|
|
subscriberCount--
|
|
if (subscriberCount === 0) disconnect()
|
|
}
|
|
}
|