Files
oikos/web/src/lib/stores/events.ts
dtoro 3b98097f58
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
fix(web): refresh expired OIDC tokens before API calls
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
2026-07-14 20:42:07 +02:00

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()
}
}