diff --git a/backend/app/api/routes/sse.py b/backend/app/api/routes/sse.py
index 6b1b406f..81fd39ba 100644
--- a/backend/app/api/routes/sse.py
+++ b/backend/app/api/routes/sse.py
@@ -4,11 +4,11 @@
from sse_starlette.sse import EventSourceResponse
from app.domain.sse import SSEHealthDomain
+from app.schemas_pydantic.notification import NotificationResponse
from app.schemas_pydantic.sse import (
ShutdownStatusResponse,
SSEExecutionEventData,
SSEHealthResponse,
- SSENotificationEventData,
)
from app.services.auth_service import AuthService
from app.services.sse.sse_service import SSEService
@@ -16,7 +16,7 @@
router = APIRouter(prefix="/events", tags=["sse"], route_class=DishkaRoute)
-@router.get("/notifications/stream", responses={200: {"model": SSENotificationEventData}})
+@router.get("/notifications/stream", responses={200: {"model": NotificationResponse}})
async def notification_stream(
request: Request,
sse_service: FromDishka[SSEService],
@@ -25,7 +25,10 @@ async def notification_stream(
"""Stream notifications for authenticated user."""
current_user = await auth_service.get_current_user(request)
- return EventSourceResponse(sse_service.create_notification_stream(user_id=current_user.user_id))
+ return EventSourceResponse(
+ sse_service.create_notification_stream(user_id=current_user.user_id),
+ ping=30,
+ )
@router.get("/executions/{execution_id}", responses={200: {"model": SSEExecutionEventData}})
diff --git a/backend/app/core/metrics/__init__.py b/backend/app/core/metrics/__init__.py
index 16f45150..77d1687d 100644
--- a/backend/app/core/metrics/__init__.py
+++ b/backend/app/core/metrics/__init__.py
@@ -1,4 +1,4 @@
-from app.core.metrics.base import BaseMetrics, MetricsConfig
+from app.core.metrics.base import BaseMetrics
from app.core.metrics.connections import ConnectionMetrics
from app.core.metrics.coordinator import CoordinatorMetrics
from app.core.metrics.database import DatabaseMetrics
@@ -14,7 +14,6 @@
__all__ = [
"BaseMetrics",
- "MetricsConfig",
"ConnectionMetrics",
"CoordinatorMetrics",
"DatabaseMetrics",
diff --git a/backend/app/core/metrics/base.py b/backend/app/core/metrics/base.py
index 911ed583..2e87d01c 100644
--- a/backend/app/core/metrics/base.py
+++ b/backend/app/core/metrics/base.py
@@ -1,76 +1,27 @@
-from dataclasses import dataclass
-
-from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
-from opentelemetry.metrics import Meter, NoOpMeterProvider
-from opentelemetry.sdk.metrics import MeterProvider as SdkMeterProvider
-from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
-from opentelemetry.sdk.resources import Resource
+from opentelemetry import metrics
from app.settings import Settings
-@dataclass
-class MetricsConfig:
- service_name: str = "integr8scode-backend"
- service_version: str = "1.0.0"
- otlp_endpoint: str | None = None
- export_interval_millis: int = 10000
- console_export_interval_millis: int = 60000
-
-
class BaseMetrics:
def __init__(self, settings: Settings, meter_name: str | None = None):
- """Initialize base metrics with its own meter.
+ """Initialize base metrics with a meter from the global MeterProvider.
+
+ The global MeterProvider is configured once by ``setup_metrics``.
+ If it hasn't been called (e.g. in tests), the default no-op provider is used.
Args:
- settings: Application settings.
+ settings: Application settings (kept for DI compatibility).
meter_name: Optional name for the meter. Defaults to class name.
"""
- config = MetricsConfig(
- service_name=settings.TRACING_SERVICE_NAME or "integr8scode-backend",
- service_version="1.0.0",
- otlp_endpoint=settings.OTEL_EXPORTER_OTLP_ENDPOINT,
- )
-
meter_name = meter_name or self.__class__.__name__
- self._meter = self._create_meter(settings, config, meter_name)
+ self._meter = metrics.get_meter(meter_name)
self._create_instruments()
- def _create_meter(self, settings: Settings, config: MetricsConfig, meter_name: str) -> Meter:
- """Create a new meter instance for this collector.
-
- Args:
- settings: Application settings
- config: Metrics configuration
- meter_name: Name for this meter
-
- Returns:
- A new meter instance
- """
- # If tracing/metrics disabled or no OTLP endpoint configured, use NoOp meter
- if not config.otlp_endpoint:
- return NoOpMeterProvider().get_meter(meter_name)
-
- resource = Resource.create(
- {"service.name": config.service_name, "service.version": config.service_version, "meter.name": meter_name}
- )
-
- reader = PeriodicExportingMetricReader(
- exporter=OTLPMetricExporter(endpoint=config.otlp_endpoint),
- export_interval_millis=config.export_interval_millis,
- )
-
- # Each collector gets its own MeterProvider
- meter_provider = SdkMeterProvider(resource=resource, metric_readers=[reader])
-
- # Return a meter from this provider
- return meter_provider.get_meter(meter_name)
-
def _create_instruments(self) -> None:
"""Create metric instruments. Override in subclasses."""
pass
def close(self) -> None:
"""Close the metrics collector and clean up resources."""
- # Subclasses can override if they need cleanup
pass
diff --git a/backend/app/core/middlewares/metrics.py b/backend/app/core/middlewares/metrics.py
index 784dc174..93a00f98 100644
--- a/backend/app/core/middlewares/metrics.py
+++ b/backend/app/core/middlewares/metrics.py
@@ -4,7 +4,6 @@
import time
import psutil
-from fastapi import FastAPI
from opentelemetry import metrics
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.metrics import CallbackOptions, Observation
@@ -118,13 +117,22 @@ def _get_path_template(path: str) -> str:
return path
-def setup_metrics(app: FastAPI, settings: Settings, logger: logging.Logger) -> None:
- """Set up OpenTelemetry metrics with OTLP exporter."""
- if not settings.OTEL_EXPORTER_OTLP_ENDPOINT:
- logger.warning("OTEL_EXPORTER_OTLP_ENDPOINT not configured, skipping metrics setup")
+def setup_metrics(settings: Settings, logger: logging.Logger) -> None:
+ """Set up the global OpenTelemetry MeterProvider with OTLP exporter.
+
+ This is the single initialization point for metrics export. ``BaseMetrics``
+ subclasses and ``MetricsMiddleware`` obtain meters via the global API
+ (``opentelemetry.metrics.get_meter``), so this must run before them.
+ When skipped (tests / missing endpoint), the default no-op provider is used.
+ """
+ if settings.TESTING or not settings.OTEL_EXPORTER_OTLP_ENDPOINT:
+ logger.info(
+ "Metrics OTLP export disabled (testing=%s, endpoint=%s)",
+ settings.TESTING,
+ settings.OTEL_EXPORTER_OTLP_ENDPOINT,
+ )
return
- # Configure OpenTelemetry resource
resource = Resource.create(
{
SERVICE_NAME: settings.SERVICE_NAME,
@@ -133,31 +141,21 @@ def setup_metrics(app: FastAPI, settings: Settings, logger: logging.Logger) -> N
}
)
- # Configure OTLP exporter (sends to OpenTelemetry Collector or compatible backend)
- # Default endpoint is localhost:4317 for gRPC
otlp_exporter = OTLPMetricExporter(endpoint=settings.OTEL_EXPORTER_OTLP_ENDPOINT, insecure=True)
- # Create metric reader with 60 second export interval
metric_reader = PeriodicExportingMetricReader(
exporter=otlp_exporter,
export_interval_millis=60000,
)
- # Set up the meter provider
meter_provider = MeterProvider(
resource=resource,
metric_readers=[metric_reader],
)
- # Set the global meter provider
metrics.set_meter_provider(meter_provider)
-
- # Create system metrics
create_system_metrics()
- # Add the metrics middleware (disabled for now to avoid DNS issues)
- # app.add_middleware(MetricsMiddleware)
-
logger.info("OpenTelemetry metrics configured with OTLP exporter")
diff --git a/backend/app/domain/enums/__init__.py b/backend/app/domain/enums/__init__.py
index 31458a8e..145ec7db 100644
--- a/backend/app/domain/enums/__init__.py
+++ b/backend/app/domain/enums/__init__.py
@@ -7,7 +7,7 @@
NotificationStatus,
)
from app.domain.enums.saga import SagaState
-from app.domain.enums.sse import SSEControlEvent, SSENotificationEvent
+from app.domain.enums.sse import SSEControlEvent
from app.domain.enums.user import UserRole
__all__ = [
@@ -30,7 +30,6 @@
"SagaState",
# SSE
"SSEControlEvent",
- "SSENotificationEvent",
# User
"UserRole",
]
diff --git a/backend/app/domain/enums/sse.py b/backend/app/domain/enums/sse.py
index 85885634..1cead85a 100644
--- a/backend/app/domain/enums/sse.py
+++ b/backend/app/domain/enums/sse.py
@@ -17,12 +17,3 @@ class SSEControlEvent(StringEnum):
SHUTDOWN = "shutdown"
STATUS = "status"
ERROR = "error"
-
-
-class SSENotificationEvent(StringEnum):
- """Event types for notification SSE streams."""
-
- CONNECTED = "connected"
- SUBSCRIBED = "subscribed"
- HEARTBEAT = "heartbeat"
- NOTIFICATION = "notification"
diff --git a/backend/app/main.py b/backend/app/main.py
index 5dfc1e7b..914d31b0 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -68,7 +68,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
container = create_app_container(settings)
setup_dishka(container, app)
- setup_metrics(app, settings, logger)
+ setup_metrics(settings, logger)
app.add_middleware(MetricsMiddleware)
app.add_middleware(RateLimitMiddleware, settings=settings)
app.add_middleware(CSRFMiddleware, container=container)
diff --git a/backend/app/schemas_pydantic/sse.py b/backend/app/schemas_pydantic/sse.py
index 14a4969c..74948cca 100644
--- a/backend/app/schemas_pydantic/sse.py
+++ b/backend/app/schemas_pydantic/sse.py
@@ -6,7 +6,7 @@
from app.domain.enums.events import EventType
from app.domain.enums.execution import ExecutionStatus
from app.domain.enums.notification import NotificationSeverity, NotificationStatus
-from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus, SSENotificationEvent
+from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus
from app.schemas_pydantic.execution import ExecutionResult, ResourceUsage
# Type variable for generic Redis message parsing
@@ -63,31 +63,6 @@ class RedisSSEMessage(BaseModel):
data: dict[str, Any] = Field(description="Full event data from BaseEvent.model_dump()")
-class SSENotificationEventData(BaseModel):
- """Typed model for SSE notification stream event payload.
-
- This represents the JSON data sent inside each SSE message for notification streams.
- """
-
- # Always present - identifies the event type
- event_type: SSENotificationEvent = Field(description="SSE notification event type")
-
- # Present in control events (connected, heartbeat)
- user_id: str | None = Field(default=None, description="User ID for the notification stream")
- timestamp: datetime | None = Field(default=None, description="Event timestamp")
- message: str | None = Field(default=None, description="Human-readable message")
-
- # Present only in notification events
- notification_id: str | None = Field(default=None, description="Unique notification ID")
- severity: NotificationSeverity | None = Field(default=None, description="Notification severity level")
- status: NotificationStatus | None = Field(default=None, description="Notification delivery status")
- tags: list[str] | None = Field(default=None, description="Notification tags")
- subject: str | None = Field(default=None, description="Notification subject/title")
- body: str | None = Field(default=None, description="Notification body content")
- action_url: str | None = Field(default=None, description="Optional action URL")
- created_at: datetime | None = Field(default=None, description="Creation timestamp")
-
-
class RedisNotificationMessage(BaseModel):
"""Message structure published to Redis for notification SSE delivery."""
diff --git a/backend/app/services/sse/sse_service.py b/backend/app/services/sse/sse_service.py
index 9cdb13ee..29b67bc8 100644
--- a/backend/app/services/sse/sse_service.py
+++ b/backend/app/services/sse/sse_service.py
@@ -7,14 +7,15 @@
from app.core.metrics import ConnectionMetrics
from app.db.repositories.sse_repository import SSERepository
from app.domain.enums.events import EventType
-from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus, SSENotificationEvent
+from app.domain.enums.notification import NotificationChannel
+from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus
from app.domain.sse import SSEHealthDomain
from app.schemas_pydantic.execution import ExecutionResult
+from app.schemas_pydantic.notification import NotificationResponse
from app.schemas_pydantic.sse import (
RedisNotificationMessage,
RedisSSEMessage,
SSEExecutionEventData,
- SSENotificationEventData,
)
from app.services.sse.kafka_redis_bridge import SSEKafkaRedisBridge
from app.services.sse.redis_bus import SSERedisBus
@@ -197,66 +198,32 @@ async def _build_sse_event_from_redis(self, execution_id: str, msg: RedisSSEMess
async def create_notification_stream(self, user_id: str) -> AsyncGenerator[dict[str, Any], None]:
subscription = None
-
try:
- # Start opening subscription concurrently, then yield handshake
- sub_task = asyncio.create_task(self.sse_bus.open_notification_subscription(user_id))
- yield self._format_notification_event(
- SSENotificationEventData(
- event_type=SSENotificationEvent.CONNECTED,
- user_id=user_id,
- timestamp=datetime.now(timezone.utc),
- message="Connected to notification stream",
- )
- )
+ subscription = await self.sse_bus.open_notification_subscription(user_id)
+ self.logger.info("Notification subscription opened", extra={"user_id": user_id})
- # Complete Redis subscription after handshake
- subscription = await sub_task
-
- # Signal that subscription is ready - safe to publish notifications now
- yield self._format_notification_event(
- SSENotificationEventData(
- event_type=SSENotificationEvent.SUBSCRIBED,
- user_id=user_id,
- timestamp=datetime.now(timezone.utc),
- message="Redis subscription established",
- )
- )
-
- last_heartbeat = datetime.now(timezone.utc)
while not self.shutdown_manager.is_shutting_down():
- # Heartbeat
- now = datetime.now(timezone.utc)
- if (now - last_heartbeat).total_seconds() >= self.heartbeat_interval:
- yield self._format_notification_event(
- SSENotificationEventData(
- event_type=SSENotificationEvent.HEARTBEAT,
- user_id=user_id,
- timestamp=now,
- message="Notification stream active",
- )
- )
- last_heartbeat = now
-
- # Forward notification messages as SSE data
redis_msg = await subscription.get(RedisNotificationMessage)
- if redis_msg:
- yield self._format_notification_event(
- SSENotificationEventData(
- event_type=SSENotificationEvent.NOTIFICATION,
- notification_id=redis_msg.notification_id,
- severity=redis_msg.severity,
- status=redis_msg.status,
- tags=redis_msg.tags,
- subject=redis_msg.subject,
- body=redis_msg.body,
- action_url=redis_msg.action_url,
- created_at=redis_msg.created_at,
- )
- )
+ if not redis_msg:
+ continue
+
+ notification = NotificationResponse(
+ notification_id=redis_msg.notification_id,
+ channel=NotificationChannel.IN_APP,
+ status=redis_msg.status,
+ subject=redis_msg.subject,
+ body=redis_msg.body,
+ action_url=redis_msg.action_url,
+ created_at=redis_msg.created_at,
+ read_at=None,
+ severity=redis_msg.severity,
+ tags=redis_msg.tags,
+ )
+ yield {"event": "notification", "data": notification.model_dump_json()}
finally:
if subscription is not None:
await asyncio.shield(subscription.close())
+ self.logger.info("Notification stream closed", extra={"user_id": user_id})
async def get_health_status(self) -> SSEHealthDomain:
router_stats = self.router.get_stats()
@@ -274,7 +241,3 @@ async def get_health_status(self) -> SSEHealthDomain:
def _format_sse_event(self, event: SSEExecutionEventData) -> dict[str, Any]:
"""Format typed SSE event for sse-starlette."""
return {"data": event.model_dump_json(exclude_none=True)}
-
- def _format_notification_event(self, event: SSENotificationEventData) -> dict[str, Any]:
- """Format typed notification SSE event for sse-starlette."""
- return {"data": event.model_dump_json(exclude_none=True)}
diff --git a/backend/config.test.toml b/backend/config.test.toml
index 42274458..f3d5306d 100644
--- a/backend/config.test.toml
+++ b/backend/config.test.toml
@@ -2,6 +2,7 @@
# Differences from config.toml: lower timeouts, faster bcrypt, relaxed rate limits
# Secrets (SECRET_KEY, MONGODB_URL) live in secrets.toml — use secrets.test.toml in CI.
+TESTING = true
PROJECT_NAME = "integr8scode"
DATABASE_NAME = "integr8scode_db"
ALGORITHM = "HS256"
diff --git a/backend/tests/unit/services/sse/test_sse_service.py b/backend/tests/unit/services/sse/test_sse_service.py
index 3c86a15a..eef1dc2c 100644
--- a/backend/tests/unit/services/sse/test_sse_service.py
+++ b/backend/tests/unit/services/sse/test_sse_service.py
@@ -198,28 +198,21 @@ async def test_execution_stream_result_stored_includes_result_payload(connection
@pytest.mark.asyncio
-async def test_notification_stream_connected_and_heartbeat_and_message(connection_metrics: ConnectionMetrics) -> None:
+async def test_notification_stream_yields_notification_and_shuts_down(connection_metrics: ConnectionMetrics) -> None:
+ """Notification stream yields {"event": "notification", "data": ...} for each message.
+
+ No control events (connected/subscribed/heartbeat) — those are handled by
+ the SSE protocol layer (sse-starlette ping, EventSourcePlus onResponse).
+ """
repo = _FakeRepo()
bus = _FakeBus()
sm = _FakeShutdown()
- settings = _make_fake_settings()
- settings.SSE_HEARTBEAT_INTERVAL = 0 # emit immediately
- svc = SSEService(repository=repo, router=_FakeRouter(), sse_bus=bus, shutdown_manager=sm, settings=settings,
- logger=_test_logger, connection_metrics=connection_metrics)
+ svc = SSEService(repository=repo, router=_FakeRouter(), sse_bus=bus, shutdown_manager=sm,
+ settings=_make_fake_settings(), logger=_test_logger, connection_metrics=connection_metrics)
agen = svc.create_notification_stream("u1")
- connected = await agen.__anext__()
- assert _decode(connected)["event_type"] == "connected"
-
- # Should emit subscribed after Redis subscription is ready
- subscribed = await agen.__anext__()
- assert _decode(subscribed)["event_type"] == "subscribed"
-
- # With 0 interval, next yield should be heartbeat
- hb = await agen.__anext__()
- assert _decode(hb)["event_type"] == "heartbeat"
- # Push a notification payload (must match RedisNotificationMessage schema)
+ # Push a notification payload before advancing (avoids blocking on empty queue)
await bus.notif_sub.push({
"notification_id": "n1",
"severity": "low",
@@ -230,16 +223,24 @@ async def test_notification_stream_connected_and_heartbeat_and_message(connectio
"action_url": "",
"created_at": "2025-01-01T00:00:00Z",
})
- notif = await agen.__anext__()
- assert _decode(notif)["event_type"] == "notification"
- # Stop the stream by initiating shutdown and advancing once more (loop checks flag)
+ notif = await asyncio.wait_for(agen.__anext__(), timeout=2.0)
+ # New format: SSE event field + JSON data (no event_type wrapper)
+ assert notif["event"] == "notification"
+ data = json.loads(notif["data"])
+ assert data["notification_id"] == "n1"
+ assert data["subject"] == "s"
+ assert data["channel"] == "in_app"
+
+ # Stop the stream by initiating shutdown
sm.initiate()
- # It may loop until it sees the flag; push a None to release get(timeout)
+ # Push None to unblock the subscription.get() timeout loop
await bus.notif_sub.push(None)
- # Give the generator a chance to observe the flag and finish
with pytest.raises(StopAsyncIteration):
- await asyncio.wait_for(agen.__anext__(), timeout=0.2)
+ await asyncio.wait_for(agen.__anext__(), timeout=2.0)
+
+ # Subscription should be closed during cleanup
+ assert bus.notif_sub.closed is True
@pytest.mark.asyncio
diff --git a/docs/reference/openapi.json b/docs/reference/openapi.json
index e81eb2fb..d21477bc 100644
--- a/docs/reference/openapi.json
+++ b/docs/reference/openapi.json
@@ -1808,7 +1808,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SSENotificationEventData"
+ "$ref": "#/components/schemas/NotificationResponse"
}
}
}
@@ -5616,9 +5616,8 @@
"title": "Memory Request"
},
"priority": {
- "type": "integer",
- "title": "Priority",
- "default": 5
+ "$ref": "#/components/schemas/QueuePriority",
+ "default": "normal"
}
},
"type": "object",
@@ -8843,9 +8842,8 @@
"title": "Estimated Wait Seconds"
},
"priority": {
- "type": "integer",
- "title": "Priority",
- "default": 5
+ "$ref": "#/components/schemas/QueuePriority",
+ "default": "normal"
}
},
"type": "object",
@@ -9387,9 +9385,8 @@
"title": "Memory Request"
},
"priority": {
- "type": "integer",
- "title": "Priority",
- "default": 5
+ "$ref": "#/components/schemas/QueuePriority",
+ "default": "normal"
}
},
"type": "object",
@@ -11542,6 +11539,18 @@
"title": "PublishEventResponse",
"description": "Response model for publishing events"
},
+ "QueuePriority": {
+ "type": "string",
+ "enum": [
+ "critical",
+ "high",
+ "normal",
+ "low",
+ "background"
+ ],
+ "title": "QueuePriority",
+ "description": "Execution priority, ordered highest to lowest."
+ },
"QuotaExceededEvent": {
"properties": {
"event_id": {
@@ -13350,166 +13359,6 @@
"title": "SSEHealthStatus",
"description": "Health status for SSE service."
},
- "SSENotificationEvent": {
- "type": "string",
- "enum": [
- "connected",
- "subscribed",
- "heartbeat",
- "notification"
- ],
- "title": "SSENotificationEvent",
- "description": "Event types for notification SSE streams."
- },
- "SSENotificationEventData": {
- "properties": {
- "event_type": {
- "$ref": "#/components/schemas/SSENotificationEvent",
- "description": "SSE notification event type"
- },
- "user_id": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "User Id",
- "description": "User ID for the notification stream"
- },
- "timestamp": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "null"
- }
- ],
- "title": "Timestamp",
- "description": "Event timestamp"
- },
- "message": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Message",
- "description": "Human-readable message"
- },
- "notification_id": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Notification Id",
- "description": "Unique notification ID"
- },
- "severity": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/NotificationSeverity"
- },
- {
- "type": "null"
- }
- ],
- "description": "Notification severity level"
- },
- "status": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/NotificationStatus"
- },
- {
- "type": "null"
- }
- ],
- "description": "Notification delivery status"
- },
- "tags": {
- "anyOf": [
- {
- "items": {
- "type": "string"
- },
- "type": "array"
- },
- {
- "type": "null"
- }
- ],
- "title": "Tags",
- "description": "Notification tags"
- },
- "subject": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Subject",
- "description": "Notification subject/title"
- },
- "body": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Body",
- "description": "Notification body content"
- },
- "action_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Action Url",
- "description": "Optional action URL"
- },
- "created_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "null"
- }
- ],
- "title": "Created At",
- "description": "Creation timestamp"
- }
- },
- "type": "object",
- "required": [
- "event_type"
- ],
- "title": "SSENotificationEventData",
- "description": "Typed model for SSE notification stream event payload.\n\nThis represents the JSON data sent inside each SSE message for notification streams."
- },
"SagaCancellationResponse": {
"properties": {
"success": {
diff --git a/frontend/src/components/admin/events/UserOverviewModal.svelte b/frontend/src/components/admin/events/UserOverviewModal.svelte
index 2fc90a72..a0782356 100644
--- a/frontend/src/components/admin/events/UserOverviewModal.svelte
+++ b/frontend/src/components/admin/events/UserOverviewModal.svelte
@@ -1,16 +1,10 @@
diff --git a/frontend/src/lib/__tests__/api-interceptors.test.ts b/frontend/src/lib/__tests__/api-interceptors.test.ts
new file mode 100644
index 00000000..4870dd9d
--- /dev/null
+++ b/frontend/src/lib/__tests__/api-interceptors.test.ts
@@ -0,0 +1,66 @@
+import { describe, it, expect } from 'vitest';
+import { getErrorMessage, unwrap, unwrapOr } from '../api-interceptors';
+
+describe('getErrorMessage', () => {
+ it.each([
+ ['null', null, undefined, 'An error occurred'],
+ ['undefined', undefined, undefined, 'An error occurred'],
+ ['zero', 0, undefined, 'An error occurred'],
+ ['empty string', '', undefined, 'An error occurred'],
+ ['null with custom fallback', null, 'custom', 'custom'],
+ ['number', 42, undefined, 'An error occurred'],
+ ['boolean', true, undefined, 'An error occurred'],
+ ['object without detail/message', { foo: 'bar' }, undefined, 'An error occurred'],
+ ] as [string, unknown, string | undefined, string][])('returns fallback for %s', (_label, input, fallback, expected) => {
+ expect(getErrorMessage(input, fallback)).toBe(expected);
+ });
+
+ it.each([
+ ['string error', 'something broke', 'something broke'],
+ ['Error instance', new Error('boom'), 'boom'],
+ ['object with .detail string', { detail: 'Not found' }, 'Not found'],
+ ['object with .message string', { message: 'Oops' }, 'Oops'],
+ ['object with both (detail wins)', { detail: 'detail wins', message: 'msg' }, 'detail wins'],
+ ['ValidationError[] with locs', {
+ detail: [
+ { loc: ['body', 'email'], msg: 'invalid email', type: 'value_error' },
+ { loc: ['body', 'name'], msg: 'required', type: 'value_error' },
+ ],
+ }, 'email: invalid email, name: required'],
+ ['ValidationError[] with empty loc', {
+ detail: [{ loc: [], msg: 'bad', type: 'value_error' }],
+ }, 'field: bad'],
+ ] as [string, unknown, string][])('extracts message from %s', (_label, input, expected) => {
+ expect(getErrorMessage(input)).toBe(expected);
+ });
+});
+
+describe('unwrap', () => {
+ it.each([
+ ['number', { data: 42 }, 42],
+ ['zero', { data: 0 }, 0],
+ ['empty string', { data: '' }, ''],
+ ['false', { data: false }, false],
+ ] as [string, { data: unknown }, unknown][])('returns data for %s', (_label, result, expected) => {
+ expect(unwrap(result)).toBe(expected);
+ });
+
+ it.each([
+ ['error present', { data: 42, error: new Error('fail') }],
+ ['data undefined', {}],
+ ] as [string, { data?: unknown; error?: unknown }][])('throws when %s', (_label, result) => {
+ expect(() => unwrap(result)).toThrow();
+ });
+});
+
+describe('unwrapOr', () => {
+ it.each([
+ ['data present', { data: 'value' }, 'fb', 'value'],
+ ['data is 0', { data: 0 }, 99, 0],
+ ['data is empty string', { data: '' }, 'fb', ''],
+ ['error present', { data: 'v', error: new Error() }, 'fb', 'fb'],
+ ['data undefined', {}, 'fb', 'fb'],
+ ] as [string, { data?: unknown; error?: unknown }, unknown, unknown][])('returns correct value when %s', (_label, result, fallback, expected) => {
+ expect(unwrapOr(result, fallback)).toBe(expected);
+ });
+});
diff --git a/frontend/src/lib/__tests__/formatters.test.ts b/frontend/src/lib/__tests__/formatters.test.ts
new file mode 100644
index 00000000..6f289317
--- /dev/null
+++ b/frontend/src/lib/__tests__/formatters.test.ts
@@ -0,0 +1,182 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import {
+ formatDate,
+ formatTimestamp,
+ formatDuration,
+ formatDurationBetween,
+ formatRelativeTime,
+ formatBytes,
+ formatNumber,
+ truncate,
+} from '../formatters';
+
+describe('formatDate', () => {
+ it.each([
+ [null, 'N/A'],
+ [undefined, 'N/A'],
+ ['', 'N/A'],
+ ['not-a-date', 'N/A'],
+ ] as const)('returns %j for %j input', (input, expected) => {
+ expect(formatDate(input as string | null | undefined)).toBe(expected);
+ });
+
+ it('formats ISO string to DD/MM/YYYY HH:mm', () => {
+ const date = new Date(2025, 0, 15, 14, 30);
+ expect(formatDate(date.toISOString())).toBe('15/01/2025 14:30');
+ });
+
+ it('accepts Date object', () => {
+ expect(formatDate(new Date(2025, 5, 1, 9, 5))).toBe('01/06/2025 09:05');
+ });
+});
+
+describe('formatTimestamp', () => {
+ it.each([null, undefined, 'garbage'] as const)('returns N/A for %j', (input) => {
+ expect(formatTimestamp(input as string | null | undefined)).toBe('N/A');
+ });
+
+ it('formats ISO string', () => {
+ const date = new Date(2025, 0, 15);
+ expect(formatTimestamp(date.toISOString())).toBe(date.toLocaleString());
+ });
+
+ it('accepts custom Intl options', () => {
+ const date = new Date(2025, 0, 15);
+ const opts: Intl.DateTimeFormatOptions = { year: 'numeric' };
+ const expected = new Intl.DateTimeFormat(undefined, opts).format(date);
+ expect(formatTimestamp(date.toISOString(), opts)).toBe(expected);
+ });
+
+ it('accepts Date object', () => {
+ const date = new Date(2025, 0, 15);
+ expect(formatTimestamp(date)).toBe(date.toLocaleString());
+ });
+});
+
+describe('formatDuration', () => {
+ it.each([
+ [0, '0ms'],
+ [0.5, '500ms'],
+ [30, '30.0s'],
+ [90, '1m 30s'],
+ [120, '2m'],
+ [3660, '1h 1m'],
+ [3600, '1h'],
+ ])('formats %d seconds as %j', (input, expected) => {
+ expect(formatDuration(input)).toBe(expected);
+ });
+
+ it.each([null, undefined, -5])('returns N/A for %j', (input) => {
+ expect(formatDuration(input as number | null | undefined)).toBe('N/A');
+ });
+});
+
+describe('formatDurationBetween', () => {
+ it('computes duration between two timestamps', () => {
+ const start = new Date(2025, 0, 1, 12, 0, 0);
+ const end = new Date(2025, 0, 1, 12, 1, 30);
+ expect(formatDurationBetween(start, end)).toBe('1m 30s');
+ });
+
+ it.each([
+ [null, '2025-01-01T12:00:00Z'],
+ ['2025-01-01T12:00:00Z', null],
+ ['garbage', '2025-01-01T12:00:00Z'],
+ ] as const)('returns N/A for (%j, %j)', (start, end) => {
+ expect(formatDurationBetween(
+ start as string | null | undefined,
+ end as string | null | undefined,
+ )).toBe('N/A');
+ });
+
+ it('returns N/A when end is before start', () => {
+ const start = new Date(2025, 0, 1, 13, 0, 0);
+ const end = new Date(2025, 0, 1, 12, 0, 0);
+ expect(formatDurationBetween(start, end)).toBe('N/A');
+ });
+});
+
+describe('formatRelativeTime', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date(2025, 6, 15, 12, 0, 0));
+ });
+ afterEach(() => vi.useRealTimers());
+
+ it.each([
+ [new Date(2025, 6, 15, 11, 59, 30), 'just now'], // 30s ago
+ [new Date(2025, 6, 15, 11, 55, 0), '5m ago'], // 5m ago
+ [new Date(2025, 6, 15, 9, 0, 0), '3h ago'], // 3h ago
+ [new Date(2025, 6, 13, 12, 0, 0), '2d ago'], // 2d ago
+ ])('formats %j as %j', (date, expected) => {
+ expect(formatRelativeTime(date.toISOString())).toBe(expected);
+ });
+
+ it('returns locale date for >7 days ago', () => {
+ const result = formatRelativeTime(new Date(2025, 6, 1).toISOString());
+ expect(result).not.toMatch(/ago$/);
+ expect(result).not.toBe('N/A');
+ });
+
+ it('returns locale date for future dates', () => {
+ const result = formatRelativeTime(new Date(2025, 6, 20).toISOString());
+ expect(result).not.toMatch(/ago$/);
+ });
+
+ it.each([null, 'not-valid'])('returns N/A for %j', (input) => {
+ expect(formatRelativeTime(input as string | null | undefined)).toBe('N/A');
+ });
+});
+
+describe('formatBytes', () => {
+ it.each([
+ [0, undefined, '0 B'],
+ [500, undefined, '500 B'],
+ [1536, undefined, '1.5 KB'],
+ [1048576, undefined, '1 MB'],
+ [1536, 0, '2 KB'],
+ ] as const)('formats %d bytes (decimals=%j) as %j', (bytes, decimals, expected) => {
+ expect(formatBytes(bytes, decimals)).toBe(expected);
+ });
+
+ it.each([null, undefined, -1])('returns N/A for %j', (input) => {
+ expect(formatBytes(input as number | null | undefined)).toBe('N/A');
+ });
+});
+
+describe('formatNumber', () => {
+ it('formats zero', () => {
+ const expected = new Intl.NumberFormat().format(0);
+ expect(formatNumber(0)).toBe(expected);
+ });
+
+ it('formats with locale separators', () => {
+ const expected = new Intl.NumberFormat().format(1234567);
+ expect(formatNumber(1234567)).toBe(expected);
+ });
+
+ it.each([null, undefined])('returns N/A for %j', (input) => {
+ expect(formatNumber(input as number | null | undefined)).toBe('N/A');
+ });
+});
+
+describe('truncate', () => {
+ it.each([
+ ['hello', 50, 'hello'],
+ ['12345', 5, '12345'],
+ [null, 50, ''],
+ ['', 50, ''],
+ ] as const)('truncate(%j, %d) → %j', (str, max, expected) => {
+ expect(truncate(str as string | null | undefined, max)).toBe(expected);
+ });
+
+ it('truncates long string with ellipsis', () => {
+ const result = truncate('a'.repeat(60), 50);
+ expect(result).toHaveLength(50);
+ expect(result).toMatch(/\.\.\.$/);
+ });
+
+ it('respects custom maxLength', () => {
+ expect(truncate('abcdefghij', 7)).toBe('abcd...');
+ });
+});
diff --git a/frontend/src/lib/admin/__tests__/autoRefresh.test.ts b/frontend/src/lib/admin/__tests__/autoRefresh.test.ts
new file mode 100644
index 00000000..a2e608c0
--- /dev/null
+++ b/frontend/src/lib/admin/__tests__/autoRefresh.test.ts
@@ -0,0 +1,140 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { effect_root } from 'svelte/internal/client';
+
+// Mock onDestroy — no component lifecycle in unit tests
+vi.mock('svelte', async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, onDestroy: vi.fn() };
+});
+
+const { createAutoRefresh } = await import('../autoRefresh.svelte');
+
+describe('createAutoRefresh', () => {
+ beforeEach(() => vi.useFakeTimers());
+ afterEach(() => vi.useRealTimers());
+
+ function make(overrides: Record = {}) {
+ const onRefresh = vi.fn();
+ let ar!: ReturnType;
+ const teardown = effect_root(() => {
+ ar = createAutoRefresh({
+ onRefresh,
+ autoCleanup: false,
+ initialEnabled: false,
+ ...overrides,
+ });
+ });
+ return { ar, onRefresh, teardown };
+ }
+
+ describe('initial state', () => {
+ it.each([
+ [{ initialEnabled: true }, true],
+ [{ initialEnabled: false }, false],
+ ])('enabled=%j → %s', (opts, expected) => {
+ const { ar, teardown } = make(opts);
+ expect(ar.enabled).toBe(expected);
+ ar.cleanup();
+ teardown();
+ });
+
+ it('defaults rate to 5', () => {
+ const { ar, teardown } = make();
+ expect(ar.rate).toBe(5);
+ ar.cleanup();
+ teardown();
+ });
+
+ it('exposes default rate options', () => {
+ const { ar, teardown } = make();
+ expect(ar.rateOptions).toEqual([
+ { value: 5, label: '5 seconds' },
+ { value: 10, label: '10 seconds' },
+ { value: 30, label: '30 seconds' },
+ { value: 60, label: '1 minute' },
+ ]);
+ ar.cleanup();
+ teardown();
+ });
+
+ it('accepts custom rate options', () => {
+ const custom = [{ value: 15, label: '15s' }];
+ const { ar, teardown } = make({ rateOptions: custom });
+ expect(ar.rateOptions).toEqual(custom);
+ ar.cleanup();
+ teardown();
+ });
+ });
+
+ describe('start / stop / cleanup', () => {
+ it('start fires onRefresh at the configured rate', () => {
+ const { ar, onRefresh, teardown } = make({ initialRate: 2 });
+ ar.enabled = true;
+ ar.start();
+
+ vi.advanceTimersByTime(2000);
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+
+ vi.advanceTimersByTime(2000);
+ expect(onRefresh).toHaveBeenCalledTimes(2);
+
+ ar.cleanup();
+ teardown();
+ });
+
+ it('does not fire when disabled', () => {
+ const { ar, onRefresh, teardown } = make({ initialEnabled: false });
+ ar.start();
+
+ vi.advanceTimersByTime(10_000);
+ expect(onRefresh).not.toHaveBeenCalled();
+
+ ar.cleanup();
+ teardown();
+ });
+
+ it('stop halts the interval', () => {
+ const { ar, onRefresh, teardown } = make({ initialRate: 1 });
+ ar.enabled = true;
+ ar.start();
+
+ vi.advanceTimersByTime(1000);
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+
+ ar.stop();
+ vi.advanceTimersByTime(5000);
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+
+ teardown();
+ });
+
+ it('restart restarts the interval', () => {
+ const { ar, onRefresh, teardown } = make({ initialRate: 1 });
+ ar.enabled = true;
+ ar.start();
+
+ vi.advanceTimersByTime(500);
+ ar.restart(); // resets timer
+ vi.advanceTimersByTime(500);
+ expect(onRefresh).not.toHaveBeenCalled(); // only 500ms since restart
+
+ vi.advanceTimersByTime(500);
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+
+ ar.cleanup();
+ teardown();
+ });
+
+ it('cleanup stops the interval', () => {
+ const { ar, onRefresh, teardown } = make({ initialRate: 1 });
+ ar.enabled = true;
+ ar.start();
+
+ ar.cleanup();
+ vi.advanceTimersByTime(5000);
+ expect(onRefresh).not.toHaveBeenCalled();
+
+ teardown();
+ });
+ });
+});
diff --git a/frontend/src/lib/admin/__tests__/pagination.test.ts b/frontend/src/lib/admin/__tests__/pagination.test.ts
new file mode 100644
index 00000000..a2894a68
--- /dev/null
+++ b/frontend/src/lib/admin/__tests__/pagination.test.ts
@@ -0,0 +1,96 @@
+import { describe, it, expect, vi } from 'vitest';
+
+const { createPaginationState } = await import('../pagination.svelte');
+
+describe('createPaginationState', () => {
+ describe('defaults', () => {
+ it('initialises with default values', () => {
+ const p = createPaginationState();
+ expect(p.currentPage).toBe(1);
+ expect(p.pageSize).toBe(10);
+ expect(p.totalItems).toBe(0);
+ expect(p.totalPages).toBe(1);
+ expect(p.skip).toBe(0);
+ });
+ });
+
+ describe('custom options', () => {
+ it.each([
+ [{ initialPage: 3, initialPageSize: 20 }, 3, 20],
+ [{ initialPage: 1, initialPageSize: 5 }, 1, 5],
+ ])('createPaginationState(%j) → page=%d, size=%d', (opts, page, size) => {
+ const p = createPaginationState(opts);
+ expect(p.currentPage).toBe(page);
+ expect(p.pageSize).toBe(size);
+ });
+ });
+
+ describe('totalPages', () => {
+ it.each([
+ [0, 10, 1], // empty → 1 page
+ [5, 10, 1], // fewer than page size
+ [10, 10, 1], // exactly one page
+ [11, 10, 2], // one extra item
+ [100, 20, 5], // even split
+ [101, 20, 6], // one extra
+ ])('totalItems=%d, pageSize=%d → totalPages=%d', (items, size, expected) => {
+ const p = createPaginationState({ initialPageSize: size });
+ p.totalItems = items;
+ expect(p.totalPages).toBe(expected);
+ });
+ });
+
+ describe('skip', () => {
+ it.each([
+ [1, 10, 0],
+ [2, 10, 10],
+ [3, 20, 40],
+ [5, 5, 20],
+ ])('page=%d, size=%d → skip=%d', (page, size, expected) => {
+ const p = createPaginationState({ initialPage: page, initialPageSize: size });
+ expect(p.skip).toBe(expected);
+ });
+ });
+
+ describe('handlePageChange', () => {
+ it('updates currentPage and calls onLoad', () => {
+ const p = createPaginationState();
+ const onLoad = vi.fn();
+ p.handlePageChange(5, onLoad);
+ expect(p.currentPage).toBe(5);
+ expect(onLoad).toHaveBeenCalledOnce();
+ });
+
+ it('works without onLoad callback', () => {
+ const p = createPaginationState();
+ p.handlePageChange(3);
+ expect(p.currentPage).toBe(3);
+ });
+ });
+
+ describe('handlePageSizeChange', () => {
+ it('updates pageSize, resets to page 1, and calls onLoad', () => {
+ const p = createPaginationState({ initialPage: 5 });
+ const onLoad = vi.fn();
+ p.handlePageSizeChange(50, onLoad);
+ expect(p.pageSize).toBe(50);
+ expect(p.currentPage).toBe(1);
+ expect(onLoad).toHaveBeenCalledOnce();
+ });
+ });
+
+ describe('reset', () => {
+ it('restores initial values', () => {
+ const p = createPaginationState({ initialPage: 2, initialPageSize: 25 });
+ p.currentPage = 10;
+ p.pageSize = 100;
+ p.totalItems = 500;
+
+ p.reset();
+
+ expect(p.currentPage).toBe(2);
+ expect(p.pageSize).toBe(25);
+ expect(p.totalItems).toBe(0);
+ });
+ });
+});
diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts
index 1f20e0cf..fbbd685c 100644
--- a/frontend/src/lib/api/index.ts
+++ b/frontend/src/lib/api/index.ts
@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts
export { aggregateEventsApiV1EventsAggregatePost, browseEventsApiV1AdminEventsBrowsePost, cancelExecutionApiV1ExecutionsExecutionIdCancelPost, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPost, cancelSagaApiV1SagasSagaIdCancelPost, cleanupOldSessionsApiV1ReplayCleanupPost, createExecutionApiV1ExecutePost, createReplaySessionApiV1ReplaySessionsPost, createSavedScriptApiV1ScriptsPost, createUserApiV1AdminUsersPost, deleteEventApiV1AdminEventsEventIdDelete, deleteEventApiV1EventsEventIdDelete, deleteExecutionApiV1ExecutionsExecutionIdDelete, deleteNotificationApiV1NotificationsNotificationIdDelete, deleteSavedScriptApiV1ScriptsScriptIdDelete, deleteUserApiV1AdminUsersUserIdDelete, discardDlqMessageApiV1DlqMessagesEventIdDelete, executionEventsApiV1EventsExecutionsExecutionIdGet, exportEventsCsvApiV1AdminEventsExportCsvGet, exportEventsJsonApiV1AdminEventsExportJsonGet, getCurrentRequestEventsApiV1EventsCurrentRequestGet, getCurrentUserProfileApiV1AuthMeGet, getDlqMessageApiV1DlqMessagesEventIdGet, getDlqMessagesApiV1DlqMessagesGet, getDlqStatisticsApiV1DlqStatsGet, getDlqTopicsApiV1DlqTopicsGet, getEventApiV1EventsEventIdGet, getEventDetailApiV1AdminEventsEventIdGet, getEventsByCorrelationApiV1EventsCorrelationCorrelationIdGet, getEventStatisticsApiV1EventsStatisticsGet, getEventStatsApiV1AdminEventsStatsGet, getExampleScriptsApiV1ExampleScriptsGet, getExecutionEventsApiV1EventsExecutionsExecutionIdEventsGet, getExecutionEventsApiV1ExecutionsExecutionIdEventsGet, getExecutionSagasApiV1SagasExecutionExecutionIdGet, getK8sResourceLimitsApiV1K8sLimitsGet, getNotificationsApiV1NotificationsGet, getReplaySessionApiV1ReplaySessionsSessionIdGet, getReplayStatusApiV1AdminEventsReplaySessionIdStatusGet, getResultApiV1ExecutionsExecutionIdResultGet, getSagaStatusApiV1SagasSagaIdGet, getSavedScriptApiV1ScriptsScriptIdGet, getSettingsHistoryApiV1UserSettingsHistoryGet, getSubscriptionsApiV1NotificationsSubscriptionsGet, getSystemSettingsApiV1AdminSettingsGet, getUnreadCountApiV1NotificationsUnreadCountGet, getUserApiV1AdminUsersUserIdGet, getUserEventsApiV1EventsUserGet, getUserExecutionsApiV1UserExecutionsGet, getUserOverviewApiV1AdminUsersUserIdOverviewGet, getUserRateLimitsApiV1AdminUsersUserIdRateLimitsGet, getUserSettingsApiV1UserSettingsGet, listEventTypesApiV1EventsTypesListGet, listReplaySessionsApiV1ReplaySessionsGet, listSagasApiV1SagasGet, listSavedScriptsApiV1ScriptsGet, listUsersApiV1AdminUsersGet, livenessApiV1HealthLiveGet, loginApiV1AuthLoginPost, logoutApiV1AuthLogoutPost, markAllReadApiV1NotificationsMarkAllReadPost, markNotificationReadApiV1NotificationsNotificationIdReadPut, notificationStreamApiV1EventsNotificationsStreamGet, type Options, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePost, publishCustomEventApiV1EventsPublishPost, queryEventsApiV1EventsQueryPost, readinessApiV1HealthReadyGet, receiveGrafanaAlertsApiV1AlertsGrafanaPost, registerApiV1AuthRegisterPost, replayAggregateEventsApiV1EventsReplayAggregateIdPost, replayEventsApiV1AdminEventsReplayPost, resetSystemSettingsApiV1AdminSettingsResetPost, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPost, resetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPost, restoreSettingsApiV1UserSettingsRestorePost, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePost, retryDlqMessagesApiV1DlqRetryPost, retryExecutionApiV1ExecutionsExecutionIdRetryPost, setRetryPolicyApiV1DlqRetryPolicyPost, sseHealthApiV1EventsHealthGet, startReplaySessionApiV1ReplaySessionsSessionIdStartPost, testGrafanaAlertEndpointApiV1AlertsGrafanaTestGet, updateCustomSettingApiV1UserSettingsCustomKeyPut, updateEditorSettingsApiV1UserSettingsEditorPut, updateNotificationSettingsApiV1UserSettingsNotificationsPut, updateSavedScriptApiV1ScriptsScriptIdPut, updateSubscriptionApiV1NotificationsSubscriptionsChannelPut, updateSystemSettingsApiV1AdminSettingsPut, updateThemeApiV1UserSettingsThemePut, updateUserApiV1AdminUsersUserIdPut, updateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPut, updateUserSettingsApiV1UserSettingsPut, verifyTokenApiV1AuthVerifyTokenGet } from './sdk.gen';
-export type { AdminUserOverview, AgeStatistics, AggregateEventsApiV1EventsAggregatePostData, AggregateEventsApiV1EventsAggregatePostError, AggregateEventsApiV1EventsAggregatePostErrors, AggregateEventsApiV1EventsAggregatePostResponse, AggregateEventsApiV1EventsAggregatePostResponses, AlertResponse, AllocateResourcesCommandEvent, AuthFailedEvent, BodyLoginApiV1AuthLoginPost, BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostError, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponse, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelExecutionRequest, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelResponse, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostError, CancelSagaApiV1SagasSagaIdCancelPostErrors, CancelSagaApiV1SagasSagaIdCancelPostResponse, CancelSagaApiV1SagasSagaIdCancelPostResponses, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostError, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponse, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CleanupResponse, ClientOptions, ContainerStatusInfo, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostError, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponse, CreateExecutionApiV1ExecutePostResponses, CreatePodCommandEvent, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostError, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponse, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostError, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponse, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostError, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponse, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteError, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponse, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteEventApiV1EventsEventIdDeleteData, DeleteEventApiV1EventsEventIdDeleteError, DeleteEventApiV1EventsEventIdDeleteErrors, DeleteEventApiV1EventsEventIdDeleteResponse, DeleteEventApiV1EventsEventIdDeleteResponses, DeleteEventResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteError, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteNotificationResponse, DeletePodCommandEvent, DeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteError, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponse, DeleteUserApiV1AdminUsersUserIdDeleteResponses, DeleteUserResponse, DerivedCounts, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteData, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteError, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteErrors, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponse, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponses, DlqBatchRetryResponse, DlqMessageDetail, DlqMessageDiscardedEvent, DlqMessageReceivedEvent, DlqMessageResponse, DlqMessageRetriedEvent, DlqMessagesResponse, DlqMessageStatus, DlqRetryResult, DlqStats, DlqTopicSummaryResponse, EditorSettings, EndpointGroup, EndpointUsageStats, Environment, EventAggregationRequest, EventBrowseRequest, EventBrowseResponse, EventDeleteResponse, EventDetailResponse, EventFilter, EventFilterRequest, EventListResponse, EventMetadata, EventReplayRequest, EventReplayResponse, EventReplayStatusResponse, EventReplayStatusResponseWritable, EventStatistics, EventStatsResponse, EventSummary, EventType, EventTypeCountSchema, EventTypeStatistic, ExampleScripts, ExecutionAcceptedEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionErrorType, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetError, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExecutionFailedEvent, ExecutionLimitsSchema, ExecutionListResponse, ExecutionQueuedEvent, ExecutionRequest, ExecutionRequestedEvent, ExecutionResponse, ExecutionResult, ExecutionRunningEvent, ExecutionStartedEvent, ExecutionStatus, ExecutionTimeoutEvent, ExportEventsCsvApiV1AdminEventsExportCsvGetData, ExportEventsCsvApiV1AdminEventsExportCsvGetError, ExportEventsCsvApiV1AdminEventsExportCsvGetErrors, ExportEventsCsvApiV1AdminEventsExportCsvGetResponses, ExportEventsJsonApiV1AdminEventsExportJsonGetData, ExportEventsJsonApiV1AdminEventsExportJsonGetError, ExportEventsJsonApiV1AdminEventsExportJsonGetErrors, ExportEventsJsonApiV1AdminEventsExportJsonGetResponses, GetCurrentRequestEventsApiV1EventsCurrentRequestGetData, GetCurrentRequestEventsApiV1EventsCurrentRequestGetError, GetCurrentRequestEventsApiV1EventsCurrentRequestGetErrors, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponse, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponses, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetResponse, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDlqMessageApiV1DlqMessagesEventIdGetData, GetDlqMessageApiV1DlqMessagesEventIdGetError, GetDlqMessageApiV1DlqMessagesEventIdGetErrors, GetDlqMessageApiV1DlqMessagesEventIdGetResponse, GetDlqMessageApiV1DlqMessagesEventIdGetResponses, GetDlqMessagesApiV1DlqMessagesGetData, GetDlqMessagesApiV1DlqMessagesGetError, GetDlqMessagesApiV1DlqMessagesGetErrors, GetDlqMessagesApiV1DlqMessagesGetResponse, GetDlqMessagesApiV1DlqMessagesGetResponses, GetDlqStatisticsApiV1DlqStatsGetData, GetDlqStatisticsApiV1DlqStatsGetResponse, GetDlqStatisticsApiV1DlqStatsGetResponses, GetDlqTopicsApiV1DlqTopicsGetData, GetDlqTopicsApiV1DlqTopicsGetResponse, GetDlqTopicsApiV1DlqTopicsGetResponses, GetEventApiV1EventsEventIdGetData, GetEventApiV1EventsEventIdGetError, GetEventApiV1EventsEventIdGetErrors, GetEventApiV1EventsEventIdGetResponse, GetEventApiV1EventsEventIdGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetError, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponse, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetData, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetError, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetErrors, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponse, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponses, GetEventStatisticsApiV1EventsStatisticsGetData, GetEventStatisticsApiV1EventsStatisticsGetError, GetEventStatisticsApiV1EventsStatisticsGetErrors, GetEventStatisticsApiV1EventsStatisticsGetResponse, GetEventStatisticsApiV1EventsStatisticsGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetError, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponse, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetResponse, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetError, GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetResponse, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetError, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponse, GetNotificationsApiV1NotificationsGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetError, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetError, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetError, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponse, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetError, GetSagaStatusApiV1SagasSagaIdGetErrors, GetSagaStatusApiV1SagasSagaIdGetResponse, GetSagaStatusApiV1SagasSagaIdGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetError, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponse, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetError, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetResponse, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetResponse, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetError, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponse, GetUserApiV1AdminUsersUserIdGetResponses, GetUserEventsApiV1EventsUserGetData, GetUserEventsApiV1EventsUserGetError, GetUserEventsApiV1EventsUserGetErrors, GetUserEventsApiV1EventsUserGetResponse, GetUserEventsApiV1EventsUserGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetError, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponse, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetData, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetError, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetErrors, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponse, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponses, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetResponse, GetUserSettingsApiV1UserSettingsGetResponses, GrafanaAlertItem, GrafanaWebhook, HourlyEventCountSchema, HttpValidationError, KafkaTopic, LanguageInfo, ListEventTypesApiV1EventsTypesListGetData, ListEventTypesApiV1EventsTypesListGetResponse, ListEventTypesApiV1EventsTypesListGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetError, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponse, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetError, ListSagasApiV1SagasGetErrors, ListSagasApiV1SagasGetResponse, ListSagasApiV1SagasGetResponses, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetResponse, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetError, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponse, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetResponse, LivenessApiV1HealthLiveGetResponses, LivenessResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponse, LoginApiV1AuthLoginPostResponses, LoginMethod, LoginResponse, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponse, LogoutApiV1AuthLogoutPostResponses, ManualRetryRequest, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostResponse, MarkAllReadApiV1NotificationsMarkAllReadPostResponses, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, MessageResponse, MonitoringSettingsSchema, NotificationAllReadEvent, NotificationChannel, NotificationClickedEvent, NotificationCreatedEvent, NotificationDeliveredEvent, NotificationFailedEvent, NotificationListResponse, NotificationPreferencesUpdatedEvent, NotificationReadEvent, NotificationResponse, NotificationSentEvent, NotificationSettings, NotificationSeverity, NotificationStatus, NotificationStreamApiV1EventsNotificationsStreamGetData, NotificationStreamApiV1EventsNotificationsStreamGetResponse, NotificationStreamApiV1EventsNotificationsStreamGetResponses, NotificationSubscription, PasswordResetRequest, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, PodCreatedEvent, PodDeletedEvent, PodFailedEvent, PodRunningEvent, PodScheduledEvent, PodSucceededEvent, PodTerminatedEvent, PublishCustomEventApiV1EventsPublishPostData, PublishCustomEventApiV1EventsPublishPostError, PublishCustomEventApiV1EventsPublishPostErrors, PublishCustomEventApiV1EventsPublishPostResponse, PublishCustomEventApiV1EventsPublishPostResponses, PublishEventRequest, PublishEventResponse, QueryEventsApiV1EventsQueryPostData, QueryEventsApiV1EventsQueryPostError, QueryEventsApiV1EventsQueryPostErrors, QueryEventsApiV1EventsQueryPostResponse, QueryEventsApiV1EventsQueryPostResponses, QuotaExceededEvent, RateLimitAlgorithm, RateLimitExceededEvent, RateLimitRuleRequest, RateLimitRuleResponse, RateLimitSummary, RateLimitUpdateRequest, RateLimitUpdateResponse, ReadinessApiV1HealthReadyGetData, ReadinessApiV1HealthReadyGetResponse, ReadinessApiV1HealthReadyGetResponses, ReadinessResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostData, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostError, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostErrors, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponse, RegisterApiV1AuthRegisterPostResponses, ReleaseResourcesCommandEvent, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostData, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostError, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostErrors, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponse, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponses, ReplayAggregateResponse, ReplayConfigSchema, ReplayError, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostError, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponse, ReplayEventsApiV1AdminEventsReplayPostResponses, ReplayFilter, ReplayFilterSchema, ReplayRequest, ReplayResponse, ReplaySession, ReplayStatus, ReplayTarget, ReplayType, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostResponse, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostData, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostError, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostErrors, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponse, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponses, ResourceLimitExceededEvent, ResourceLimits, ResourceUsage, ResourceUsageDomain, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostError, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponse, RestoreSettingsApiV1UserSettingsRestorePostResponses, RestoreSettingsRequest, ResultFailedEvent, ResultStoredEvent, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryDlqMessagesApiV1DlqRetryPostData, RetryDlqMessagesApiV1DlqRetryPostError, RetryDlqMessagesApiV1DlqRetryPostErrors, RetryDlqMessagesApiV1DlqRetryPostResponse, RetryDlqMessagesApiV1DlqRetryPostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, RetryExecutionRequest, RetryPolicyRequest, RetryStrategy, SagaCancellationResponse, SagaCancelledEvent, SagaCompensatedEvent, SagaCompensatingEvent, SagaCompletedEvent, SagaFailedEvent, SagaListResponse, SagaStartedEvent, SagaState, SagaStatusResponse, SavedScriptCreateRequest, SavedScriptResponse, SavedScriptUpdate, ScriptDeletedEvent, ScriptSavedEvent, ScriptSharedEvent, SecuritySettingsSchema, SecurityViolationEvent, ServiceEventCountSchema, ServiceRecoveredEvent, ServiceUnhealthyEvent, SessionSummary, SessionSummaryWritable, SetRetryPolicyApiV1DlqRetryPolicyPostData, SetRetryPolicyApiV1DlqRetryPolicyPostError, SetRetryPolicyApiV1DlqRetryPolicyPostErrors, SetRetryPolicyApiV1DlqRetryPolicyPostResponse, SetRetryPolicyApiV1DlqRetryPolicyPostResponses, SettingsHistoryEntry, SettingsHistoryResponse, ShutdownStatusResponse, SortOrder, SseControlEvent, SseExecutionEventData, SseHealthApiV1EventsHealthGetData, SseHealthApiV1EventsHealthGetResponse, SseHealthApiV1EventsHealthGetResponses, SseHealthResponse, SseHealthStatus, SseNotificationEvent, SseNotificationEventData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, SubscriptionsResponse, SubscriptionUpdate, SystemErrorEvent, SystemSettings, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetData, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponse, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponses, Theme, ThemeUpdateRequest, TokenValidationResponse, TopicStatistic, UnreadCountResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutError, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutError, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutError, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponse, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutError, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponse, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutError, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponse, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutData, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutError, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutErrors, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponse, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutError, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponse, UpdateUserSettingsApiV1UserSettingsPutResponses, UserCreate, UserDeletedEvent, UserEventCountSchema, UserListResponse, UserLoggedInEvent, UserLoggedOutEvent, UserLoginEvent, UserRateLimitConfigResponse, UserRateLimitsResponse, UserRegisteredEvent, UserResponse, UserRole, UserSettings, UserSettingsUpdate, UserSettingsUpdatedEvent, UserUpdate, UserUpdatedEvent, ValidationError, VerifyTokenApiV1AuthVerifyTokenGetData, VerifyTokenApiV1AuthVerifyTokenGetResponse, VerifyTokenApiV1AuthVerifyTokenGetResponses } from './types.gen';
+export type { AdminUserOverview, AgeStatistics, AggregateEventsApiV1EventsAggregatePostData, AggregateEventsApiV1EventsAggregatePostError, AggregateEventsApiV1EventsAggregatePostErrors, AggregateEventsApiV1EventsAggregatePostResponse, AggregateEventsApiV1EventsAggregatePostResponses, AlertResponse, AllocateResourcesCommandEvent, AuthFailedEvent, BodyLoginApiV1AuthLoginPost, BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostError, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponse, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelExecutionRequest, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelResponse, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostError, CancelSagaApiV1SagasSagaIdCancelPostErrors, CancelSagaApiV1SagasSagaIdCancelPostResponse, CancelSagaApiV1SagasSagaIdCancelPostResponses, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostError, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponse, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CleanupResponse, ClientOptions, ContainerStatusInfo, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostError, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponse, CreateExecutionApiV1ExecutePostResponses, CreatePodCommandEvent, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostError, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponse, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostError, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponse, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostError, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponse, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteError, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponse, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteEventApiV1EventsEventIdDeleteData, DeleteEventApiV1EventsEventIdDeleteError, DeleteEventApiV1EventsEventIdDeleteErrors, DeleteEventApiV1EventsEventIdDeleteResponse, DeleteEventApiV1EventsEventIdDeleteResponses, DeleteEventResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteError, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteNotificationResponse, DeletePodCommandEvent, DeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteError, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponse, DeleteUserApiV1AdminUsersUserIdDeleteResponses, DeleteUserResponse, DerivedCounts, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteData, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteError, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteErrors, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponse, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponses, DlqBatchRetryResponse, DlqMessageDetail, DlqMessageDiscardedEvent, DlqMessageReceivedEvent, DlqMessageResponse, DlqMessageRetriedEvent, DlqMessagesResponse, DlqMessageStatus, DlqRetryResult, DlqStats, DlqTopicSummaryResponse, EditorSettings, EndpointGroup, EndpointUsageStats, Environment, EventAggregationRequest, EventBrowseRequest, EventBrowseResponse, EventDeleteResponse, EventDetailResponse, EventFilter, EventFilterRequest, EventListResponse, EventMetadata, EventReplayRequest, EventReplayResponse, EventReplayStatusResponse, EventReplayStatusResponseWritable, EventStatistics, EventStatsResponse, EventSummary, EventType, EventTypeCountSchema, EventTypeStatistic, ExampleScripts, ExecutionAcceptedEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionErrorType, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetError, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExecutionFailedEvent, ExecutionLimitsSchema, ExecutionListResponse, ExecutionQueuedEvent, ExecutionRequest, ExecutionRequestedEvent, ExecutionResponse, ExecutionResult, ExecutionRunningEvent, ExecutionStartedEvent, ExecutionStatus, ExecutionTimeoutEvent, ExportEventsCsvApiV1AdminEventsExportCsvGetData, ExportEventsCsvApiV1AdminEventsExportCsvGetError, ExportEventsCsvApiV1AdminEventsExportCsvGetErrors, ExportEventsCsvApiV1AdminEventsExportCsvGetResponses, ExportEventsJsonApiV1AdminEventsExportJsonGetData, ExportEventsJsonApiV1AdminEventsExportJsonGetError, ExportEventsJsonApiV1AdminEventsExportJsonGetErrors, ExportEventsJsonApiV1AdminEventsExportJsonGetResponses, GetCurrentRequestEventsApiV1EventsCurrentRequestGetData, GetCurrentRequestEventsApiV1EventsCurrentRequestGetError, GetCurrentRequestEventsApiV1EventsCurrentRequestGetErrors, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponse, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponses, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetResponse, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDlqMessageApiV1DlqMessagesEventIdGetData, GetDlqMessageApiV1DlqMessagesEventIdGetError, GetDlqMessageApiV1DlqMessagesEventIdGetErrors, GetDlqMessageApiV1DlqMessagesEventIdGetResponse, GetDlqMessageApiV1DlqMessagesEventIdGetResponses, GetDlqMessagesApiV1DlqMessagesGetData, GetDlqMessagesApiV1DlqMessagesGetError, GetDlqMessagesApiV1DlqMessagesGetErrors, GetDlqMessagesApiV1DlqMessagesGetResponse, GetDlqMessagesApiV1DlqMessagesGetResponses, GetDlqStatisticsApiV1DlqStatsGetData, GetDlqStatisticsApiV1DlqStatsGetResponse, GetDlqStatisticsApiV1DlqStatsGetResponses, GetDlqTopicsApiV1DlqTopicsGetData, GetDlqTopicsApiV1DlqTopicsGetResponse, GetDlqTopicsApiV1DlqTopicsGetResponses, GetEventApiV1EventsEventIdGetData, GetEventApiV1EventsEventIdGetError, GetEventApiV1EventsEventIdGetErrors, GetEventApiV1EventsEventIdGetResponse, GetEventApiV1EventsEventIdGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetError, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponse, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetData, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetError, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetErrors, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponse, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponses, GetEventStatisticsApiV1EventsStatisticsGetData, GetEventStatisticsApiV1EventsStatisticsGetError, GetEventStatisticsApiV1EventsStatisticsGetErrors, GetEventStatisticsApiV1EventsStatisticsGetResponse, GetEventStatisticsApiV1EventsStatisticsGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetError, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponse, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetResponse, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetError, GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetResponse, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetError, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponse, GetNotificationsApiV1NotificationsGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetError, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetError, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetError, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponse, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetError, GetSagaStatusApiV1SagasSagaIdGetErrors, GetSagaStatusApiV1SagasSagaIdGetResponse, GetSagaStatusApiV1SagasSagaIdGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetError, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponse, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetError, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetResponse, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetResponse, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetError, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponse, GetUserApiV1AdminUsersUserIdGetResponses, GetUserEventsApiV1EventsUserGetData, GetUserEventsApiV1EventsUserGetError, GetUserEventsApiV1EventsUserGetErrors, GetUserEventsApiV1EventsUserGetResponse, GetUserEventsApiV1EventsUserGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetError, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponse, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetData, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetError, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetErrors, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponse, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponses, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetResponse, GetUserSettingsApiV1UserSettingsGetResponses, GrafanaAlertItem, GrafanaWebhook, HourlyEventCountSchema, HttpValidationError, KafkaTopic, LanguageInfo, ListEventTypesApiV1EventsTypesListGetData, ListEventTypesApiV1EventsTypesListGetResponse, ListEventTypesApiV1EventsTypesListGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetError, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponse, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetError, ListSagasApiV1SagasGetErrors, ListSagasApiV1SagasGetResponse, ListSagasApiV1SagasGetResponses, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetResponse, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetError, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponse, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetResponse, LivenessApiV1HealthLiveGetResponses, LivenessResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponse, LoginApiV1AuthLoginPostResponses, LoginMethod, LoginResponse, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponse, LogoutApiV1AuthLogoutPostResponses, ManualRetryRequest, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostResponse, MarkAllReadApiV1NotificationsMarkAllReadPostResponses, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, MessageResponse, MonitoringSettingsSchema, NotificationAllReadEvent, NotificationChannel, NotificationClickedEvent, NotificationCreatedEvent, NotificationDeliveredEvent, NotificationFailedEvent, NotificationListResponse, NotificationPreferencesUpdatedEvent, NotificationReadEvent, NotificationResponse, NotificationSentEvent, NotificationSettings, NotificationSeverity, NotificationStatus, NotificationStreamApiV1EventsNotificationsStreamGetData, NotificationStreamApiV1EventsNotificationsStreamGetResponse, NotificationStreamApiV1EventsNotificationsStreamGetResponses, NotificationSubscription, PasswordResetRequest, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, PodCreatedEvent, PodDeletedEvent, PodFailedEvent, PodRunningEvent, PodScheduledEvent, PodSucceededEvent, PodTerminatedEvent, PublishCustomEventApiV1EventsPublishPostData, PublishCustomEventApiV1EventsPublishPostError, PublishCustomEventApiV1EventsPublishPostErrors, PublishCustomEventApiV1EventsPublishPostResponse, PublishCustomEventApiV1EventsPublishPostResponses, PublishEventRequest, PublishEventResponse, QueryEventsApiV1EventsQueryPostData, QueryEventsApiV1EventsQueryPostError, QueryEventsApiV1EventsQueryPostErrors, QueryEventsApiV1EventsQueryPostResponse, QueryEventsApiV1EventsQueryPostResponses, QueuePriority, QuotaExceededEvent, RateLimitAlgorithm, RateLimitExceededEvent, RateLimitRuleRequest, RateLimitRuleResponse, RateLimitSummary, RateLimitUpdateRequest, RateLimitUpdateResponse, ReadinessApiV1HealthReadyGetData, ReadinessApiV1HealthReadyGetResponse, ReadinessApiV1HealthReadyGetResponses, ReadinessResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostData, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostError, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostErrors, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponse, RegisterApiV1AuthRegisterPostResponses, ReleaseResourcesCommandEvent, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostData, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostError, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostErrors, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponse, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponses, ReplayAggregateResponse, ReplayConfigSchema, ReplayError, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostError, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponse, ReplayEventsApiV1AdminEventsReplayPostResponses, ReplayFilter, ReplayFilterSchema, ReplayRequest, ReplayResponse, ReplaySession, ReplayStatus, ReplayTarget, ReplayType, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostResponse, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostData, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostError, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostErrors, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponse, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponses, ResourceLimitExceededEvent, ResourceLimits, ResourceUsage, ResourceUsageDomain, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostError, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponse, RestoreSettingsApiV1UserSettingsRestorePostResponses, RestoreSettingsRequest, ResultFailedEvent, ResultStoredEvent, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryDlqMessagesApiV1DlqRetryPostData, RetryDlqMessagesApiV1DlqRetryPostError, RetryDlqMessagesApiV1DlqRetryPostErrors, RetryDlqMessagesApiV1DlqRetryPostResponse, RetryDlqMessagesApiV1DlqRetryPostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, RetryExecutionRequest, RetryPolicyRequest, RetryStrategy, SagaCancellationResponse, SagaCancelledEvent, SagaCompensatedEvent, SagaCompensatingEvent, SagaCompletedEvent, SagaFailedEvent, SagaListResponse, SagaStartedEvent, SagaState, SagaStatusResponse, SavedScriptCreateRequest, SavedScriptResponse, SavedScriptUpdate, ScriptDeletedEvent, ScriptSavedEvent, ScriptSharedEvent, SecuritySettingsSchema, SecurityViolationEvent, ServiceEventCountSchema, ServiceRecoveredEvent, ServiceUnhealthyEvent, SessionSummary, SessionSummaryWritable, SetRetryPolicyApiV1DlqRetryPolicyPostData, SetRetryPolicyApiV1DlqRetryPolicyPostError, SetRetryPolicyApiV1DlqRetryPolicyPostErrors, SetRetryPolicyApiV1DlqRetryPolicyPostResponse, SetRetryPolicyApiV1DlqRetryPolicyPostResponses, SettingsHistoryEntry, SettingsHistoryResponse, ShutdownStatusResponse, SortOrder, SseControlEvent, SseExecutionEventData, SseHealthApiV1EventsHealthGetData, SseHealthApiV1EventsHealthGetResponse, SseHealthApiV1EventsHealthGetResponses, SseHealthResponse, SseHealthStatus, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, SubscriptionsResponse, SubscriptionUpdate, SystemErrorEvent, SystemSettings, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetData, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponse, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponses, Theme, ThemeUpdateRequest, TokenValidationResponse, TopicStatistic, UnreadCountResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutError, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutError, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutError, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponse, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutError, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponse, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutError, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponse, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutData, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutError, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutErrors, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponse, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutError, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponse, UpdateUserSettingsApiV1UserSettingsPutResponses, UserCreate, UserDeletedEvent, UserEventCountSchema, UserListResponse, UserLoggedInEvent, UserLoggedOutEvent, UserLoginEvent, UserRateLimitConfigResponse, UserRateLimitsResponse, UserRegisteredEvent, UserResponse, UserRole, UserSettings, UserSettingsUpdate, UserSettingsUpdatedEvent, UserUpdate, UserUpdatedEvent, ValidationError, VerifyTokenApiV1AuthVerifyTokenGetData, VerifyTokenApiV1AuthVerifyTokenGetResponse, VerifyTokenApiV1AuthVerifyTokenGetResponses } from './types.gen';
diff --git a/frontend/src/lib/api/types.gen.ts b/frontend/src/lib/api/types.gen.ts
index ef3c492e..a5f20f14 100644
--- a/frontend/src/lib/api/types.gen.ts
+++ b/frontend/src/lib/api/types.gen.ts
@@ -439,10 +439,7 @@ export type CreatePodCommandEvent = {
* Memory Request
*/
memory_request?: string;
- /**
- * Priority
- */
- priority?: number;
+ priority?: QueuePriority;
};
/**
@@ -2230,10 +2227,7 @@ export type ExecutionAcceptedEvent = {
* Estimated Wait Seconds
*/
estimated_wait_seconds?: number | null;
- /**
- * Priority
- */
- priority?: number;
+ priority?: QueuePriority;
};
/**
@@ -2577,10 +2571,7 @@ export type ExecutionRequestedEvent = {
* Memory Request
*/
memory_request?: string;
- /**
- * Priority
- */
- priority?: number;
+ priority?: QueuePriority;
};
/**
@@ -3878,6 +3869,13 @@ export type PublishEventResponse = {
timestamp: string;
};
+/**
+ * QueuePriority
+ *
+ * Execution priority, ordered highest to lowest.
+ */
+export type QueuePriority = 'critical' | 'high' | 'normal' | 'low' | 'background';
+
/**
* QuotaExceededEvent
*/
@@ -4938,89 +4936,6 @@ export type SseHealthResponse = {
*/
export type SseHealthStatus = 'healthy' | 'draining';
-/**
- * SSENotificationEvent
- *
- * Event types for notification SSE streams.
- */
-export type SseNotificationEvent = 'connected' | 'subscribed' | 'heartbeat' | 'notification';
-
-/**
- * SSENotificationEventData
- *
- * Typed model for SSE notification stream event payload.
- *
- * This represents the JSON data sent inside each SSE message for notification streams.
- */
-export type SseNotificationEventData = {
- /**
- * SSE notification event type
- */
- event_type: SseNotificationEvent;
- /**
- * User Id
- *
- * User ID for the notification stream
- */
- user_id?: string | null;
- /**
- * Timestamp
- *
- * Event timestamp
- */
- timestamp?: string | null;
- /**
- * Message
- *
- * Human-readable message
- */
- message?: string | null;
- /**
- * Notification Id
- *
- * Unique notification ID
- */
- notification_id?: string | null;
- /**
- * Notification severity level
- */
- severity?: NotificationSeverity | null;
- /**
- * Notification delivery status
- */
- status?: NotificationStatus | null;
- /**
- * Tags
- *
- * Notification tags
- */
- tags?: Array | null;
- /**
- * Subject
- *
- * Notification subject/title
- */
- subject?: string | null;
- /**
- * Body
- *
- * Notification body content
- */
- body?: string | null;
- /**
- * Action Url
- *
- * Optional action URL
- */
- action_url?: string | null;
- /**
- * Created At
- *
- * Creation timestamp
- */
- created_at?: string | null;
-};
-
/**
* SagaCancellationResponse
*
@@ -7912,7 +7827,7 @@ export type NotificationStreamApiV1EventsNotificationsStreamGetResponses = {
/**
* Successful Response
*/
- 200: SseNotificationEventData;
+ 200: NotificationResponse;
};
export type NotificationStreamApiV1EventsNotificationsStreamGetResponse = NotificationStreamApiV1EventsNotificationsStreamGetResponses[keyof NotificationStreamApiV1EventsNotificationsStreamGetResponses];
diff --git a/frontend/src/lib/editor/__tests__/execution.test.ts b/frontend/src/lib/editor/__tests__/execution.test.ts
new file mode 100644
index 00000000..e10d9dd3
--- /dev/null
+++ b/frontend/src/lib/editor/__tests__/execution.test.ts
@@ -0,0 +1,222 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import type { ExecutionResult } from '$lib/api';
+
+// ── Mocks ──────────────────────────────────────────────────────────────
+const mockCreateExecution = vi.fn();
+const mockGetResult = vi.fn();
+
+vi.mock('$lib/api', () => ({
+ createExecutionApiV1ExecutePost: (...a: unknown[]) => mockCreateExecution(...a),
+ getResultApiV1ExecutionsExecutionIdResultGet: (...a: unknown[]) => mockGetResult(...a),
+}));
+
+vi.mock('$lib/api-interceptors', () => ({
+ getErrorMessage: (_err: unknown, fallback: string) => fallback,
+}));
+
+const { createExecutionState } = await import('../execution.svelte');
+
+// ── Helpers ────────────────────────────────────────────────────────────
+const RESULT: ExecutionResult = {
+ execution_id: 'exec-1',
+ status: 'completed',
+ stdout: 'hello',
+ stderr: '',
+ exit_code: 0,
+ lang: 'python',
+ lang_version: '3.12',
+ execution_time: 0.1,
+ memory_used_kb: 64,
+};
+
+/** Build an SSE ReadableStream from a list of "data: …" lines. */
+function sseStream(lines: string[]): ReadableStream {
+ const text = lines.map((l) => `data: ${l}\n\n`).join('');
+ const encoder = new TextEncoder();
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(text));
+ controller.close();
+ },
+ });
+}
+
+function mockFetchSSE(lines: string[]) {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ body: sseStream(lines),
+ }),
+ );
+}
+
+// ── Tests ──────────────────────────────────────────────────────────────
+describe('createExecutionState', () => {
+ beforeEach(() => {
+ mockCreateExecution.mockReset();
+ mockGetResult.mockReset();
+ });
+
+ afterEach(() => vi.unstubAllGlobals());
+
+ describe('initial state', () => {
+ it.each([
+ ['phase', 'idle'],
+ ['result', null],
+ ['error', null],
+ ['isExecuting', false],
+ ] as const)('%s is %j', (key, expected) => {
+ const s = createExecutionState();
+ expect(s[key]).toBe(expected);
+ });
+ });
+
+ describe('execute → stream result_stored', () => {
+ it('yields result from SSE stream', async () => {
+ mockCreateExecution.mockResolvedValue({
+ data: { execution_id: 'exec-1', status: 'queued' },
+ error: null,
+ });
+
+ mockFetchSSE([
+ JSON.stringify({ event_type: 'status', status: 'running' }),
+ JSON.stringify({ event_type: 'result_stored', result: RESULT }),
+ ]);
+
+ const s = createExecutionState();
+ await s.execute('print("hi")', 'python', '3.12');
+
+ expect(s.result).toEqual(RESULT);
+ expect(s.phase).toBe('idle');
+ expect(s.error).toBeNull();
+ });
+ });
+
+ describe('execute → terminal failure falls back to fetchResult', () => {
+ it.each(['execution_failed', 'execution_timeout', 'result_failed'])(
+ 'fetches result on %s event',
+ async (eventType) => {
+ mockCreateExecution.mockResolvedValue({
+ data: { execution_id: 'exec-1', status: 'queued' },
+ error: null,
+ });
+
+ mockFetchSSE([JSON.stringify({ event_type: eventType })]);
+ mockGetResult.mockResolvedValue({ data: RESULT, error: null });
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ expect(mockGetResult).toHaveBeenCalledOnce();
+ expect(s.result).toEqual(RESULT);
+ },
+ );
+ });
+
+ describe('execute → stream ends without terminal event', () => {
+ it('falls back to fetchResult', async () => {
+ mockCreateExecution.mockResolvedValue({
+ data: { execution_id: 'exec-1', status: 'queued' },
+ error: null,
+ });
+
+ mockFetchSSE([JSON.stringify({ event_type: 'status', status: 'running' })]);
+ mockGetResult.mockResolvedValue({ data: RESULT, error: null });
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ expect(mockGetResult).toHaveBeenCalledOnce();
+ expect(s.result).toEqual(RESULT);
+ });
+ });
+
+ describe('execute → non-OK fetch falls back to fetchResult', () => {
+ it('fetches result on 500', async () => {
+ mockCreateExecution.mockResolvedValue({
+ data: { execution_id: 'exec-1', status: 'queued' },
+ error: null,
+ });
+
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500 }));
+ mockGetResult.mockResolvedValue({ data: RESULT, error: null });
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ expect(s.result).toEqual(RESULT);
+ });
+
+ it('sets error on 401', async () => {
+ mockCreateExecution.mockResolvedValue({
+ data: { execution_id: 'exec-1', status: 'queued' },
+ error: null,
+ });
+
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401 }));
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ expect(s.error).toBe('Error executing script.');
+ });
+ });
+
+ describe('execute → API error', () => {
+ it('sets error when createExecution fails', async () => {
+ mockCreateExecution.mockResolvedValue({
+ data: null,
+ error: { detail: 'rate limited' },
+ });
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ expect(s.error).toBe('Error executing script.');
+ expect(s.phase).toBe('idle');
+ });
+ });
+
+ describe('abort', () => {
+ it('resets phase to idle', () => {
+ const s = createExecutionState();
+ s.abort();
+ expect(s.phase).toBe('idle');
+ });
+ });
+
+ describe('reset', () => {
+ it('clears all state', async () => {
+ mockCreateExecution.mockResolvedValue({ data: null, error: 'fail' });
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ s.reset();
+ expect(s.phase).toBe('idle');
+ expect(s.result).toBeNull();
+ expect(s.error).toBeNull();
+ });
+ });
+
+ describe('malformed SSE', () => {
+ it('skips invalid JSON and continues', async () => {
+ mockCreateExecution.mockResolvedValue({
+ data: { execution_id: 'exec-1', status: 'queued' },
+ error: null,
+ });
+
+ mockFetchSSE([
+ '{broken',
+ JSON.stringify({ event_type: 'result_stored', result: RESULT }),
+ ]);
+
+ const s = createExecutionState();
+ await s.execute('x', 'python', '3.12');
+
+ expect(s.result).toEqual(RESULT);
+ });
+ });
+});
diff --git a/frontend/src/lib/editor/__tests__/languages.test.ts b/frontend/src/lib/editor/__tests__/languages.test.ts
new file mode 100644
index 00000000..09cf87cd
--- /dev/null
+++ b/frontend/src/lib/editor/__tests__/languages.test.ts
@@ -0,0 +1,31 @@
+import { describe, it, expect } from 'vitest';
+import { getLanguageExtension } from '$lib/editor/languages';
+
+describe('getLanguageExtension', () => {
+ it.each(['python', 'node', 'go', 'ruby', 'bash'])(
+ 'returns an extension for "%s"',
+ (lang) => {
+ const ext = getLanguageExtension(lang);
+ expect(ext).toBeDefined();
+ },
+ );
+
+ it('returns python extension for unknown language', () => {
+ const fallback = getLanguageExtension('unknown-lang');
+ const python = getLanguageExtension('python');
+ // Both should return a python() extension — compare structure
+ expect(typeof fallback).toBe(typeof python);
+ });
+
+ it('returns python extension for empty string', () => {
+ const ext = getLanguageExtension('');
+ expect(ext).toBeDefined();
+ });
+
+ it('returns distinct extensions for different languages', () => {
+ const py = getLanguageExtension('python');
+ const js = getLanguageExtension('node');
+ // Different language extensions should not be referentially equal
+ expect(py).not.toBe(js);
+ });
+});
diff --git a/frontend/src/lib/notifications/__tests__/stream.test.ts b/frontend/src/lib/notifications/__tests__/stream.test.ts
new file mode 100644
index 00000000..81e442bb
--- /dev/null
+++ b/frontend/src/lib/notifications/__tests__/stream.test.ts
@@ -0,0 +1,140 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import type { NotificationResponse } from '$lib/api';
+
+// Capture the listen callbacks so tests can invoke them directly
+let listenCallbacks: Record void> = {};
+const mockAbort = vi.fn();
+
+vi.mock('event-source-plus', () => {
+ const EventSourcePlus = function () {
+ // noop constructor
+ };
+ EventSourcePlus.prototype.listen = function (cbs: Record void>) {
+ listenCallbacks = cbs;
+ return { abort: mockAbort };
+ };
+ return { EventSourcePlus };
+});
+
+// Must import after mocking
+const { notificationStream } = await import('../stream.svelte');
+
+function validPayload(overrides: Partial = {}): NotificationResponse {
+ return {
+ notification_id: 'n-1',
+ channel: 'in_app',
+ status: 'delivered',
+ subject: 'Test',
+ body: 'Hello',
+ action_url: '/action',
+ created_at: '2025-01-01T00:00:00Z',
+ read_at: null,
+ severity: 'medium',
+ tags: ['tag1'],
+ ...overrides,
+ };
+}
+
+describe('NotificationStream', () => {
+ let callback: ReturnType;
+
+ beforeEach(() => {
+ callback = vi.fn();
+ listenCallbacks = {};
+ mockAbort.mockClear();
+ notificationStream.disconnect();
+ });
+
+ it('sets connected=true on onResponse', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onResponse!();
+ expect(notificationStream.connected).toBe(true);
+ expect(notificationStream.error).toBeNull();
+ });
+
+ it('calls callback with parsed NotificationResponse', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onMessage!({ event: 'notification', data: JSON.stringify(validPayload()) });
+
+ expect(callback).toHaveBeenCalledOnce();
+ const notification = callback.mock.calls[0][0];
+ expect(notification).toMatchObject({
+ notification_id: 'n-1',
+ channel: 'in_app',
+ status: 'delivered',
+ subject: 'Test',
+ body: 'Hello',
+ read_at: null,
+ severity: 'medium',
+ tags: ['tag1'],
+ action_url: '/action',
+ });
+ });
+
+ it('ignores messages with non-notification event type', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onMessage!({ event: 'ping', data: '{}' });
+ listenCallbacks.onMessage!({ event: '', data: '{}' });
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('handles invalid JSON without crashing', () => {
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
+ notificationStream.connect(callback);
+ listenCallbacks.onMessage!({ event: 'notification', data: '{broken' });
+ expect(callback).not.toHaveBeenCalled();
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+
+ it('sets error on request error', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onResponse!();
+ listenCallbacks.onRequestError!({ error: new Error('Network down') });
+ expect(notificationStream.connected).toBe(false);
+ expect(notificationStream.error).toBe('Network down');
+ });
+
+ it('falls back to "Connection failed" when error has no message', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onRequestError!({ error: null });
+ expect(notificationStream.error).toBe('Connection failed');
+ });
+
+ it('disconnects on 401 response error', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onResponse!();
+ listenCallbacks.onResponseError!({ response: { status: 401 } });
+ expect(notificationStream.connected).toBe(false);
+ expect(notificationStream.error).toBe('Unauthorized');
+ expect(mockAbort).toHaveBeenCalled();
+ });
+
+ it('sets connected=false on non-401 response error', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onResponse!();
+ listenCallbacks.onResponseError!({ response: { status: 500 } });
+ expect(notificationStream.connected).toBe(false);
+ expect(notificationStream.error).toBeNull();
+ });
+
+ it('disconnect aborts controller and resets state', () => {
+ notificationStream.connect(callback);
+ listenCallbacks.onResponse!();
+ notificationStream.disconnect();
+ expect(mockAbort).toHaveBeenCalled();
+ expect(notificationStream.connected).toBe(false);
+ });
+
+ it('fires browser Notification when permission is granted', () => {
+ const MockNotification = vi.fn();
+ Object.defineProperty(MockNotification, 'permission', { value: 'granted' });
+ vi.stubGlobal('Notification', MockNotification);
+
+ notificationStream.connect(callback);
+ listenCallbacks.onMessage!({ event: 'notification', data: JSON.stringify(validPayload()) });
+
+ expect(MockNotification).toHaveBeenCalledWith('Test', { body: 'Hello', icon: '/favicon.png' });
+ vi.unstubAllGlobals();
+ });
+});
diff --git a/frontend/src/lib/notifications/stream.svelte.ts b/frontend/src/lib/notifications/stream.svelte.ts
index ab642951..ed8fad4b 100644
--- a/frontend/src/lib/notifications/stream.svelte.ts
+++ b/frontend/src/lib/notifications/stream.svelte.ts
@@ -1,13 +1,8 @@
import { EventSourcePlus } from 'event-source-plus';
-import type { NotificationResponse, SseNotificationEventData } from '$lib/api';
+import type { NotificationResponse } from '$lib/api';
type NotificationCallback = (data: NotificationResponse) => void;
-// Validates SSE notification has all required fields for UI
-function isCompleteNotification(data: SseNotificationEventData): boolean {
- return !!(data.notification_id && data.subject && data.body && data.status && data.created_at);
-}
-
class NotificationStream {
#controller: ReturnType | null = null;
#onNotification: NotificationCallback | null = null;
@@ -34,18 +29,14 @@ class NotificationStream {
this.error = null;
console.log('Notification stream connected');
},
- onMessage: (event) => {
+ onMessage: (message) => {
+ if (message.event !== 'notification') return;
try {
- const data: SseNotificationEventData = JSON.parse(event.data);
-
- // Only process actual notification events with complete data
- if (data.event_type === 'notification' && isCompleteNotification(data)) {
- // SSE data matches NotificationResponse except 'channel' (unused by UI)
- this.#onNotification?.(data as unknown as NotificationResponse);
+ const data: NotificationResponse = JSON.parse(message.data);
+ this.#onNotification?.(data);
- if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
- new Notification(data.subject!, { body: data.body!, icon: '/favicon.png' });
- }
+ if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
+ new Notification(data.subject, { body: data.body, icon: '/favicon.png' });
}
} catch (err) {
console.error('Error processing notification:', err);
diff --git a/frontend/src/stores/__tests__/userSettings.test.ts b/frontend/src/stores/__tests__/userSettings.test.ts
new file mode 100644
index 00000000..05e81ce6
--- /dev/null
+++ b/frontend/src/stores/__tests__/userSettings.test.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { get } from 'svelte/store';
+import { userSettings, editorSettings, setUserSettings, clearUserSettings } from '$stores/userSettings';
+import type { UserSettings } from '$lib/api';
+
+const DEFAULTS = {
+ theme: 'auto',
+ font_size: 14,
+ tab_size: 4,
+ use_tabs: false,
+ word_wrap: true,
+ show_line_numbers: true,
+};
+
+describe('userSettings store', () => {
+ beforeEach(() => clearUserSettings());
+
+ it('starts as null', () => {
+ expect(get(userSettings)).toBeNull();
+ });
+
+ it.each([
+ ['object', { editor: { font_size: 16 } } as UserSettings],
+ ['null', null],
+ ])('setUserSettings accepts %s', (_, value) => {
+ setUserSettings(value);
+ expect(get(userSettings)).toEqual(value);
+ });
+
+ it('clearUserSettings resets to null', () => {
+ setUserSettings({ editor: { font_size: 20 } } as UserSettings);
+ clearUserSettings();
+ expect(get(userSettings)).toBeNull();
+ });
+
+ describe('editorSettings (derived)', () => {
+ it('returns defaults when userSettings is null', () => {
+ expect(get(editorSettings)).toEqual(DEFAULTS);
+ });
+
+ it.each([
+ ['partial override', { font_size: 20, tab_size: 2 }, { ...DEFAULTS, font_size: 20, tab_size: 2 }],
+ ['full override', { theme: 'dark', font_size: 18, tab_size: 8, use_tabs: true, word_wrap: false, show_line_numbers: false },
+ { theme: 'dark', font_size: 18, tab_size: 8, use_tabs: true, word_wrap: false, show_line_numbers: false }],
+ ])('merges %s with defaults', (_, editor, expected) => {
+ setUserSettings({ editor } as UserSettings);
+ expect(get(editorSettings)).toEqual(expected);
+ });
+
+ it('reverts to defaults when cleared', () => {
+ setUserSettings({ editor: { font_size: 20 } } as UserSettings);
+ clearUserSettings();
+ expect(get(editorSettings)).toEqual(DEFAULTS);
+ });
+ });
+});
diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js
new file mode 100644
index 00000000..f7bdd44c
--- /dev/null
+++ b/frontend/svelte.config.js
@@ -0,0 +1,6 @@
+export default {
+ compilerOptions: {
+ dev: true,
+ runes: true,
+ },
+};
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index e72b5e2d..0bb4a2a6 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -4,13 +4,7 @@ import { svelteTesting } from '@testing-library/svelte/vite';
export default defineConfig({
plugins: [
- svelte({
- hot: !process.env.VITEST,
- compilerOptions: {
- dev: true,
- runes: true,
- },
- }),
+ svelte({ hot: !process.env.VITEST }),
svelteTesting(),
],
test: {