diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 261aee8..32d8de5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: run: chmod +x gradlew - name: Compile and build (skip integration tests) - run: ./gradlew build -x :eventlens-kafka:test -x :eventlens-pg:test --no-daemon + run: ./gradlew build -x :eventlens-stream-kafka:test -x :eventlens-source-postgres:test --no-daemon integration-tests: runs-on: ubuntu-latest @@ -47,7 +47,7 @@ jobs: run: chmod +x gradlew - name: Run Kafka integration tests - run: ./gradlew :eventlens-kafka:test --no-daemon + run: ./gradlew :eventlens-stream-kafka:test --no-daemon - name: Run PostgreSQL integration tests - run: ./gradlew :eventlens-pg:test --no-daemon + run: ./gradlew :eventlens-source-postgres:test --no-daemon diff --git a/.gitignore b/.gitignore index c182c2f..c33bc93 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ eventlens-ui/.env.development.local .idea/ *.iml .vscode/ +.cursor/ .DS_Store Thumbs.db .cursor/ @@ -35,6 +36,9 @@ logs/ *.log *.txt +# Local scratchpad plans (not part of the repo) +plans/ + # Config with real credentials — commit *.yaml.example instead eventlens.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index daf6482..9c23fbd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,13 +20,24 @@ This compiles all modules, runs the React Vite build, and produces the fat JAR a ## Running Tests ```bash -# Unit tests only +# Unit tests + contract tests ./gradlew test -# Integration tests (requires Docker/Podman for Testcontainers) +# Full verification gate +./gradlew check + +# Integration and contract tests (requires Docker/Podman for Testcontainers) ./gradlew test --info ``` +Built-in plugins should keep passing the shared contract harness in `eventlens-plugin-test`. + +For a compact v3 release smoke pass, run: + +```bash +pwsh ./scripts/v3-release-smoke.ps1 +``` + ## Running Locally ```bash @@ -58,3 +69,10 @@ Please open a GitHub Issue with: - Java version (`java -version`) - Steps to reproduce - Expected vs. actual behaviour + +## Plugin Development + +- Start with [docs/plugin-authoring.md](C:/Java%20Developer/EventDebug/docs/plugin-authoring.md). +- Reuse the shared contract harness from ventlens-plugin-test for new source or stream plugins. +- Register plugin entry points with META-INF/services/... so discovery works from classpath and /plugins. + diff --git a/Dockerfile b/Dockerfile index 55faac3..de26d28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,9 +20,12 @@ COPY settings.gradle.kts build.gradle.kts gradlew gradlew.bat ./ COPY gradle ./gradle # Project sources +COPY eventlens-spi ./eventlens-spi COPY eventlens-core ./eventlens-core -COPY eventlens-pg ./eventlens-pg -COPY eventlens-kafka ./eventlens-kafka +COPY eventlens-source-postgres ./eventlens-source-postgres +COPY eventlens-source-mysql ./eventlens-source-mysql +COPY eventlens-stream-kafka ./eventlens-stream-kafka +COPY eventlens-plugin-test ./eventlens-plugin-test COPY eventlens-api ./eventlens-api COPY eventlens-cli ./eventlens-cli COPY eventlens-ui ./eventlens-ui diff --git a/README.md b/README.md index 3db2236..83bed9f 100644 --- a/README.md +++ b/README.md @@ -662,3 +662,19 @@ In containerized environments, direct logs to stdout/stderr and collect them via | [CHANGELOG.md](CHANGELOG.md) | Version history and release notes | | [CONTRIBUTING.md](CONTRIBUTING.md) | Build, test, and PR guidelines | | [eventlens.yaml.example](eventlens.yaml.example) | Annotated config template | + +## v3 Plugin Docs + +- [Plugin authoring guide](C:/Java%20Developer/EventDebug/docs/plugin-authoring.md) +- [v3 GA checklist](C:/Java%20Developer/EventDebug/docs/v3-ga-checklist.md) +- [SPI README](C:/Java%20Developer/EventDebug/eventlens-spi/README.md) + +## v3 Release Smoke + +Run the compact cross-phase smoke gate with: + +`ash +pwsh ./scripts/v3-release-smoke.ps1 +` + +This verifies key v3 evidence files are present and runs est + check as the release gate. diff --git a/docs/plugin-authoring.md b/docs/plugin-authoring.md new file mode 100644 index 0000000..bec6ea3 --- /dev/null +++ b/docs/plugin-authoring.md @@ -0,0 +1,223 @@ +# Plugin Authoring Guide + +EventLens v3 exposes a small SPI so external plugins can add new event sources, stream adapters, and reducers without modifying the core app. + +## Plugin Types + +### Event source plugins +Use [`EventSourcePlugin`](C:/Java%20Developer/EventDebug/eventlens-spi/src/main/java/io/eventlens/spi/EventSourcePlugin.java) when your plugin reads events from a database or event store. + +Responsibilities: +- Initialize once from config. +- Stay thread-safe after initialization. +- Never mutate the underlying store. +- Return health independently from other plugins. + +### Stream adapter plugins +Use [`StreamAdapterPlugin`](C:/Java%20Developer/EventDebug/eventlens-spi/src/main/java/io/eventlens/spi/StreamAdapterPlugin.java) when your plugin subscribes to live events from Kafka or another broker. + +Responsibilities: +- Start non-blocking background consumption in `subscribe(...)`. +- Stop cleanly in `unsubscribe()` and `close()`. +- Keep health checks lightweight. + +### Reducer plugins +Use [`ReducerPlugin`](C:/Java%20Developer/EventDebug/eventlens-spi/src/main/java/io/eventlens/spi/ReducerPlugin.java) when you want custom replay/state reconstruction behavior for a domain aggregate type. + +## Module Rules + +- Depend on `eventlens-spi`, not runtime modules like `eventlens-core` or existing source plugins. +- Register plugins with `META-INF/services/` so `ServiceLoader` can discover them. +- Prefer parent-first dependency loading assumptions: shade only when truly necessary. +- Keep constructors no-arg so built-in discovery works. +- Treat `spiVersion()` compatibility as part of your release contract. + +## Minimal Event Source Example + +```java +package com.acme.eventlens; + +import io.eventlens.spi.*; +import com.fasterxml.jackson.databind.node.NullNode; + +import java.util.Map; + +public final class AcmeEventSourcePlugin implements EventSourcePlugin { + + private AcmeReader reader; + + @Override + public String typeId() { + return "acme-db"; + } + + @Override + public String displayName() { + return "Acme Event Store"; + } + + @Override + public void initialize(String instanceId, Map config) { + reader = new AcmeReader(config); + } + + @Override + public EventQueryResult query(EventQuery query) { + return reader.query(query); + } + + @Override + public HealthStatus healthCheck() { + return reader != null ? HealthStatus.up() : HealthStatus.down("not initialized"); + } + + @Override + public com.fasterxml.jackson.databind.JsonNode configSchema() { + return NullNode.getInstance(); + } + + @Override + public void close() { + if (reader != null) { + reader.close(); + } + } +} +``` + +Service registration file: + +```text +META-INF/services/io.eventlens.spi.EventSourcePlugin +``` + +Contents: + +```text +com.acme.eventlens.AcmeEventSourcePlugin +``` + +## Config Shape + +Datasource and stream instances are configured from `eventlens.yaml`. + +Example datasource entry: + +```yaml +datasources: + - id: reporting-mysql + type: mysql + url: jdbc:mysql://localhost:3306/eventlens_reporting + username: root + password: secret + table: event_store +``` + +EventLens converts each configured block into the `Map` passed to `initialize(...)`. +Built-in JDBC sources currently receive keys such as: +- `jdbcUrl` +- `username` +- `password` +- `tableName` +- `columnOverrides` +- `pool` +- `queryTimeoutSeconds` + +If your plugin needs a schema, return it from `configSchema()` so future tooling and validation can surface it. + +## Classloading and Dependency Guidance + +- Compile against the SPI only. +- Avoid depending on app modules or implementation packages from built-in plugins. +- Keep transitive dependencies small and predictable. +- Prefer widely used drivers or clients that your plugin alone owns. +- Assume plugins may be loaded from `/plugins` with parent-first classloading. +- Do not rely on mutable global state or static caches shared across plugin instances. + +## Versioning and Compatibility + +Compatibility is checked through [`SpiVersions`](C:/Java%20Developer/EventDebug/eventlens-spi/src/main/java/io/eventlens/spi/SpiVersions.java). + +Guidelines: +- Adding a `default` method is backward-compatible. +- Adding a required interface method is a breaking change. +- Changing a method signature is a breaking change. +- Keep `typeId()` stable once released. + +## Using the Plugin Test Kit + +The shared contract harness lives in [`eventlens-plugin-test`](C:/Java%20Developer/EventDebug/eventlens-plugin-test). + +### Event source contract example + +```java +@Testcontainers(disabledWithoutDocker = true) +class MyPluginContractTest extends EventSourcePluginTestKit { + + @Override + protected EventSourcePlugin createPlugin() { + var plugin = new AcmeEventSourcePlugin(); + plugin.initialize("contract", Map.of( + "jdbcUrl", container.getJdbcUrl(), + "username", container.getUsername(), + "password", container.getPassword() + )); + return plugin; + } + + @Override + protected void seedCanonicalEvents() throws Exception { + // Insert CanonicalEventSet.defaultEvents() into your backing store. + } + + @Override + protected void cleanupStore() throws Exception { + // Truncate backing tables. + } +} +``` + +The contract kit verifies: +- health checks +- timeline ordering +- cursor pagination +- metadata-only payload behavior +- search results +- empty-state handling + +### Stream adapter contract example + +```java +@Testcontainers(disabledWithoutDocker = true) +class MyStreamContractTest extends StreamAdapterPluginTestKit { + + @Override + protected StreamAdapterPlugin createPlugin() { + var plugin = new MyStreamPlugin(); + plugin.initialize("contract", Map.of("topic", "events")); + return plugin; + } + + @Override + protected void emitCanonicalEvents() throws Exception { + // Publish at least two events for aggregate ACC-001. + } +} +``` + +## Packaging and Publishing + +- Build your plugin as a normal JAR. +- Include the ServiceLoader registration file. +- Target Java 21 to match the current EventLens runtime. +- Publish your plugin artifact independently; EventLens loads it from the classpath or `/plugins` directory. +- The SPI module is configured for Maven publishing in [`eventlens-spi/build.gradle.kts`](C:/Java%20Developer/EventDebug/eventlens-spi/build.gradle.kts). + +## Practical Checklist + +- Implement the correct SPI interface. +- Keep initialization deterministic and fail fast on invalid config. +- Return `HealthStatus.down(...)` with an actionable message. +- Add a contract test using `eventlens-plugin-test`. +- Register the plugin in `META-INF/services`. +- Document required config keys and expected dependencies. diff --git a/docs/v3-ga-checklist.md b/docs/v3-ga-checklist.md new file mode 100644 index 0000000..e161329 --- /dev/null +++ b/docs/v3-ga-checklist.md @@ -0,0 +1,46 @@ +# v3 GA Checklist + +This checklist translates the v3 release criteria from [`versions/v3.md`](C:/Java%20Developer/EventDebug/versions/v3.md) into repo-local release evidence. + +## MUST + +- [x] `eventlens-spi` exists as a dedicated module with stable SPI types. +- [x] PostgreSQL plugin contract coverage added through [`PostgresEventSourcePluginContractTest.java`](C:/Java%20Developer/EventDebug/eventlens-source-postgres/src/test/java/io/eventlens/pg/PostgresEventSourcePluginContractTest.java). +- [x] MySQL plugin contract coverage added through [`MySqlEventSourcePluginContractTest.java`](C:/Java%20Developer/EventDebug/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventSourcePluginContractTest.java). +- [x] Kafka stream contract coverage added through [`KafkaStreamAdapterPluginContractTest.java`](C:/Java%20Developer/EventDebug/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaStreamAdapterPluginContractTest.java). +- [x] v2 single-source config remains supported through migrator and validator tests. +- [x] v3 multi-source config is implemented and exercised through config tests and runtime wiring. +- [x] Existing API routes remain in place while source-aware v3 routes were added. +- [x] Optional `source` support is implemented for search and timeline flows. +- [x] UI datasource selector and plugin health page are implemented. +- [x] Plugin authoring guide is published in [`docs/plugin-authoring.md`](C:/Java%20Developer/EventDebug/docs/plugin-authoring.md). +- [~] SPI publishing is prepared via Maven publication config in [`eventlens-spi/build.gradle.kts`](C:/Java%20Developer/EventDebug/eventlens-spi/build.gradle.kts). + +## SHOULD + +- [x] External plugin JAR loading is verified with a dummy plugin artifact in [`PluginDiscoveryExternalJarTest.java`](C:/Java%20Developer/EventDebug/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginDiscoveryExternalJarTest.java). +- [x] Cache hit ratio is measured under repeated-query synthetic load in [`QueryResultCacheBenchmarkTest.java`](C:/Java%20Developer/EventDebug/eventlens-api/src/test/java/io/eventlens/api/cache/QueryResultCacheBenchmarkTest.java). +- [x] Metadata-only mode is benchmarked for response-size reduction in [`TimelineMetadataPayloadBenchmarkTest.java`](C:/Java%20Developer/EventDebug/eventlens-api/src/test/java/io/eventlens/api/routes/TimelineMetadataPayloadBenchmarkTest.java). + +## MUST NOT + +- [x] No plugin sandboxing code was introduced. +- [x] No Vault or AWS Secrets Manager integration was introduced. +- [x] No gRPC dependency was added. +- [x] No MongoDB dependency was added. +- [x] No RabbitMQ, NATS, or Pulsar dependency was added. +- [x] No scripting runtime was added. +- [x] No metadata database or Flyway layer was added. +- [x] No event store write path was introduced. + +## Verification Run + +Recommended gate before a release candidate: + +```bash +./gradlew.bat test +./gradlew.bat check +``` + +These commands validate the shared plugin contracts, built-in plugin modules, API tests, and UI build. + diff --git a/eventlens-api/build.gradle.kts b/eventlens-api/build.gradle.kts index e0135c6..d6d6b38 100644 --- a/eventlens-api/build.gradle.kts +++ b/eventlens-api/build.gradle.kts @@ -1,11 +1,16 @@ dependencies { implementation(project(":eventlens-core")) - implementation(project(":eventlens-pg")) - implementation(project(":eventlens-kafka")) + implementation(project(":eventlens-spi")) implementation("io.javalin:javalin:7.1.0") implementation("com.fasterxml.jackson.core:jackson-databind:2.21.2") implementation("ch.qos.logback:logback-classic:1.5.32") implementation("io.micrometer:micrometer-core:1.16.4") implementation("io.micrometer:micrometer-registry-prometheus:1.16.4") testImplementation("com.fasterxml.jackson.core:jackson-databind:2.21.2") + testImplementation(project(":eventlens-source-postgres")) + testImplementation(project(":eventlens-source-mysql")) + testImplementation("org.testcontainers:junit-jupiter:1.20.1") + testImplementation("org.testcontainers:postgresql:1.20.1") + testImplementation("org.testcontainers:mysql:1.20.1") } + diff --git a/eventlens-api/src/main/java/io/eventlens/api/EventLensServer.java b/eventlens-api/src/main/java/io/eventlens/api/EventLensServer.java index ce5e052..31a8f81 100644 --- a/eventlens-api/src/main/java/io/eventlens/api/EventLensServer.java +++ b/eventlens-api/src/main/java/io/eventlens/api/EventLensServer.java @@ -1,6 +1,8 @@ package io.eventlens.api; +import io.eventlens.api.cache.QueryResultCache; import io.eventlens.api.routes.*; +import io.eventlens.api.source.SourceRegistry; import io.eventlens.api.websocket.LiveTailWebSocket; import io.eventlens.api.export.ExportService; import io.eventlens.api.shutdown.GracefulShutdown; @@ -8,12 +10,14 @@ import io.eventlens.api.routes.MetricsRoutes; import io.eventlens.api.http.RequestContextMdcFilter; import io.eventlens.core.EventLensConfig; +import io.eventlens.core.aggregator.ReducerRegistry; import io.eventlens.core.RateLimiter; import io.eventlens.core.audit.AuditEvent; import io.eventlens.core.audit.AuditLogger; import io.eventlens.core.engine.*; import io.eventlens.core.exception.QueryTimeoutException; import io.eventlens.core.pii.PiiMasker; +import io.eventlens.core.plugin.PluginManager; import io.eventlens.core.spi.EventStoreReader; import io.javalin.Javalin; import io.javalin.compression.CompressionStrategy; @@ -22,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Duration; import java.util.Map; import java.util.UUID; @@ -56,10 +61,28 @@ public EventLensServer( EventLensConfig config, EventStoreReader reader, ReplayEngine replayEngine, + ReducerRegistry reducerRegistry, + PluginManager pluginManager, + String defaultSourceId, BisectEngine bisectEngine, AnomalyDetector anomalyDetector, ExportEngine exportEngine, DiffEngine diffEngine) { + this(config, reader, replayEngine, reducerRegistry, pluginManager, defaultSourceId, bisectEngine, anomalyDetector, exportEngine, diffEngine, Map.of()); + } + + public EventLensServer( + EventLensConfig config, + EventStoreReader reader, + ReplayEngine replayEngine, + ReducerRegistry reducerRegistry, + PluginManager pluginManager, + String defaultSourceId, + BisectEngine bisectEngine, + AnomalyDetector anomalyDetector, + ExportEngine exportEngine, + DiffEngine diffEngine, + Map sourceStreamBindings) { this.port = config.getServer().getPort(); this.reader = reader; @@ -77,6 +100,11 @@ public EventLensServer( // 4.1 Metrics: JVM binders + per-request instrumentation EventLensMetrics.initJvmMetrics(EventLensMetrics.registry); + var sourceRegistry = new SourceRegistry(defaultSourceId, reader, replayEngine, reducerRegistry, pluginManager); + var queryCache = new QueryResultCache( + config.getQueryCache().isEnabled(), + config.getQueryCache().getMaxEntries()); + // ── Security middleware preparation ──────────────────────────────── var rateLimitCfg = config.getServer().getSecurity() != null ? config.getServer().getSecurity().getRateLimit() @@ -86,16 +114,20 @@ public EventLensServer( : null; // ── Route handler instances ─────────────────────────────────────── - var aggregateRoutes = new AggregateRoutes(reader, auditLogger); - var timelineRoutes = new TimelineRoutes(replayEngine, auditLogger, piiMasker); + var aggregateRoutes = new AggregateRoutes(sourceRegistry, auditLogger, queryCache, + Duration.ofSeconds(config.getQueryCache().getSearchTtlSeconds())); + var timelineRoutes = new TimelineRoutes(sourceRegistry, auditLogger, piiMasker, queryCache, + Duration.ofSeconds(config.getQueryCache().getTimelineTtlSeconds())); + var datasourceRoutes = new DatasourceRoutes(sourceRegistry); + var pluginRoutes = new PluginRoutes(sourceRegistry); var bisectRoutes = new BisectRoutes(bisectEngine); - var anomalyRoutes = new AnomalyRoutes(anomalyDetector, auditLogger); + var anomalyRoutes = new AnomalyRoutes(sourceRegistry, config.getAnomaly(), auditLogger); var exportRoutes = new ExportRoutes(exportEngine, auditLogger); var asyncExportRoutes = new AsyncExportRoutes(exportService); var healthRoutes = new HealthRoutes(reader, config.getVersion()); var metricsRoutes = new MetricsRoutes(); var openApiRoutes = new OpenApiRoutes(); - var liveTailWs = new LiveTailWebSocket(reader, auditLogger); + var liveTailWs = new LiveTailWebSocket(sourceRegistry, pluginManager, auditLogger, defaultSourceId, sourceStreamBindings); var authConfig = config.getServer().getAuth(); @@ -299,6 +331,9 @@ public EventLensServer( cfg.routes.get("/api/v1/aggregates/search", aggregateRoutes::search); cfg.routes.get("/api/v1/meta/types", aggregateRoutes::types); cfg.routes.get("/api/v1/events/recent", aggregateRoutes::recentEvents); + cfg.routes.get("/api/v1/datasources", datasourceRoutes::list); + cfg.routes.get("/api/v1/datasources/{id}/health", datasourceRoutes::health); + cfg.routes.get("/api/v1/plugins", pluginRoutes::list); // Legacy aggregate routes (no redirect, but marked deprecated) cfg.routes.get("/api/aggregates/search", ctx -> { @@ -462,3 +497,4 @@ private static String extractClientIp(io.javalin.http.Context ctx) { return ctx.ip(); } } + diff --git a/eventlens-api/src/main/java/io/eventlens/api/cache/QueryResultCache.java b/eventlens-api/src/main/java/io/eventlens/api/cache/QueryResultCache.java new file mode 100644 index 0000000..1784b27 --- /dev/null +++ b/eventlens-api/src/main/java/io/eventlens/api/cache/QueryResultCache.java @@ -0,0 +1,98 @@ +package io.eventlens.api.cache; + +import io.eventlens.api.metrics.EventLensMetrics; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; + +import java.time.Duration; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +public final class QueryResultCache { + + private final boolean enabled; + private final int maxEntries; + private final Map entries = new ConcurrentHashMap<>(); + private final AtomicInteger size = new AtomicInteger(); + private final Counter hits = Counter.builder("eventlens_query_cache_hits_total") + .description("Cache hits for query results") + .register(EventLensMetrics.registry); + private final Counter misses = Counter.builder("eventlens_query_cache_misses_total") + .description("Cache misses for query results") + .register(EventLensMetrics.registry); + private final Counter evictions = Counter.builder("eventlens_query_cache_evictions_total") + .description("Cache evictions for query results") + .register(EventLensMetrics.registry); + + public QueryResultCache(boolean enabled, int maxEntries) { + this.enabled = enabled; + this.maxEntries = Math.max(1, maxEntries); + Gauge.builder("eventlens_query_cache_size", size, AtomicInteger::get) + .description("Current number of cached query entries") + .register(EventLensMetrics.registry); + } + + public T getOrCompute(String namespace, String key, Duration ttl, Supplier supplier) { + if (!enabled) { + return supplier.get(); + } + + String cacheKey = namespace + "::" + key; + long now = System.nanoTime(); + CacheEntry current = entries.get(cacheKey); + if (current != null && current.expiresAtNanos > now) { + hits.increment(); + @SuppressWarnings("unchecked") + T value = (T) current.value; + return value; + } + + misses.increment(); + T value = supplier.get(); + entries.put(cacheKey, new CacheEntry(value, now + Math.max(1L, ttl.toNanos()))); + size.set(entries.size()); + evictExpired(now); + evictOverflow(); + return value; + } + + public double hitRatio() { + double hitCount = hits.count(); + double missCount = misses.count(); + double total = hitCount + missCount; + return total == 0 ? 0.0 : hitCount / total; + } + + private void evictExpired(long now) { + entries.entrySet().removeIf(entry -> entry.getValue().expiresAtNanos <= now); + size.set(entries.size()); + } + + private void evictOverflow() { + if (entries.size() <= maxEntries) { + return; + } + + Iterator iterator = entries.keySet().iterator(); + while (entries.size() > maxEntries && iterator.hasNext()) { + iterator.next(); + iterator.remove(); + evictions.increment(); + } + size.set(entries.size()); + } + + private static final class CacheEntry { + private final Object value; + private final long expiresAtNanos; + + private CacheEntry(Object value, long expiresAtNanos) { + this.value = Objects.requireNonNull(value); + this.expiresAtNanos = expiresAtNanos; + } + } +} diff --git a/eventlens-api/src/main/java/io/eventlens/api/routes/AggregateRoutes.java b/eventlens-api/src/main/java/io/eventlens/api/routes/AggregateRoutes.java index 6e21bd9..22e6ecd 100644 --- a/eventlens-api/src/main/java/io/eventlens/api/routes/AggregateRoutes.java +++ b/eventlens-api/src/main/java/io/eventlens/api/routes/AggregateRoutes.java @@ -1,47 +1,61 @@ package io.eventlens.api.routes; +import io.eventlens.api.cache.QueryResultCache; import io.eventlens.api.http.ConditionalGet; +import io.eventlens.api.source.SourceRegistry; import io.eventlens.core.InputValidator; import io.eventlens.core.audit.AuditEvent; import io.eventlens.core.audit.AuditLogger; import io.eventlens.core.spi.EventStoreReader; import io.javalin.http.Context; +import java.time.Duration; import java.util.Map; /** * Aggregate search, type listing, and recent event endpoints. * - *

v2 — emits {@link AuditEvent#ACTION_SEARCH} audit entries for every + *

v2 - emits {@link AuditEvent#ACTION_SEARCH} audit entries for every * search request. */ public class AggregateRoutes { - /** Hard cap on any client-supplied limit to prevent unbounded DB queries. */ private static final int MAX_LIMIT = 1_000; - private final EventStoreReader reader; + private final SourceRegistry sourceRegistry; private final AuditLogger auditLogger; - - public AggregateRoutes(EventStoreReader reader, AuditLogger auditLogger) { - this.reader = reader; + private final QueryResultCache queryCache; + private final Duration searchTtl; + + public AggregateRoutes( + SourceRegistry sourceRegistry, + AuditLogger auditLogger, + QueryResultCache queryCache, + Duration searchTtl) { + this.sourceRegistry = sourceRegistry; this.auditLogger = auditLogger; + this.queryCache = queryCache; + this.searchTtl = searchTtl; } - /** GET /api/aggregates/search?q=ACC&limit=20 */ public void search(Context ctx) { String query = ctx.queryParam("q"); if (query == null || query.isBlank()) { ctx.status(400).json(Map.of("error", "Missing query parameter: q")); return; } + int limit = Math.min( InputValidator.validateLimit(ctx.queryParam("limit"), 20, MAX_LIMIT), MAX_LIMIT); + var source = sourceRegistry.resolve(ctx.queryParam("source")); - var result = reader.searchAggregates(query, limit); + var result = queryCache.getOrCompute( + "aggregate-search", + source.id() + "|" + query + "|" + limit, + searchTtl, + () -> source.reader().searchAggregates(query, limit)); - // 1.8 — audit auditLogger.log(AuditEvent.builder() .action(AuditEvent.ACTION_SEARCH) .resourceType(AuditEvent.RT_AGGREGATE) @@ -50,27 +64,29 @@ public void search(Context ctx) { .clientIp(clientIp(ctx)) .requestId(requestId(ctx)) .userAgent(ctx.userAgent()) - .details(Map.of("q", query, "limit", limit, "resultCount", result.size())) + .details(Map.of( + "q", query, + "limit", limit, + "source", source.id(), + "resultCount", result.size())) .build()); ConditionalGet.json(ctx, result); } - /** GET /api/meta/types */ public void types(Context ctx) { + EventStoreReader reader = sourceRegistry.resolve(ctx.queryParam("source")).reader(); ConditionalGet.json(ctx, reader.getAggregateTypes()); } - /** GET /api/events/recent?limit=50 */ public void recentEvents(Context ctx) { int limit = Math.min( InputValidator.validateLimit(ctx.queryParam("limit"), 50, MAX_LIMIT), MAX_LIMIT); + EventStoreReader reader = sourceRegistry.resolve(ctx.queryParam("source")).reader(); ConditionalGet.json(ctx, reader.getRecentEvents(limit)); } - // ── Helpers ────────────────────────────────────────────────────────────── - private static String userId(Context ctx) { String v = ctx.attribute("auditUserId"); return v != null ? v : "anonymous"; diff --git a/eventlens-api/src/main/java/io/eventlens/api/routes/AnomalyRoutes.java b/eventlens-api/src/main/java/io/eventlens/api/routes/AnomalyRoutes.java index 216d2cf..01cbac0 100644 --- a/eventlens-api/src/main/java/io/eventlens/api/routes/AnomalyRoutes.java +++ b/eventlens-api/src/main/java/io/eventlens/api/routes/AnomalyRoutes.java @@ -1,5 +1,7 @@ package io.eventlens.api.routes; +import io.eventlens.api.source.SourceRegistry; +import io.eventlens.core.EventLensConfig; import io.eventlens.core.InputValidator; import io.eventlens.core.audit.AuditEvent; import io.eventlens.core.audit.AuditLogger; @@ -7,30 +9,37 @@ import io.javalin.http.Context; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * Anomaly detection endpoints. * - *

v2 — emits {@link AuditEvent#ACTION_VIEW_ANOMALIES} audit entries. + *

v2 - emits {@link AuditEvent#ACTION_VIEW_ANOMALIES} audit entries. */ public class AnomalyRoutes { private static final int MAX_SCAN_LIMIT = 500; - private final AnomalyDetector anomalyDetector; - private final AuditLogger auditLogger; - - public AnomalyRoutes(AnomalyDetector anomalyDetector, AuditLogger auditLogger) { - this.anomalyDetector = anomalyDetector; - this.auditLogger = auditLogger; + private final SourceRegistry sourceRegistry; + private final EventLensConfig.AnomalyConfig anomalyConfig; + private final AuditLogger auditLogger; + private final Map detectors = new ConcurrentHashMap<>(); + + public AnomalyRoutes( + SourceRegistry sourceRegistry, + EventLensConfig.AnomalyConfig anomalyConfig, + AuditLogger auditLogger) { + this.sourceRegistry = sourceRegistry; + this.anomalyConfig = anomalyConfig; + this.auditLogger = auditLogger; } /** GET /api/aggregates/{id}/anomalies */ public void scanAggregate(Context ctx) { String id = InputValidator.validateAggregateId(ctx.pathParam("id")); - var result = anomalyDetector.scan(id); + var source = sourceRegistry.resolve(ctx.queryParam("source")); + var result = detectorFor(source.id(), source).scan(id); - // 1.8 — audit auditLogger.log(AuditEvent.builder() .action(AuditEvent.ACTION_VIEW_ANOMALIES) .resourceType(AuditEvent.RT_ANOMALY) @@ -40,7 +49,9 @@ public void scanAggregate(Context ctx) { .clientIp(clientIp(ctx)) .requestId(requestId(ctx)) .userAgent(ctx.userAgent()) - .details(Map.of("anomalyCount", result.size())) + .details(Map.of( + "anomalyCount", result.size(), + "source", source.id())) .build()); ctx.json(result); @@ -51,10 +62,9 @@ public void scanRecent(Context ctx) { int limit = Math.min( InputValidator.validateLimit(ctx.queryParam("limit"), 100, MAX_SCAN_LIMIT), MAX_SCAN_LIMIT); + var source = sourceRegistry.resolve(ctx.queryParam("source")); + var result = detectorFor(source.id(), source).scanRecent(limit); - var result = anomalyDetector.scanRecent(limit); - - // 1.8 — audit auditLogger.log(AuditEvent.builder() .action(AuditEvent.ACTION_VIEW_ANOMALIES) .resourceType(AuditEvent.RT_ANOMALY) @@ -63,13 +73,20 @@ public void scanRecent(Context ctx) { .clientIp(clientIp(ctx)) .requestId(requestId(ctx)) .userAgent(ctx.userAgent()) - .details(Map.of("limit", limit, "anomalyCount", result.size())) + .details(Map.of( + "limit", limit, + "anomalyCount", result.size(), + "source", source.id())) .build()); ctx.json(result); } - // ── Helpers ────────────────────────────────────────────────────────────── + private AnomalyDetector detectorFor(String sourceId, SourceRegistry.ResolvedSource source) { + return detectors.computeIfAbsent( + sourceId, + ignored -> new AnomalyDetector(source.reader(), source.replayEngine(), anomalyConfig)); + } private static String userId(Context ctx) { String v = ctx.attribute("auditUserId"); diff --git a/eventlens-api/src/main/java/io/eventlens/api/routes/DatasourceRoutes.java b/eventlens-api/src/main/java/io/eventlens/api/routes/DatasourceRoutes.java new file mode 100644 index 0000000..88ccf9f --- /dev/null +++ b/eventlens-api/src/main/java/io/eventlens/api/routes/DatasourceRoutes.java @@ -0,0 +1,22 @@ +package io.eventlens.api.routes; + +import io.eventlens.api.http.ConditionalGet; +import io.eventlens.api.source.SourceRegistry; +import io.javalin.http.Context; + +public final class DatasourceRoutes { + + private final SourceRegistry sourceRegistry; + + public DatasourceRoutes(SourceRegistry sourceRegistry) { + this.sourceRegistry = sourceRegistry; + } + + public void list(Context ctx) { + ConditionalGet.json(ctx, sourceRegistry.listDatasources()); + } + + public void health(Context ctx) { + ConditionalGet.json(ctx, sourceRegistry.datasourceHealth(ctx.pathParam("id"))); + } +} diff --git a/eventlens-api/src/main/java/io/eventlens/api/routes/PluginRoutes.java b/eventlens-api/src/main/java/io/eventlens/api/routes/PluginRoutes.java new file mode 100644 index 0000000..c1f14cf --- /dev/null +++ b/eventlens-api/src/main/java/io/eventlens/api/routes/PluginRoutes.java @@ -0,0 +1,18 @@ +package io.eventlens.api.routes; + +import io.eventlens.api.http.ConditionalGet; +import io.eventlens.api.source.SourceRegistry; +import io.javalin.http.Context; + +public final class PluginRoutes { + + private final SourceRegistry sourceRegistry; + + public PluginRoutes(SourceRegistry sourceRegistry) { + this.sourceRegistry = sourceRegistry; + } + + public void list(Context ctx) { + ConditionalGet.json(ctx, sourceRegistry.listPlugins()); + } +} diff --git a/eventlens-api/src/main/java/io/eventlens/api/routes/TimelineRoutes.java b/eventlens-api/src/main/java/io/eventlens/api/routes/TimelineRoutes.java index 9b6c803..b61afc2 100644 --- a/eventlens-api/src/main/java/io/eventlens/api/routes/TimelineRoutes.java +++ b/eventlens-api/src/main/java/io/eventlens/api/routes/TimelineRoutes.java @@ -3,82 +3,68 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import io.eventlens.core.InputValidator; +import io.eventlens.api.cache.QueryResultCache; import io.eventlens.api.http.ConditionalGet; +import io.eventlens.api.source.SourceRegistry; +import io.eventlens.core.InputValidator; import io.eventlens.core.audit.AuditEvent; import io.eventlens.core.audit.AuditLogger; -import io.eventlens.core.engine.ReplayEngine; -import io.eventlens.core.pagination.CursorCodec; import io.eventlens.core.model.AggregateTimeline; import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.pagination.CursorCodec; import io.eventlens.core.pii.PiiMasker; import io.javalin.http.Context; +import java.time.Duration; import java.util.List; import java.util.Map; /** * Timeline, replay, and state transition endpoints. - * - *

v2 additions: - *

    - *
  • 1.8 — emits {@link AuditEvent#ACTION_VIEW_TIMELINE} on every - * timeline fetch.
  • - *
  • 1.9 — passes event payloads through {@link PiiMasker} before - * returning them to the client.
  • - *
*/ public class TimelineRoutes { - /** Hard cap on any client-supplied limit to prevent unbounded DB queries. */ private static final int MAX_LIMIT = 1_000; - private final ReplayEngine replayEngine; - private final AuditLogger auditLogger; - private final PiiMasker piiMasker; + private final SourceRegistry sourceRegistry; + private final AuditLogger auditLogger; + private final PiiMasker piiMasker; + private final QueryResultCache queryCache; + private final Duration timelineTtl; private final ObjectMapper mapper; - public TimelineRoutes(ReplayEngine replayEngine, - AuditLogger auditLogger, - PiiMasker piiMasker) { - this.replayEngine = replayEngine; - this.auditLogger = auditLogger; - this.piiMasker = piiMasker; + public TimelineRoutes( + SourceRegistry sourceRegistry, + AuditLogger auditLogger, + PiiMasker piiMasker, + QueryResultCache queryCache, + Duration timelineTtl) { + this.sourceRegistry = sourceRegistry; + this.auditLogger = auditLogger; + this.piiMasker = piiMasker; + this.queryCache = queryCache; + this.timelineTtl = timelineTtl; this.mapper = new ObjectMapper() .findAndRegisterModules() .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } - /** GET /api/aggregates/{id}/timeline */ public void getTimeline(Context ctx) { - String id = InputValidator.validateAggregateId(ctx.pathParam("id")); - int limit = Math.min( + String id = InputValidator.validateAggregateId(ctx.pathParam("id")); + int limit = Math.min( InputValidator.validateLimit(ctx.queryParam("limit"), 500, MAX_LIMIT), MAX_LIMIT); String cursorParam = ctx.queryParam("cursor"); int offset = InputValidator.validateOffset(ctx.queryParam("offset")); + String fields = normalizeFields(ctx.queryParam("fields")); + var source = sourceRegistry.resolve(ctx.queryParam("source")); - AggregateTimeline timeline; - boolean hasMore = false; - String nextCursor = null; + TimelineEnvelope envelope = queryCache.getOrCompute( + "timeline", + source.id() + "|" + id + "|" + limit + "|" + offset + "|" + fields + "|" + (cursorParam == null ? "" : cursorParam), + timelineTtl, + () -> buildTimelineEnvelope(source, id, limit, offset, cursorParam, fields)); - if (cursorParam != null && !cursorParam.isBlank()) { - var cursor = CursorCodec.decode(cursorParam); - var page = replayEngine.buildTimelineAfter(id, limit, cursor.sequence()); - timeline = new AggregateTimeline(page.aggregateId(), page.aggregateType(), page.events(), page.totalEvents()); - hasMore = page.hasMore(); - if (!page.events().isEmpty()) { - var last = page.events().getLast(); - nextCursor = CursorCodec.encode(last.sequenceNumber(), last.timestamp()); - } - } else { - timeline = replayEngine.buildTimeline(id, limit, offset); - } - - // 1.9 — apply PII masking on each event payload - AggregateTimeline masked = maskTimeline(timeline); - - // 1.8 — audit auditLogger.log(AuditEvent.builder() .action(AuditEvent.ACTION_VIEW_TIMELINE) .resourceType(AuditEvent.RT_AGGREGATE) @@ -92,52 +78,112 @@ public void getTimeline(Context ctx) { "limit", limit, "offset", offset, "cursor", cursorParam != null ? cursorParam : "", - "eventCount", masked.events().size())) + "fields", fields, + "source", source.id(), + "eventCount", envelope.timeline().events().size())) .build()); - Object response = nextCursor != null - ? Map.of( - "aggregateId", masked.aggregateId(), - "aggregateType", masked.aggregateType(), - "events", masked.events(), - "totalEvents", masked.totalEvents(), - "pagination", Map.of( - "limit", limit, - "hasMore", hasMore, - "nextCursor", nextCursor - )) - : masked; - ConditionalGet.json(ctx, response); + if (envelope.nextCursor() != null) { + ConditionalGet.json(ctx, Map.of( + "aggregateId", envelope.timeline().aggregateId(), + "aggregateType", envelope.timeline().aggregateType(), + "events", envelope.timeline().events(), + "totalEvents", envelope.timeline().totalEvents(), + "pagination", Map.of( + "limit", limit, + "hasMore", envelope.hasMore(), + "nextCursor", envelope.nextCursor() + ) + )); + return; + } + + ConditionalGet.json(ctx, envelope.timeline()); } - /** GET /api/aggregates/{id}/replay */ public void replay(Context ctx) { String id = InputValidator.validateAggregateId(ctx.pathParam("id")); - ctx.json(replayEngine.replayFull(id)); + var source = sourceRegistry.resolve(ctx.queryParam("source")); + ctx.json(source.replayEngine().replayFull(id)); } - /** GET /api/aggregates/{id}/replay/{seq} */ public void replayTo(Context ctx) { - String id = InputValidator.validateAggregateId(ctx.pathParam("id")); - long seq = Long.parseLong(ctx.pathParam("seq")); - ctx.json(replayEngine.replayTo(id, seq)); + String id = InputValidator.validateAggregateId(ctx.pathParam("id")); + long seq = Long.parseLong(ctx.pathParam("seq")); + var source = sourceRegistry.resolve(ctx.queryParam("source")); + ctx.json(source.replayEngine().replayTo(id, seq)); } - /** GET /api/aggregates/{id}/transitions */ public void transitions(Context ctx) { String id = InputValidator.validateAggregateId(ctx.pathParam("id")); - ctx.json(replayEngine.replayFull(id)); + var source = sourceRegistry.resolve(ctx.queryParam("source")); + ctx.json(source.replayEngine().replayFull(id)); } - // ── PII masking ────────────────────────────────────────────────────────── + private TimelineEnvelope buildTimelineEnvelope( + SourceRegistry.ResolvedSource source, + String aggregateId, + int limit, + int offset, + String cursorParam, + String fields) { + AggregateTimeline timeline; + boolean hasMore = false; + String nextCursor = null; + + if (cursorParam != null && !cursorParam.isBlank()) { + var cursor = CursorCodec.decode(cursorParam); + var page = source.replayEngine().buildTimelineAfter(aggregateId, limit, cursor.sequence()); + timeline = new AggregateTimeline(page.aggregateId(), page.aggregateType(), page.events(), page.totalEvents()); + hasMore = page.hasMore(); + if (!page.events().isEmpty()) { + var last = page.events().getLast(); + nextCursor = CursorCodec.encode(last.sequenceNumber(), last.timestamp()); + } + } else { + timeline = source.replayEngine().buildTimeline(aggregateId, limit, offset); + } + + AggregateTimeline masked = maskTimeline(timeline); + AggregateTimeline shaped = "metadata".equals(fields) ? metadataOnly(masked) : masked; + return new TimelineEnvelope(shaped, hasMore, nextCursor); + } + + private String normalizeFields(String fields) { + if (fields == null || fields.isBlank()) { + return "full"; + } + if (!fields.equals("full") && !fields.equals("metadata")) { + throw new IllegalArgumentException("Unsupported fields value: " + fields); + } + return fields; + } + + private AggregateTimeline metadataOnly(AggregateTimeline timeline) { + List events = timeline.events().stream() + .map(event -> new StoredEvent( + event.eventId(), + event.aggregateId(), + event.aggregateType(), + event.sequenceNumber(), + event.eventType(), + null, + event.metadata(), + event.timestamp(), + event.globalPosition())) + .toList(); + + return new AggregateTimeline( + timeline.aggregateId(), + timeline.aggregateType(), + events, + timeline.totalEvents()); + } - /** - * Returns a new {@link AggregateTimeline} whose events have their payload - * fields masked. If masking is disabled the original object is returned - * unchanged (no copy). - */ private AggregateTimeline maskTimeline(AggregateTimeline timeline) { - if (timeline == null || timeline.events() == null) return timeline; + if (timeline == null || timeline.events() == null) { + return timeline; + } List maskedEvents = timeline.events().stream() .map(this::maskEvent) @@ -147,17 +193,20 @@ private AggregateTimeline maskTimeline(AggregateTimeline timeline) { timeline.aggregateId(), timeline.aggregateType(), maskedEvents, - timeline.totalEvents() - ); + timeline.totalEvents()); } private StoredEvent maskEvent(StoredEvent event) { - if (event == null || event.payload() == null) return event; + if (event == null || event.payload() == null) { + return event; + } try { String raw = event.payload(); JsonNode tree = mapper.readTree(raw); JsonNode maskedTree = piiMasker.mask(tree, raw); - if (maskedTree == tree) return event; // nothing changed — return original + if (maskedTree == tree) { + return event; + } return new StoredEvent( event.eventId(), event.aggregateId(), @@ -167,16 +216,12 @@ private StoredEvent maskEvent(StoredEvent event) { mapper.writeValueAsString(maskedTree), event.metadata(), event.timestamp(), - event.globalPosition() - ); + event.globalPosition()); } catch (Exception e) { - // If JSON parsing fails, return the event unmasked rather than failing the request return event; } } - // ── Helpers ────────────────────────────────────────────────────────────── - private static String userId(Context ctx) { String v = ctx.attribute("auditUserId"); return v != null ? v : "anonymous"; @@ -201,4 +246,8 @@ private static String requestId(Context ctx) { String v = ctx.attribute("requestId"); return v != null ? v : "unknown"; } + + private record TimelineEnvelope(AggregateTimeline timeline, boolean hasMore, String nextCursor) { + } } + diff --git a/eventlens-api/src/main/java/io/eventlens/api/source/SourceRegistry.java b/eventlens-api/src/main/java/io/eventlens/api/source/SourceRegistry.java new file mode 100644 index 0000000..05cf4bf --- /dev/null +++ b/eventlens-api/src/main/java/io/eventlens/api/source/SourceRegistry.java @@ -0,0 +1,114 @@ +package io.eventlens.api.source; + +import io.eventlens.core.aggregator.ReducerRegistry; +import io.eventlens.core.engine.ReplayEngine; +import io.eventlens.core.plugin.DatasourceListingModel; +import io.eventlens.core.plugin.PluginInstance; +import io.eventlens.spi.HealthStatus; +import io.eventlens.core.plugin.PluginListingModel; +import io.eventlens.core.plugin.PluginManager; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.spi.PluginLifecycle; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public final class SourceRegistry { + + private final String defaultSourceId; + private final EventStoreReader defaultReader; + private final ReplayEngine defaultReplayEngine; + private final ReducerRegistry reducerRegistry; + private final PluginManager pluginManager; + private final Map replayEngines = new ConcurrentHashMap<>(); + + public SourceRegistry( + String defaultSourceId, + EventStoreReader defaultReader, + ReplayEngine defaultReplayEngine, + ReducerRegistry reducerRegistry, + PluginManager pluginManager) { + this.defaultSourceId = defaultSourceId; + this.defaultReader = defaultReader; + this.defaultReplayEngine = defaultReplayEngine; + this.reducerRegistry = reducerRegistry; + this.pluginManager = pluginManager; + } + + public ResolvedSource resolve(String requestedSourceId) { + if (requestedSourceId == null || requestedSourceId.isBlank() || requestedSourceId.equals(defaultSourceId)) { + return new ResolvedSource(defaultSourceId, "Primary datasource", defaultReader, defaultReplayEngine, true); + } + + PluginInstance instance = pluginManager.getInstance(requestedSourceId) + .orElseThrow(() -> new IllegalArgumentException("Unknown datasource: " + requestedSourceId)); + + if (instance.pluginType() != PluginInstance.PluginType.EVENT_SOURCE) { + throw new IllegalArgumentException("Plugin is not a datasource: " + requestedSourceId); + } + + if (instance.lifecycle() != PluginLifecycle.READY && instance.lifecycle() != PluginLifecycle.DEGRADED) { + throw new IllegalArgumentException("Datasource is not ready: " + requestedSourceId); + } + + if (!(instance.plugin() instanceof EventStoreReader reader)) { + throw new IllegalArgumentException("Datasource does not expose an EventStoreReader: " + requestedSourceId); + } + + ReplayEngine replayEngine = replayEngines.computeIfAbsent(requestedSourceId, ignored -> new ReplayEngine(reader, reducerRegistry)); + return new ResolvedSource(instance.instanceId(), instance.displayName(), reader, replayEngine, false); + } + + public List listDatasources() { + return pluginManager.listByType(PluginInstance.PluginType.EVENT_SOURCE).stream() + .sorted(Comparator.comparing(PluginInstance::instanceId)) + .map(DatasourceListingModel::from) + .toList(); + } + + public Map datasourceHealth(String datasourceId) { + PluginInstance instance = pluginManager.getInstance(datasourceId) + .orElseThrow(() -> new IllegalArgumentException("Unknown datasource: " + datasourceId)); + + if (instance.pluginType() != PluginInstance.PluginType.EVENT_SOURCE) { + throw new IllegalArgumentException("Plugin is not a datasource: " + datasourceId); + } + + HealthStatus health = instance.health(); + Map healthMap = new LinkedHashMap<>(); + healthMap.put("state", health != null && health.state() != null + ? health.state().name().toLowerCase() : "unknown"); + healthMap.put("message", health != null + ? Objects.toString(health.message(), "") : "Health not yet checked"); + + Map result = new LinkedHashMap<>(); + result.put("id", Objects.toString(instance.instanceId(), datasourceId)); + result.put("displayName", Objects.toString(instance.displayName(), datasourceId)); + result.put("status", instance.lifecycle() != null + ? instance.lifecycle().name().toLowerCase() : "unknown"); + result.put("health", healthMap); + result.put("lastHealthCheck", Objects.toString(instance.lastHealthCheck(), "")); + result.put("failureReason", Objects.toString(instance.failureReason(), "")); + return result; + } + + public List listPlugins() { + return pluginManager.listAll().stream() + .sorted(Comparator.comparing(PluginInstance::instanceId)) + .map(PluginListingModel::from) + .toList(); + } + + public record ResolvedSource( + String id, + String displayName, + EventStoreReader reader, + ReplayEngine replayEngine, + boolean primary) { + } +} diff --git a/eventlens-api/src/main/java/io/eventlens/api/websocket/LiveTailWebSocket.java b/eventlens-api/src/main/java/io/eventlens/api/websocket/LiveTailWebSocket.java index 41ea944..2d4a2e3 100644 --- a/eventlens-api/src/main/java/io/eventlens/api/websocket/LiveTailWebSocket.java +++ b/eventlens-api/src/main/java/io/eventlens/api/websocket/LiveTailWebSocket.java @@ -2,162 +2,185 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import io.eventlens.api.metrics.EventLensMetrics; +import io.eventlens.api.source.SourceRegistry; import io.eventlens.core.audit.AuditEvent; import io.eventlens.core.audit.AuditLogger; import io.eventlens.core.model.StoredEvent; -import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.core.plugin.PluginManager; +import io.eventlens.spi.Event; +import io.eventlens.spi.StreamAdapterPlugin; import io.javalin.websocket.WsConfig; import io.javalin.websocket.WsContext; -import io.eventlens.api.metrics.EventLensMetrics; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; /** - * WebSocket live tail — streams events to connected browser clients in + * WebSocket live tail - streams events to connected browser clients in * real-time. - * - *

- * Two modes: - *

    - *
  • Kafka mode: forwards events from KafkaLiveTail via listener - * callback
  • - *
  • Poll mode: falls back to polling PostgreSQL every second when - * Kafka is disabled
  • - *
- * - *

- * On connect: sends the last N events as backfill so clients don't join a - * blank screen. Backfill is sent asynchronously to avoid blocking the - * Jetty onConnect handler thread. - * - *

v2 — emits {@link AuditEvent#ACTION_VIEW_LIVE_STREAM} on WebSocket - * connect (1.8 Audit Logging). */ public class LiveTailWebSocket { private static final Logger log = LoggerFactory.getLogger(LiveTailWebSocket.class); private static final int MAX_CONNECTIONS = 500; - /** Recent events sent on each new WebSocket connection (was 20; too small vs total store count). */ private static final int BACKFILL_EVENT_COUNT = 100; - private final Set sessions = ConcurrentHashMap.newKeySet(); + private final Map> sessionsBySource = new ConcurrentHashMap<>(); + private final Set subscribedSources = ConcurrentHashMap.newKeySet(); private final ObjectMapper mapper = new ObjectMapper() .findAndRegisterModules() .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - private final EventStoreReader reader; - private final AuditLogger auditLogger; - private final ExecutorService backfillExecutor = Executors.newCachedThreadPool( + private final SourceRegistry sourceRegistry; + private final PluginManager pluginManager; + private final AuditLogger auditLogger; + private final String defaultSourceId; + private final Map sourceStreamBindings; + private final ExecutorService backfillExecutor = java.util.concurrent.Executors.newCachedThreadPool( Thread.ofVirtual().name("eventlens-backfill-", 0).factory()); - public LiveTailWebSocket(EventStoreReader reader, AuditLogger auditLogger) { - this.reader = reader; + public LiveTailWebSocket( + SourceRegistry sourceRegistry, + PluginManager pluginManager, + AuditLogger auditLogger, + String defaultSourceId, + Map sourceStreamBindings) { + this.sourceRegistry = sourceRegistry; + this.pluginManager = pluginManager; this.auditLogger = auditLogger; + this.defaultSourceId = defaultSourceId; + this.sourceStreamBindings = sourceStreamBindings == null ? Map.of() : Map.copyOf(sourceStreamBindings); } - /** - * Set up WebSocket event handlers. The route itself is registered by - * the caller (EventLensServer) via {@code cfg.routes.ws("/ws/live", ...)}. - */ public void configureHandlers(WsConfig ws) { ws.onConnect(ctx -> { - if (sessions.size() >= MAX_CONNECTIONS) { + if (totalSessions() >= MAX_CONNECTIONS) { log.warn("WebSocket connection rejected: max connections ({}) reached", MAX_CONNECTIONS); ctx.closeSession(1008, "Too many connections"); return; } + + String sourceId = requestedSource(ctx); + ctx.attribute("eventlensSourceId", sourceId); ctx.enableAutomaticPings(); - sessions.add(ctx); - EventLensMetrics.setWebsocketConnections(sessions.size()); - log.debug("WebSocket client connected: {} ({} active)", ctx.sessionId(), sessions.size()); + sessionsBySource.computeIfAbsent(sourceId, ignored -> ConcurrentHashMap.newKeySet()).add(ctx); + EventLensMetrics.setWebsocketConnections(totalSessions()); + log.debug("WebSocket client connected: {} on source {} ({} active)", ctx.sessionId(), sourceId, totalSessions()); - // 1.8 — audit live-stream connection - String userAgent = ctx.header("User-Agent"); auditLogger.log(AuditEvent.builder() .action(AuditEvent.ACTION_VIEW_LIVE_STREAM) .resourceType(AuditEvent.RT_STREAM) - .userId("anonymous") // WS upgrade doesn't carry the ctx attributes + .userId("anonymous") .authMethod("anonymous") .clientIp(extractIp(ctx)) .requestId("ws-" + ctx.sessionId()) - .userAgent(userAgent) - .details(Map.of("sessionId", ctx.sessionId(), - "activeSessions", sessions.size())) + .userAgent(ctx.header("User-Agent")) + .details(Map.of( + "sessionId", ctx.sessionId(), + "activeSessions", totalSessions(), + "source", sourceId)) .build()); - backfillExecutor.submit(() -> backfill(ctx)); - }); - - ws.onClose(ctx -> { - sessions.remove(ctx); - EventLensMetrics.setWebsocketConnections(sessions.size()); - log.debug("WebSocket client disconnected: {}", ctx.sessionId()); + Optional streamAdapter = streamForSource(sourceId); + if (streamAdapter.isPresent()) { + ensureSubscribed(sourceId, streamAdapter.get()); + backfillExecutor.submit(() -> backfill(ctx, sourceId)); + } else { + sendControl(ctx, new ControlMessage("NO_LIVE_STREAM", sourceId)); + } }); - ws.onError(ctx -> { - sessions.remove(ctx); - EventLensMetrics.setWebsocketConnections(sessions.size()); - log.debug("WebSocket error for session: {}", ctx.sessionId()); - }); + ws.onClose(this::removeSession); + ws.onError(this::removeSession); } - private void backfill(WsContext ctx) { + private void backfill(WsContext ctx, String sourceId) { try { Thread.sleep(250); - var recent = reader.getRecentEvents(BACKFILL_EVENT_COUNT); + var recent = sourceRegistry.resolve(sourceId).reader().getRecentEvents(BACKFILL_EVENT_COUNT); for (var event : recent) { - if (!trySend(ctx, event)) break; + if (!trySend(ctx, event)) { + break; + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { - log.debug("Backfill failed for {}: {}", ctx.sessionId(), e.getMessage()); + log.debug("Backfill failed for {} on source {}: {}", ctx.sessionId(), sourceId, e.getMessage()); + } + } + + private void ensureSubscribed(String sourceId, StreamAdapterPlugin adapter) { + if (!subscribedSources.add(sourceId)) { + return; } + adapter.subscribe(event -> broadcast(sourceId, toStoredEvent(event))); } - /** Broadcast a new event to all connected clients. */ - public void broadcast(StoredEvent event) { - if (sessions.isEmpty()) + public void broadcast(String sourceId, StoredEvent event) { + Set sessions = sessionsBySource.get(sourceId); + if (sessions == null || sessions.isEmpty()) { return; + } sessions.removeIf(session -> !trySend(session, event)); + EventLensMetrics.setWebsocketConnections(totalSessions()); + } + + private Optional streamForSource(String sourceId) { + String explicit = sourceStreamBindings.get(sourceId); + if (explicit != null) { + if (explicit.isBlank()) { + return Optional.empty(); + } + return pluginManager.getStreamAdapter(explicit); + } + + Optional sameId = pluginManager.getStreamAdapter(sourceId); + if (sameId.isPresent()) { + return sameId; + } + + if (defaultSourceId.equals(sourceId)) { + return pluginManager.getFirstReadyStreamAdapter(); + } + + return Optional.empty(); } - /** - * Start polling PostgreSQL for new events (used when Kafka is not configured). - * Polls every 1 second using a virtual thread scheduler. - */ - public void startPolling() { - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( - Thread.ofVirtual().name("eventlens-poll").factory()); - - final AtomicLong lastPosition = new AtomicLong(0); - - scheduler.scheduleAtFixedRate(() -> { - try { - var events = reader.getEventsAfter(lastPosition.get(), 50); - for (var event : events) { - broadcast(event); - if (event.globalPosition() > lastPosition.get()) { - lastPosition.set(event.globalPosition()); - } + private String requestedSource(WsContext ctx) { + String requested = ctx.queryParam("source"); + if (requested == null || requested.isBlank()) { + return defaultSourceId; + } + return sourceRegistry.resolve(requested).id(); + } + + private void removeSession(WsContext ctx) { + Object sourceAttr = ctx.attribute("eventlensSourceId"); + if (sourceAttr instanceof String sourceId) { + Set sessions = sessionsBySource.get(sourceId); + if (sessions != null) { + sessions.remove(ctx); + if (sessions.isEmpty()) { + sessionsBySource.remove(sourceId); } - } catch (Exception e) { - log.warn("Live tail polling error: {}", e.getMessage()); } - }, 0, 1, TimeUnit.SECONDS); + } else { + sessionsBySource.values().forEach(sessions -> sessions.remove(ctx)); + } + EventLensMetrics.setWebsocketConnections(totalSessions()); + log.debug("WebSocket session ended: {}", ctx.sessionId()); + } - log.info("PostgreSQL polling live tail started (fallback mode)"); + private int totalSessions() { + return sessionsBySource.values().stream().mapToInt(Set::size).sum(); } - /** - * Non-throwing send. Returns true if the message was sent successfully. - * In Javalin 7 we no longer have direct access to the underlying session, - * so we rely on try-catch to detect disconnected clients. - */ private boolean trySend(WsContext ctx, StoredEvent event) { try { ctx.send(mapper.writeValueAsString(event)); @@ -168,8 +191,28 @@ private boolean trySend(WsContext ctx, StoredEvent event) { } } + private void sendControl(WsContext ctx, ControlMessage message) { + try { + ctx.send(mapper.writeValueAsString(message)); + } catch (Exception e) { + log.debug("WebSocket control send failed for {}: {}", ctx.sessionId(), e.getMessage()); + } + } + + private StoredEvent toStoredEvent(Event event) { + return new StoredEvent( + event.eventId(), + event.aggregateId(), + event.aggregateType(), + event.sequenceNumber(), + event.eventType(), + io.eventlens.core.JsonUtil.toJson(event.payload()), + io.eventlens.core.JsonUtil.toJson(event.metadata()), + event.timestamp(), + event.globalPosition()); + } + private static String extractIp(WsContext ctx) { - // WsContext exposes the underlying HTTP upgrade headers String xff = ctx.header("X-Forwarded-For"); if (xff != null && !xff.isBlank()) { int c = xff.indexOf(','); @@ -178,4 +221,7 @@ private static String extractIp(WsContext ctx) { String xri = ctx.header("X-Real-IP"); return xri != null && !xri.isBlank() ? xri.trim() : "unknown"; } + + private record ControlMessage(String type, String source) { + } } diff --git a/eventlens-api/src/main/resources/web/assets/index-B-pPVu7c.js b/eventlens-api/src/main/resources/web/assets/index-B-pPVu7c.js new file mode 100644 index 0000000..e9fa2fb --- /dev/null +++ b/eventlens-api/src/main/resources/web/assets/index-B-pPVu7c.js @@ -0,0 +1,14 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},ee=Object.prototype.hasOwnProperty;function te(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ne(e,t){return te(e.type,t,e.props)}function T(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function re(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ie=/\/+/g;function ae(e,t){return typeof e==`object`&&e&&e.key!=null?re(``+e.key):t.toString(36)}function oe(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function se(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,se(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ae(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(ie,`$&/`)+`/`),se(o,r,i,``,function(e){return e})):o!=null&&(T(o)&&(o=ne(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ie,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=u()})),f=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,T());else{var t=n(l);t!==null&&ae(x,t.startTime-e)}}var S=!1,C=-1,w=5,ee=-1;function te(){return g?!0:!(e.unstable_now()-eet&&te());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&ae(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?T():S=!1}}}var T;if(typeof y==`function`)T=function(){y(ne)};else if(typeof MessageChannel<`u`){var re=new MessageChannel,ie=re.port2;re.port1.onmessage=ne,T=function(){ie.postMessage(null)}}else T=function(){_(ne,0)};function ae(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,ae(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,T()))),r},e.unstable_shouldYield=te,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),p=o(((e,t)=>{t.exports=f()})),m=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=p(),n=d(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1fe||(e.current=de[fe],de[fe]=null,fe--)}function O(e,t){fe++,de[fe]=e.current,e.current=t}var he=pe(null),ge=pe(null),_e=pe(null),ve=pe(null);function ye(e,t){switch(O(_e,t),O(ge,e),O(he,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}me(he),O(he,e)}function be(){me(he),me(ge),me(_e)}function xe(e){e.memoizedState!==null&&O(ve,e);var t=he.current,n=Hd(t,e.type);t!==n&&(O(ge,e),O(he,n))}function Se(e){ge.current===e&&(me(he),me(ge)),ve.current===e&&(me(ve),Qf._currentValue=ue)}var k,Ce;function we(e){if(k===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);k=t&&t[1]||``,Ce=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Te=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?we(n):``}function De(e,t){switch(e.tag){case 26:case 27:case 5:return we(e.type);case 16:return we(`Lazy`);case 13:return e.child!==t&&t!==null?we(`Suspense Fallback`):we(`Suspense`);case 19:return we(`SuspenseList`);case 0:case 15:return Ee(e.type,!1);case 11:return Ee(e.type.render,!1);case 1:return Ee(e.type,!0);case 31:return we(`Activity`);default:return``}}function Oe(e){try{var t=``,n=null;do t+=De(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var ke=Object.prototype.hasOwnProperty,Ae=t.unstable_scheduleCallback,je=t.unstable_cancelCallback,Me=t.unstable_shouldYield,Ne=t.unstable_requestPaint,Pe=t.unstable_now,Fe=t.unstable_getCurrentPriorityLevel,Ie=t.unstable_ImmediatePriority,Le=t.unstable_UserBlockingPriority,Re=t.unstable_NormalPriority,ze=t.unstable_LowPriority,Be=t.unstable_IdlePriority,Ve=t.log,He=t.unstable_setDisableYieldValue,Ue=null,We=null;function Ge(e){if(typeof Ve==`function`&&He(e),We&&typeof We.setStrictMode==`function`)try{We.setStrictMode(Ue,e)}catch{}}var Ke=Math.clz32?Math.clz32:Ye,qe=Math.log,Je=Math.LN2;function Ye(e){return e>>>=0,e===0?32:31-(qe(e)/Je|0)|0}var Xe=256,Ze=262144,Qe=4194304;function $e(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function A(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=$e(n))):i=$e(o):i=$e(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=$e(n))):i=$e(o)):i=$e(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function j(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function et(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function tt(){var e=Qe;return Qe<<=1,!(Qe&62914560)&&(Qe=4194304),e}function nt(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function rt(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function it(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),gn=!1;if(hn)try{var _n={};Object.defineProperty(_n,`passive`,{get:function(){gn=!0}}),window.addEventListener(`test`,_n,_n),window.removeEventListener(`test`,_n,_n)}catch{gn=!1}var vn=null,yn=null,bn=null;function xn(){if(bn)return bn;var e,t=yn,n=t.length,r,i=`value`in vn?vn.value:vn.textContent,a=i.length;for(e=0;e=Zn),er=` `,tr=!1;function nr(e,t){switch(e){case`keyup`:return Yn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function rr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var ir=!1;function ar(e,t){switch(e){case`compositionend`:return rr(t);case`keypress`:return t.which===32?(tr=!0,er):null;case`textInput`:return e=t.data,e===er&&tr?null:e;default:return null}}function or(e,t){if(ir)return e===`compositionend`||!Xn&&nr(e,t)?(e=xn(),bn=yn=vn=null,ir=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Or(n)}}function Ar(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ar(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function jr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ht(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ht(e.document)}return t}function Mr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Nr=hn&&`documentMode`in document&&11>=document.documentMode,Pr=null,Fr=null,Ir=null,Lr=!1;function Rr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Lr||Pr==null||Pr!==Ht(r)||(r=Pr,`selectionStart`in r&&Mr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ir&&Dr(Ir,r)||(Ir=r,r=Ed(Fr,`onSelect`),0>=o,i-=o,ki=1<<32-Ke(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),L&&ji(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),L&&ji(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return L&&ji(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),L&&ji(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===T&&ka(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ia(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=gi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=hi(o.type,o.key,o.props,null,e.mode,c),Ia(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=yi(o,e.mode,c),c.return=e,e=c}return s(e);case T:return o=ka(o),b(e,r,o,c)}if(le(o))return h(e,r,o,c);if(oe(o)){if(l=oe(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Fa(o),c);if(o.$$typeof===C)return b(e,r,ia(e,o),c);La(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=_i(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Pa=0;var i=b(e,t,n,r);return Na=null,i}catch(t){if(t===Ca||t===Ta)throw t;var a=di(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var za=Ra(!0),Ba=Ra(!1),Va=!1;function Ha(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ua(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Wa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ga(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=li(e),ci(e,null,n),t}return ai(e,r,t,n),li(e)}function Ka(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ot(e,n)}}function qa(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ja=!1;function Ya(){if(Ja){var e=ma;if(e!==null)throw e}}function Xa(e,t,n,r){Ja=!1;var i=e.updateQueue;Va=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===pa&&(Ja=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:Va=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Za(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Qa(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=E.T,s={};E.T=s,Fs(e,!1,t,n);try{var c=i(),l=E.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ps(e,t,_a(c,r),pu(e)):Ps(e,t,r,pu(e))}catch(n){Ps(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{D.p=a,o!==null&&s.types!==null&&(o.types=s.types),E.T=o}}function ws(){}function Ts(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Es(e).queue;Cs(e,a,t,ue,n===null?ws:function(){return Ds(e),n(r)})}function Es(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:ue},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ds(e){var t=Es(e);t.next===null&&(t=e.alternate.memoizedState),Ps(e,t.next.queue,{},pu())}function Os(){return ra(Qf)}function ks(){return jo().memoizedState}function As(){return jo().memoizedState}function js(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Wa(n);var r=Ga(t,e,n);r!==null&&(hu(r,t,n),Ka(r,t,n)),t={cache:la()},e.payload=t;return}t=t.return}}function Ms(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e)?Ls(t,n):(n=oi(e,t,n,r),n!==null&&(hu(n,e,r),Rs(n,t,r)))}function Ns(e,t,n){Ps(e,t,n,pu())}function Ps(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Er(s,o))return ai(e,t,i,0),K===null&&ii(),!1}catch{}if(n=oi(e,t,i,r),n!==null)return hu(n,e,r),Rs(n,t,r),!0}return!1}function Fs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Is(e)){if(t)throw Error(i(479))}else t=oi(e,n,r,2),t!==null&&hu(t,e,2)}function Is(e){var t=e.alternate;return e===B||t!==null&&t===B}function Ls(e,t){go=ho=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Rs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ot(e,n)}}var zs={readContext:ra,use:Po,useCallback:H,useContext:H,useEffect:H,useImperativeHandle:H,useLayoutEffect:H,useInsertionEffect:H,useMemo:H,useReducer:H,useRef:H,useState:H,useDebugValue:H,useDeferredValue:H,useTransition:H,useSyncExternalStore:H,useId:H,useHostTransitionStatus:H,useFormState:H,useActionState:H,useOptimistic:H,useMemoCache:H,useCacheRefresh:H};zs.useEffectEvent=H;var Bs={readContext:ra,use:Po,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:ra,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,gs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=Ao();t=t===void 0?null:t;var r=e();if(_o){Ge(!0);try{e()}finally{Ge(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Ao();if(n!==void 0){var i=n(t);if(_o){Ge(!0);try{n(t)}finally{Ge(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ms.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=Ns.bind(null,B,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:vs,useDeferredValue:function(e,t){return xs(Ao(),e,t)},useTransition:function(){var e=Ko(!1);return e=Cs.bind(null,B,e.queue,!0,!1),Ao().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=B,a=Ao();if(L){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Vo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,us(Uo.bind(null,r,o,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,o,n,t),null),n},useId:function(){var e=Ao(),t=K.identifierPrefix;if(L){var n=Ai,r=ki;n=(r&~(1<<32-Ke(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=vo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[pt]=t,o[mt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Pc(t)}}return U(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=_e.current,Ui(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ii,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[pt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Bi(t,!0)}else e=Bd(e).createTextNode(r),e[pt]=t,t.stateNode=e}return U(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ui(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[pt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),e=!1}else n=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(uo(t),t):(uo(t),null);if(t.flags&128)throw Error(i(558))}return U(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ui(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[pt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),a=!1}else a=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(uo(t),t):(uo(t),null)}return uo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Lc(t,t.updateQueue),U(t),null);case 4:return be(),e===null&&Sd(t.stateNode.containerInfo),U(t),null;case 10:return Zi(t.type),U(t),null;case 19:if(me(z),r=t.memoizedState,r===null)return U(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Rc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=fo(e),o!==null){for(t.flags|=128,Rc(r,!1),e=o.updateQueue,t.updateQueue=e,Lc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)mi(n,e),n=n.sibling;return O(z,z.current&1|2),L&&ji(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Pe()>tu&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304)}else{if(!a)if(e=fo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Lc(t,e),Rc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!L)return U(t),null}else 2*Pe()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(U(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Pe(),e.sibling=null,n=z.current,O(z,a?n&1|2:n&1),L&&ji(t,r.treeForkCount),e);case 22:case 23:return uo(t),ro(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(U(t),t.subtreeFlags&6&&(t.flags|=8192)):U(t),n=t.updateQueue,n!==null&&Lc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&me(ya),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Zi(R),U(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Bc(e,t){switch(Pi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Zi(R),be(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Se(t),null;case 31:if(t.memoizedState!==null){if(uo(t),t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(uo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return me(z),null;case 4:return be(),null;case 10:return Zi(t.type),null;case 22:case 23:return uo(t),ro(),e!==null&&me(ya),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Zi(R),null;case 25:return null;default:return null}}function Vc(e,t){switch(Pi(t),t.tag){case 3:Zi(R),be();break;case 26:case 27:case 5:Se(t);break;case 4:be();break;case 31:t.memoizedState!==null&&uo(t);break;case 13:uo(t);break;case 19:me(z);break;case 10:Zi(t.type);break;case 22:case 23:uo(t),ro(),e!==null&&me(ya);break;case 24:Zi(R)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Qa(t,n)}catch(t){Z(e,e.return,t)}}}function Gc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[mt]=t}catch(t){Z(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=on));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[pt]=e,t[mt]=n}catch(t){Z(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=jr(e),Mr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[pt]=e,M(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=kr(s,h),v=kr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,E.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),We&&typeof We.onPostCommitFiberRoot==`function`)try{We.onPostCommitFiberRoot(Ue,o)}catch{}return!0}finally{D.p=a,E.T=r,Vu(e,t)}}function Wu(e,t,n){t=xi(n,t),t=$s(e.stateNode,t,2),e=Ga(e,t,2),e!==null&&(rt(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=xi(n,e),n=ec(2),r=Ga(t,n,2),r!==null&&(tc(n,r,t,e),rt(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>Pe()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=tt()),e=si(e,t),e!==null&&(rt(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Ae(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ke(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=A(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||j(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Pe(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Wt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),M(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Wt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Wt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Wt(n.imageSizes)+`"]`)):i+=`[href="`+Wt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),M(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Wt(r)+`"][href="`+Wt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),M(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Tt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);M(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Tt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),M(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Tt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),M(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=_e.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Tt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=Tt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Tt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Wt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),M(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Wt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Wt(n.href)+`"]`);if(r)return t.instance=r,M(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),M(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,M(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),M(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,M(a),a):(r=n,(a=mf.get(o))&&(r=m({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),M(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,M(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),M(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},y={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},b=new class{#e=y;setTimeoutProvider(e){this.#e=e}setTimeout(e,t){return this.#e.setTimeout(e,t)}clearTimeout(e){this.#e.clearTimeout(e)}setInterval(e,t){return this.#e.setInterval(e,t)}clearInterval(e){this.#e.clearInterval(e)}};function x(e){setTimeout(e,0)}var S=typeof window>`u`||`Deno`in globalThis;function C(){}function w(e,t){return typeof e==`function`?e(t):e}function ee(e){return typeof e==`number`&&e>=0&&e!==1/0}function te(e,t){return Math.max(e+(t||0)-Date.now(),0)}function ne(e,t){return typeof e==`function`?e(t):e}function T(e,t){return typeof e==`function`?e(t):e}function re(e,t){let{type:n=`all`,exact:r,fetchStatus:i,predicate:a,queryKey:o,stale:s}=e;if(o){if(r){if(t.queryHash!==ae(o,t.options))return!1}else if(!se(t.queryKey,o))return!1}if(n!==`all`){let e=t.isActive();if(n===`active`&&!e||n===`inactive`&&e)return!1}return!(typeof s==`boolean`&&t.isStale()!==s||i&&i!==t.state.fetchStatus||a&&!a(t))}function ie(e,t){let{exact:n,status:r,predicate:i,mutationKey:a}=e;if(a){if(!t.options.mutationKey)return!1;if(n){if(oe(t.options.mutationKey)!==oe(a))return!1}else if(!se(t.options.mutationKey,a))return!1}return!(r&&t.state.status!==r||i&&!i(t))}function ae(e,t){return(t?.queryKeyHashFn||oe)(e)}function oe(e){return JSON.stringify(e,(e,t)=>ue(t)?Object.keys(t).sort().reduce((e,n)=>(e[n]=t[n],e),{}):t)}function se(e,t){return e===t?!0:typeof e==typeof t&&e&&t&&typeof e==`object`&&typeof t==`object`?Object.keys(t).every(n=>se(e[n],t[n])):!1}var ce=Object.prototype.hasOwnProperty;function le(e,t,n=0){if(e===t)return e;if(n>500)return t;let r=D(e)&&D(t);if(!r&&!(ue(e)&&ue(t)))return t;let i=(r?e:Object.keys(e)).length,a=r?t:Object.keys(t),o=a.length,s=r?Array(o):{},c=0;for(let l=0;l{b.setTimeout(t,e)})}function pe(e,t,n){return typeof n.structuralSharing==`function`?n.structuralSharing(e,t):n.structuralSharing===!1?t:le(e,t)}function me(e,t,n=0){let r=[...e,t];return n&&r.length>n?r.slice(1):r}function O(e,t,n=0){let r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var he=Symbol();function ge(e,t){return!e.queryFn&&t?.initialPromise?()=>t.initialPromise:!e.queryFn||e.queryFn===he?()=>Promise.reject(Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function _e(e,t){return typeof e==`function`?e(...t):!!e}function ve(e,t,n){let r=!1,i;return Object.defineProperty(e,`signal`,{enumerable:!0,get:()=>(i??=t(),r?i:(r=!0,i.aborted?n():i.addEventListener(`abort`,n,{once:!0}),i))}),e}var ye=new class extends v{#e;#t;#n;constructor(){super(),this.#n=e=>{if(!S&&window.addEventListener){let t=()=>e();return window.addEventListener(`visibilitychange`,t,!1),()=>{window.removeEventListener(`visibilitychange`,t)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(e){this.#n=e,this.#t?.(),this.#t=e(e=>{typeof e==`boolean`?this.setFocused(e):this.onFocus()})}setFocused(e){this.#e!==e&&(this.#e=e,this.onFocus())}onFocus(){let e=this.isFocused();this.listeners.forEach(t=>{t(e)})}isFocused(){return typeof this.#e==`boolean`?this.#e:globalThis.document?.visibilityState!==`hidden`}};function be(){let e,t,n=new Promise((n,r)=>{e=n,t=r});n.status=`pending`,n.catch(()=>{});function r(e){Object.assign(n,e),delete n.resolve,delete n.reject}return n.resolve=t=>{r({status:`fulfilled`,value:t}),e(t)},n.reject=e=>{r({status:`rejected`,reason:e}),t(e)},n}var xe=x;function Se(){let e=[],t=0,n=e=>{e()},r=e=>{e()},i=xe,a=r=>{t?e.push(r):i(()=>{n(r)})},o=()=>{let t=e;e=[],t.length&&i(()=>{r(()=>{t.forEach(e=>{n(e)})})})};return{batch:e=>{let n;t++;try{n=e()}finally{t--,t||o()}return n},batchCalls:e=>(...t)=>{a(()=>{e(...t)})},schedule:a,setNotifyFunction:e=>{n=e},setBatchNotifyFunction:e=>{r=e},setScheduler:e=>{i=e}}}var k=Se(),Ce=new class extends v{#e=!0;#t;#n;constructor(){super(),this.#n=e=>{if(!S&&window.addEventListener){let t=()=>e(!0),n=()=>e(!1);return window.addEventListener(`online`,t,!1),window.addEventListener(`offline`,n,!1),()=>{window.removeEventListener(`online`,t),window.removeEventListener(`offline`,n)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(e){this.#n=e,this.#t?.(),this.#t=e(this.setOnline.bind(this))}setOnline(e){this.#e!==e&&(this.#e=e,this.listeners.forEach(t=>{t(e)}))}isOnline(){return this.#e}};function we(e){return Math.min(1e3*2**e,3e4)}function Te(e){return(e??`online`)===`online`?Ce.isOnline():!0}var Ee=class extends Error{constructor(e){super(`CancelledError`),this.revert=e?.revert,this.silent=e?.silent}};function De(e){let t=!1,n=0,r,i=be(),a=()=>i.status!==`pending`,o=t=>{if(!a()){let n=new Ee(t);f(n),e.onCancel?.(n)}},s=()=>{t=!0},c=()=>{t=!1},l=()=>ye.isFocused()&&(e.networkMode===`always`||Ce.isOnline())&&e.canRun(),u=()=>Te(e.networkMode)&&e.canRun(),d=e=>{a()||(r?.(),i.resolve(e))},f=e=>{a()||(r?.(),i.reject(e))},p=()=>new Promise(t=>{r=e=>{(a()||l())&&t(e)},e.onPause?.()}).then(()=>{r=void 0,a()||e.onContinue?.()}),m=()=>{if(a())return;let r,i=n===0?e.initialPromise:void 0;try{r=i??e.fn()}catch(e){r=Promise.reject(e)}Promise.resolve(r).then(d).catch(r=>{if(a())return;let i=e.retry??(S?0:3),o=e.retryDelay??we,s=typeof o==`function`?o(n,r):o,c=i===!0||typeof i==`number`&&nl()?void 0:p()).then(()=>{t?f(r):m()})})};return{promise:i,status:()=>i.status,cancel:o,continue:()=>(r?.(),i),cancelRetry:s,continueRetry:c,canStart:u,start:()=>(u()?m():p().then(m),i)}}var Oe=class{#e;destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),ee(this.gcTime)&&(this.#e=b.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(S?1/0:300*1e3))}clearGcTimeout(){this.#e&&=(b.clearTimeout(this.#e),void 0)}},ke=class extends Oe{#e;#t;#n;#r;#i;#a;#o;constructor(e){super(),this.#o=!1,this.#a=e.defaultOptions,this.setOptions(e.options),this.observers=[],this.#r=e.client,this.#n=this.#r.getQueryCache(),this.queryKey=e.queryKey,this.queryHash=e.queryHash,this.#e=Me(this.options),this.state=e.state??this.#e,this.scheduleGc()}get meta(){return this.options.meta}get promise(){return this.#i?.promise}setOptions(e){if(this.options={...this.#a,...e},this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){let e=Me(this.options);e.data!==void 0&&(this.setState(je(e.data,e.dataUpdatedAt)),this.#e=e)}}optionalRemove(){!this.observers.length&&this.state.fetchStatus===`idle`&&this.#n.remove(this)}setData(e,t){let n=pe(this.state.data,e,this.options);return this.#s({data:n,type:`success`,dataUpdatedAt:t?.updatedAt,manual:t?.manual}),n}setState(e,t){this.#s({type:`setState`,state:e,setStateOptions:t})}cancel(e){let t=this.#i?.promise;return this.#i?.cancel(e),t?t.then(C).catch(C):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}reset(){this.destroy(),this.setState(this.#e)}isActive(){return this.observers.some(e=>T(e.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===he||this.state.dataUpdateCount+this.state.errorUpdateCount===0}isStatic(){return this.getObserversCount()>0?this.observers.some(e=>ne(e.options.staleTime,this)===`static`):!1}isStale(){return this.getObserversCount()>0?this.observers.some(e=>e.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(e=0){return this.state.data===void 0?!0:e===`static`?!1:this.state.isInvalidated?!0:!te(this.state.dataUpdatedAt,e)}onFocus(){this.observers.find(e=>e.shouldFetchOnWindowFocus())?.refetch({cancelRefetch:!1}),this.#i?.continue()}onOnline(){this.observers.find(e=>e.shouldFetchOnReconnect())?.refetch({cancelRefetch:!1}),this.#i?.continue()}addObserver(e){this.observers.includes(e)||(this.observers.push(e),this.clearGcTimeout(),this.#n.notify({type:`observerAdded`,query:this,observer:e}))}removeObserver(e){this.observers.includes(e)&&(this.observers=this.observers.filter(t=>t!==e),this.observers.length||(this.#i&&(this.#o?this.#i.cancel({revert:!0}):this.#i.cancelRetry()),this.scheduleGc()),this.#n.notify({type:`observerRemoved`,query:this,observer:e}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||this.#s({type:`invalidate`})}async fetch(e,t){if(this.state.fetchStatus!==`idle`&&this.#i?.status()!==`rejected`){if(this.state.data!==void 0&&t?.cancelRefetch)this.cancel({silent:!0});else if(this.#i)return this.#i.continueRetry(),this.#i.promise}if(e&&this.setOptions(e),!this.options.queryFn){let e=this.observers.find(e=>e.options.queryFn);e&&this.setOptions(e.options)}let n=new AbortController,r=e=>{Object.defineProperty(e,`signal`,{enumerable:!0,get:()=>(this.#o=!0,n.signal)})},i=()=>{let e=ge(this.options,t),n=(()=>{let e={client:this.#r,queryKey:this.queryKey,meta:this.meta};return r(e),e})();return this.#o=!1,this.options.persister?this.options.persister(e,n,this):e(n)},a=(()=>{let e={fetchOptions:t,options:this.options,queryKey:this.queryKey,client:this.#r,state:this.state,fetchFn:i};return r(e),e})();this.options.behavior?.onFetch(a,this),this.#t=this.state,(this.state.fetchStatus===`idle`||this.state.fetchMeta!==a.fetchOptions?.meta)&&this.#s({type:`fetch`,meta:a.fetchOptions?.meta}),this.#i=De({initialPromise:t?.initialPromise,fn:a.fetchFn,onCancel:e=>{e instanceof Ee&&e.revert&&this.setState({...this.#t,fetchStatus:`idle`}),n.abort()},onFail:(e,t)=>{this.#s({type:`failed`,failureCount:e,error:t})},onPause:()=>{this.#s({type:`pause`})},onContinue:()=>{this.#s({type:`continue`})},retry:a.options.retry,retryDelay:a.options.retryDelay,networkMode:a.options.networkMode,canRun:()=>!0});try{let e=await this.#i.start();if(e===void 0)throw Error(`${this.queryHash} data is undefined`);return this.setData(e),this.#n.config.onSuccess?.(e,this),this.#n.config.onSettled?.(e,this.state.error,this),e}catch(e){if(e instanceof Ee){if(e.silent)return this.#i.promise;if(e.revert){if(this.state.data===void 0)throw e;return this.state.data}}throw this.#s({type:`error`,error:e}),this.#n.config.onError?.(e,this),this.#n.config.onSettled?.(this.state.data,e,this),e}finally{this.scheduleGc()}}#s(e){this.state=(t=>{switch(e.type){case`failed`:return{...t,fetchFailureCount:e.failureCount,fetchFailureReason:e.error};case`pause`:return{...t,fetchStatus:`paused`};case`continue`:return{...t,fetchStatus:`fetching`};case`fetch`:return{...t,...Ae(t.data,this.options),fetchMeta:e.meta??null};case`success`:let n={...t,...je(e.data,e.dataUpdatedAt),dataUpdateCount:t.dataUpdateCount+1,...!e.manual&&{fetchStatus:`idle`,fetchFailureCount:0,fetchFailureReason:null}};return this.#t=e.manual?n:void 0,n;case`error`:let r=e.error;return{...t,error:r,errorUpdateCount:t.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:t.fetchFailureCount+1,fetchFailureReason:r,fetchStatus:`idle`,status:`error`,isInvalidated:!0};case`invalidate`:return{...t,isInvalidated:!0};case`setState`:return{...t,...e.state}}})(this.state),k.batch(()=>{this.observers.forEach(e=>{e.onQueryUpdate()}),this.#n.notify({query:this,type:`updated`,action:e})})}};function Ae(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:Te(t.networkMode)?`fetching`:`paused`,...e===void 0&&{error:null,status:`pending`}}}function je(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:`success`}}function Me(e){let t=typeof e.initialData==`function`?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt==`function`?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?`success`:`pending`,fetchStatus:`idle`}}var Ne=class extends v{constructor(e,t){super(),this.options=t,this.#e=e,this.#s=null,this.#o=be(),this.bindMethods(),this.setOptions(t)}#e;#t=void 0;#n=void 0;#r=void 0;#i;#a;#o;#s;#c;#l;#u;#d;#f;#p;#m=new Set;bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(this.#t.addObserver(this),Fe(this.#t,this.options)?this.#h():this.updateResult(),this.#y())}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Ie(this.#t,this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Ie(this.#t,this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,this.#b(),this.#x(),this.#t.removeObserver(this)}setOptions(e){let t=this.options,n=this.#t;if(this.options=this.#e.defaultQueryOptions(e),this.options.enabled!==void 0&&typeof this.options.enabled!=`boolean`&&typeof this.options.enabled!=`function`&&typeof T(this.options.enabled,this.#t)!=`boolean`)throw Error(`Expected enabled to be a boolean or a callback that returns a boolean`);this.#S(),this.#t.setOptions(this.options),t._defaulted&&!E(this.options,t)&&this.#e.getQueryCache().notify({type:`observerOptionsUpdated`,query:this.#t,observer:this});let r=this.hasListeners();r&&Le(this.#t,n,this.options,t)&&this.#h(),this.updateResult(),r&&(this.#t!==n||T(this.options.enabled,this.#t)!==T(t.enabled,this.#t)||ne(this.options.staleTime,this.#t)!==ne(t.staleTime,this.#t))&&this.#g();let i=this.#_();r&&(this.#t!==n||T(this.options.enabled,this.#t)!==T(t.enabled,this.#t)||i!==this.#p)&&this.#v(i)}getOptimisticResult(e){let t=this.#e.getQueryCache().build(this.#e,e),n=this.createResult(t,e);return ze(this,n)&&(this.#r=n,this.#a=this.options,this.#i=this.#t.state),n}getCurrentResult(){return this.#r}trackResult(e,t){return new Proxy(e,{get:(e,n)=>(this.trackProp(n),t?.(n),n===`promise`&&(this.trackProp(`data`),!this.options.experimental_prefetchInRender&&this.#o.status===`pending`&&this.#o.reject(Error(`experimental_prefetchInRender feature flag is not enabled`))),Reflect.get(e,n))})}trackProp(e){this.#m.add(e)}getCurrentQuery(){return this.#t}refetch({...e}={}){return this.fetch({...e})}fetchOptimistic(e){let t=this.#e.defaultQueryOptions(e),n=this.#e.getQueryCache().build(this.#e,t);return n.fetch().then(()=>this.createResult(n,t))}fetch(e){return this.#h({...e,cancelRefetch:e.cancelRefetch??!0}).then(()=>(this.updateResult(),this.#r))}#h(e){this.#S();let t=this.#t.fetch(this.options,e);return e?.throwOnError||(t=t.catch(C)),t}#g(){this.#b();let e=ne(this.options.staleTime,this.#t);if(S||this.#r.isStale||!ee(e))return;let t=te(this.#r.dataUpdatedAt,e)+1;this.#d=b.setTimeout(()=>{this.#r.isStale||this.updateResult()},t)}#_(){return(typeof this.options.refetchInterval==`function`?this.options.refetchInterval(this.#t):this.options.refetchInterval)??!1}#v(e){this.#x(),this.#p=e,!(S||T(this.options.enabled,this.#t)===!1||!ee(this.#p)||this.#p===0)&&(this.#f=b.setInterval(()=>{(this.options.refetchIntervalInBackground||ye.isFocused())&&this.#h()},this.#p))}#y(){this.#g(),this.#v(this.#_())}#b(){this.#d&&=(b.clearTimeout(this.#d),void 0)}#x(){this.#f&&=(b.clearInterval(this.#f),void 0)}createResult(e,t){let n=this.#t,r=this.options,i=this.#r,a=this.#i,o=this.#a,s=e===n?this.#n:e.state,{state:c}=e,l={...c},u=!1,d;if(t._optimisticResults){let i=this.hasListeners(),a=!i&&Fe(e,t),o=i&&Le(e,n,t,r);(a||o)&&(l={...l,...Ae(c.data,e.options)}),t._optimisticResults===`isRestoring`&&(l.fetchStatus=`idle`)}let{error:f,errorUpdatedAt:p,status:m}=l;d=l.data;let h=!1;if(t.placeholderData!==void 0&&d===void 0&&m===`pending`){let e;i?.isPlaceholderData&&t.placeholderData===o?.placeholderData?(e=i.data,h=!0):e=typeof t.placeholderData==`function`?t.placeholderData(this.#u?.state.data,this.#u):t.placeholderData,e!==void 0&&(m=`success`,d=pe(i?.data,e,t),u=!0)}if(t.select&&d!==void 0&&!h)if(i&&d===a?.data&&t.select===this.#c)d=this.#l;else try{this.#c=t.select,d=t.select(d),d=pe(i?.data,d,t),this.#l=d,this.#s=null}catch(e){this.#s=e}this.#s&&(f=this.#s,d=this.#l,p=Date.now(),m=`error`);let g=l.fetchStatus===`fetching`,_=m===`pending`,v=m===`error`,y=_&&g,b=d!==void 0,x={status:m,fetchStatus:l.fetchStatus,isPending:_,isSuccess:m===`success`,isError:v,isInitialLoading:y,isLoading:y,data:d,dataUpdatedAt:l.dataUpdatedAt,error:f,errorUpdatedAt:p,failureCount:l.fetchFailureCount,failureReason:l.fetchFailureReason,errorUpdateCount:l.errorUpdateCount,isFetched:l.dataUpdateCount>0||l.errorUpdateCount>0,isFetchedAfterMount:l.dataUpdateCount>s.dataUpdateCount||l.errorUpdateCount>s.errorUpdateCount,isFetching:g,isRefetching:g&&!_,isLoadingError:v&&!b,isPaused:l.fetchStatus===`paused`,isPlaceholderData:u,isRefetchError:v&&b,isStale:Re(e,t),refetch:this.refetch,promise:this.#o,isEnabled:T(t.enabled,e)!==!1};if(this.options.experimental_prefetchInRender){let t=x.data!==void 0,r=x.status===`error`&&!t,i=e=>{r?e.reject(x.error):t&&e.resolve(x.data)},a=()=>{i(this.#o=x.promise=be())},o=this.#o;switch(o.status){case`pending`:e.queryHash===n.queryHash&&i(o);break;case`fulfilled`:(r||x.data!==o.value)&&a();break;case`rejected`:(!r||x.error!==o.reason)&&a();break}}return x}updateResult(){let e=this.#r,t=this.createResult(this.#t,this.options);this.#i=this.#t.state,this.#a=this.options,this.#i.data!==void 0&&(this.#u=this.#t),!E(t,e)&&(this.#r=t,this.#C({listeners:(()=>{if(!e)return!0;let{notifyOnChangeProps:t}=this.options,n=typeof t==`function`?t():t;if(n===`all`||!n&&!this.#m.size)return!0;let r=new Set(n??this.#m);return this.options.throwOnError&&r.add(`error`),Object.keys(this.#r).some(t=>{let n=t;return this.#r[n]!==e[n]&&r.has(n)})})()}))}#S(){let e=this.#e.getQueryCache().build(this.#e,this.options);if(e===this.#t)return;let t=this.#t;this.#t=e,this.#n=e.state,this.hasListeners()&&(t?.removeObserver(this),e.addObserver(this))}onQueryUpdate(){this.updateResult(),this.hasListeners()&&this.#y()}#C(e){k.batch(()=>{e.listeners&&this.listeners.forEach(e=>{e(this.#r)}),this.#e.getQueryCache().notify({query:this.#t,type:`observerResultsUpdated`})})}};function Pe(e,t){return T(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status===`error`&&t.retryOnMount===!1)}function Fe(e,t){return Pe(e,t)||e.state.data!==void 0&&Ie(e,t,t.refetchOnMount)}function Ie(e,t,n){if(T(t.enabled,e)!==!1&&ne(t.staleTime,e)!==`static`){let r=typeof n==`function`?n(e):n;return r===`always`||r!==!1&&Re(e,t)}return!1}function Le(e,t,n,r){return(e!==t||T(r.enabled,e)===!1)&&(!n.suspense||e.state.status!==`error`)&&Re(e,n)}function Re(e,t){return T(t.enabled,e)!==!1&&e.isStaleByTime(ne(t.staleTime,e))}function ze(e,t){return!E(e.getCurrentResult(),t)}function Be(e){return{onFetch:(t,n)=>{let r=t.options,i=t.fetchOptions?.meta?.fetchMore?.direction,a=t.state.data?.pages||[],o=t.state.data?.pageParams||[],s={pages:[],pageParams:[]},c=0,l=async()=>{let n=!1,l=e=>{ve(e,()=>t.signal,()=>n=!0)},u=ge(t.options,t.fetchOptions),d=async(e,r,i)=>{if(n)return Promise.reject();if(r==null&&e.pages.length)return Promise.resolve(e);let a=await u((()=>{let e={client:t.client,queryKey:t.queryKey,pageParam:r,direction:i?`backward`:`forward`,meta:t.options.meta};return l(e),e})()),{maxPages:o}=t.options,s=i?O:me;return{pages:s(e.pages,a,o),pageParams:s(e.pageParams,r,o)}};if(i&&a.length){let e=i===`backward`,t=e?He:Ve,n={pages:a,pageParams:o};s=await d(n,t(r,n),e)}else{let t=e??a.length;do{let e=c===0?o[0]??r.initialPageParam:Ve(r,s);if(c>0&&e==null)break;s=await d(s,e),c++}while(ct.options.persister?.(l,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n):t.fetchFn=l}}}function Ve(e,{pages:t,pageParams:n}){let r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function He(e,{pages:t,pageParams:n}){return t.length>0?e.getPreviousPageParam?.(t[0],t,n[0],n):void 0}var Ue=class extends Oe{#e;#t;#n;#r;constructor(e){super(),this.#e=e.client,this.mutationId=e.mutationId,this.#n=e.mutationCache,this.#t=[],this.state=e.state||We(),this.setOptions(e.options),this.scheduleGc()}setOptions(e){this.options=e,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(e){this.#t.includes(e)||(this.#t.push(e),this.clearGcTimeout(),this.#n.notify({type:`observerAdded`,mutation:this,observer:e}))}removeObserver(e){this.#t=this.#t.filter(t=>t!==e),this.scheduleGc(),this.#n.notify({type:`observerRemoved`,mutation:this,observer:e})}optionalRemove(){this.#t.length||(this.state.status===`pending`?this.scheduleGc():this.#n.remove(this))}continue(){return this.#r?.continue()??this.execute(this.state.variables)}async execute(e){let t=()=>{this.#i({type:`continue`})},n={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};this.#r=De({fn:()=>this.options.mutationFn?this.options.mutationFn(e,n):Promise.reject(Error(`No mutationFn found`)),onFail:(e,t)=>{this.#i({type:`failed`,failureCount:e,error:t})},onPause:()=>{this.#i({type:`pause`})},onContinue:t,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>this.#n.canRun(this)});let r=this.state.status===`pending`,i=!this.#r.canStart();try{if(r)t();else{this.#i({type:`pending`,variables:e,isPaused:i}),this.#n.config.onMutate&&await this.#n.config.onMutate(e,this,n);let t=await this.options.onMutate?.(e,n);t!==this.state.context&&this.#i({type:`pending`,context:t,variables:e,isPaused:i})}let a=await this.#r.start();return await this.#n.config.onSuccess?.(a,e,this.state.context,this,n),await this.options.onSuccess?.(a,e,this.state.context,n),await this.#n.config.onSettled?.(a,null,this.state.variables,this.state.context,this,n),await this.options.onSettled?.(a,null,e,this.state.context,n),this.#i({type:`success`,data:a}),a}catch(t){try{await this.#n.config.onError?.(t,e,this.state.context,this,n)}catch(e){Promise.reject(e)}try{await this.options.onError?.(t,e,this.state.context,n)}catch(e){Promise.reject(e)}try{await this.#n.config.onSettled?.(void 0,t,this.state.variables,this.state.context,this,n)}catch(e){Promise.reject(e)}try{await this.options.onSettled?.(void 0,t,e,this.state.context,n)}catch(e){Promise.reject(e)}throw this.#i({type:`error`,error:t}),t}finally{this.#n.runNext(this)}}#i(e){this.state=(t=>{switch(e.type){case`failed`:return{...t,failureCount:e.failureCount,failureReason:e.error};case`pause`:return{...t,isPaused:!0};case`continue`:return{...t,isPaused:!1};case`pending`:return{...t,context:e.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:e.isPaused,status:`pending`,variables:e.variables,submittedAt:Date.now()};case`success`:return{...t,data:e.data,failureCount:0,failureReason:null,error:null,status:`success`,isPaused:!1};case`error`:return{...t,data:void 0,error:e.error,failureCount:t.failureCount+1,failureReason:e.error,isPaused:!1,status:`error`}}})(this.state),k.batch(()=>{this.#t.forEach(t=>{t.onMutationUpdate(e)}),this.#n.notify({mutation:this,type:`updated`,action:e})})}};function We(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:`idle`,variables:void 0,submittedAt:0}}var Ge=class extends v{constructor(e={}){super(),this.config=e,this.#e=new Set,this.#t=new Map,this.#n=0}#e;#t;#n;build(e,t,n){let r=new Ue({client:e,mutationCache:this,mutationId:++this.#n,options:e.defaultMutationOptions(t),state:n});return this.add(r),r}add(e){this.#e.add(e);let t=Ke(e);if(typeof t==`string`){let n=this.#t.get(t);n?n.push(e):this.#t.set(t,[e])}this.notify({type:`added`,mutation:e})}remove(e){if(this.#e.delete(e)){let t=Ke(e);if(typeof t==`string`){let n=this.#t.get(t);if(n)if(n.length>1){let t=n.indexOf(e);t!==-1&&n.splice(t,1)}else n[0]===e&&this.#t.delete(t)}}this.notify({type:`removed`,mutation:e})}canRun(e){let t=Ke(e);if(typeof t==`string`){let n=this.#t.get(t)?.find(e=>e.state.status===`pending`);return!n||n===e}else return!0}runNext(e){let t=Ke(e);return typeof t==`string`?(this.#t.get(t)?.find(t=>t!==e&&t.state.isPaused))?.continue()??Promise.resolve():Promise.resolve()}clear(){k.batch(()=>{this.#e.forEach(e=>{this.notify({type:`removed`,mutation:e})}),this.#e.clear(),this.#t.clear()})}getAll(){return Array.from(this.#e)}find(e){let t={exact:!0,...e};return this.getAll().find(e=>ie(t,e))}findAll(e={}){return this.getAll().filter(t=>ie(e,t))}notify(e){k.batch(()=>{this.listeners.forEach(t=>{t(e)})})}resumePausedMutations(){let e=this.getAll().filter(e=>e.state.isPaused);return k.batch(()=>Promise.all(e.map(e=>e.continue().catch(C))))}};function Ke(e){return e.options.scope?.id}function qe(e,t){let n=new Set(t);return e.filter(e=>!n.has(e))}function Je(e,t,n){let r=e.slice(0);return r[t]=n,r}var Ye=class extends v{#e;#t;#n;#r;#i;#a;#o;#s;#c;#l=[];constructor(e,t,n){super(),this.#e=e,this.#r=n,this.#n=[],this.#i=[],this.#t=[],this.setQueries(t)}onSubscribe(){this.listeners.size===1&&this.#i.forEach(e=>{e.subscribe(t=>{this.#p(e,t)})})}onUnsubscribe(){this.listeners.size||this.destroy()}destroy(){this.listeners=new Set,this.#i.forEach(e=>{e.destroy()})}setQueries(e,t){this.#n=e,this.#r=t,k.batch(()=>{let e=this.#i,t=this.#f(this.#n);t.forEach(e=>e.observer.setOptions(e.defaultedQueryOptions));let n=t.map(e=>e.observer),r=n.map(e=>e.getCurrentResult()),i=e.length!==n.length,a=n.some((t,n)=>t!==e[n]),o=i||a,s=o?!0:r.some((e,t)=>{let n=this.#t[t];return!n||!E(e,n)});!o&&!s||(o&&(this.#l=t,this.#i=n),this.#t=r,this.hasListeners()&&(o&&(qe(e,n).forEach(e=>{e.destroy()}),qe(n,e).forEach(e=>{e.subscribe(t=>{this.#p(e,t)})})),this.#m()))})}getCurrentResult(){return this.#t}getQueries(){return this.#i.map(e=>e.getCurrentQuery())}getObservers(){return this.#i}getOptimisticResult(e,t){let n=this.#f(e),r=n.map(e=>e.observer.getOptimisticResult(e.defaultedQueryOptions)),i=n.map(e=>e.defaultedQueryOptions.queryHash);return[r,e=>this.#d(e??r,t,i),()=>this.#u(r,n)]}#u(e,t){return t.map((n,r)=>{let i=e[r];return n.defaultedQueryOptions.notifyOnChangeProps?i:n.observer.trackResult(i,e=>{t.forEach(t=>{t.observer.trackProp(e)})})})}#d(e,t,n){if(t){let r=this.#c,i=n!==void 0&&r!==void 0&&(r.length!==n.length||n.some((e,t)=>e!==r[t]));return(!this.#a||this.#t!==this.#s||i||t!==this.#o)&&(this.#o=t,this.#s=this.#t,n!==void 0&&(this.#c=n),this.#a=le(this.#a,t(e))),this.#a}return e}#f(e){let t=new Map;this.#i.forEach(e=>{let n=e.options.queryHash;if(!n)return;let r=t.get(n);r?r.push(e):t.set(n,[e])});let n=[];return e.forEach(e=>{let r=this.#e.defaultQueryOptions(e),i=t.get(r.queryHash)?.shift()??new Ne(this.#e,r);n.push({defaultedQueryOptions:r,observer:i})}),n}#p(e,t){let n=this.#i.indexOf(e);n!==-1&&(this.#t=Je(this.#t,n,t),this.#m())}#m(){if(this.hasListeners()){let e=this.#a,t=this.#u(this.#t,this.#l);e!==this.#d(t,this.#r?.combine)&&k.batch(()=>{this.listeners.forEach(e=>{e(this.#t)})})}}},Xe=class extends v{constructor(e={}){super(),this.config=e,this.#e=new Map}#e;build(e,t,n){let r=t.queryKey,i=t.queryHash??ae(r,t),a=this.get(i);return a||(a=new ke({client:e,queryKey:r,queryHash:i,options:e.defaultQueryOptions(t),state:n,defaultOptions:e.getQueryDefaults(r)}),this.add(a)),a}add(e){this.#e.has(e.queryHash)||(this.#e.set(e.queryHash,e),this.notify({type:`added`,query:e}))}remove(e){let t=this.#e.get(e.queryHash);t&&(e.destroy(),t===e&&this.#e.delete(e.queryHash),this.notify({type:`removed`,query:e}))}clear(){k.batch(()=>{this.getAll().forEach(e=>{this.remove(e)})})}get(e){return this.#e.get(e)}getAll(){return[...this.#e.values()]}find(e){let t={exact:!0,...e};return this.getAll().find(e=>re(t,e))}findAll(e={}){let t=this.getAll();return Object.keys(e).length>0?t.filter(t=>re(e,t)):t}notify(e){k.batch(()=>{this.listeners.forEach(t=>{t(e)})})}onFocus(){k.batch(()=>{this.getAll().forEach(e=>{e.onFocus()})})}onOnline(){k.batch(()=>{this.getAll().forEach(e=>{e.onOnline()})})}},Ze=class{#e;#t;#n;#r;#i;#a;#o;#s;constructor(e={}){this.#e=e.queryCache||new Xe,this.#t=e.mutationCache||new Ge,this.#n=e.defaultOptions||{},this.#r=new Map,this.#i=new Map,this.#a=0}mount(){this.#a++,this.#a===1&&(this.#o=ye.subscribe(async e=>{e&&(await this.resumePausedMutations(),this.#e.onFocus())}),this.#s=Ce.subscribe(async e=>{e&&(await this.resumePausedMutations(),this.#e.onOnline())}))}unmount(){this.#a--,this.#a===0&&(this.#o?.(),this.#o=void 0,this.#s?.(),this.#s=void 0)}isFetching(e){return this.#e.findAll({...e,fetchStatus:`fetching`}).length}isMutating(e){return this.#t.findAll({...e,status:`pending`}).length}getQueryData(e){let t=this.defaultQueryOptions({queryKey:e});return this.#e.get(t.queryHash)?.state.data}ensureQueryData(e){let t=this.defaultQueryOptions(e),n=this.#e.build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(ne(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return this.#e.findAll(e).map(({queryKey:e,state:t})=>[e,t.data])}setQueryData(e,t,n){let r=this.defaultQueryOptions({queryKey:e}),i=this.#e.get(r.queryHash)?.state.data,a=w(t,i);if(a!==void 0)return this.#e.build(this,r).setData(a,{...n,manual:!0})}setQueriesData(e,t,n){return k.batch(()=>this.#e.findAll(e).map(({queryKey:e})=>[e,this.setQueryData(e,t,n)]))}getQueryState(e){let t=this.defaultQueryOptions({queryKey:e});return this.#e.get(t.queryHash)?.state}removeQueries(e){let t=this.#e;k.batch(()=>{t.findAll(e).forEach(e=>{t.remove(e)})})}resetQueries(e,t){let n=this.#e;return k.batch(()=>(n.findAll(e).forEach(e=>{e.reset()}),this.refetchQueries({type:`active`,...e},t)))}cancelQueries(e,t={}){let n={revert:!0,...t},r=k.batch(()=>this.#e.findAll(e).map(e=>e.cancel(n)));return Promise.all(r).then(C).catch(C)}invalidateQueries(e,t={}){return k.batch(()=>(this.#e.findAll(e).forEach(e=>{e.invalidate()}),e?.refetchType===`none`?Promise.resolve():this.refetchQueries({...e,type:e?.refetchType??e?.type??`active`},t)))}refetchQueries(e,t={}){let n={...t,cancelRefetch:t.cancelRefetch??!0},r=k.batch(()=>this.#e.findAll(e).filter(e=>!e.isDisabled()&&!e.isStatic()).map(e=>{let t=e.fetch(void 0,n);return n.throwOnError||(t=t.catch(C)),e.state.fetchStatus===`paused`?Promise.resolve():t}));return Promise.all(r).then(C)}fetchQuery(e){let t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);let n=this.#e.build(this,t);return n.isStaleByTime(ne(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(C).catch(C)}fetchInfiniteQuery(e){return e.behavior=Be(e.pages),this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(C).catch(C)}ensureInfiniteQueryData(e){return e.behavior=Be(e.pages),this.ensureQueryData(e)}resumePausedMutations(){return Ce.isOnline()?this.#t.resumePausedMutations():Promise.resolve()}getQueryCache(){return this.#e}getMutationCache(){return this.#t}getDefaultOptions(){return this.#n}setDefaultOptions(e){this.#n=e}setQueryDefaults(e,t){this.#r.set(oe(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){let t=[...this.#r.values()],n={};return t.forEach(t=>{se(e,t.queryKey)&&Object.assign(n,t.defaultOptions)}),n}setMutationDefaults(e,t){this.#i.set(oe(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){let t=[...this.#i.values()],n={};return t.forEach(t=>{se(e,t.mutationKey)&&Object.assign(n,t.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;let t={...this.#n.queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||=ae(t.queryKey,t),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!==`always`),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode=`offlineFirst`),t.queryFn===he&&(t.enabled=!1),t}defaultMutationOptions(e){return e?._defaulted?e:{...this.#n.mutations,...e?.mutationKey&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){this.#e.clear(),this.#t.clear()}},Qe=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),$e=o(((e,t)=>{t.exports=Qe()})),A=l(d(),1),j=$e(),et=A.createContext(void 0),tt=e=>{let t=A.useContext(et);if(e)return e;if(!t)throw Error(`No QueryClient set, use QueryClientProvider to set one`);return t},nt=({client:e,children:t})=>(A.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),(0,j.jsx)(et.Provider,{value:e,children:t})),rt=A.createContext(!1),it=()=>A.useContext(rt);rt.Provider;function at(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var ot=A.createContext(at()),st=()=>A.useContext(ot),ct=(e,t,n)=>{let r=n?.state.error&&typeof e.throwOnError==`function`?_e(e.throwOnError,[n.state.error,n]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||r)&&(t.isReset()||(e.retryOnMount=!1))},lt=e=>{A.useEffect(()=>{e.clearReset()},[e])},ut=({result:e,errorResetBoundary:t,throwOnError:n,query:r,suspense:i})=>e.isError&&!t.isReset()&&!e.isFetching&&r&&(i&&e.data===void 0||_e(n,[e.error,r])),dt=e=>{if(e.suspense){let t=1e3,n=e=>e===`static`?e:Math.max(e??t,t),r=e.staleTime;e.staleTime=typeof r==`function`?(...e)=>n(r(...e)):n(r),typeof e.gcTime==`number`&&(e.gcTime=Math.max(e.gcTime,t))}},ft=(e,t)=>e.isLoading&&e.isFetching&&!t,pt=(e,t)=>e?.suspense&&t.isPending,mt=(e,t,n)=>t.fetchOptimistic(e).catch(()=>{n.clearReset()});function ht({queries:e,...t},n){let r=tt(n),i=it(),a=st(),o=A.useMemo(()=>e.map(e=>{let t=r.defaultQueryOptions(e);return t._optimisticResults=i?`isRestoring`:`optimistic`,t}),[e,r,i]);o.forEach(e=>{dt(e),ct(e,a,r.getQueryCache().get(e.queryHash))}),lt(a);let[s]=A.useState(()=>new Ye(r,o,t)),[c,l,u]=s.getOptimisticResult(o,t.combine),d=!i&&t.subscribed!==!1;A.useSyncExternalStore(A.useCallback(e=>d?s.subscribe(k.batchCalls(e)):C,[s,d]),()=>s.getCurrentResult(),()=>s.getCurrentResult()),A.useEffect(()=>{s.setQueries(o,t)},[o,t,s]);let f=c.some((e,t)=>pt(o[t],e))?c.flatMap((e,t)=>{let n=o[t];return n&&pt(n,e)?mt(n,new Ne(r,n),a):[]}):[];if(f.length>0)throw Promise.all(f);let p=c.find((e,t)=>{let n=o[t];return n&&ut({result:e,errorResetBoundary:a,throwOnError:n.throwOnError,query:r.getQueryCache().get(n.queryHash),suspense:n.suspense})});if(p?.error)throw p.error;return l(u())}function gt(e,t,n){let r=it(),i=st(),a=tt(n),o=a.defaultQueryOptions(e);a.getDefaultOptions().queries?._experimental_beforeQuery?.(o);let s=a.getQueryCache().get(o.queryHash);o._optimisticResults=r?`isRestoring`:`optimistic`,dt(o),ct(o,i,s),lt(i);let c=!a.getQueryCache().get(o.queryHash),[l]=A.useState(()=>new t(a,o)),u=l.getOptimisticResult(o),d=!r&&e.subscribed!==!1;if(A.useSyncExternalStore(A.useCallback(e=>{let t=d?l.subscribe(k.batchCalls(e)):C;return l.updateResult(),t},[l,d]),()=>l.getCurrentResult(),()=>l.getCurrentResult()),A.useEffect(()=>{l.setOptions(o)},[o,l]),pt(o,u))throw mt(o,l,i);if(ut({result:u,errorResetBoundary:i,throwOnError:o.throwOnError,query:s,suspense:o.suspense}))throw u.error;return a.getDefaultOptions().queries?._experimental_afterQuery?.(o,u),o.experimental_prefetchInRender&&!S&&ft(u,r)&&(c?mt(o,l,i):s?.promise)?.catch(C).finally(()=>{l.updateResult()}),o.notifyOnChangeProps?u:l.trackResult(u)}function _t(e,t){return gt(e,Ne,t)}function vt(e,t){return function(){return e.apply(t,arguments)}}var{toString:yt}=Object.prototype,{getPrototypeOf:bt}=Object,{iterator:xt,toStringTag:St}=Symbol,Ct=(e=>t=>{let n=yt.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),wt=e=>(e=e.toLowerCase(),t=>Ct(t)===e),Tt=e=>t=>typeof t===e,{isArray:M}=Array,Et=Tt(`undefined`);function Dt(e){return e!==null&&!Et(e)&&e.constructor!==null&&!Et(e.constructor)&&jt(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}var Ot=wt(`ArrayBuffer`);function kt(e){let t;return t=typeof ArrayBuffer<`u`&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&Ot(e.buffer),t}var At=Tt(`string`),jt=Tt(`function`),Mt=Tt(`number`),Nt=e=>typeof e==`object`&&!!e,Pt=e=>e===!0||e===!1,Ft=e=>{if(Ct(e)!==`object`)return!1;let t=bt(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(St in e)&&!(xt in e)},It=e=>{if(!Nt(e)||Dt(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},Lt=wt(`Date`),Rt=wt(`File`),zt=e=>!!(e&&e.uri!==void 0),Bt=e=>e&&e.getParts!==void 0,Vt=wt(`Blob`),Ht=wt(`FileList`),Ut=e=>Nt(e)&&jt(e.pipe);function Wt(){return typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{}}var Gt=Wt(),Kt=Gt.FormData===void 0?void 0:Gt.FormData,qt=e=>{let t;return e&&(Kt&&e instanceof Kt||jt(e.append)&&((t=Ct(e))===`formdata`||t===`object`&&jt(e.toString)&&e.toString()===`[object FormData]`))},Jt=wt(`URLSearchParams`),[Yt,Xt,Zt,Qt]=[`ReadableStream`,`Request`,`Response`,`Headers`].map(wt),$t=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,``);function en(e,t,{allOwnKeys:n=!1}={}){if(e==null)return;let r,i;if(typeof e!=`object`&&(e=[e]),M(e))for(r=0,i=e.length;r0;)if(i=n[r],t===i.toLowerCase())return i;return null}var nn=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:global,rn=e=>!Et(e)&&e!==nn;function an(){let{caseless:e,skipUndefined:t}=rn(this)&&this||{},n={},r=(r,i)=>{if(i===`__proto__`||i===`constructor`||i===`prototype`)return;let a=e&&tn(n,i)||i;Ft(n[a])&&Ft(r)?n[a]=an(n[a],r):Ft(r)?n[a]=an({},r):M(r)?n[a]=r.slice():(!t||!Et(r))&&(n[a]=r)};for(let e=0,t=arguments.length;e(en(t,(t,r)=>{n&&jt(t)?Object.defineProperty(e,r,{value:vt(t,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,r,{value:t,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:r}),e),sn=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),cn=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,`constructor`,{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,`super`,{value:t.prototype}),n&&Object.assign(e.prototype,n)},ln=(e,t,n,r)=>{let i,a,o,s={};if(t||={},e==null)return t;do{for(i=Object.getOwnPropertyNames(e),a=i.length;a-- >0;)o=i[a],(!r||r(o,e,t))&&!s[o]&&(t[o]=e[o],s[o]=!0);e=n!==!1&&bt(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},un=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;let r=e.indexOf(t,n);return r!==-1&&r===n},dn=e=>{if(!e)return null;if(M(e))return e;let t=e.length;if(!Mt(t))return null;let n=Array(t);for(;t-- >0;)n[t]=e[t];return n},fn=(e=>t=>e&&t instanceof e)(typeof Uint8Array<`u`&&bt(Uint8Array)),pn=(e,t)=>{let n=(e&&e[xt]).call(e),r;for(;(r=n.next())&&!r.done;){let n=r.value;t.call(e,n[0],n[1])}},mn=(e,t)=>{let n,r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},hn=wt(`HTMLFormElement`),gn=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(e,t,n){return t.toUpperCase()+n}),_n=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),vn=wt(`RegExp`),yn=(e,t)=>{let n=Object.getOwnPropertyDescriptors(e),r={};en(n,(n,i)=>{let a;(a=t(n,i,e))!==!1&&(r[i]=a||n)}),Object.defineProperties(e,r)},bn=e=>{yn(e,(t,n)=>{if(jt(e)&&[`arguments`,`caller`,`callee`].indexOf(n)!==-1)return!1;let r=e[n];if(jt(r)){if(t.enumerable=!1,`writable`in t){t.writable=!1;return}t.set||=()=>{throw Error(`Can not rewrite read-only method '`+n+`'`)}}})},xn=(e,t)=>{let n={},r=e=>{e.forEach(e=>{n[e]=!0})};return M(e)?r(e):r(String(e).split(t)),n},Sn=()=>{},Cn=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function wn(e){return!!(e&&jt(e.append)&&e[St]===`FormData`&&e[xt])}var Tn=e=>{let t=Array(10),n=(e,r)=>{if(Nt(e)){if(t.indexOf(e)>=0)return;if(Dt(e))return e;if(!(`toJSON`in e)){t[r]=e;let i=M(e)?[]:{};return en(e,(e,t)=>{let a=n(e,r+1);!Et(a)&&(i[t]=a)}),t[r]=void 0,i}}return e};return n(e,0)},En=wt(`AsyncFunction`),Dn=e=>e&&(Nt(e)||jt(e))&&jt(e.then)&&jt(e.catch),On=((e,t)=>e?setImmediate:t?((e,t)=>(nn.addEventListener(`message`,({source:n,data:r})=>{n===nn&&r===e&&t.length&&t.shift()()},!1),n=>{t.push(n),nn.postMessage(e,`*`)}))(`axios@${Math.random()}`,[]):e=>setTimeout(e))(typeof setImmediate==`function`,jt(nn.postMessage)),N={isArray:M,isArrayBuffer:Ot,isBuffer:Dt,isFormData:qt,isArrayBufferView:kt,isString:At,isNumber:Mt,isBoolean:Pt,isObject:Nt,isPlainObject:Ft,isEmptyObject:It,isReadableStream:Yt,isRequest:Xt,isResponse:Zt,isHeaders:Qt,isUndefined:Et,isDate:Lt,isFile:Rt,isReactNativeBlob:zt,isReactNative:Bt,isBlob:Vt,isRegExp:vn,isFunction:jt,isStream:Ut,isURLSearchParams:Jt,isTypedArray:fn,isFileList:Ht,forEach:en,merge:an,extend:on,trim:$t,stripBOM:sn,inherits:cn,toFlatObject:ln,kindOf:Ct,kindOfTest:wt,endsWith:un,toArray:dn,forEachEntry:pn,matchAll:mn,isHTMLForm:hn,hasOwnProperty:_n,hasOwnProp:_n,reduceDescriptors:yn,freezeMethods:bn,toObjectSet:xn,toCamelCase:gn,noop:Sn,toFiniteNumber:Cn,findKey:tn,global:nn,isContextDefined:rn,isSpecCompliantForm:wn,toJSONObject:Tn,isAsyncFn:En,isThenable:Dn,setImmediate:On,asap:typeof queueMicrotask<`u`?queueMicrotask.bind(nn):typeof process<`u`&&process.nextTick||On,isIterable:e=>e!=null&&jt(e[xt])},P=class e extends Error{static from(t,n,r,i,a,o){let s=new e(t.message,n||t.code,r,i,a);return s.cause=t,s.name=t.name,t.status!=null&&s.status==null&&(s.status=t.status),o&&Object.assign(s,o),s}constructor(e,t,n,r,i){super(e),Object.defineProperty(this,`message`,{value:e,enumerable:!0,writable:!0,configurable:!0}),this.name=`AxiosError`,this.isAxiosError=!0,t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),i&&(this.response=i,this.status=i.status)}toJSON(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:N.toJSONObject(this.config),code:this.code,status:this.status}}};P.ERR_BAD_OPTION_VALUE=`ERR_BAD_OPTION_VALUE`,P.ERR_BAD_OPTION=`ERR_BAD_OPTION`,P.ECONNABORTED=`ECONNABORTED`,P.ETIMEDOUT=`ETIMEDOUT`,P.ERR_NETWORK=`ERR_NETWORK`,P.ERR_FR_TOO_MANY_REDIRECTS=`ERR_FR_TOO_MANY_REDIRECTS`,P.ERR_DEPRECATED=`ERR_DEPRECATED`,P.ERR_BAD_RESPONSE=`ERR_BAD_RESPONSE`,P.ERR_BAD_REQUEST=`ERR_BAD_REQUEST`,P.ERR_CANCELED=`ERR_CANCELED`,P.ERR_NOT_SUPPORT=`ERR_NOT_SUPPORT`,P.ERR_INVALID_URL=`ERR_INVALID_URL`;function kn(e){return N.isPlainObject(e)||N.isArray(e)}function An(e){return N.endsWith(e,`[]`)?e.slice(0,-2):e}function jn(e,t,n){return e?e.concat(t).map(function(e,t){return e=An(e),!n&&t?`[`+e+`]`:e}).join(n?`.`:``):t}function Mn(e){return N.isArray(e)&&!e.some(kn)}var Nn=N.toFlatObject(N,{},null,function(e){return/^is[A-Z]/.test(e)});function Pn(e,t,n){if(!N.isObject(e))throw TypeError(`target must be an object`);t||=new FormData,n=N.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(e,t){return!N.isUndefined(t[e])});let r=n.metaTokens,i=n.visitor||l,a=n.dots,o=n.indexes,s=(n.Blob||typeof Blob<`u`&&Blob)&&N.isSpecCompliantForm(t);if(!N.isFunction(i))throw TypeError(`visitor must be a function`);function c(e){if(e===null)return``;if(N.isDate(e))return e.toISOString();if(N.isBoolean(e))return e.toString();if(!s&&N.isBlob(e))throw new P(`Blob is not supported. Use a Buffer instead.`);return N.isArrayBuffer(e)||N.isTypedArray(e)?s&&typeof Blob==`function`?new Blob([e]):Buffer.from(e):e}function l(e,n,i){let s=e;if(N.isReactNative(t)&&N.isReactNativeBlob(e))return t.append(jn(i,n,a),c(e)),!1;if(e&&!i&&typeof e==`object`){if(N.endsWith(n,`{}`))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(N.isArray(e)&&Mn(e)||(N.isFileList(e)||N.endsWith(n,`[]`))&&(s=N.toArray(e)))return n=An(n),s.forEach(function(e,r){!(N.isUndefined(e)||e===null)&&t.append(o===!0?jn([n],r,a):o===null?n:n+`[]`,c(e))}),!1}return kn(e)?!0:(t.append(jn(i,n,a),c(e)),!1)}let u=[],d=Object.assign(Nn,{defaultVisitor:l,convertValue:c,isVisitable:kn});function f(e,n){if(!N.isUndefined(e)){if(u.indexOf(e)!==-1)throw Error(`Circular reference detected in `+n.join(`.`));u.push(e),N.forEach(e,function(e,r){(!(N.isUndefined(e)||e===null)&&i.call(t,e,N.isString(r)?r.trim():r,n,d))===!0&&f(e,n?n.concat(r):[r])}),u.pop()}}if(!N.isObject(e))throw TypeError(`data must be an object`);return f(e),t}function Fn(e){let t={"!":`%21`,"'":`%27`,"(":`%28`,")":`%29`,"~":`%7E`,"%20":`+`,"%00":`\0`};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(e){return t[e]})}function In(e,t){this._pairs=[],e&&Pn(e,this,t)}var Ln=In.prototype;Ln.append=function(e,t){this._pairs.push([e,t])},Ln.toString=function(e){let t=e?function(t){return e.call(this,t,Fn)}:Fn;return this._pairs.map(function(e){return t(e[0])+`=`+t(e[1])},``).join(`&`)};function Rn(e){return encodeURIComponent(e).replace(/%3A/gi,`:`).replace(/%24/g,`$`).replace(/%2C/gi,`,`).replace(/%20/g,`+`)}function zn(e,t,n){if(!t)return e;let r=n&&n.encode||Rn,i=N.isFunction(n)?{serialize:n}:n,a=i&&i.serialize,o;if(o=a?a(t,i):N.isURLSearchParams(t)?t.toString():new In(t,i).toString(r),o){let t=e.indexOf(`#`);t!==-1&&(e=e.slice(0,t)),e+=(e.indexOf(`?`)===-1?`?`:`&`)+o}return e}var Bn=class{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:n?n.synchronous:!1,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&=[]}forEach(e){N.forEach(this.handlers,function(t){t!==null&&e(t)})}},Vn={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0},Hn={isBrowser:!0,classes:{URLSearchParams:typeof URLSearchParams<`u`?URLSearchParams:In,FormData:typeof FormData<`u`?FormData:null,Blob:typeof Blob<`u`?Blob:null},protocols:[`http`,`https`,`file`,`blob`,`url`,`data`]},Un=s({hasBrowserEnv:()=>Wn,hasStandardBrowserEnv:()=>Kn,hasStandardBrowserWebWorkerEnv:()=>qn,navigator:()=>Gn,origin:()=>Jn}),Wn=typeof window<`u`&&typeof document<`u`,Gn=typeof navigator==`object`&&navigator||void 0,Kn=Wn&&(!Gn||[`ReactNative`,`NativeScript`,`NS`].indexOf(Gn.product)<0),qn=typeof WorkerGlobalScope<`u`&&self instanceof WorkerGlobalScope&&typeof self.importScripts==`function`,Jn=Wn&&window.location.href||`http://localhost`,Yn={...Un,...Hn};function Xn(e,t){return Pn(e,new Yn.classes.URLSearchParams,{visitor:function(e,t,n,r){return Yn.isNode&&N.isBuffer(e)?(this.append(t,e.toString(`base64`)),!1):r.defaultVisitor.apply(this,arguments)},...t})}function Zn(e){return N.matchAll(/\w+|\[(\w*)]/g,e).map(e=>e[0]===`[]`?``:e[1]||e[0])}function Qn(e){let t={},n=Object.keys(e),r,i=n.length,a;for(r=0;r=e.length;return a=!a&&N.isArray(r)?r.length:a,s?(N.hasOwnProp(r,a)?r[a]=[r[a],n]:r[a]=n,!o):((!r[a]||!N.isObject(r[a]))&&(r[a]=[]),t(e,n,r[a],i)&&N.isArray(r[a])&&(r[a]=Qn(r[a])),!o)}if(N.isFormData(e)&&N.isFunction(e.entries)){let n={};return N.forEachEntry(e,(e,r)=>{t(Zn(e),r,n,0)}),n}return null}function er(e,t,n){if(N.isString(e))try{return(t||JSON.parse)(e),N.trim(e)}catch(e){if(e.name!==`SyntaxError`)throw e}return(n||JSON.stringify)(e)}var tr={transitional:Vn,adapter:[`xhr`,`http`,`fetch`],transformRequest:[function(e,t){let n=t.getContentType()||``,r=n.indexOf(`application/json`)>-1,i=N.isObject(e);if(i&&N.isHTMLForm(e)&&(e=new FormData(e)),N.isFormData(e))return r?JSON.stringify($n(e)):e;if(N.isArrayBuffer(e)||N.isBuffer(e)||N.isStream(e)||N.isFile(e)||N.isBlob(e)||N.isReadableStream(e))return e;if(N.isArrayBufferView(e))return e.buffer;if(N.isURLSearchParams(e))return t.setContentType(`application/x-www-form-urlencoded;charset=utf-8`,!1),e.toString();let a;if(i){if(n.indexOf(`application/x-www-form-urlencoded`)>-1)return Xn(e,this.formSerializer).toString();if((a=N.isFileList(e))||n.indexOf(`multipart/form-data`)>-1){let t=this.env&&this.env.FormData;return Pn(a?{"files[]":e}:e,t&&new t,this.formSerializer)}}return i||r?(t.setContentType(`application/json`,!1),er(e)):e}],transformResponse:[function(e){let t=this.transitional||tr.transitional,n=t&&t.forcedJSONParsing,r=this.responseType===`json`;if(N.isResponse(e)||N.isReadableStream(e))return e;if(e&&N.isString(e)&&(n&&!this.responseType||r)){let n=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e,this.parseReviver)}catch(e){if(n)throw e.name===`SyntaxError`?P.from(e,P.ERR_BAD_RESPONSE,this,null,this.response):e}}return e}],timeout:0,xsrfCookieName:`XSRF-TOKEN`,xsrfHeaderName:`X-XSRF-TOKEN`,maxContentLength:-1,maxBodyLength:-1,env:{FormData:Yn.classes.FormData,Blob:Yn.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:`application/json, text/plain, */*`,"Content-Type":void 0}}};N.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`],e=>{tr.headers[e]={}});var nr=N.toObjectSet([`age`,`authorization`,`content-length`,`content-type`,`etag`,`expires`,`from`,`host`,`if-modified-since`,`if-unmodified-since`,`last-modified`,`location`,`max-forwards`,`proxy-authorization`,`referer`,`retry-after`,`user-agent`]),rr=e=>{let t={},n,r,i;return e&&e.split(` +`).forEach(function(e){i=e.indexOf(`:`),n=e.substring(0,i).trim().toLowerCase(),r=e.substring(i+1).trim(),!(!n||t[n]&&nr[n])&&(n===`set-cookie`?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+`, `+r:r)}),t},ir=Symbol(`internals`);function ar(e){return e&&String(e).trim().toLowerCase()}function or(e){return e===!1||e==null?e:N.isArray(e)?e.map(or):String(e)}function sr(e){let t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g,r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}var cr=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function lr(e,t,n,r,i){if(N.isFunction(r))return r.call(this,t,n);if(i&&(t=n),N.isString(t)){if(N.isString(r))return t.indexOf(r)!==-1;if(N.isRegExp(r))return r.test(t)}}function ur(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,t,n)=>t.toUpperCase()+n)}function dr(e,t){let n=N.toCamelCase(` `+t);[`get`,`set`,`has`].forEach(r=>{Object.defineProperty(e,r+n,{value:function(e,n,i){return this[r].call(this,t,e,n,i)},configurable:!0})})}var fr=class{constructor(e){e&&this.set(e)}set(e,t,n){let r=this;function i(e,t,n){let i=ar(t);if(!i)throw Error(`header name must be a non-empty string`);let a=N.findKey(r,i);(!a||r[a]===void 0||n===!0||n===void 0&&r[a]!==!1)&&(r[a||t]=or(e))}let a=(e,t)=>N.forEach(e,(e,n)=>i(e,n,t));if(N.isPlainObject(e)||e instanceof this.constructor)a(e,t);else if(N.isString(e)&&(e=e.trim())&&!cr(e))a(rr(e),t);else if(N.isObject(e)&&N.isIterable(e)){let n={},r,i;for(let t of e){if(!N.isArray(t))throw TypeError(`Object iterator must return a key-value pair`);n[i=t[0]]=(r=n[i])?N.isArray(r)?[...r,t[1]]:[r,t[1]]:t[1]}a(n,t)}else e!=null&&i(t,e,n);return this}get(e,t){if(e=ar(e),e){let n=N.findKey(this,e);if(n){let e=this[n];if(!t)return e;if(t===!0)return sr(e);if(N.isFunction(t))return t.call(this,e,n);if(N.isRegExp(t))return t.exec(e);throw TypeError(`parser must be boolean|regexp|function`)}}}has(e,t){if(e=ar(e),e){let n=N.findKey(this,e);return!!(n&&this[n]!==void 0&&(!t||lr(this,this[n],n,t)))}return!1}delete(e,t){let n=this,r=!1;function i(e){if(e=ar(e),e){let i=N.findKey(n,e);i&&(!t||lr(n,n[i],i,t))&&(delete n[i],r=!0)}}return N.isArray(e)?e.forEach(i):i(e),r}clear(e){let t=Object.keys(this),n=t.length,r=!1;for(;n--;){let i=t[n];(!e||lr(this,this[i],i,e,!0))&&(delete this[i],r=!0)}return r}normalize(e){let t=this,n={};return N.forEach(this,(r,i)=>{let a=N.findKey(n,i);if(a){t[a]=or(r),delete t[i];return}let o=e?ur(i):String(i).trim();o!==i&&delete t[i],t[o]=or(r),n[o]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){let t=Object.create(null);return N.forEach(this,(n,r)=>{n!=null&&n!==!1&&(t[r]=e&&N.isArray(n)?n.join(`, `):n)}),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,t])=>e+`: `+t).join(` +`)}getSetCookie(){return this.get(`set-cookie`)||[]}get[Symbol.toStringTag](){return`AxiosHeaders`}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){let n=new this(e);return t.forEach(e=>n.set(e)),n}static accessor(e){let t=(this[ir]=this[ir]={accessors:{}}).accessors,n=this.prototype;function r(e){let r=ar(e);t[r]||(dr(n,e),t[r]=!0)}return N.isArray(e)?e.forEach(r):r(e),this}};fr.accessor([`Content-Type`,`Content-Length`,`Accept`,`Accept-Encoding`,`User-Agent`,`Authorization`]),N.reduceDescriptors(fr.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}}),N.freezeMethods(fr);function pr(e,t){let n=this||tr,r=t||n,i=fr.from(r.headers),a=r.data;return N.forEach(e,function(e){a=e.call(n,a,i.normalize(),t?t.status:void 0)}),i.normalize(),a}function mr(e){return!!(e&&e.__CANCEL__)}var hr=class extends P{constructor(e,t,n){super(e??`canceled`,P.ERR_CANCELED,t,n),this.name=`CanceledError`,this.__CANCEL__=!0}};function gr(e,t,n){let r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new P(`Request failed with status code `+n.status,[P.ERR_BAD_REQUEST,P.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function _r(e){let t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||``}function vr(e,t){e||=10;let n=Array(e),r=Array(e),i=0,a=0,o;return t=t===void 0?1e3:t,function(s){let c=Date.now(),l=r[a];o||=c,n[i]=s,r[i]=c;let u=a,d=0;for(;u!==i;)d+=n[u++],u%=e;if(i=(i+1)%e,i===a&&(a=(a+1)%e),c-o{n=r,i=null,a&&=(clearTimeout(a),null),e(...t)};return[(...e)=>{let t=Date.now(),s=t-n;s>=r?o(e,t):(i=e,a||=setTimeout(()=>{a=null,o(i)},r-s))},()=>i&&o(i)]}var br=(e,t,n=3)=>{let r=0,i=vr(50,250);return yr(n=>{let a=n.loaded,o=n.lengthComputable?n.total:void 0,s=a-r,c=i(s),l=a<=o;r=a,e({loaded:a,total:o,progress:o?a/o:void 0,bytes:s,rate:c||void 0,estimated:c&&o&&l?(o-a)/c:void 0,event:n,lengthComputable:o!=null,[t?`download`:`upload`]:!0})},n)},xr=(e,t)=>{let n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},Sr=e=>(...t)=>N.asap(()=>e(...t)),Cr=Yn.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,Yn.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(Yn.origin),Yn.navigator&&/(msie|trident)/i.test(Yn.navigator.userAgent)):()=>!0,wr=Yn.hasStandardBrowserEnv?{write(e,t,n,r,i,a,o){if(typeof document>`u`)return;let s=[`${e}=${encodeURIComponent(t)}`];N.isNumber(n)&&s.push(`expires=${new Date(n).toUTCString()}`),N.isString(r)&&s.push(`path=${r}`),N.isString(i)&&s.push(`domain=${i}`),a===!0&&s.push(`secure`),N.isString(o)&&s.push(`SameSite=${o}`),document.cookie=s.join(`; `)},read(e){if(typeof document>`u`)return null;let t=document.cookie.match(RegExp(`(?:^|; )`+e+`=([^;]*)`));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,``,Date.now()-864e5,`/`)}}:{write(){},read(){return null},remove(){}};function Tr(e){return typeof e==`string`?/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e):!1}function Er(e,t){return t?e.replace(/\/?\/$/,``)+`/`+t.replace(/^\/+/,``):e}function Dr(e,t,n){let r=!Tr(t);return e&&(r||n==0)?Er(e,t):t}var Or=e=>e instanceof fr?{...e}:e;function kr(e,t){t||={};let n={};function r(e,t,n,r){return N.isPlainObject(e)&&N.isPlainObject(t)?N.merge.call({caseless:r},e,t):N.isPlainObject(t)?N.merge({},t):N.isArray(t)?t.slice():t}function i(e,t,n,i){if(!N.isUndefined(t))return r(e,t,n,i);if(!N.isUndefined(e))return r(void 0,e,n,i)}function a(e,t){if(!N.isUndefined(t))return r(void 0,t)}function o(e,t){if(!N.isUndefined(t))return r(void 0,t);if(!N.isUndefined(e))return r(void 0,e)}function s(n,i,a){if(a in t)return r(n,i);if(a in e)return r(void 0,n)}let c={url:a,method:a,data:a,baseURL:o,transformRequest:o,transformResponse:o,paramsSerializer:o,timeout:o,timeoutMessage:o,withCredentials:o,withXSRFToken:o,adapter:o,responseType:o,xsrfCookieName:o,xsrfHeaderName:o,onUploadProgress:o,onDownloadProgress:o,decompress:o,maxContentLength:o,maxBodyLength:o,beforeRedirect:o,transport:o,httpAgent:o,httpsAgent:o,cancelToken:o,socketPath:o,responseEncoding:o,validateStatus:s,headers:(e,t,n)=>i(Or(e),Or(t),n,!0)};return N.forEach(Object.keys({...e,...t}),function(r){if(r===`__proto__`||r===`constructor`||r===`prototype`)return;let a=N.hasOwnProp(c,r)?c[r]:i,o=a(e[r],t[r],r);N.isUndefined(o)&&a!==s||(n[r]=o)}),n}var Ar=e=>{let t=kr({},e),{data:n,withXSRFToken:r,xsrfHeaderName:i,xsrfCookieName:a,headers:o,auth:s}=t;if(t.headers=o=fr.from(o),t.url=zn(Dr(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),s&&o.set(`Authorization`,`Basic `+btoa((s.username||``)+`:`+(s.password?unescape(encodeURIComponent(s.password)):``))),N.isFormData(n)){if(Yn.hasStandardBrowserEnv||Yn.hasStandardBrowserWebWorkerEnv)o.setContentType(void 0);else if(N.isFunction(n.getHeaders)){let e=n.getHeaders(),t=[`content-type`,`content-length`];Object.entries(e).forEach(([e,n])=>{t.includes(e.toLowerCase())&&o.set(e,n)})}}if(Yn.hasStandardBrowserEnv&&(r&&N.isFunction(r)&&(r=r(t)),r||r!==!1&&Cr(t.url))){let e=i&&a&&wr.read(a);e&&o.set(i,e)}return t},jr=typeof XMLHttpRequest<`u`&&function(e){return new Promise(function(t,n){let r=Ar(e),i=r.data,a=fr.from(r.headers).normalize(),{responseType:o,onUploadProgress:s,onDownloadProgress:c}=r,l,u,d,f,p;function m(){f&&f(),p&&p(),r.cancelToken&&r.cancelToken.unsubscribe(l),r.signal&&r.signal.removeEventListener(`abort`,l)}let h=new XMLHttpRequest;h.open(r.method.toUpperCase(),r.url,!0),h.timeout=r.timeout;function g(){if(!h)return;let r=fr.from(`getAllResponseHeaders`in h&&h.getAllResponseHeaders());gr(function(e){t(e),m()},function(e){n(e),m()},{data:!o||o===`text`||o===`json`?h.responseText:h.response,status:h.status,statusText:h.statusText,headers:r,config:e,request:h}),h=null}`onloadend`in h?h.onloadend=g:h.onreadystatechange=function(){!h||h.readyState!==4||h.status===0&&!(h.responseURL&&h.responseURL.indexOf(`file:`)===0)||setTimeout(g)},h.onabort=function(){h&&=(n(new P(`Request aborted`,P.ECONNABORTED,e,h)),null)},h.onerror=function(t){let r=new P(t&&t.message?t.message:`Network Error`,P.ERR_NETWORK,e,h);r.event=t||null,n(r),h=null},h.ontimeout=function(){let t=r.timeout?`timeout of `+r.timeout+`ms exceeded`:`timeout exceeded`,i=r.transitional||Vn;r.timeoutErrorMessage&&(t=r.timeoutErrorMessage),n(new P(t,i.clarifyTimeoutError?P.ETIMEDOUT:P.ECONNABORTED,e,h)),h=null},i===void 0&&a.setContentType(null),`setRequestHeader`in h&&N.forEach(a.toJSON(),function(e,t){h.setRequestHeader(t,e)}),N.isUndefined(r.withCredentials)||(h.withCredentials=!!r.withCredentials),o&&o!==`json`&&(h.responseType=r.responseType),c&&([d,p]=br(c,!0),h.addEventListener(`progress`,d)),s&&h.upload&&([u,f]=br(s),h.upload.addEventListener(`progress`,u),h.upload.addEventListener(`loadend`,f)),(r.cancelToken||r.signal)&&(l=t=>{h&&=(n(!t||t.type?new hr(null,e,h):t),h.abort(),null)},r.cancelToken&&r.cancelToken.subscribe(l),r.signal&&(r.signal.aborted?l():r.signal.addEventListener(`abort`,l)));let _=_r(r.url);if(_&&Yn.protocols.indexOf(_)===-1){n(new P(`Unsupported protocol `+_+`:`,P.ERR_BAD_REQUEST,e));return}h.send(i||null)})},Mr=(e,t)=>{let{length:n}=e=e?e.filter(Boolean):[];if(t||n){let n=new AbortController,r,i=function(e){if(!r){r=!0,o();let t=e instanceof Error?e:this.reason;n.abort(t instanceof P?t:new hr(t instanceof Error?t.message:t))}},a=t&&setTimeout(()=>{a=null,i(new P(`timeout of ${t}ms exceeded`,P.ETIMEDOUT))},t),o=()=>{e&&=(a&&clearTimeout(a),a=null,e.forEach(e=>{e.unsubscribe?e.unsubscribe(i):e.removeEventListener(`abort`,i)}),null)};e.forEach(e=>e.addEventListener(`abort`,i));let{signal:s}=n;return s.unsubscribe=()=>N.asap(o),s}},Nr=function*(e,t){let n=e.byteLength;if(!t||n{let i=Pr(e,t),a=0,o,s=e=>{o||(o=!0,r&&r(e))};return new ReadableStream({async pull(e){try{let{done:t,value:r}=await i.next();if(t){s(),e.close();return}let o=r.byteLength;n&&n(a+=o),e.enqueue(new Uint8Array(r))}catch(e){throw s(e),e}},cancel(e){return s(e),i.return()}},{highWaterMark:2})},Lr=64*1024,{isFunction:Rr}=N,zr=(({Request:e,Response:t})=>({Request:e,Response:t}))(N.global),{ReadableStream:Br,TextEncoder:Vr}=N.global,Hr=(e,...t)=>{try{return!!e(...t)}catch{return!1}},Ur=e=>{e=N.merge.call({skipUndefined:!0},zr,e);let{fetch:t,Request:n,Response:r}=e,i=t?Rr(t):typeof fetch==`function`,a=Rr(n),o=Rr(r);if(!i)return!1;let s=i&&Rr(Br),c=i&&(typeof Vr==`function`?(e=>t=>e.encode(t))(new Vr):async e=>new Uint8Array(await new n(e).arrayBuffer())),l=a&&s&&Hr(()=>{let e=!1,t=new n(Yn.origin,{body:new Br,method:`POST`,get duplex(){return e=!0,`half`}}).headers.has(`Content-Type`);return e&&!t}),u=o&&s&&Hr(()=>N.isReadableStream(new r(``).body)),d={stream:u&&(e=>e.body)};i&&[`text`,`arrayBuffer`,`blob`,`formData`,`stream`].forEach(e=>{!d[e]&&(d[e]=(t,n)=>{let r=t&&t[e];if(r)return r.call(t);throw new P(`Response type '${e}' is not supported`,P.ERR_NOT_SUPPORT,n)})});let f=async e=>{if(e==null)return 0;if(N.isBlob(e))return e.size;if(N.isSpecCompliantForm(e))return(await new n(Yn.origin,{method:`POST`,body:e}).arrayBuffer()).byteLength;if(N.isArrayBufferView(e)||N.isArrayBuffer(e))return e.byteLength;if(N.isURLSearchParams(e)&&(e+=``),N.isString(e))return(await c(e)).byteLength},p=async(e,t)=>N.toFiniteNumber(e.getContentLength())??f(t);return async e=>{let{url:i,method:o,data:s,signal:c,cancelToken:f,timeout:m,onDownloadProgress:h,onUploadProgress:g,responseType:_,headers:v,withCredentials:y=`same-origin`,fetchOptions:b}=Ar(e),x=t||fetch;_=_?(_+``).toLowerCase():`text`;let S=Mr([c,f&&f.toAbortSignal()],m),C=null,w=S&&S.unsubscribe&&(()=>{S.unsubscribe()}),ee;try{if(g&&l&&o!==`get`&&o!==`head`&&(ee=await p(v,s))!==0){let e=new n(i,{method:`POST`,body:s,duplex:`half`}),t;if(N.isFormData(s)&&(t=e.headers.get(`content-type`))&&v.setContentType(t),e.body){let[t,n]=xr(ee,br(Sr(g)));s=Ir(e.body,Lr,t,n)}}N.isString(y)||(y=y?`include`:`omit`);let t=a&&`credentials`in n.prototype,c={...b,signal:S,method:o.toUpperCase(),headers:v.normalize().toJSON(),body:s,duplex:`half`,credentials:t?y:void 0};C=a&&new n(i,c);let f=await(a?x(C,b):x(i,c)),m=u&&(_===`stream`||_===`response`);if(u&&(h||m&&w)){let e={};[`status`,`statusText`,`headers`].forEach(t=>{e[t]=f[t]});let t=N.toFiniteNumber(f.headers.get(`content-length`)),[n,i]=h&&xr(t,br(Sr(h),!0))||[];f=new r(Ir(f.body,Lr,n,()=>{i&&i(),w&&w()}),e)}_||=`text`;let te=await d[N.findKey(d,_)||`text`](f,e);return!m&&w&&w(),await new Promise((t,n)=>{gr(t,n,{data:te,headers:fr.from(f.headers),status:f.status,statusText:f.statusText,config:e,request:C})})}catch(t){throw w&&w(),t&&t.name===`TypeError`&&/Load failed|fetch/i.test(t.message)?Object.assign(new P(`Network Error`,P.ERR_NETWORK,e,C,t&&t.response),{cause:t.cause||t}):P.from(t,t&&t.code,e,C,t&&t.response)}}},Wr=new Map,Gr=e=>{let t=e&&e.env||{},{fetch:n,Request:r,Response:i}=t,a=[r,i,n],o=a.length,s,c,l=Wr;for(;o--;)s=a[o],c=l.get(s),c===void 0&&l.set(s,c=o?new Map:Ur(t)),l=c;return c};Gr();var Kr={http:null,xhr:jr,fetch:{get:Gr}};N.forEach(Kr,(e,t)=>{if(e){try{Object.defineProperty(e,`name`,{value:t})}catch{}Object.defineProperty(e,`adapterName`,{value:t})}});var qr=e=>`- ${e}`,Jr=e=>N.isFunction(e)||e===null||e===!1;function Yr(e,t){e=N.isArray(e)?e:[e];let{length:n}=e,r,i,a={};for(let o=0;o`adapter ${e} `+(t===!1?`is not supported by the environment`:`is not available in the build`));throw new P(`There is no suitable adapter to dispatch the request `+(n?e.length>1?`since : +`+e.map(qr).join(` +`):` `+qr(e[0]):`as no adapter specified`),`ERR_NOT_SUPPORT`)}return i}var Xr={getAdapter:Yr,adapters:Kr};function Zr(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new hr(null,e)}function Qr(e){return Zr(e),e.headers=fr.from(e.headers),e.data=pr.call(e,e.transformRequest),[`post`,`put`,`patch`].indexOf(e.method)!==-1&&e.headers.setContentType(`application/x-www-form-urlencoded`,!1),Xr.getAdapter(e.adapter||tr.adapter,e)(e).then(function(t){return Zr(e),t.data=pr.call(e,e.transformResponse,t),t.headers=fr.from(t.headers),t},function(t){return mr(t)||(Zr(e),t&&t.response&&(t.response.data=pr.call(e,e.transformResponse,t.response),t.response.headers=fr.from(t.response.headers))),Promise.reject(t)})}var $r=`1.13.6`,ei={};[`object`,`boolean`,`number`,`function`,`string`,`symbol`].forEach((e,t)=>{ei[e]=function(n){return typeof n===e||`a`+(t<1?`n `:` `)+e}});var ti={};ei.transitional=function(e,t,n){function r(e,t){return`[Axios v`+$r+`] Transitional option '`+e+`'`+t+(n?`. `+n:``)}return(n,i,a)=>{if(e===!1)throw new P(r(i,` has been removed`+(t?` in `+t:``)),P.ERR_DEPRECATED);return t&&!ti[i]&&(ti[i]=!0,console.warn(r(i,` has been deprecated since v`+t+` and will be removed in the near future`))),e?e(n,i,a):!0}},ei.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};function ni(e,t,n){if(typeof e!=`object`)throw new P(`options must be an object`,P.ERR_BAD_OPTION_VALUE);let r=Object.keys(e),i=r.length;for(;i-- >0;){let a=r[i],o=t[a];if(o){let t=e[a],n=t===void 0||o(t,a,e);if(n!==!0)throw new P(`option `+a+` must be `+n,P.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new P(`Unknown option `+a,P.ERR_BAD_OPTION)}}var ri={assertOptions:ni,validators:ei},ii=ri.validators,ai=class{constructor(e){this.defaults=e||{},this.interceptors={request:new Bn,response:new Bn}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=Error();let n=t.stack?t.stack.replace(/^.+\n/,``):``;try{e.stack?n&&!String(e.stack).endsWith(n.replace(/^.+\n.+\n/,``))&&(e.stack+=` +`+n):e.stack=n}catch{}}throw e}}_request(e,t){typeof e==`string`?(t||={},t.url=e):t=e||{},t=kr(this.defaults,t);let{transitional:n,paramsSerializer:r,headers:i}=t;n!==void 0&&ri.assertOptions(n,{silentJSONParsing:ii.transitional(ii.boolean),forcedJSONParsing:ii.transitional(ii.boolean),clarifyTimeoutError:ii.transitional(ii.boolean),legacyInterceptorReqResOrdering:ii.transitional(ii.boolean)},!1),r!=null&&(N.isFunction(r)?t.paramsSerializer={serialize:r}:ri.assertOptions(r,{encode:ii.function,serialize:ii.function},!0)),t.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls===void 0?t.allowAbsoluteUrls=!0:t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls),ri.assertOptions(t,{baseUrl:ii.spelling(`baseURL`),withXsrfToken:ii.spelling(`withXSRFToken`)},!0),t.method=(t.method||this.defaults.method||`get`).toLowerCase();let a=i&&N.merge(i.common,i[t.method]);i&&N.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`,`common`],e=>{delete i[e]}),t.headers=fr.concat(a,i);let o=[],s=!0;this.interceptors.request.forEach(function(e){if(typeof e.runWhen==`function`&&e.runWhen(t)===!1)return;s&&=e.synchronous;let n=t.transitional||Vn;n&&n.legacyInterceptorReqResOrdering?o.unshift(e.fulfilled,e.rejected):o.push(e.fulfilled,e.rejected)});let c=[];this.interceptors.response.forEach(function(e){c.push(e.fulfilled,e.rejected)});let l,u=0,d;if(!s){let e=[Qr.bind(this),void 0];for(e.unshift(...o),e.push(...c),d=e.length,l=Promise.resolve(t);u{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null}),this.promise.then=e=>{let t,r=new Promise(e=>{n.subscribe(e),t=e}).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e(function(e,r,i){n.reason||(n.reason=new hr(e,r,i),t(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;let t=this._listeners.indexOf(e);t!==-1&&this._listeners.splice(t,1)}toAbortSignal(){let e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let t;return{token:new e(function(e){t=e}),cancel:t}}};function si(e){return function(t){return e.apply(null,t)}}function ci(e){return N.isObject(e)&&e.isAxiosError===!0}var li={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(li).forEach(([e,t])=>{li[t]=e});function ui(e){let t=new ai(e),n=vt(ai.prototype.request,t);return N.extend(n,ai.prototype,t,{allOwnKeys:!0}),N.extend(n,t,null,{allOwnKeys:!0}),n.create=function(t){return ui(kr(e,t))},n}var F=ui(tr);F.Axios=ai,F.CanceledError=hr,F.CancelToken=oi,F.isCancel=mr,F.VERSION=$r,F.toFormData=Pn,F.AxiosError=P,F.Cancel=F.CanceledError,F.all=function(e){return Promise.all(e)},F.spread=si,F.isAxiosError=ci,F.mergeConfig=kr,F.AxiosHeaders=fr,F.formToJSON=e=>$n(N.isHTMLForm(e)?new FormData(e):e),F.getAdapter=Xr.getAdapter,F.HttpStatusCode=li,F.default=F;var di=l(_()),fi=`order-demo-001`;function pi(e,t,n,r,i,a){return{eventId:e,aggregateId:fi,aggregateType:`ORDER`,sequenceNumber:t,eventType:n,payload:a,metadata:JSON.stringify({source:`demo`,correlationId:`corr-demo-${t}`}),timestamp:r,globalPosition:i}}function mi(){let e=[],t=Date.parse(`2025-01-15T08:00:00.000Z`);for(let n=1;n<=100;n++){let r=new Date(t+n*45e3).toISOString(),i=5e4+n,a,o;if(n===1)a=`ORDER_PLACED`,o={customerId:`cust-77`,channel:`web`,status:`PENDING`,totalCents:0,itemCount:0};else if(n>=2&&n<=48){a=`LINE_ITEM_ADDED`;let e=350+n*73%1200;o={sku:`SKU-${String(1e4+n*17).slice(-4)}`,qty:n%4+1,lineTotalCents:e,lineIndex:n-1}}else if(n>=49&&n<=58)a=`PAYMENT_PROGRESS`,o={paymentId:`pay-chunk-${n}`,amountCents:1500+n*120,balanceCents:Math.max(0,48e3-n*700)};else if(n>=59&&n<=72){let e=[`inventory`,`fraud_check`,`address_verify`,`manual_review`,`carrier_delay`];a=`FULFILLMENT_BLOCKED`,o={reason:e[n%e.length],caseId:`CASE-${n}`,retryAfterMinutes:15+n%45}}else n>=73&&n<=88?(a=`SHIPMENT_EVENT`,o={leg:n-72,carrier:n%3==0?`FAST`:n%3==1?`ECONOMY`:`OVERNIGHT`,status:`IN_TRANSIT`,trackingToken:`trk-${n}${(n*7919).toString(36)}`}):n>=89&&n<=99?(a=`NOTE_APPENDED`,o={author:`agent-${n%6+1}`,noteId:`n-${n}`,preview:`Ops note #${n}: SLA watch / customer ping`}):(a=`REFUND_ISSUED`,o={refundCents:88e3,balanceCents:-12500,reason:`bulk_settlement_adjustment`});e.push(pi(`evt-demo-${n}`,n,a,r,i,JSON.stringify(o)))}return e}var hi=mi();function gi(e){return[{code:`NEGATIVE_BALANCE`,severity:`HIGH`,description:`Ledger balance dropped below zero after refund batch`},{code:`REFUND_EXCEEDS_CAPTURE`,severity:`CRITICAL`,description:`Cumulative refunds exceed captured payments for this aggregate`},{code:`DUPLICATE_PAYMENT_CHUNK`,severity:`MEDIUM`,description:`Two payment chunks share the same window and amount fingerprint`},{code:`LINE_ITEM_PRICE_OUTLIER`,severity:`LOW`,description:`Line total deviates >3σ from cohort for this SKU family`},{code:`FULFILLMENT_STALL`,severity:`HIGH`,description:`Order blocked in fulfillment longer than SLA for channel`},{code:`CARRIER_MISMATCH`,severity:`MEDIUM`,description:`Shipment leg carrier differs from preferred routing profile`},{code:`MANUAL_REVIEW_BACKLOG`,severity:`LOW`,description:`Case reopened multiple times without resolution`},{code:`VELOCITY_SPIKE`,severity:`HIGH`,description:`Event rate on this aggregate exceeded rolling baseline`},{code:`ADDRESS_VERIFY_LOOP`,severity:`MEDIUM`,description:`Address verification failed three times with same payload hash`},{code:`INVENTORY_HOLD`,severity:`MEDIUM`,description:`Inventory hold exceeded expected release window`},{code:`FRAUD_SCORE_EDGE`,severity:`LOW`,description:`Fraud score landed in manual-review gray band`},{code:`DISCOUNT_STACK`,severity:`LOW`,description:`Multiple discount signals present without explicit approval event`},{code:`SHIPMENT_GAP`,severity:`HIGH`,description:`Missing scan between expected hub handoffs`},{code:`NOTE_SPAM`,severity:`LOW`,description:`Unusually high operator notes density in short interval`},{code:`PAYMENT_PARTIAL_CLUSTER`,severity:`MEDIUM`,description:`Several partial captures without closing settlement event`},{code:`SKU_QUANTITY_ANOMALY`,severity:`MEDIUM`,description:`Quantity pattern inconsistent with historical order curve`},{code:`CASE_ESCALATION`,severity:`HIGH`,description:`Support case escalated without prior tier-1 closure`},{code:`TRACKING_TOKEN_REUSE`,severity:`CRITICAL`,description:`Tracking token collision across two concurrent legs`},{code:`SLA_BREACH_RISK`,severity:`HIGH`,description:`Projected delivery crosses committed SLA if delay persists`},{code:`SETTLEMENT_BATCH_DRIFT`,severity:`CRITICAL`,description:`Settlement batch totals diverge from summed payment chunks`}].map((t,n)=>{let r=Math.min(100,5+n*5),i=e.find(e=>e.sequenceNumber===r)??e[e.length-1];return{code:t.code,description:t.description,severity:t.severity,aggregateId:fi,atSequence:r,triggeringEventType:i.eventType,timestamp:i.timestamp,stateAtAnomaly:{demoIndex:n+1,atSequence:r,code:t.code}}})}var _i=gi(hi);function vi(e,t){let n={...e},r={};try{r=JSON.parse(t.payload||`{}`)}catch{}n._version=t.sequenceNumber,n._lastEventType=t.eventType,n._lastUpdated=t.timestamp;let i=t.eventType.toLowerCase();return i.includes(`created`)||i.includes(`opened`)||i.includes(`placed`)||i.includes(`submitted`)||(i.includes(`deleted`)||i.includes(`closed`)||i.includes(`cancelled`)||i.includes(`rejected`))&&(n.status=`DELETED`),Object.assign(n,r),n}function yi(e,t){let n={};for(let r of Object.keys(t)){let i=e[r],a=t[r];JSON.stringify(i)!==JSON.stringify(a)&&(n[r]={oldValue:i,newValue:a})}for(let r of Object.keys(e))r in t||(n[r]={oldValue:e[r],newValue:void 0});return n}function bi(e){let t=[],n={};for(let r of e){let e={...n};n=vi(n,r);let i={...n};t.push({event:r,stateBefore:e,stateAfter:i,diff:yi(e,i)})}return t}var xi=bi(hi);function Si(e){let t=e.trim().toLowerCase();return t.length<2?!1:t.includes(`demo`)||`order-demo-001`.includes(t)}function Ci(e){return Si(e)?[fi]:[]}function wi(e){let t=Math.min(Math.max(e,1),500);return[...hi].sort((e,t)=>t.globalPosition-e.globalPosition).slice(0,t)}function Ti(e,t,n){if(e!==`order-demo-001`)return{events:[],totalEvents:0};let r=hi.length,i=Math.max(0,n),a=Math.min(Math.max(t,1),1e3);return i>=r?{events:[],totalEvents:r}:{events:hi.slice(i,i+a),totalEvents:r}}function Ei(e){return e===`order-demo-001`?xi:[]}function Di(e){let t=Math.min(Math.max(e,1),500);return _i.slice(0,t)}function Oi(){return[...hi].sort((e,t)=>t.globalPosition-e.globalPosition).slice(0,40)}function ki(){return{status:`UP`,version:`demo`,demo:!0}}function Ai(){return!1}var ji=F.create({baseURL:`/api`});function Mi(e){return new Promise(t=>{setTimeout(t,e)})}function Ni(e,t){return t?`${e}${e.includes(`?`)?`&`:`?`}source=${encodeURIComponent(t)}`:e}var Pi=async(e,t=20,n)=>{let r=Ni(`/aggregates/search?q=${encodeURIComponent(e)}&limit=${t}`,n);if(Ai()){await Mi(40);let n=Ci(e);try{let e=await ji.get(r);return[...new Set([...n,...e.data])].slice(0,t)}catch{return n}}return ji.get(r).then(e=>e.data)},Fi=async(e,t=500,n=0,r,i=`full`)=>{if(Ai()&&e===`order-demo-001`)return await Mi(50),Ti(e,t,n);let a=Ni(`/aggregates/${e}/timeline?limit=${t}&offset=${n}&fields=${i}`,r);return ji.get(a).then(e=>e.data)},Ii=async(e,t)=>Ai()&&e===`order-demo-001`?(await Mi(50),Ei(e)):ji.get(Ni(`/aggregates/${e}/transitions`,t)).then(e=>e.data),I=async(e=100,t)=>Ai()?(await Mi(45),Di(e)):ji.get(Ni(`/anomalies/recent?limit=${e}`,t)).then(e=>e.data),L=async(e=50,t)=>Ai()?(await Mi(35),wi(e)):ji.get(Ni(`/events/recent?limit=${e}`,t)).then(e=>e.data),Li=async()=>Ai()?(await Mi(20),ki()):ji.get(`/health`).then(e=>e.data),Ri=async()=>ji.get(`/v1/datasources`).then(e=>e.data),zi=async e=>ji.get(`/v1/datasources/${encodeURIComponent(e)}/health`).then(e=>e.data),Bi=async()=>ji.get(`/v1/plugins`).then(e=>e.data);function Vi(e,t){let[n,r]=(0,A.useState)(e);return(0,A.useEffect)(()=>{let n=setTimeout(()=>r(e),t);return()=>clearTimeout(n)},[e,t]),n}function Hi({onSelect:e,source:t}){let[n,r]=(0,A.useState)(``),[i,a]=(0,A.useState)(!1),o=(0,A.useRef)(null),s=Vi(n,300),{data:c=[]}=_t({queryKey:[`search`,s,t??`default`],queryFn:()=>Pi(s,20,t),enabled:s.length>=2,staleTime:5e3});(0,A.useEffect)(()=>{let e=e=>{o.current&&!o.current.contains(e.target)&&a(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[]);let l=(0,A.useRef)(null),u=(0,A.useCallback)(()=>{l.current?.focus(),l.current?.select()},[]);(0,A.useEffect)(()=>{document.getElementById(`aggregate-search`)?.addEventListener(`focus`,u)},[u]);let d=t=>{r(t),a(!1),e(t)};return(0,j.jsxs)(`div`,{className:`search-wrapper`,ref:o,children:[(0,j.jsx)(`span`,{className:`search-icon`,children:`??`}),(0,j.jsx)(`input`,{id:`aggregate-search`,ref:l,type:`text`,className:`search-input`,placeholder:`Search by aggregate ID (e.g. UUID or stream key)`,value:n,onChange:e=>{r(e.target.value),a(!0)},onFocus:()=>n.length>=2&&a(!0),onKeyDown:e=>{e.key===`Enter`&&n.trim()&&d(n.trim()),e.key===`Escape`&&a(!1)},autoComplete:`off`}),i&&c.length>0&&(0,j.jsx)(`div`,{className:`search-results`,role:`listbox`,children:c.map(e=>(0,j.jsxs)(`button`,{type:`button`,className:`search-result-item`,onClick:()=>d(e),role:`option`,children:[(0,j.jsx)(`span`,{className:`search-result-chevron`,"aria-hidden":!0,children:`?`}),(0,j.jsxs)(`span`,{className:`search-result-body`,children:[(0,j.jsx)(`span`,{className:`search-result-label`,children:`ID`}),(0,j.jsx)(`span`,{className:`search-result-colon`,children:`:`}),(0,j.jsx)(`span`,{className:`search-result-value`,children:e})]})]},e))})]})}function Ui(e,t){return _t({queryKey:[`timeline`,e,t??`default`,`metadata`],queryFn:()=>Fi(e,500,0,t,`metadata`)})}function Wi(e){if(typeof e==`number`)return Number.isNaN(e)?new Date:e<0xe8d4a51000?new Date(e*1e3):new Date(e);let t=String(e).trim();if(!t)return new Date;if(t.includes(`T`)||/^\d{4}-\d{2}-\d{2}/.test(t)){let e=Date.parse(t);if(!Number.isNaN(e))return new Date(e)}let n=parseFloat(t);return Number.isNaN(n)?new Date:n<0xe8d4a51000?new Date(n*1e3):new Date(n)}var Gi=4;function Ki(e){let t=e.toLowerCase();return t.includes(`created`)||t.includes(`opened`)||t.includes(`placed`)||t.includes(`submitted`)?`created`:t.includes(`deleted`)||t.includes(`closed`)||t.includes(`cancelled`)||t.includes(`rejected`)?`deleted`:t.includes(`completed`)||t.includes(`resolved`)||t.includes(`accepted`)||t.includes(`approved`)||t.includes(`assigned`)?`completed`:t.includes(`failed`)||t.includes(`error`)||t.includes(`blocked`)?`failed`:t.includes(`transfer`)?`transfer`:t.includes(`line_item`)||t.includes(`item`)&&t.includes(`add`)?`item`:t.includes(`payment`)||t.includes(`progress`)?`progress`:`default`}function qi(e){let t=[],n=0;for(;n=Gi)t.push({kind:`group`,eventType:r,items:e.slice(n,i),startIndex:n}),n=i;else{for(let r=n;rr(e.sequenceNumber),title:`${e.eventType}\n${Wi(e.timestamp).toLocaleString()}`,"aria-current":o?`step`:void 0,"aria-label":`Event ${t}, sequence ${e.sequenceNumber}, ${e.eventType}`,children:[(0,j.jsxs)(`span`,{className:`timeline-step-badge`,children:[`Event `,t]}),(0,j.jsxs)(`span`,{className:`timeline-step-seq`,children:[`seq #`,e.sequenceNumber]}),(0,j.jsx)(`span`,{className:`timeline-step-type`,children:e.eventType})]})}function Xi({aggregateId:e,selectedSequence:t,onSelectEvent:n,source:r}){let{data:i,isLoading:a}=Ui(e,r),o=i?.events??[],s=i?.totalEvents??0,[c,l]=(0,A.useState)(null),[u,d]=(0,A.useState)(``),[f,p]=(0,A.useState)(``),m=(0,A.useMemo)(()=>o.length?qi(o):[],[o]),h=(0,A.useMemo)(()=>o.length?[...new Set(o.map(e=>e.eventType))].sort():[],[o]),g=(0,A.useMemo)(()=>f?o.filter(e=>e.eventType===f):o,[o,f]),_=(0,A.useMemo)(()=>g.length?qi(g):[],[g]),v=f?_:m,y=f?g:o,b=t==null?-1:y.findIndex(e=>e.sequenceNumber===t),x=b>=0?b+1:null,S=y[0]?.sequenceNumber??0,C=y[y.length-1]?.sequenceNumber??0,w=(0,A.useMemo)(()=>{if(!c)return null;for(let e of v)if(e.kind===`group`&&Ji(e.startIndex,e.items.length)===c)return e;return null},[c,v]);(0,A.useEffect)(()=>{if(!(t==null||b<0)){for(let e of v){if(e.kind!==`group`)continue;let t=e.startIndex+e.items.length-1;if(b>=e.startIndex&&b<=t){l(Ji(e.startIndex,e.items.length));return}}l(null)}},[t,b,v]),(0,A.useEffect)(()=>{if(t==null)return;let e=requestAnimationFrame(()=>{let e=document.querySelector(`[data-timeline-seq="${t}"]`),n=document.querySelector(`[data-timeline-group-anchor="1"]`);(e??n)?.scrollIntoView({inline:`center`,block:`nearest`,behavior:`smooth`})});return()=>cancelAnimationFrame(e)},[t,c,v]);let ee=(e,t)=>{let n=Ji(e,t);l(e=>e===n?null:n)},te=(0,A.useCallback)(e=>{if(!y.length)return;let t=e.target;if(t.tagName!==`INPUT`){if(e.key===`ArrowLeft`||e.key===`ArrowRight`){e.preventDefault();let t=e.key===`ArrowLeft`?-1:1;if(e.shiftKey){let e=b>=0?v.find(e=>e.kind===`group`?b>=e.startIndex&&b=0&&e{let e=e=>ne.current(e);return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);let T=()=>{let e=parseInt(u,10);!Number.isNaN(e)&&y.some(t=>t.sequenceNumber===e)&&(n(e),d(``))};return a?(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsx)(`div`,{className:`card-title`,children:`Event sequence`}),(0,j.jsx)(`div`,{className:`skeleton`,style:{height:64}})]}):o.length?(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsxs)(`div`,{className:`timeline-header-row`,children:[(0,j.jsxs)(`div`,{className:`card-title`,style:{marginBottom:0},children:[`Event sequence`,(0,j.jsxs)(`span`,{className:`timeline-count-pill`,children:[f?`${y.length} / ${s}`:s,` events`]})]}),(0,j.jsxs)(`div`,{className:`timeline-jump-group`,children:[(0,j.jsx)(`input`,{className:`timeline-jump-input`,type:`number`,placeholder:`Jump to seq`,value:u,onChange:e=>d(e.target.value),onKeyDown:e=>e.key===`Enter`&&T(),"aria-label":`Jump to sequence number`}),(0,j.jsx)(`button`,{type:`button`,className:`timeline-jump-btn`,onClick:T,children:`Go`})]})]}),h.length>1&&(0,j.jsxs)(`div`,{className:`timeline-filter-chips`,role:`group`,"aria-label":`Filter by event type`,children:[(0,j.jsx)(`button`,{type:`button`,className:`filter-chip ${f?``:`active`}`,onClick:()=>p(``),children:`All`}),h.map(e=>(0,j.jsx)(`button`,{type:`button`,className:`filter-chip ${f===e?`active`:``}`,onClick:()=>p(t=>t===e?``:e),children:e},e))]}),(0,j.jsxs)(`div`,{className:`timeline-rail`,children:[(0,j.jsx)(`div`,{className:`timeline-stepper`,role:`navigation`,"aria-label":`Events in order`,children:(0,j.jsx)(`div`,{className:`timeline-stepper-track`,children:v.map((e,r)=>(0,j.jsxs)(A.Fragment,{children:[r>0&&(0,j.jsx)(`span`,{className:`timeline-step-arrow`,"aria-hidden":!0,children:`>`}),e.kind===`single`?(0,j.jsx)(Yi,{event:e.event,stepNumber:e.index+1,selectedSequence:t,onSelectEvent:n}):(0,j.jsx)(Zi,{segment:e,selectedSequence:t,expanded:c===Ji(e.startIndex,e.items.length),onToggle:()=>ee(e.startIndex,e.items.length)})]},e.kind===`group`?`g-${e.startIndex}`:`s-${e.event.sequenceNumber}`))})}),w&&(0,j.jsxs)(`div`,{className:`timeline-expanded-deck`,children:[(0,j.jsxs)(`div`,{className:`timeline-expanded-head`,children:[(0,j.jsx)(`span`,{className:`timeline-expanded-title`,children:w.eventType}),(0,j.jsxs)(`span`,{className:`timeline-expanded-meta`,children:[w.items.length,` events steps `,w.startIndex+1,`-`,w.startIndex+w.items.length]}),(0,j.jsx)(`button`,{type:`button`,className:`timeline-expanded-close`,onClick:()=>l(null),children:`Collapse`})]}),(0,j.jsx)(`div`,{className:`timeline-expanded-strip`,children:w.items.map((e,r)=>(0,j.jsxs)(A.Fragment,{children:[r>0&&(0,j.jsx)(`span`,{className:`timeline-step-arrow timeline-step-arrow-compact`,"aria-hidden":!0,children:`>`}),(0,j.jsx)(Yi,{event:e,stepNumber:w.startIndex+r+1,selectedSequence:t,onSelectEvent:n,compact:!0})]},e.sequenceNumber))})]})]}),(0,j.jsx)(`input`,{type:`range`,className:`timeline-slider`,min:S,max:C,value:t??C,onChange:e=>n(Number(e.target.value)),"aria-label":`Scrub event sequence`}),(0,j.jsxs)(`div`,{className:`timeline-info`,children:[(0,j.jsxs)(`span`,{className:`timeline-info-edge`,children:[`First seq #`,S]}),(0,j.jsx)(`span`,{className:`timeline-info-center`,children:x==null?`Select an event above or drag the scrubber`:(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`strong`,{children:[`Step `,x]}),` of `,y.length,(0,j.jsxs)(`span`,{className:`timeline-info-muted`,children:[` sequence #`,t]}),(0,j.jsx)(`br`,{}),(0,j.jsx)(`span`,{className:`timeline-info-type`,children:y.find(e=>e.sequenceNumber===t)?.eventType??``})]})}),(0,j.jsxs)(`span`,{className:`timeline-info-edge`,children:[`Last seq #`,C]})]})]}):(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsx)(`div`,{className:`card-title`,children:`Event sequence`}),(0,j.jsx)(`p`,{style:{color:`var(--text-muted)`,fontSize:13},children:`No events found for this aggregate.`})]})}function Zi({segment:e,selectedSequence:t,expanded:n,onToggle:r}){let{items:i,startIndex:a,eventType:o}=e,s=i[0],c=i[i.length-1],l=Ki(o),u=t!=null&&i.some(e=>e.sequenceNumber===t),d=u&&!n;return(0,j.jsxs)(`button`,{type:`button`,className:`timeline-group-chip timeline-step-${l} ${u?`has-selection`:``} ${n?`expanded`:``} ${u&&!n?`active`:``}`,onClick:r,"aria-expanded":n,"data-timeline-group-anchor":d?`1`:void 0,title:`${i.length} x ${o}. Click to ${n?`collapse`:`show every step`}.`,children:[(0,j.jsxs)(`span`,{className:`timeline-group-chip-top`,children:[(0,j.jsxs)(`span`,{className:`timeline-group-count`,children:[`x`,i.length]}),(0,j.jsx)(`span`,{className:`timeline-group-chevron`,"aria-hidden":!0,children:n?`v`:`>`})]}),(0,j.jsx)(`span`,{className:`timeline-group-type`,children:o}),(0,j.jsxs)(`span`,{className:`timeline-group-range`,children:[`steps `,a+1,`-`,a+i.length,` seq #`,s.sequenceNumber,`-#`,c.sequenceNumber]})]})}function Qi(e,t){return _t({queryKey:[`transitions`,e,t??`default`],queryFn:()=>Ii(e,t)})}function $i({diff:e}){let t=Object.entries(e),n=t.length>0,[r,i]=(0,A.useState)(`inline`);return n?(0,j.jsxs)(`div`,{className:`diff-panel`,children:[(0,j.jsxs)(`div`,{className:`diff-toolbar`,children:[(0,j.jsxs)(`div`,{className:`diff-toolbar-title`,children:[`Changes`,(0,j.jsxs)(`span`,{className:`diff-count-badge`,children:[t.length,` `,t.length===1?`field`:`fields`,` modified`]})]}),(0,j.jsxs)(`div`,{className:`diff-view-toggle`,role:`group`,"aria-label":`Diff layout`,children:[(0,j.jsx)(`button`,{type:`button`,className:r===`inline`?`active`:``,onClick:()=>i(`inline`),children:`Inline`}),(0,j.jsx)(`button`,{type:`button`,className:r===`split`?`active`:``,onClick:()=>i(`split`),children:`Side by side`})]})]}),(0,j.jsx)(`div`,{className:`diff-body`,children:(0,j.jsx)(`div`,{className:`diff-scroll`,children:r===`inline`?(0,j.jsx)(`div`,{className:`diff-list diff-list-inline`,children:t.map(([e,t],n)=>(0,j.jsxs)(`div`,{className:`diff-row`,children:[(0,j.jsx)(`span`,{className:`diff-line-no`,"aria-hidden":!0,children:n+1}),(0,j.jsxs)(`div`,{className:`diff-row-body`,children:[(0,j.jsx)(`span`,{className:`diff-field`,children:e}),(0,j.jsxs)(`span`,{className:`diff-values-inline`,children:[(0,j.jsx)(`span`,{className:`diff-old`,children:JSON.stringify(t.oldValue)}),(0,j.jsx)(`span`,{className:`diff-arrow`,children:`→`}),(0,j.jsx)(`span`,{className:`diff-new`,children:JSON.stringify(t.newValue)})]})]})]},e))}):(0,j.jsxs)(`div`,{className:`diff-list diff-list-split`,children:[(0,j.jsxs)(`div`,{className:`diff-split-head`,children:[(0,j.jsx)(`span`,{className:`diff-split-label diff-split-old-label`,children:`Before`}),(0,j.jsx)(`span`,{className:`diff-split-label diff-split-new-label`,children:`After`})]}),t.map(([e,t],n)=>(0,j.jsxs)(`div`,{className:`diff-split-row`,children:[(0,j.jsx)(`span`,{className:`diff-line-no`,"aria-hidden":!0,children:n+1}),(0,j.jsxs)(`div`,{className:`diff-split-cells`,children:[(0,j.jsxs)(`div`,{className:`diff-split-cell diff-split-old`,children:[(0,j.jsx)(`span`,{className:`diff-field`,children:e}),(0,j.jsx)(`span`,{className:`diff-cell-value`,children:JSON.stringify(t.oldValue)})]}),(0,j.jsxs)(`div`,{className:`diff-split-cell diff-split-new`,children:[(0,j.jsx)(`span`,{className:`diff-field`,children:e}),(0,j.jsx)(`span`,{className:`diff-cell-value`,children:JSON.stringify(t.newValue)})]})]})]},e))]})})})]}):null}function ea({open:e,onToggle:t}){return(0,j.jsx)(`button`,{type:`button`,className:`json-tree-toggle`,onClick:e=>{e.stopPropagation(),t()},"aria-expanded":e,"aria-label":e?`Collapse`:`Expand`,children:e?`▼`:`▶`})}function ta({value:e}){return e===null?(0,j.jsx)(`span`,{className:`json-null`,children:`null`}):typeof e==`boolean`?(0,j.jsx)(`span`,{className:`json-boolean`,children:String(e)}):typeof e==`number`?(0,j.jsx)(`span`,{className:`json-number`,children:e}):(0,j.jsx)(`span`,{className:`json-string`,children:JSON.stringify(e)})}function na({value:e,changedKeys:t}){return(0,j.jsx)(`div`,{className:`json-tree json-tree-root`,role:`tree`,children:(0,j.jsx)(ra,{value:e,depth:0,changedKeys:t,keyPath:``})})}function ra({value:e,depth:t,propertyKey:n,changedKeys:r,keyPath:i=``}){let a=r&&n!==void 0&&r.has(n),o=r&&i&&[...r].some(e=>e.startsWith(i+`.`)),[s,c]=(0,A.useState)(r?t<3||!!a||!!o:t<3),l={paddingLeft:Math.min(t,12)*14},u=a?{background:`rgba(255, 170, 0, 0.12)`,borderRadius:3}:{};if(e===null||typeof e==`boolean`||typeof e==`number`||typeof e==`string`)return(0,j.jsxs)(`div`,{className:`json-tree-line${a?` json-tree-changed`:``}`,style:{...l,...u},children:[n!==void 0&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,j.jsx)(ta,{value:e})]});let d=e=>i?`${i}.${e}`:e;if(Array.isArray(e))return e.length===0?(0,j.jsxs)(`div`,{className:`json-tree-line`,style:l,children:[n!==void 0&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`[]`})]}):(0,j.jsxs)(`div`,{className:`json-tree-branch`,children:[(0,j.jsxs)(`div`,{className:`json-tree-line${a?` json-tree-changed`:``}`,style:{...l,...u},children:[(0,j.jsx)(ea,{open:s,onToggle:()=>c(e=>!e)}),n!==void 0&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`[`}),!s&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-ellipsis`,children:[` `,e.length,` items `]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`]`})]})]}),s&&(0,j.jsxs)(j.Fragment,{children:[e.map((e,n)=>(0,j.jsx)(ra,{value:e,depth:t+1,changedKeys:r,keyPath:d(String(n))},n)),(0,j.jsx)(`div`,{className:`json-tree-line`,style:l,children:(0,j.jsx)(`span`,{className:`json-punct`,children:`]`})})]})]});if(typeof e==`object`){let i=Object.entries(e);return i.length===0?(0,j.jsxs)(`div`,{className:`json-tree-line`,style:l,children:[n!==void 0&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`{}`})]}):(0,j.jsxs)(`div`,{className:`json-tree-branch`,children:[(0,j.jsxs)(`div`,{className:`json-tree-line${a?` json-tree-changed`:``}`,style:{...l,...u},children:[(0,j.jsx)(ea,{open:s,onToggle:()=>c(e=>!e)}),n!==void 0&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`{`}),!s&&(0,j.jsxs)(j.Fragment,{children:[(0,j.jsxs)(`span`,{className:`json-ellipsis`,children:[` `,i.length,` keys `]}),(0,j.jsx)(`span`,{className:`json-punct`,children:`}`})]})]}),s&&(0,j.jsxs)(j.Fragment,{children:[i.map(([e,n])=>(0,j.jsx)(ra,{value:n,depth:t+1,propertyKey:e,changedKeys:r,keyPath:d(e)},e)),(0,j.jsx)(`div`,{className:`json-tree-line`,style:l,children:(0,j.jsx)(`span`,{className:`json-punct`,children:`}`})})]})]})}return(0,j.jsx)(`div`,{className:`json-tree-line`,style:l,children:(0,j.jsx)(`span`,{className:`json-unknown`,children:String(e)})})}var ia=[{id:`changes`,label:`Changes`,emoji:`±`},{id:`before-after`,label:`Before / After`,emoji:`⇄`},{id:`raw`,label:`Raw JSON`,emoji:`{ }`}];function aa({aggregateId:e,sequence:t,activeTab:n,onTabChange:r,source:i}){let{data:a,isLoading:o}=Qi(e,i),[s,c]=(0,A.useState)(`changes`),l=n??s,u=e=>{c(e),r?.(e)};if(o)return(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsx)(`div`,{className:`card-title`,children:`🔬 State at Event`}),(0,j.jsx)(`div`,{className:`skeleton`,style:{height:120}})]});let d=a?.find(e=>e.event.sequenceNumber===t);if(!d)return null;let{event:f,stateBefore:p,stateAfter:m,diff:h}=d,g=Object.entries(h),_=g.length>0,v=new Set(g.map(([e])=>e));return(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsxs)(`div`,{className:`card-title`,children:[`🔬 State at Event #`,f.sequenceNumber,(0,j.jsx)(`span`,{style:{color:`var(--accent-blue)`,background:`var(--accent-blue-dim)`,padding:`2px 8px`,borderRadius:4,fontSize:12},children:f.eventType}),_&&(0,j.jsxs)(`span`,{className:`diff-count-badge`,children:[g.length,` `,g.length===1?`change`:`changes`]})]}),(0,j.jsx)(`div`,{className:`state-tabs`,role:`tablist`,children:ia.map(e=>(0,j.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":l===e.id,className:`state-tab ${l===e.id?`active`:``}`,onClick:()=>u(e.id),children:[(0,j.jsx)(`span`,{className:`state-tab-emoji`,"aria-hidden":!0,children:e.emoji}),e.label]},e.id))}),(0,j.jsxs)(`div`,{className:`state-tab-content`,role:`tabpanel`,children:[l===`changes`&&(0,j.jsx)(`div`,{children:_?(0,j.jsx)($i,{diff:h}):(0,j.jsx)(`p`,{style:{color:`var(--text-muted)`,marginTop:12,fontSize:13},children:`No field changes at this event.`})}),l===`before-after`&&(0,j.jsxs)(`div`,{className:`state-grid`,style:{marginTop:12},children:[(0,j.jsxs)(`div`,{className:`state-panel state-panel-before`,children:[(0,j.jsx)(`h4`,{children:`Before`}),(0,j.jsx)(na,{value:p,changedKeys:v})]}),(0,j.jsxs)(`div`,{className:`state-panel state-panel-after`,children:[(0,j.jsx)(`h4`,{children:`After`}),(0,j.jsx)(na,{value:m,changedKeys:v})]})]}),l===`raw`&&(0,j.jsxs)(`div`,{style:{marginTop:12},children:[(0,j.jsx)(`div`,{className:`json-block`,style:{maxHeight:340},children:JSON.stringify(f,null,2)}),(0,j.jsx)(`button`,{className:`copy-btn`,type:`button`,onClick:()=>navigator.clipboard.writeText(JSON.stringify(f,null,2)),children:`📋 Copy Event JSON`})]})]})]})}var oa=1e3,sa=3e4;function ca(e,t,n){let r=n?.enabled??!0,[i,a]=(0,A.useState)(()=>r?`connecting`:`connected`),o=(0,A.useRef)(null),s=(0,A.useRef)(t),c=(0,A.useRef)(oa);return s.current=t,(0,A.useEffect)(()=>{if(!r)return a(`connected`),()=>{};let t=!1,n,i=()=>{if(t)return;let r=new WebSocket(`${window.location.protocol===`https:`?`wss`:`ws`}://${window.location.host}${e}`);o.current=r,r.onopen=()=>{c.current=oa,a(`connected`)},r.onclose=()=>{if(a(`disconnected`),!t){let e=c.current;n=setTimeout(()=>{c.current=Math.min(e*2,sa),i()},e)}},r.onerror=()=>a(`disconnected`),r.onmessage=e=>{try{let t=JSON.parse(e.data);s.current(t)}catch{}}};return i(),()=>{t=!0,clearTimeout(n),o.current?.close()}},[e,r]),i}var R=(0,A.createContext)(void 0);function la({children:e}){let[t,n]=(0,A.useState)([]),r=(0,A.useCallback)(e=>{let t=Date.now();n(n=>[...n,{id:t,message:e}]),setTimeout(()=>{n(e=>e.filter(e=>e.id!==t))},4e3)},[]);return(0,j.jsxs)(R.Provider,{value:{notify:r},children:[e,(0,j.jsx)(`div`,{className:`toast-container`,children:t.map(e=>(0,j.jsx)(`div`,{className:`toast`,children:e.message},e.id))})]})}function ua(){let e=(0,A.useContext)(R);if(!e)throw Error(`useToast must be used within ToastProvider`);return e}function da(e){let t=e.toLowerCase();return t.includes(`deleted`)||t.includes(`closed`)||t.includes(`cancelled`)||t.includes(`rejected`)?`type-deleted`:t.includes(`withdrawn`)||t.includes(`debit`)?`type-withdrawn`:t.includes(`deposited`)||t.includes(`credit`)?`type-deposited`:t.includes(`created`)||t.includes(`opened`)||t.includes(`placed`)||t.includes(`submitted`)?`type-created`:t.includes(`completed`)||t.includes(`resolved`)||t.includes(`accepted`)||t.includes(`approved`)||t.includes(`assigned`)?`type-completed`:t.includes(`failed`)||t.includes(`error`)?`type-failed`:t.includes(`transfer`)?`type-transfer`:`type-default`}function fa(e){return da(e)}var pa=100;function ma(e){let t=e.toLowerCase();return t.includes(`deleted`)||t.includes(`closed`)||t.includes(`cancelled`)||t.includes(`rejected`)?`✖`:t.includes(`withdrawn`)||t.includes(`debit`)?`↩`:t.includes(`deposited`)||t.includes(`credit`)?`↪`:t.includes(`created`)||t.includes(`opened`)||t.includes(`placed`)||t.includes(`submitted`)?`✦`:t.includes(`completed`)||t.includes(`resolved`)||t.includes(`accepted`)||t.includes(`approved`)?`✔`:t.includes(`failed`)||t.includes(`error`)?`⚠`:t.includes(`transfer`)?`⇄`:`◆`}function ha({source:e}){return(0,j.jsx)(va,{source:e})}function ga(e){return`type`in e&&e.type===`NO_LIVE_STREAM`}function _a(e){return e?`/ws/live?source=${encodeURIComponent(e)}`:`/ws/live`}function va({source:e}){let t=Ai(),[n,r]=(0,A.useState)(()=>t?Oi():[]),[i,a]=(0,A.useState)(!1),[o,s]=(0,A.useState)(null),c=(0,A.useRef)(null),l=(0,A.useRef)(i);l.current=i;let{notify:u}=ua();(0,A.useEffect)(()=>{s(null),r(t?Oi():[])},[e,t]);let d=ca(_a(e),e=>{if(ga(e)){s(e.source),r([]);return}s(null),!l.current&&r(t=>[...t.slice(-(pa-1)),e])},{enabled:!t}),f=(0,A.useRef)(u);f.current=u;let p=(0,A.useRef)(0);return(0,A.useEffect)(()=>{t||(d===`disconnected`?(p.current++,p.current<=1&&f.current(`Live stream disconnected. Retrying…`)):d===`connected`&&(p.current=0))},[d,t]),(0,A.useEffect)(()=>{!i&&c.current&&(c.current.scrollTop=c.current.scrollHeight)},[n,i]),(0,A.useEffect)(()=>{let e=()=>a(e=>!e);return window.addEventListener(`eventlens:togglestream`,e),()=>window.removeEventListener(`eventlens:togglestream`,e)},[]),(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsxs)(`div`,{className:`live-header`,children:[(0,j.jsx)(`div`,{className:`card-title`,style:{marginBottom:0},children:`📡 Live Event Stream`}),(0,j.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,j.jsxs)(`div`,{className:`live-indicator`,children:[(0,j.jsx)(`span`,{className:`dot ${d===`connected`?`dot-green`:d===`connecting`?`dot-yellow`:`dot-red`}`}),(0,j.jsx)(`span`,{style:{color:d===`connected`?`var(--neon-green)`:`var(--text-muted)`,fontSize:11},children:d===`connected`?i?`Paused`:`Live`:d})]}),(0,j.jsx)(`button`,{className:`pause-btn`,onClick:()=>a(!i),children:i?`▶ Resume`:`⏸ Pause`})]})]}),(0,j.jsxs)(`div`,{className:`event-stream`,ref:c,children:[o&&(0,j.jsxs)(`div`,{style:{color:`var(--text-muted)`,padding:`20px 0`,fontSize:12,fontFamily:`var(--font-mono)`},children:[`Live stream not available for this source`,e?` (${e})`:o?` (${o})`:``,`.`]}),n.length===0&&(0,j.jsx)(`div`,{style:{color:`var(--text-muted)`,padding:`20px 0`,fontSize:12,fontFamily:`var(--font-mono)`},children:o?null:t?`Demo stream (static sample events)`:`Waiting for events…`}),n.map(e=>(0,j.jsxs)(`div`,{className:`event-row ${fa(e.eventType)}`,children:[(0,j.jsx)(`span`,{className:`event-icon`,children:ma(e.eventType)}),(0,j.jsx)(`span`,{className:`event-time`,children:Wi(e.timestamp).toLocaleTimeString()}),(0,j.jsx)(`span`,{className:`event-type ${da(e.eventType)}`,children:e.eventType}),(0,j.jsx)(`span`,{className:`event-agg`,children:e.aggregateId})]},e.eventId))]})]})}function ya(e){switch(e){case`CRITICAL`:return`sev-critical`;case`HIGH`:return`sev-high`;case`MEDIUM`:return`sev-medium`;case`LOW`:return`sev-low`;default:return`sev-low`}}function ba(e){switch(e){case`CRITICAL`:return`Critical`;case`HIGH`:return`High`;case`MEDIUM`:return`Warning`;case`LOW`:return`Info`;default:return e}}function xa(){return(0,j.jsxs)(`svg`,{viewBox:`0 0 64 64`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,j.jsx)(`defs`,{children:(0,j.jsxs)(`linearGradient`,{id:`shield-grad`,x1:`16`,y1:`8`,x2:`48`,y2:`56`,children:[(0,j.jsx)(`stop`,{offset:`0%`,stopColor:`#00ff88`,stopOpacity:`0.9`}),(0,j.jsx)(`stop`,{offset:`100%`,stopColor:`#00cc66`,stopOpacity:`0.6`})]})}),(0,j.jsx)(`path`,{d:`M32 4 L52 14 L52 32 C52 46 32 58 32 58 C32 58 12 46 12 32 L12 14 Z`,stroke:`url(#shield-grad)`,strokeWidth:`2`,fill:`rgba(0, 255, 136, 0.06)`}),(0,j.jsx)(`path`,{d:`M32 10 L48 18 L48 32 C48 43 32 53 32 53 C32 53 16 43 16 32 L16 18 Z`,stroke:`rgba(0, 255, 136, 0.2)`,strokeWidth:`1`,fill:`none`}),(0,j.jsx)(`polyline`,{points:`22,32 29,40 42,24`,stroke:`#00ff88`,strokeWidth:`3`,strokeLinecap:`round`,strokeLinejoin:`round`,fill:`none`})]})}function Sa({color:e}){return(0,j.jsx)(`div`,{className:`gauge-wave`,children:Array.from({length:12},(e,t)=>t).map(t=>(0,j.jsx)(`div`,{className:`gauge-wave-bar ${e}`,style:{animationDelay:`${t*.12}s`}},t))})}function Ca({source:e}){let{data:t,isLoading:n}=_t({queryKey:[`anomalies`,e??`default`],queryFn:()=>I(100,e),refetchInterval:3e4}),r=t&&t.length>0;return(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsxs)(`div`,{className:`card-title anomaly-card-title-row`,children:[(0,j.jsx)(`span`,{className:`anomaly-title-text`,children:`⚠️ Anomaly Detection`}),!n&&r&&(0,j.jsx)(`span`,{className:`anomaly-header-count`,"aria-label":`${t.length} anomalies`,children:t.length})]}),n&&(0,j.jsx)(`div`,{className:`skeleton`,style:{height:120}}),!n&&!r&&(0,j.jsxs)(`div`,{className:`anomaly-panel-inner`,children:[(0,j.jsxs)(`div`,{className:`anomaly-shield`,children:[(0,j.jsx)(`div`,{className:`shield-icon`,children:(0,j.jsx)(xa,{})}),(0,j.jsx)(`div`,{className:`shield-text`,children:`No anomalies detected`})]}),(0,j.jsxs)(`div`,{className:`gauge-row`,children:[(0,j.jsxs)(`div`,{className:`gauge-card optimal`,children:[(0,j.jsx)(`div`,{className:`gauge-label`,children:`Data Integrity`}),(0,j.jsx)(`div`,{className:`gauge-value optimal`,children:`OPTIMAL`}),(0,j.jsx)(Sa,{color:`green`})]}),(0,j.jsxs)(`div`,{className:`gauge-card baseline`,children:[(0,j.jsx)(`div`,{className:`gauge-label`,children:`Pattern Scan`}),(0,j.jsx)(`div`,{className:`gauge-value baseline`,children:`BASELINE`}),(0,j.jsx)(Sa,{color:`cyan`})]}),(0,j.jsxs)(`div`,{className:`gauge-card zero`,children:[(0,j.jsx)(`div`,{className:`gauge-label`,children:`Threat Level`}),(0,j.jsx)(`div`,{className:`gauge-value zero`,children:`ZERO`}),(0,j.jsx)(Sa,{color:`green`})]})]})]}),!n&&r&&(0,j.jsx)(`div`,{className:`anomaly-scroll-region`,children:(0,j.jsx)(`div`,{className:`anomaly-list-inner`,children:t.map((e,t)=>(0,j.jsxs)(`details`,{className:`anomaly-card ${e.severity}`,children:[(0,j.jsxs)(`summary`,{className:`anomaly-card-summary`,children:[(0,j.jsx)(`span`,{className:`anomaly-severity-badge ${ya(e.severity)}`,children:ba(e.severity)}),(0,j.jsx)(`span`,{className:`anomaly-card-title`,children:e.description}),(0,j.jsx)(`span`,{className:`anomaly-card-chevron`,"aria-hidden":!0,children:`▼`})]}),(0,j.jsxs)(`div`,{className:`anomaly-card-body`,children:[(0,j.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,j.jsx)(`span`,{className:`anomaly-meta-label`,children:`Aggregate`}),(0,j.jsx)(`code`,{className:`anomaly-meta-value`,children:e.aggregateId})]}),(0,j.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,j.jsx)(`span`,{className:`anomaly-meta-label`,children:`Sequence`}),(0,j.jsxs)(`span`,{className:`anomaly-meta-value`,children:[`#`,e.atSequence]})]}),(0,j.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,j.jsx)(`span`,{className:`anomaly-meta-label`,children:`Event type`}),(0,j.jsx)(`span`,{className:`anomaly-meta-value`,children:e.triggeringEventType})]}),(0,j.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,j.jsx)(`span`,{className:`anomaly-meta-label`,children:`When`}),(0,j.jsx)(`span`,{className:`anomaly-meta-value`,children:Wi(e.timestamp).toLocaleString()})]}),(0,j.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,j.jsx)(`span`,{className:`anomaly-meta-label`,children:`Code`}),(0,j.jsx)(`code`,{className:`anomaly-meta-value`,children:e.code})]})]})]},`${e.aggregateId}-${e.atSequence}-${t}`))})})]})}var wa=[{keys:`← →`,desc:`Navigate events`},{keys:`Shift+← →`,desc:`Jump to group boundary`},{keys:`1 – 3`,desc:`Switch tabs (Changes / ⇄ Before-After / Raw)`},{keys:`Cmd+K`,desc:`Focus search`},{keys:`Space`,desc:`Pause / resume live stream`},{keys:`?`,desc:`Toggle this hint bar`}];function Ta(){let[e,t]=(0,A.useState)(!1);return(0,A.useEffect)(()=>{let e=e=>{e.target.tagName!==`INPUT`&&e.key===`?`&&(e.preventDefault(),t(e=>!e))};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]),(0,j.jsx)(`div`,{className:`keyboard-hints ${e?`keyboard-hints--expanded`:``}`,"aria-label":`Keyboard shortcuts`,children:e?(0,j.jsxs)(`div`,{className:`keyboard-hints-grid`,children:[wa.map(e=>(0,j.jsxs)(`div`,{className:`keyboard-hint-row`,children:[(0,j.jsx)(`kbd`,{className:`keyboard-key`,children:e.keys}),(0,j.jsx)(`span`,{className:`keyboard-hint-desc`,children:e.desc})]},e.keys)),(0,j.jsx)(`button`,{type:`button`,className:`keyboard-hints-close`,onClick:()=>t(!1),"aria-label":`Close shortcuts`,children:`✕ Close`})]}):(0,j.jsxs)(`div`,{className:`keyboard-hints-bar`,children:[(0,j.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,j.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`← →`}),` Navigate`]}),(0,j.jsx)(`span`,{className:`keyboard-hints-sep`,children:`·`}),(0,j.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,j.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`1–3`}),` Tabs`]}),(0,j.jsx)(`span`,{className:`keyboard-hints-sep`,children:`·`}),(0,j.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,j.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`Space`}),` Pause stream`]}),(0,j.jsx)(`span`,{className:`keyboard-hints-sep`,children:`·`}),(0,j.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,j.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`?`}),` All shortcuts`]})]})})}function Ea(){return(0,j.jsxs)(`svg`,{viewBox:`0 0 40 40`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,j.jsx)(`defs`,{children:(0,j.jsxs)(`linearGradient`,{id:`lens-grad`,x1:`0`,y1:`0`,x2:`40`,y2:`40`,children:[(0,j.jsx)(`stop`,{offset:`0%`,stopColor:`#00f0ff`}),(0,j.jsx)(`stop`,{offset:`100%`,stopColor:`#ff00e5`})]})}),(0,j.jsx)(`circle`,{cx:`18`,cy:`18`,r:`11`,stroke:`url(#lens-grad)`,strokeWidth:`2.5`,fill:`none`}),(0,j.jsx)(`circle`,{cx:`18`,cy:`18`,r:`6`,stroke:`#00f0ff`,strokeWidth:`1`,fill:`none`,opacity:`0.5`}),(0,j.jsx)(`line`,{x1:`26`,y1:`26`,x2:`36`,y2:`36`,stroke:`url(#lens-grad)`,strokeWidth:`3`,strokeLinecap:`round`}),(0,j.jsx)(`polygon`,{points:`18,12 21,18 18,24 15,18`,fill:`#00f0ff`,opacity:`0.3`})]})}function Da(){return(0,j.jsx)(`div`,{className:`mini-wave`,children:[6,12,8,16,10,14,7,11,15,9].map((e,t)=>(0,j.jsx)(`div`,{className:`mini-wave-bar`,style:{height:e,animationDelay:`${t*.1}s`}},t))})}function Oa(e){let t=e.toLowerCase();return t===`ready`||t===`up`?`#00ff88`:t===`degraded`||t===`initializing`?`#ffd166`:`#ff6b6b`}function ka(e){let t=e.toLowerCase();return t===`ready`||t===`up`}function Aa(e){return e.toLowerCase()===`ready`}function ja({isUp:e,source:t}){let[n,r]=(0,A.useState)(0),[i,a]=(0,A.useState)(null),o=(0,A.useRef)(void 0);return(0,A.useEffect)(()=>{let e=Date.now();return o.current=setInterval(()=>{r(Math.floor((Date.now()-e)/1e3))},1e3),()=>clearInterval(o.current)},[]),(0,A.useEffect)(()=>{let e=()=>{L(500,t).then(e=>a(e.length)).catch(()=>{})};e();let n=setInterval(e,15e3);return()=>clearInterval(n)},[t]),(0,j.jsxs)(`div`,{className:`conn-stats`,children:[(0,j.jsx)(Da,{}),(0,j.jsxs)(`div`,{className:`conn-stat`,children:[(0,j.jsx)(`span`,{className:`conn-stat-label`,children:`API`}),(0,j.jsx)(`span`,{className:`conn-stat-value ${e?`green`:``}`,children:e?`Healthy`:`Down`})]}),(0,j.jsxs)(`div`,{className:`conn-stat conn-stat--metric`,children:[(0,j.jsx)(`span`,{className:`conn-stat-label`,children:`Events`}),(0,j.jsx)(`span`,{className:`conn-stat-value`,children:i??`...`})]}),(0,j.jsxs)(`div`,{className:`conn-stat conn-stat--uptime`,children:[(0,j.jsx)(`span`,{className:`conn-stat-label`,children:`Uptime`}),(0,j.jsx)(`span`,{className:`conn-stat-value green conn-stat-value--uptime`,children:(e=>{let t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60;return t>0?`${t}h ${String(n).padStart(2,`0`)}m`:n>0?`${String(n).padStart(2,`0`)}m ${String(r).padStart(2,`0`)}s`:`${String(r).padStart(2,`0`)}s`})(n)})]})]})}function Ma({aggregateId:e,sequence:t,totalEvents:n,source:r}){let{data:i}=_t({queryKey:[`transitions`,e,r??`default`],queryFn:()=>Ii(e,r),staleTime:3e4}),a=i?.find(e=>e.event.sequenceNumber===t);if(!a)return null;let{event:o,diff:s}=a,c=Object.keys(s).length,l=i?i.findIndex(e=>e.event.sequenceNumber===t)+1:null;return(0,j.jsxs)(`div`,{className:`event-summary-bar`,children:[(0,j.jsxs)(`div`,{className:`event-summary-left`,children:[(0,j.jsx)(`span`,{className:`event-summary-type`,children:o.eventType}),(0,j.jsxs)(`span`,{className:`event-summary-meta`,children:[`seq #`,t,l!==null&&` step ${l} of ${n}`,` `,Wi(o.timestamp).toLocaleTimeString(),r?` source ${r}`:``]})]}),c>0&&(0,j.jsxs)(`span`,{className:`event-summary-changes`,children:[c,` `,c===1?`field`:`fields`,` changed`]})]})}function Na({datasources:e,datasourceHealth:t,plugins:n}){return(0,j.jsxs)(`div`,{className:`plugin-dashboard`,children:[(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsx)(`div`,{className:`card-title`,children:`Datasources`}),(0,j.jsx)(`div`,{className:`plugin-cards-grid`,children:e.map((e,n)=>{let r=t[n],i=Oa(e.status);return(0,j.jsxs)(`article`,{className:`plugin-card plugin-card--interactive`,style:{borderLeft:`3px solid ${i}`},children:[(0,j.jsxs)(`div`,{className:`plugin-card-head`,children:[(0,j.jsx)(`strong`,{children:e.displayName}),(0,j.jsx)(`span`,{className:`plugin-pill`,style:{color:i,borderColor:`${i}55`},children:e.status})]}),(0,j.jsx)(`div`,{className:`plugin-card-meta`,children:e.id}),r&&(0,j.jsxs)(`div`,{className:`plugin-card-detail`,children:[r.health.message,r.failureReason?` | ${r.failureReason}`:``]})]},e.id)})})]}),(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsx)(`div`,{className:`card-title`,children:`All Plugins`}),(0,j.jsx)(`div`,{className:`plugin-cards-grid plugin-cards-grid--dense`,children:n.map(e=>(0,j.jsxs)(`article`,{className:`plugin-card plugin-card--interactive`,children:[(0,j.jsxs)(`div`,{className:`plugin-card-head`,children:[(0,j.jsx)(`strong`,{children:e.displayName}),(0,j.jsx)(`span`,{className:`plugin-pill`,style:{color:Oa(e.lifecycle),borderColor:`${Oa(e.lifecycle)}55`},children:e.lifecycle})]}),(0,j.jsxs)(`div`,{className:`plugin-card-meta`,children:[e.pluginType,` | `,e.typeId]}),(0,j.jsx)(`div`,{className:`plugin-card-meta`,children:e.instanceId}),(0,j.jsxs)(`div`,{className:`plugin-card-detail`,children:[e.health.message,e.failureReason?` | ${e.failureReason}`:``]})]},e.instanceId))})]})]})}function Pa(){let[e,t]=(0,A.useState)(null),[n,r]=(0,A.useState)(null),[i,a]=(0,A.useState)(`changes`),[o,s]=(0,A.useState)(``),[c,l]=(0,A.useState)(window.location.hash||``),[u,d]=(0,A.useState)(!1);(0,A.useEffect)(()=>{let e=e=>{let t=e.detail;t&&a(t)};return window.addEventListener(`eventlens:switchtab`,e),()=>window.removeEventListener(`eventlens:switchtab`,e)},[]),(0,A.useEffect)(()=>{let e=()=>l(window.location.hash||``);return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,A.useEffect)(()=>{let e=new URLSearchParams(window.location.search),n=e.get(`aggregateId`),i=e.get(`seq`),o=e.get(`tab`),c=e.get(`source`);if(n&&t(n),i!==null){let e=Number(i);Number.isNaN(e)||r(e)}o&&[`changes`,`before-after`,`raw`].includes(o)&&a(o),c&&s(c)},[]),(0,A.useEffect)(()=>{let t=new URLSearchParams(window.location.search);e?t.set(`aggregateId`,e):t.delete(`aggregateId`),n==null?t.delete(`seq`):t.set(`seq`,String(n)),t.set(`tab`,i),o?t.set(`source`,o):t.delete(`source`);let r=t.toString(),a=`${window.location.pathname}${r?`?${r}`:``}${window.location.hash}`;window.history.replaceState(null,``,a)},[e,n,i,o]);let{data:f}=_t({queryKey:[`health`],queryFn:Li,refetchInterval:3e4}),{data:p=[]}=_t({queryKey:[`datasources`],queryFn:Ri,staleTime:1e4}),{data:m=[]}=_t({queryKey:[`plugins`],queryFn:Bi,staleTime:1e4}),h=ht({queries:p.map(e=>({queryKey:[`datasource-health`,e.id],queryFn:()=>zi(e.id),staleTime:1e4}))}).map(e=>e.data),g=f?.status===`UP`,_=e=>{t(e),r(null)},{data:v}=_t({queryKey:[`timeline-summary`,e,o||`default`],queryFn:()=>Fi(e,500,0,o||null,`metadata`),enabled:!!e,staleTime:3e4}),y=v?.totalEvents??0,b=c===`#/plugins`,x=p.filter(e=>ka(e.status)).length,S=m.filter(e=>ka(e.lifecycle)).length,C=p.length-x+(m.length-S);return(0,j.jsxs)(`div`,{className:`app`,children:[(0,j.jsxs)(`header`,{className:`app-header`,children:[(0,j.jsxs)(`div`,{className:`brand`,children:[(0,j.jsx)(`div`,{className:`brand-logo`,children:(0,j.jsx)(Ea,{})}),(0,j.jsxs)(`div`,{children:[(0,j.jsx)(`div`,{className:`brand-name`,children:`EventLens`}),(0,j.jsx)(`div`,{className:`brand-sub`,children:`Event Store Visual Debugger`})]})]}),(0,j.jsxs)(`div`,{className:`header-center`,children:[Ai()&&(0,j.jsx)(`div`,{className:`header-demo-pill`,role:`status`,children:`Demo mode`}),(0,j.jsx)(`div`,{className:`header-title`,children:`EventLens`})]}),(0,j.jsxs)(`div`,{className:`header-actions`,children:[(0,j.jsx)(ja,{isUp:g,source:o||null}),(0,j.jsxs)(`div`,{className:`header-status`,children:[(0,j.jsx)(`span`,{className:`dot ${g?`dot-green`:`dot-red`}`}),(0,j.jsx)(`span`,{className:`status-text ${g?``:`offline`}`,children:g?`Connected`:f?.status??`Connecting`})]})]})]}),(0,j.jsxs)(`aside`,{className:`workspace-dock${u?` workspace-dock--open`:``}`,"aria-label":`Workspace`,children:[(0,j.jsxs)(`div`,{className:`workspace-dock-panel`,id:`workspace-dock-panel`,hidden:!u,children:[(0,j.jsx)(`div`,{className:`workspace-dock-title`,children:`Workspace`}),(0,j.jsxs)(`label`,{className:`workspace-datasource`,children:[(0,j.jsx)(`span`,{className:`workspace-datasource-label`,children:`Datasource`}),(0,j.jsxs)(`select`,{id:`workspace-datasource-select`,className:`workspace-datasource-select`,value:o,onChange:e=>{s(e.target.value),r(null)},children:[(0,j.jsx)(`option`,{value:``,children:`Auto (primary datasource)`}),p.map(e=>(0,j.jsxs)(`option`,{value:e.id,disabled:!Aa(e.status),children:[e.id,` [`,e.status,`]`]},e.id))]})]}),(0,j.jsxs)(`div`,{className:`workspace-sidebar-kpis`,children:[(0,j.jsxs)(`div`,{className:`workspace-kpi-row`,children:[(0,j.jsx)(`span`,{children:`Datasources Healthy`}),(0,j.jsxs)(`strong`,{children:[x,`/`,p.length||0]})]}),(0,j.jsxs)(`div`,{className:`workspace-kpi-row`,children:[(0,j.jsx)(`span`,{children:`Plugins Healthy`}),(0,j.jsxs)(`strong`,{children:[S,`/`,m.length||0]})]}),(0,j.jsxs)(`div`,{className:`workspace-kpi-row`,children:[(0,j.jsx)(`span`,{children:`Issues`}),(0,j.jsx)(`strong`,{children:C})]})]}),(0,j.jsxs)(`div`,{className:`workspace-sidebar-links`,children:[(0,j.jsx)(`span`,{children:`Datasources`}),(0,j.jsx)(`span`,{children:`All Plugins`})]})]}),(0,j.jsx)(`button`,{type:`button`,className:`workspace-dock-handle`,onClick:()=>d(e=>!e),"aria-expanded":u,"aria-controls":`workspace-dock-panel`,"aria-label":u?`Collapse workspace`:`Expand workspace`,title:u?`Collapse workspace`:`Expand workspace`,children:(0,j.jsx)(`span`,{className:`workspace-dock-chevron`,"aria-hidden":!0,children:u?`›`:`‹`})})]}),u&&(0,j.jsx)(`button`,{type:`button`,className:`workspace-dock-scrim`,"aria-label":`Close workspace panel`,onClick:()=>d(!1)}),(0,j.jsx)(`main`,{className:`app-main`,children:(0,j.jsxs)(`div`,{className:`workspace-content`,children:[!b&&(0,j.jsxs)(`div`,{className:`card search-panel card--dropdown-host`,children:[(0,j.jsx)(`label`,{className:`control-field-label`,htmlFor:`aggregate-search`,children:`Search Aggregates`}),(0,j.jsx)(Hi,{onSelect:_,source:o||null}),e&&(0,j.jsxs)(`div`,{className:`selection-summary`,children:[`Viewing: `,(0,j.jsx)(`span`,{style:{color:`var(--neon-cyan)`,textShadow:`0 0 6px rgba(0,240,255,0.3)`},children:e}),o?(0,j.jsxs)(`span`,{children:[` on `,o]}):(0,j.jsx)(`span`,{children:` on primary datasource`}),(0,j.jsx)(`button`,{className:`selection-clear-btn`,onClick:()=>t(null),children:`× clear`})]})]}),b?(0,j.jsx)(Na,{datasources:p,datasourceHealth:h,plugins:m}):(0,j.jsxs)(j.Fragment,{children:[e&&(0,j.jsx)(Xi,{aggregateId:e,selectedSequence:n,onSelectEvent:r,source:o||null}),e&&n!==null&&(0,j.jsx)(Ma,{aggregateId:e,sequence:n,totalEvents:y,source:o||null}),e&&n!==null&&(0,j.jsx)(aa,{aggregateId:e,sequence:n,activeTab:i,onTabChange:a,source:o||null}),(0,j.jsxs)(`div`,{className:`bottom-grid`,children:[(0,j.jsx)(ha,{source:o||null}),(0,j.jsx)(Ca,{source:o||null})]})]})]})}),(0,j.jsx)(Ta,{})]})}var Fa=class extends A.Component{state={hasError:!1};static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e,t){console.error(`UI error boundary caught:`,e,t)}render(){return this.state.hasError?(0,j.jsx)(`div`,{className:`app`,children:(0,j.jsx)(`main`,{className:`app-main`,children:(0,j.jsxs)(`div`,{className:`card`,children:[(0,j.jsx)(`div`,{className:`card-title`,children:`Something went wrong`}),(0,j.jsx)(`p`,{style:{color:`var(--text-muted)`,fontSize:13},children:`The UI hit an unexpected error. Try refreshing the page. If the problem persists, check the browser console and server logs.`})]})})}):this.props.children}},Ia=new Ze({defaultOptions:{queries:{staleTime:3e4,retry:2}}});di.createRoot(document.getElementById(`root`)).render((0,j.jsx)(A.StrictMode,{children:(0,j.jsx)(nt,{client:Ia,children:(0,j.jsx)(la,{children:(0,j.jsx)(Fa,{children:(0,j.jsx)(Pa,{})})})})})); \ No newline at end of file diff --git a/eventlens-api/src/main/resources/web/assets/index-C0DJTCkS.js b/eventlens-api/src/main/resources/web/assets/index-C0DJTCkS.js deleted file mode 100644 index 3eebd33..0000000 --- a/eventlens-api/src/main/resources/web/assets/index-C0DJTCkS.js +++ /dev/null @@ -1,14 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},ee=Object.prototype.hasOwnProperty;function te(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ne(e,t){return te(e.type,t,e.props)}function T(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function re(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ie=/\/+/g;function ae(e,t){return typeof e==`object`&&e&&e.key!=null?re(``+e.key):t.toString(36)}function oe(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function se(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,se(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ae(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(ie,`$&/`)+`/`),se(o,r,i,``,function(e){return e})):o!=null&&(T(o)&&(o=ne(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ie,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=u()})),f=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,T());else{var t=n(l);t!==null&&ae(x,t.startTime-e)}}var S=!1,C=-1,w=5,ee=-1;function te(){return g?!0:!(e.unstable_now()-eet&&te());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&ae(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?T():S=!1}}}var T;if(typeof y==`function`)T=function(){y(ne)};else if(typeof MessageChannel<`u`){var re=new MessageChannel,ie=re.port2;re.port1.onmessage=ne,T=function(){ie.postMessage(null)}}else T=function(){_(ne,0)};function ae(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,ae(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,T()))),r},e.unstable_shouldYield=te,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),p=o(((e,t)=>{t.exports=f()})),m=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=p(),n=d(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1fe||(e.current=de[fe],de[fe]=null,fe--)}function O(e,t){fe++,de[fe]=e.current,e.current=t}var he=pe(null),ge=pe(null),_e=pe(null),ve=pe(null);function ye(e,t){switch(O(_e,t),O(ge,e),O(he,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}me(he),O(he,e)}function be(){me(he),me(ge),me(_e)}function xe(e){e.memoizedState!==null&&O(ve,e);var t=he.current,n=Hd(t,e.type);t!==n&&(O(ge,e),O(he,n))}function Se(e){ge.current===e&&(me(he),me(ge)),ve.current===e&&(me(ve),Qf._currentValue=ue)}var Ce,we;function Te(e){if(Ce===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Ce=t&&t[1]||``,we=-1)`:-1i||c[r]!==l[i]){var u=` -`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ee=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?Te(n):``}function Oe(e,t){switch(e.tag){case 26:case 27:case 5:return Te(e.type);case 16:return Te(`Lazy`);case 13:return e.child!==t&&t!==null?Te(`Suspense Fallback`):Te(`Suspense`);case 19:return Te(`SuspenseList`);case 0:case 15:return De(e.type,!1);case 11:return De(e.type.render,!1);case 1:return De(e.type,!0);case 31:return Te(`Activity`);default:return``}}function ke(e){try{var t=``,n=null;do t+=Oe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var Ae=Object.prototype.hasOwnProperty,je=t.unstable_scheduleCallback,Me=t.unstable_cancelCallback,Ne=t.unstable_shouldYield,Pe=t.unstable_requestPaint,Fe=t.unstable_now,Ie=t.unstable_getCurrentPriorityLevel,Le=t.unstable_ImmediatePriority,Re=t.unstable_UserBlockingPriority,ze=t.unstable_NormalPriority,Be=t.unstable_LowPriority,Ve=t.unstable_IdlePriority,He=t.log,Ue=t.unstable_setDisableYieldValue,We=null,Ge=null;function Ke(e){if(typeof He==`function`&&Ue(e),Ge&&typeof Ge.setStrictMode==`function`)try{Ge.setStrictMode(We,e)}catch{}}var qe=Math.clz32?Math.clz32:Xe,Je=Math.log,Ye=Math.LN2;function Xe(e){return e>>>=0,e===0?32:31-(Je(e)/Ye|0)|0}var Ze=256,k=262144,A=4194304;function Qe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function $e(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Qe(n))):i=Qe(o):i=Qe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Qe(n))):i=Qe(o)):i=Qe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function et(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function tt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function nt(){var e=A;return A<<=1,!(A&62914560)&&(A=4194304),e}function rt(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function it(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function at(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),_n=!1;if(gn)try{var vn={};Object.defineProperty(vn,`passive`,{get:function(){_n=!0}}),window.addEventListener(`test`,vn,vn),window.removeEventListener(`test`,vn,vn)}catch{_n=!1}var yn=null,bn=null,xn=null;function Sn(){if(xn)return xn;var e,t=bn,n=t.length,r,i=`value`in yn?yn.value:yn.textContent,a=i.length;for(e=0;e=Qn),tr=` `,nr=!1;function rr(e,t){switch(e){case`keyup`:return Xn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function ir(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var ar=!1;function or(e,t){switch(e){case`compositionend`:return ir(t);case`keypress`:return t.which===32?(nr=!0,tr):null;case`textInput`:return e=t.data,e===tr&&nr?null:e;default:return null}}function sr(e,t){if(ar)return e===`compositionend`||!Zn&&rr(e,t)?(e=Sn(),xn=bn=yn=null,ar=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=kr(n)}}function jr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?jr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Mr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ut(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ut(e.document)}return t}function Nr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Pr=gn&&`documentMode`in document&&11>=document.documentMode,Fr=null,Ir=null,Lr=null,Rr=!1;function zr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Rr||Fr==null||Fr!==Ut(r)||(r=Fr,`selectionStart`in r&&Nr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Lr&&Or(Lr,r)||(Lr=r,r=Ed(Ir,`onSelect`),0>=o,i-=o,Ai=1<<32-qe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),I&&Mi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),I&&Mi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return I&&Mi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),I&&Mi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===T&&Aa(l)===r.type){n(e,r.sibling),c=a(r,o.props),La(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=_i(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=gi(o.type,o.key,o.props,null,e.mode,c),La(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=bi(o,e.mode,c),c.return=e,e=c}return s(e);case T:return o=Aa(o),b(e,r,o,c)}if(le(o))return h(e,r,o,c);if(oe(o)){if(l=oe(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Ia(o),c);if(o.$$typeof===C)return b(e,r,aa(e,o),c);Ra(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=vi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Fa=0;var i=b(e,t,n,r);return Pa=null,i}catch(t){if(t===wa||t===Ea)throw t;var a=fi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ba=za(!0),Va=za(!1),Ha=!1;function Ua(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=li(e),ci(e,null,n),t}return oi(e,r,t,n),li(e)}function qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,st(e,n)}}function Ja(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ya=!1;function Xa(){if(Ya){var e=ha;if(e!==null)throw e}}function Za(e,t,n,r){Ya=!1;var i=e.updateQueue;Ha=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===ma&&(Ya=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:Ha=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function $a(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=E.T,s={};E.T=s,Fs(e,!1,t,n);try{var c=i(),l=E.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ps(e,t,va(c,r),pu(e)):Ps(e,t,r,pu(e))}catch(n){Ps(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{D.p=a,o!==null&&s.types!==null&&(o.types=s.types),E.T=o}}function ws(){}function Ts(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Es(e).queue;Cs(e,a,t,ue,n===null?ws:function(){return Ds(e),n(r)})}function Es(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:ue},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ds(e){var t=Es(e);t.next===null&&(t=e.alternate.memoizedState),Ps(e,t.next.queue,{},pu())}function Os(){return ia(Qf)}function ks(){return H().memoizedState}function As(){return H().memoizedState}function js(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ga(n);var r=Ka(t,e,n);r!==null&&(hu(r,t,n),qa(r,t,n)),t={cache:ua()},e.payload=t;return}t=t.return}}function Ms(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e)?Ls(t,n):(n=si(e,t,n,r),n!==null&&(hu(n,e,r),Rs(n,t,r)))}function Ns(e,t,n){Ps(e,t,n,pu())}function Ps(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Dr(s,o))return oi(e,t,i,0),K===null&&ai(),!1}catch{}if(n=si(e,t,i,r),n!==null)return hu(n,e,r),Rs(n,t,r),!0}return!1}function Fs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Is(e)){if(t)throw Error(i(479))}else t=si(e,n,r,2),t!==null&&hu(t,e,2)}function Is(e){var t=e.alternate;return e===z||t!==null&&t===z}function Ls(e,t){_o=go=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Rs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,st(e,n)}}var zs={readContext:ia,use:Po,useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useLayoutEffect:V,useInsertionEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V,useDeferredValue:V,useTransition:V,useSyncExternalStore:V,useId:V,useHostTransitionStatus:V,useFormState:V,useActionState:V,useOptimistic:V,useMemoCache:V,useCacheRefresh:V};zs.useEffectEvent=V;var Bs={readContext:ia,use:Po,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:ia,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,gs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=jo();t=t===void 0?null:t;var r=e();if(vo){Ke(!0);try{e()}finally{Ke(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=jo();if(n!==void 0){var i=n(t);if(vo){Ke(!0);try{n(t)}finally{Ke(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ms.bind(null,z,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=Ns.bind(null,z,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:vs,useDeferredValue:function(e,t){return xs(jo(),e,t)},useTransition:function(){var e=Ko(!1);return e=Cs.bind(null,z,e.queue,!0,!1),jo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=z,a=jo();if(I){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Vo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,us(Uo.bind(null,r,o,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,o,n,t),null),n},useId:function(){var e=jo(),t=K.identifierPrefix;if(I){var n=ji,r=Ai;n=(r&~(1<<32-qe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=yo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[j]=t,o[mt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Pc(t)}}return U(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=_e.current,Wi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Li,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[j]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Vi(t,!0)}else e=Bd(e).createTextNode(r),e[j]=t,t.stateNode=e}return U(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Wi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[j]=t}else Gi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),e=!1}else n=Ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(fo(t),t):(fo(t),null);if(t.flags&128)throw Error(i(558))}return U(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Wi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[j]=t}else Gi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),a=!1}else a=Ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(fo(t),t):(fo(t),null)}return fo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Lc(t,t.updateQueue),U(t),null);case 4:return be(),e===null&&Sd(t.stateNode.containerInfo),U(t),null;case 10:return Qi(t.type),U(t),null;case 19:if(me(R),r=t.memoizedState,r===null)return U(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Rc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=po(e),o!==null){for(t.flags|=128,Rc(r,!1),e=o.updateQueue,t.updateQueue=e,Lc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)hi(n,e),n=n.sibling;return O(R,R.current&1|2),I&&Mi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Fe()>tu&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304)}else{if(!a)if(e=po(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Lc(t,e),Rc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!I)return U(t),null}else 2*Fe()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(U(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Fe(),e.sibling=null,n=R.current,O(R,a?n&1|2:n&1),I&&Mi(t,r.treeForkCount),e);case 22:case 23:return fo(t),io(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(U(t),t.subtreeFlags&6&&(t.flags|=8192)):U(t),n=t.updateQueue,n!==null&&Lc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&me(ba),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Qi(L),U(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Bc(e,t){switch(Fi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Qi(L),be(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Se(t),null;case 31:if(t.memoizedState!==null){if(fo(t),t.alternate===null)throw Error(i(340));Gi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(fo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Gi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return me(R),null;case 4:return be(),null;case 10:return Qi(t.type),null;case 22:case 23:return fo(t),io(),e!==null&&me(ba),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Qi(L),null;case 25:return null;default:return null}}function Vc(e,t){switch(Fi(t),t.tag){case 3:Qi(L),be();break;case 26:case 27:case 5:Se(t);break;case 4:be();break;case 31:t.memoizedState!==null&&fo(t);break;case 13:fo(t);break;case 19:me(R);break;case 10:Qi(t.type);break;case 22:case 23:fo(t),io(),e!==null&&me(ba);break;case 24:Qi(L)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{$a(t,n)}catch(t){Z(e,e.return,t)}}}function Gc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[mt]=t}catch(t){Z(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=sn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[j]=e,t[mt]=n}catch(t){Z(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Mr(e),Nr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[j]=e,Et(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Ar(s,h),v=Ar(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,E.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),Ge&&typeof Ge.onPostCommitFiberRoot==`function`)try{Ge.onPostCommitFiberRoot(We,o)}catch{}return!0}finally{D.p=a,E.T=r,Vu(e,t)}}function Wu(e,t,n){t=Si(n,t),t=$s(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&(it(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=Si(n,e),n=ec(2),r=Ka(t,n,2),r!==null&&(tc(n,r,t,e),it(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>Fe()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=nt()),e=P(e,t),e!==null&&(it(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return je(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-qe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=$e(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||et(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Fe(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Gt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),Et(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Gt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Gt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Gt(n.imageSizes)+`"]`)):i+=`[href="`+Gt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),Et(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Gt(r)+`"][href="`+Gt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),Et(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Tt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);Et(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Tt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Et(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Tt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Et(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=_e.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Tt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=Tt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Tt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Gt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),Et(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Gt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Gt(n.href)+`"]`);if(r)return t.instance=r,Et(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),Et(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,Et(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),Et(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,Et(a),a):(r=n,(a=mf.get(o))&&(r=m({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),Et(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,Et(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),Et(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},y={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},b=new class{#e=y;setTimeoutProvider(e){this.#e=e}setTimeout(e,t){return this.#e.setTimeout(e,t)}clearTimeout(e){this.#e.clearTimeout(e)}setInterval(e,t){return this.#e.setInterval(e,t)}clearInterval(e){this.#e.clearInterval(e)}};function x(e){setTimeout(e,0)}var S=typeof window>`u`||`Deno`in globalThis;function C(){}function w(e,t){return typeof e==`function`?e(t):e}function ee(e){return typeof e==`number`&&e>=0&&e!==1/0}function te(e,t){return Math.max(e+(t||0)-Date.now(),0)}function ne(e,t){return typeof e==`function`?e(t):e}function T(e,t){return typeof e==`function`?e(t):e}function re(e,t){let{type:n=`all`,exact:r,fetchStatus:i,predicate:a,queryKey:o,stale:s}=e;if(o){if(r){if(t.queryHash!==ae(o,t.options))return!1}else if(!se(t.queryKey,o))return!1}if(n!==`all`){let e=t.isActive();if(n===`active`&&!e||n===`inactive`&&e)return!1}return!(typeof s==`boolean`&&t.isStale()!==s||i&&i!==t.state.fetchStatus||a&&!a(t))}function ie(e,t){let{exact:n,status:r,predicate:i,mutationKey:a}=e;if(a){if(!t.options.mutationKey)return!1;if(n){if(oe(t.options.mutationKey)!==oe(a))return!1}else if(!se(t.options.mutationKey,a))return!1}return!(r&&t.state.status!==r||i&&!i(t))}function ae(e,t){return(t?.queryKeyHashFn||oe)(e)}function oe(e){return JSON.stringify(e,(e,t)=>ue(t)?Object.keys(t).sort().reduce((e,n)=>(e[n]=t[n],e),{}):t)}function se(e,t){return e===t?!0:typeof e==typeof t&&e&&t&&typeof e==`object`&&typeof t==`object`?Object.keys(t).every(n=>se(e[n],t[n])):!1}var ce=Object.prototype.hasOwnProperty;function le(e,t,n=0){if(e===t)return e;if(n>500)return t;let r=D(e)&&D(t);if(!r&&!(ue(e)&&ue(t)))return t;let i=(r?e:Object.keys(e)).length,a=r?t:Object.keys(t),o=a.length,s=r?Array(o):{},c=0;for(let l=0;l{b.setTimeout(t,e)})}function pe(e,t,n){return typeof n.structuralSharing==`function`?n.structuralSharing(e,t):n.structuralSharing===!1?t:le(e,t)}function me(e,t,n=0){let r=[...e,t];return n&&r.length>n?r.slice(1):r}function O(e,t,n=0){let r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var he=Symbol();function ge(e,t){return!e.queryFn&&t?.initialPromise?()=>t.initialPromise:!e.queryFn||e.queryFn===he?()=>Promise.reject(Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function _e(e,t){return typeof e==`function`?e(...t):!!e}function ve(e,t,n){let r=!1,i;return Object.defineProperty(e,`signal`,{enumerable:!0,get:()=>(i??=t(),r?i:(r=!0,i.aborted?n():i.addEventListener(`abort`,n,{once:!0}),i))}),e}var ye=new class extends v{#e;#t;#n;constructor(){super(),this.#n=e=>{if(!S&&window.addEventListener){let t=()=>e();return window.addEventListener(`visibilitychange`,t,!1),()=>{window.removeEventListener(`visibilitychange`,t)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(e){this.#n=e,this.#t?.(),this.#t=e(e=>{typeof e==`boolean`?this.setFocused(e):this.onFocus()})}setFocused(e){this.#e!==e&&(this.#e=e,this.onFocus())}onFocus(){let e=this.isFocused();this.listeners.forEach(t=>{t(e)})}isFocused(){return typeof this.#e==`boolean`?this.#e:globalThis.document?.visibilityState!==`hidden`}};function be(){let e,t,n=new Promise((n,r)=>{e=n,t=r});n.status=`pending`,n.catch(()=>{});function r(e){Object.assign(n,e),delete n.resolve,delete n.reject}return n.resolve=t=>{r({status:`fulfilled`,value:t}),e(t)},n.reject=e=>{r({status:`rejected`,reason:e}),t(e)},n}var xe=x;function Se(){let e=[],t=0,n=e=>{e()},r=e=>{e()},i=xe,a=r=>{t?e.push(r):i(()=>{n(r)})},o=()=>{let t=e;e=[],t.length&&i(()=>{r(()=>{t.forEach(e=>{n(e)})})})};return{batch:e=>{let n;t++;try{n=e()}finally{t--,t||o()}return n},batchCalls:e=>(...t)=>{a(()=>{e(...t)})},schedule:a,setNotifyFunction:e=>{n=e},setBatchNotifyFunction:e=>{r=e},setScheduler:e=>{i=e}}}var Ce=Se(),we=new class extends v{#e=!0;#t;#n;constructor(){super(),this.#n=e=>{if(!S&&window.addEventListener){let t=()=>e(!0),n=()=>e(!1);return window.addEventListener(`online`,t,!1),window.addEventListener(`offline`,n,!1),()=>{window.removeEventListener(`online`,t),window.removeEventListener(`offline`,n)}}}}onSubscribe(){this.#t||this.setEventListener(this.#n)}onUnsubscribe(){this.hasListeners()||(this.#t?.(),this.#t=void 0)}setEventListener(e){this.#n=e,this.#t?.(),this.#t=e(this.setOnline.bind(this))}setOnline(e){this.#e!==e&&(this.#e=e,this.listeners.forEach(t=>{t(e)}))}isOnline(){return this.#e}};function Te(e){return Math.min(1e3*2**e,3e4)}function Ee(e){return(e??`online`)===`online`?we.isOnline():!0}var De=class extends Error{constructor(e){super(`CancelledError`),this.revert=e?.revert,this.silent=e?.silent}};function Oe(e){let t=!1,n=0,r,i=be(),a=()=>i.status!==`pending`,o=t=>{if(!a()){let n=new De(t);f(n),e.onCancel?.(n)}},s=()=>{t=!0},c=()=>{t=!1},l=()=>ye.isFocused()&&(e.networkMode===`always`||we.isOnline())&&e.canRun(),u=()=>Ee(e.networkMode)&&e.canRun(),d=e=>{a()||(r?.(),i.resolve(e))},f=e=>{a()||(r?.(),i.reject(e))},p=()=>new Promise(t=>{r=e=>{(a()||l())&&t(e)},e.onPause?.()}).then(()=>{r=void 0,a()||e.onContinue?.()}),m=()=>{if(a())return;let r,i=n===0?e.initialPromise:void 0;try{r=i??e.fn()}catch(e){r=Promise.reject(e)}Promise.resolve(r).then(d).catch(r=>{if(a())return;let i=e.retry??(S?0:3),o=e.retryDelay??Te,s=typeof o==`function`?o(n,r):o,c=i===!0||typeof i==`number`&&nl()?void 0:p()).then(()=>{t?f(r):m()})})};return{promise:i,status:()=>i.status,cancel:o,continue:()=>(r?.(),i),cancelRetry:s,continueRetry:c,canStart:u,start:()=>(u()?m():p().then(m),i)}}var ke=class{#e;destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),ee(this.gcTime)&&(this.#e=b.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(S?1/0:300*1e3))}clearGcTimeout(){this.#e&&=(b.clearTimeout(this.#e),void 0)}},Ae=class extends ke{#e;#t;#n;#r;#i;#a;#o;constructor(e){super(),this.#o=!1,this.#a=e.defaultOptions,this.setOptions(e.options),this.observers=[],this.#r=e.client,this.#n=this.#r.getQueryCache(),this.queryKey=e.queryKey,this.queryHash=e.queryHash,this.#e=Ne(this.options),this.state=e.state??this.#e,this.scheduleGc()}get meta(){return this.options.meta}get promise(){return this.#i?.promise}setOptions(e){if(this.options={...this.#a,...e},this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){let e=Ne(this.options);e.data!==void 0&&(this.setState(Me(e.data,e.dataUpdatedAt)),this.#e=e)}}optionalRemove(){!this.observers.length&&this.state.fetchStatus===`idle`&&this.#n.remove(this)}setData(e,t){let n=pe(this.state.data,e,this.options);return this.#s({data:n,type:`success`,dataUpdatedAt:t?.updatedAt,manual:t?.manual}),n}setState(e,t){this.#s({type:`setState`,state:e,setStateOptions:t})}cancel(e){let t=this.#i?.promise;return this.#i?.cancel(e),t?t.then(C).catch(C):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}reset(){this.destroy(),this.setState(this.#e)}isActive(){return this.observers.some(e=>T(e.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===he||this.state.dataUpdateCount+this.state.errorUpdateCount===0}isStatic(){return this.getObserversCount()>0?this.observers.some(e=>ne(e.options.staleTime,this)===`static`):!1}isStale(){return this.getObserversCount()>0?this.observers.some(e=>e.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(e=0){return this.state.data===void 0?!0:e===`static`?!1:this.state.isInvalidated?!0:!te(this.state.dataUpdatedAt,e)}onFocus(){this.observers.find(e=>e.shouldFetchOnWindowFocus())?.refetch({cancelRefetch:!1}),this.#i?.continue()}onOnline(){this.observers.find(e=>e.shouldFetchOnReconnect())?.refetch({cancelRefetch:!1}),this.#i?.continue()}addObserver(e){this.observers.includes(e)||(this.observers.push(e),this.clearGcTimeout(),this.#n.notify({type:`observerAdded`,query:this,observer:e}))}removeObserver(e){this.observers.includes(e)&&(this.observers=this.observers.filter(t=>t!==e),this.observers.length||(this.#i&&(this.#o?this.#i.cancel({revert:!0}):this.#i.cancelRetry()),this.scheduleGc()),this.#n.notify({type:`observerRemoved`,query:this,observer:e}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||this.#s({type:`invalidate`})}async fetch(e,t){if(this.state.fetchStatus!==`idle`&&this.#i?.status()!==`rejected`){if(this.state.data!==void 0&&t?.cancelRefetch)this.cancel({silent:!0});else if(this.#i)return this.#i.continueRetry(),this.#i.promise}if(e&&this.setOptions(e),!this.options.queryFn){let e=this.observers.find(e=>e.options.queryFn);e&&this.setOptions(e.options)}let n=new AbortController,r=e=>{Object.defineProperty(e,`signal`,{enumerable:!0,get:()=>(this.#o=!0,n.signal)})},i=()=>{let e=ge(this.options,t),n=(()=>{let e={client:this.#r,queryKey:this.queryKey,meta:this.meta};return r(e),e})();return this.#o=!1,this.options.persister?this.options.persister(e,n,this):e(n)},a=(()=>{let e={fetchOptions:t,options:this.options,queryKey:this.queryKey,client:this.#r,state:this.state,fetchFn:i};return r(e),e})();this.options.behavior?.onFetch(a,this),this.#t=this.state,(this.state.fetchStatus===`idle`||this.state.fetchMeta!==a.fetchOptions?.meta)&&this.#s({type:`fetch`,meta:a.fetchOptions?.meta}),this.#i=Oe({initialPromise:t?.initialPromise,fn:a.fetchFn,onCancel:e=>{e instanceof De&&e.revert&&this.setState({...this.#t,fetchStatus:`idle`}),n.abort()},onFail:(e,t)=>{this.#s({type:`failed`,failureCount:e,error:t})},onPause:()=>{this.#s({type:`pause`})},onContinue:()=>{this.#s({type:`continue`})},retry:a.options.retry,retryDelay:a.options.retryDelay,networkMode:a.options.networkMode,canRun:()=>!0});try{let e=await this.#i.start();if(e===void 0)throw Error(`${this.queryHash} data is undefined`);return this.setData(e),this.#n.config.onSuccess?.(e,this),this.#n.config.onSettled?.(e,this.state.error,this),e}catch(e){if(e instanceof De){if(e.silent)return this.#i.promise;if(e.revert){if(this.state.data===void 0)throw e;return this.state.data}}throw this.#s({type:`error`,error:e}),this.#n.config.onError?.(e,this),this.#n.config.onSettled?.(this.state.data,e,this),e}finally{this.scheduleGc()}}#s(e){this.state=(t=>{switch(e.type){case`failed`:return{...t,fetchFailureCount:e.failureCount,fetchFailureReason:e.error};case`pause`:return{...t,fetchStatus:`paused`};case`continue`:return{...t,fetchStatus:`fetching`};case`fetch`:return{...t,...je(t.data,this.options),fetchMeta:e.meta??null};case`success`:let n={...t,...Me(e.data,e.dataUpdatedAt),dataUpdateCount:t.dataUpdateCount+1,...!e.manual&&{fetchStatus:`idle`,fetchFailureCount:0,fetchFailureReason:null}};return this.#t=e.manual?n:void 0,n;case`error`:let r=e.error;return{...t,error:r,errorUpdateCount:t.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:t.fetchFailureCount+1,fetchFailureReason:r,fetchStatus:`idle`,status:`error`,isInvalidated:!0};case`invalidate`:return{...t,isInvalidated:!0};case`setState`:return{...t,...e.state}}})(this.state),Ce.batch(()=>{this.observers.forEach(e=>{e.onQueryUpdate()}),this.#n.notify({query:this,type:`updated`,action:e})})}};function je(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:Ee(t.networkMode)?`fetching`:`paused`,...e===void 0&&{error:null,status:`pending`}}}function Me(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:`success`}}function Ne(e){let t=typeof e.initialData==`function`?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt==`function`?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?`success`:`pending`,fetchStatus:`idle`}}var Pe=class extends v{constructor(e,t){super(),this.options=t,this.#e=e,this.#s=null,this.#o=be(),this.bindMethods(),this.setOptions(t)}#e;#t=void 0;#n=void 0;#r=void 0;#i;#a;#o;#s;#c;#l;#u;#d;#f;#p;#m=new Set;bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(this.#t.addObserver(this),Ie(this.#t,this.options)?this.#h():this.updateResult(),this.#y())}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Le(this.#t,this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Le(this.#t,this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,this.#b(),this.#x(),this.#t.removeObserver(this)}setOptions(e){let t=this.options,n=this.#t;if(this.options=this.#e.defaultQueryOptions(e),this.options.enabled!==void 0&&typeof this.options.enabled!=`boolean`&&typeof this.options.enabled!=`function`&&typeof T(this.options.enabled,this.#t)!=`boolean`)throw Error(`Expected enabled to be a boolean or a callback that returns a boolean`);this.#S(),this.#t.setOptions(this.options),t._defaulted&&!E(this.options,t)&&this.#e.getQueryCache().notify({type:`observerOptionsUpdated`,query:this.#t,observer:this});let r=this.hasListeners();r&&Re(this.#t,n,this.options,t)&&this.#h(),this.updateResult(),r&&(this.#t!==n||T(this.options.enabled,this.#t)!==T(t.enabled,this.#t)||ne(this.options.staleTime,this.#t)!==ne(t.staleTime,this.#t))&&this.#g();let i=this.#_();r&&(this.#t!==n||T(this.options.enabled,this.#t)!==T(t.enabled,this.#t)||i!==this.#p)&&this.#v(i)}getOptimisticResult(e){let t=this.#e.getQueryCache().build(this.#e,e),n=this.createResult(t,e);return Be(this,n)&&(this.#r=n,this.#a=this.options,this.#i=this.#t.state),n}getCurrentResult(){return this.#r}trackResult(e,t){return new Proxy(e,{get:(e,n)=>(this.trackProp(n),t?.(n),n===`promise`&&(this.trackProp(`data`),!this.options.experimental_prefetchInRender&&this.#o.status===`pending`&&this.#o.reject(Error(`experimental_prefetchInRender feature flag is not enabled`))),Reflect.get(e,n))})}trackProp(e){this.#m.add(e)}getCurrentQuery(){return this.#t}refetch({...e}={}){return this.fetch({...e})}fetchOptimistic(e){let t=this.#e.defaultQueryOptions(e),n=this.#e.getQueryCache().build(this.#e,t);return n.fetch().then(()=>this.createResult(n,t))}fetch(e){return this.#h({...e,cancelRefetch:e.cancelRefetch??!0}).then(()=>(this.updateResult(),this.#r))}#h(e){this.#S();let t=this.#t.fetch(this.options,e);return e?.throwOnError||(t=t.catch(C)),t}#g(){this.#b();let e=ne(this.options.staleTime,this.#t);if(S||this.#r.isStale||!ee(e))return;let t=te(this.#r.dataUpdatedAt,e)+1;this.#d=b.setTimeout(()=>{this.#r.isStale||this.updateResult()},t)}#_(){return(typeof this.options.refetchInterval==`function`?this.options.refetchInterval(this.#t):this.options.refetchInterval)??!1}#v(e){this.#x(),this.#p=e,!(S||T(this.options.enabled,this.#t)===!1||!ee(this.#p)||this.#p===0)&&(this.#f=b.setInterval(()=>{(this.options.refetchIntervalInBackground||ye.isFocused())&&this.#h()},this.#p))}#y(){this.#g(),this.#v(this.#_())}#b(){this.#d&&=(b.clearTimeout(this.#d),void 0)}#x(){this.#f&&=(b.clearInterval(this.#f),void 0)}createResult(e,t){let n=this.#t,r=this.options,i=this.#r,a=this.#i,o=this.#a,s=e===n?this.#n:e.state,{state:c}=e,l={...c},u=!1,d;if(t._optimisticResults){let i=this.hasListeners(),a=!i&&Ie(e,t),o=i&&Re(e,n,t,r);(a||o)&&(l={...l,...je(c.data,e.options)}),t._optimisticResults===`isRestoring`&&(l.fetchStatus=`idle`)}let{error:f,errorUpdatedAt:p,status:m}=l;d=l.data;let h=!1;if(t.placeholderData!==void 0&&d===void 0&&m===`pending`){let e;i?.isPlaceholderData&&t.placeholderData===o?.placeholderData?(e=i.data,h=!0):e=typeof t.placeholderData==`function`?t.placeholderData(this.#u?.state.data,this.#u):t.placeholderData,e!==void 0&&(m=`success`,d=pe(i?.data,e,t),u=!0)}if(t.select&&d!==void 0&&!h)if(i&&d===a?.data&&t.select===this.#c)d=this.#l;else try{this.#c=t.select,d=t.select(d),d=pe(i?.data,d,t),this.#l=d,this.#s=null}catch(e){this.#s=e}this.#s&&(f=this.#s,d=this.#l,p=Date.now(),m=`error`);let g=l.fetchStatus===`fetching`,_=m===`pending`,v=m===`error`,y=_&&g,b=d!==void 0,x={status:m,fetchStatus:l.fetchStatus,isPending:_,isSuccess:m===`success`,isError:v,isInitialLoading:y,isLoading:y,data:d,dataUpdatedAt:l.dataUpdatedAt,error:f,errorUpdatedAt:p,failureCount:l.fetchFailureCount,failureReason:l.fetchFailureReason,errorUpdateCount:l.errorUpdateCount,isFetched:l.dataUpdateCount>0||l.errorUpdateCount>0,isFetchedAfterMount:l.dataUpdateCount>s.dataUpdateCount||l.errorUpdateCount>s.errorUpdateCount,isFetching:g,isRefetching:g&&!_,isLoadingError:v&&!b,isPaused:l.fetchStatus===`paused`,isPlaceholderData:u,isRefetchError:v&&b,isStale:ze(e,t),refetch:this.refetch,promise:this.#o,isEnabled:T(t.enabled,e)!==!1};if(this.options.experimental_prefetchInRender){let t=x.data!==void 0,r=x.status===`error`&&!t,i=e=>{r?e.reject(x.error):t&&e.resolve(x.data)},a=()=>{i(this.#o=x.promise=be())},o=this.#o;switch(o.status){case`pending`:e.queryHash===n.queryHash&&i(o);break;case`fulfilled`:(r||x.data!==o.value)&&a();break;case`rejected`:(!r||x.error!==o.reason)&&a();break}}return x}updateResult(){let e=this.#r,t=this.createResult(this.#t,this.options);this.#i=this.#t.state,this.#a=this.options,this.#i.data!==void 0&&(this.#u=this.#t),!E(t,e)&&(this.#r=t,this.#C({listeners:(()=>{if(!e)return!0;let{notifyOnChangeProps:t}=this.options,n=typeof t==`function`?t():t;if(n===`all`||!n&&!this.#m.size)return!0;let r=new Set(n??this.#m);return this.options.throwOnError&&r.add(`error`),Object.keys(this.#r).some(t=>{let n=t;return this.#r[n]!==e[n]&&r.has(n)})})()}))}#S(){let e=this.#e.getQueryCache().build(this.#e,this.options);if(e===this.#t)return;let t=this.#t;this.#t=e,this.#n=e.state,this.hasListeners()&&(t?.removeObserver(this),e.addObserver(this))}onQueryUpdate(){this.updateResult(),this.hasListeners()&&this.#y()}#C(e){Ce.batch(()=>{e.listeners&&this.listeners.forEach(e=>{e(this.#r)}),this.#e.getQueryCache().notify({query:this.#t,type:`observerResultsUpdated`})})}};function Fe(e,t){return T(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status===`error`&&t.retryOnMount===!1)}function Ie(e,t){return Fe(e,t)||e.state.data!==void 0&&Le(e,t,t.refetchOnMount)}function Le(e,t,n){if(T(t.enabled,e)!==!1&&ne(t.staleTime,e)!==`static`){let r=typeof n==`function`?n(e):n;return r===`always`||r!==!1&&ze(e,t)}return!1}function Re(e,t,n,r){return(e!==t||T(r.enabled,e)===!1)&&(!n.suspense||e.state.status!==`error`)&&ze(e,n)}function ze(e,t){return T(t.enabled,e)!==!1&&e.isStaleByTime(ne(t.staleTime,e))}function Be(e,t){return!E(e.getCurrentResult(),t)}function Ve(e){return{onFetch:(t,n)=>{let r=t.options,i=t.fetchOptions?.meta?.fetchMore?.direction,a=t.state.data?.pages||[],o=t.state.data?.pageParams||[],s={pages:[],pageParams:[]},c=0,l=async()=>{let n=!1,l=e=>{ve(e,()=>t.signal,()=>n=!0)},u=ge(t.options,t.fetchOptions),d=async(e,r,i)=>{if(n)return Promise.reject();if(r==null&&e.pages.length)return Promise.resolve(e);let a=await u((()=>{let e={client:t.client,queryKey:t.queryKey,pageParam:r,direction:i?`backward`:`forward`,meta:t.options.meta};return l(e),e})()),{maxPages:o}=t.options,s=i?O:me;return{pages:s(e.pages,a,o),pageParams:s(e.pageParams,r,o)}};if(i&&a.length){let e=i===`backward`,t=e?Ue:He,n={pages:a,pageParams:o};s=await d(n,t(r,n),e)}else{let t=e??a.length;do{let e=c===0?o[0]??r.initialPageParam:He(r,s);if(c>0&&e==null)break;s=await d(s,e),c++}while(ct.options.persister?.(l,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n):t.fetchFn=l}}}function He(e,{pages:t,pageParams:n}){let r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function Ue(e,{pages:t,pageParams:n}){return t.length>0?e.getPreviousPageParam?.(t[0],t,n[0],n):void 0}var We=class extends ke{#e;#t;#n;#r;constructor(e){super(),this.#e=e.client,this.mutationId=e.mutationId,this.#n=e.mutationCache,this.#t=[],this.state=e.state||Ge(),this.setOptions(e.options),this.scheduleGc()}setOptions(e){this.options=e,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(e){this.#t.includes(e)||(this.#t.push(e),this.clearGcTimeout(),this.#n.notify({type:`observerAdded`,mutation:this,observer:e}))}removeObserver(e){this.#t=this.#t.filter(t=>t!==e),this.scheduleGc(),this.#n.notify({type:`observerRemoved`,mutation:this,observer:e})}optionalRemove(){this.#t.length||(this.state.status===`pending`?this.scheduleGc():this.#n.remove(this))}continue(){return this.#r?.continue()??this.execute(this.state.variables)}async execute(e){let t=()=>{this.#i({type:`continue`})},n={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};this.#r=Oe({fn:()=>this.options.mutationFn?this.options.mutationFn(e,n):Promise.reject(Error(`No mutationFn found`)),onFail:(e,t)=>{this.#i({type:`failed`,failureCount:e,error:t})},onPause:()=>{this.#i({type:`pause`})},onContinue:t,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>this.#n.canRun(this)});let r=this.state.status===`pending`,i=!this.#r.canStart();try{if(r)t();else{this.#i({type:`pending`,variables:e,isPaused:i}),this.#n.config.onMutate&&await this.#n.config.onMutate(e,this,n);let t=await this.options.onMutate?.(e,n);t!==this.state.context&&this.#i({type:`pending`,context:t,variables:e,isPaused:i})}let a=await this.#r.start();return await this.#n.config.onSuccess?.(a,e,this.state.context,this,n),await this.options.onSuccess?.(a,e,this.state.context,n),await this.#n.config.onSettled?.(a,null,this.state.variables,this.state.context,this,n),await this.options.onSettled?.(a,null,e,this.state.context,n),this.#i({type:`success`,data:a}),a}catch(t){try{await this.#n.config.onError?.(t,e,this.state.context,this,n)}catch(e){Promise.reject(e)}try{await this.options.onError?.(t,e,this.state.context,n)}catch(e){Promise.reject(e)}try{await this.#n.config.onSettled?.(void 0,t,this.state.variables,this.state.context,this,n)}catch(e){Promise.reject(e)}try{await this.options.onSettled?.(void 0,t,e,this.state.context,n)}catch(e){Promise.reject(e)}throw this.#i({type:`error`,error:t}),t}finally{this.#n.runNext(this)}}#i(e){this.state=(t=>{switch(e.type){case`failed`:return{...t,failureCount:e.failureCount,failureReason:e.error};case`pause`:return{...t,isPaused:!0};case`continue`:return{...t,isPaused:!1};case`pending`:return{...t,context:e.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:e.isPaused,status:`pending`,variables:e.variables,submittedAt:Date.now()};case`success`:return{...t,data:e.data,failureCount:0,failureReason:null,error:null,status:`success`,isPaused:!1};case`error`:return{...t,data:void 0,error:e.error,failureCount:t.failureCount+1,failureReason:e.error,isPaused:!1,status:`error`}}})(this.state),Ce.batch(()=>{this.#t.forEach(t=>{t.onMutationUpdate(e)}),this.#n.notify({mutation:this,type:`updated`,action:e})})}};function Ge(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:`idle`,variables:void 0,submittedAt:0}}var Ke=class extends v{constructor(e={}){super(),this.config=e,this.#e=new Set,this.#t=new Map,this.#n=0}#e;#t;#n;build(e,t,n){let r=new We({client:e,mutationCache:this,mutationId:++this.#n,options:e.defaultMutationOptions(t),state:n});return this.add(r),r}add(e){this.#e.add(e);let t=qe(e);if(typeof t==`string`){let n=this.#t.get(t);n?n.push(e):this.#t.set(t,[e])}this.notify({type:`added`,mutation:e})}remove(e){if(this.#e.delete(e)){let t=qe(e);if(typeof t==`string`){let n=this.#t.get(t);if(n)if(n.length>1){let t=n.indexOf(e);t!==-1&&n.splice(t,1)}else n[0]===e&&this.#t.delete(t)}}this.notify({type:`removed`,mutation:e})}canRun(e){let t=qe(e);if(typeof t==`string`){let n=this.#t.get(t)?.find(e=>e.state.status===`pending`);return!n||n===e}else return!0}runNext(e){let t=qe(e);return typeof t==`string`?(this.#t.get(t)?.find(t=>t!==e&&t.state.isPaused))?.continue()??Promise.resolve():Promise.resolve()}clear(){Ce.batch(()=>{this.#e.forEach(e=>{this.notify({type:`removed`,mutation:e})}),this.#e.clear(),this.#t.clear()})}getAll(){return Array.from(this.#e)}find(e){let t={exact:!0,...e};return this.getAll().find(e=>ie(t,e))}findAll(e={}){return this.getAll().filter(t=>ie(e,t))}notify(e){Ce.batch(()=>{this.listeners.forEach(t=>{t(e)})})}resumePausedMutations(){let e=this.getAll().filter(e=>e.state.isPaused);return Ce.batch(()=>Promise.all(e.map(e=>e.continue().catch(C))))}};function qe(e){return e.options.scope?.id}var Je=class extends v{constructor(e={}){super(),this.config=e,this.#e=new Map}#e;build(e,t,n){let r=t.queryKey,i=t.queryHash??ae(r,t),a=this.get(i);return a||(a=new Ae({client:e,queryKey:r,queryHash:i,options:e.defaultQueryOptions(t),state:n,defaultOptions:e.getQueryDefaults(r)}),this.add(a)),a}add(e){this.#e.has(e.queryHash)||(this.#e.set(e.queryHash,e),this.notify({type:`added`,query:e}))}remove(e){let t=this.#e.get(e.queryHash);t&&(e.destroy(),t===e&&this.#e.delete(e.queryHash),this.notify({type:`removed`,query:e}))}clear(){Ce.batch(()=>{this.getAll().forEach(e=>{this.remove(e)})})}get(e){return this.#e.get(e)}getAll(){return[...this.#e.values()]}find(e){let t={exact:!0,...e};return this.getAll().find(e=>re(t,e))}findAll(e={}){let t=this.getAll();return Object.keys(e).length>0?t.filter(t=>re(e,t)):t}notify(e){Ce.batch(()=>{this.listeners.forEach(t=>{t(e)})})}onFocus(){Ce.batch(()=>{this.getAll().forEach(e=>{e.onFocus()})})}onOnline(){Ce.batch(()=>{this.getAll().forEach(e=>{e.onOnline()})})}},Ye=class{#e;#t;#n;#r;#i;#a;#o;#s;constructor(e={}){this.#e=e.queryCache||new Je,this.#t=e.mutationCache||new Ke,this.#n=e.defaultOptions||{},this.#r=new Map,this.#i=new Map,this.#a=0}mount(){this.#a++,this.#a===1&&(this.#o=ye.subscribe(async e=>{e&&(await this.resumePausedMutations(),this.#e.onFocus())}),this.#s=we.subscribe(async e=>{e&&(await this.resumePausedMutations(),this.#e.onOnline())}))}unmount(){this.#a--,this.#a===0&&(this.#o?.(),this.#o=void 0,this.#s?.(),this.#s=void 0)}isFetching(e){return this.#e.findAll({...e,fetchStatus:`fetching`}).length}isMutating(e){return this.#t.findAll({...e,status:`pending`}).length}getQueryData(e){let t=this.defaultQueryOptions({queryKey:e});return this.#e.get(t.queryHash)?.state.data}ensureQueryData(e){let t=this.defaultQueryOptions(e),n=this.#e.build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(ne(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return this.#e.findAll(e).map(({queryKey:e,state:t})=>[e,t.data])}setQueryData(e,t,n){let r=this.defaultQueryOptions({queryKey:e}),i=this.#e.get(r.queryHash)?.state.data,a=w(t,i);if(a!==void 0)return this.#e.build(this,r).setData(a,{...n,manual:!0})}setQueriesData(e,t,n){return Ce.batch(()=>this.#e.findAll(e).map(({queryKey:e})=>[e,this.setQueryData(e,t,n)]))}getQueryState(e){let t=this.defaultQueryOptions({queryKey:e});return this.#e.get(t.queryHash)?.state}removeQueries(e){let t=this.#e;Ce.batch(()=>{t.findAll(e).forEach(e=>{t.remove(e)})})}resetQueries(e,t){let n=this.#e;return Ce.batch(()=>(n.findAll(e).forEach(e=>{e.reset()}),this.refetchQueries({type:`active`,...e},t)))}cancelQueries(e,t={}){let n={revert:!0,...t},r=Ce.batch(()=>this.#e.findAll(e).map(e=>e.cancel(n)));return Promise.all(r).then(C).catch(C)}invalidateQueries(e,t={}){return Ce.batch(()=>(this.#e.findAll(e).forEach(e=>{e.invalidate()}),e?.refetchType===`none`?Promise.resolve():this.refetchQueries({...e,type:e?.refetchType??e?.type??`active`},t)))}refetchQueries(e,t={}){let n={...t,cancelRefetch:t.cancelRefetch??!0},r=Ce.batch(()=>this.#e.findAll(e).filter(e=>!e.isDisabled()&&!e.isStatic()).map(e=>{let t=e.fetch(void 0,n);return n.throwOnError||(t=t.catch(C)),e.state.fetchStatus===`paused`?Promise.resolve():t}));return Promise.all(r).then(C)}fetchQuery(e){let t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);let n=this.#e.build(this,t);return n.isStaleByTime(ne(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(C).catch(C)}fetchInfiniteQuery(e){return e.behavior=Ve(e.pages),this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(C).catch(C)}ensureInfiniteQueryData(e){return e.behavior=Ve(e.pages),this.ensureQueryData(e)}resumePausedMutations(){return we.isOnline()?this.#t.resumePausedMutations():Promise.resolve()}getQueryCache(){return this.#e}getMutationCache(){return this.#t}getDefaultOptions(){return this.#n}setDefaultOptions(e){this.#n=e}setQueryDefaults(e,t){this.#r.set(oe(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){let t=[...this.#r.values()],n={};return t.forEach(t=>{se(e,t.queryKey)&&Object.assign(n,t.defaultOptions)}),n}setMutationDefaults(e,t){this.#i.set(oe(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){let t=[...this.#i.values()],n={};return t.forEach(t=>{se(e,t.mutationKey)&&Object.assign(n,t.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;let t={...this.#n.queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||=ae(t.queryKey,t),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!==`always`),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode=`offlineFirst`),t.queryFn===he&&(t.enabled=!1),t}defaultMutationOptions(e){return e?._defaulted?e:{...this.#n.mutations,...e?.mutationKey&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){this.#e.clear(),this.#t.clear()}},Xe=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),Ze=o(((e,t)=>{t.exports=Xe()})),k=l(d(),1),A=Ze(),Qe=k.createContext(void 0),$e=e=>{let t=k.useContext(Qe);if(e)return e;if(!t)throw Error(`No QueryClient set, use QueryClientProvider to set one`);return t},et=({client:e,children:t})=>(k.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),(0,A.jsx)(Qe.Provider,{value:e,children:t})),tt=k.createContext(!1),nt=()=>k.useContext(tt);tt.Provider;function rt(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var it=k.createContext(rt()),at=()=>k.useContext(it),ot=(e,t,n)=>{let r=n?.state.error&&typeof e.throwOnError==`function`?_e(e.throwOnError,[n.state.error,n]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||r)&&(t.isReset()||(e.retryOnMount=!1))},st=e=>{k.useEffect(()=>{e.clearReset()},[e])},ct=({result:e,errorResetBoundary:t,throwOnError:n,query:r,suspense:i})=>e.isError&&!t.isReset()&&!e.isFetching&&r&&(i&&e.data===void 0||_e(n,[e.error,r])),lt=e=>{if(e.suspense){let t=1e3,n=e=>e===`static`?e:Math.max(e??t,t),r=e.staleTime;e.staleTime=typeof r==`function`?(...e)=>n(r(...e)):n(r),typeof e.gcTime==`number`&&(e.gcTime=Math.max(e.gcTime,t))}},ut=(e,t)=>e.isLoading&&e.isFetching&&!t,dt=(e,t)=>e?.suspense&&t.isPending,ft=(e,t,n)=>t.fetchOptimistic(e).catch(()=>{n.clearReset()});function pt(e,t,n){let r=nt(),i=at(),a=$e(n),o=a.defaultQueryOptions(e);a.getDefaultOptions().queries?._experimental_beforeQuery?.(o);let s=a.getQueryCache().get(o.queryHash);o._optimisticResults=r?`isRestoring`:`optimistic`,lt(o),ot(o,i,s),st(i);let c=!a.getQueryCache().get(o.queryHash),[l]=k.useState(()=>new t(a,o)),u=l.getOptimisticResult(o),d=!r&&e.subscribed!==!1;if(k.useSyncExternalStore(k.useCallback(e=>{let t=d?l.subscribe(Ce.batchCalls(e)):C;return l.updateResult(),t},[l,d]),()=>l.getCurrentResult(),()=>l.getCurrentResult()),k.useEffect(()=>{l.setOptions(o)},[o,l]),dt(o,u))throw ft(o,l,i);if(ct({result:u,errorResetBoundary:i,throwOnError:o.throwOnError,query:s,suspense:o.suspense}))throw u.error;return a.getDefaultOptions().queries?._experimental_afterQuery?.(o,u),o.experimental_prefetchInRender&&!S&&ut(u,r)&&(c?ft(o,l,i):s?.promise)?.catch(C).finally(()=>{l.updateResult()}),o.notifyOnChangeProps?u:l.trackResult(u)}function j(e,t){return pt(e,Pe,t)}function mt(e,t){return function(){return e.apply(t,arguments)}}var{toString:ht}=Object.prototype,{getPrototypeOf:gt}=Object,{iterator:_t,toStringTag:vt}=Symbol,yt=(e=>t=>{let n=ht.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),bt=e=>(e=e.toLowerCase(),t=>yt(t)===e),xt=e=>t=>typeof t===e,{isArray:St}=Array,Ct=xt(`undefined`);function wt(e){return e!==null&&!Ct(e)&&e.constructor!==null&&!Ct(e.constructor)&&Ot(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}var Tt=bt(`ArrayBuffer`);function Et(e){let t;return t=typeof ArrayBuffer<`u`&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&Tt(e.buffer),t}var Dt=xt(`string`),Ot=xt(`function`),kt=xt(`number`),At=e=>typeof e==`object`&&!!e,jt=e=>e===!0||e===!1,Mt=e=>{if(yt(e)!==`object`)return!1;let t=gt(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(vt in e)&&!(_t in e)},Nt=e=>{if(!At(e)||wt(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},Pt=bt(`Date`),Ft=bt(`File`),It=e=>!!(e&&e.uri!==void 0),Lt=e=>e&&e.getParts!==void 0,Rt=bt(`Blob`),zt=bt(`FileList`),Bt=e=>At(e)&&Ot(e.pipe);function Vt(){return typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{}}var Ht=Vt(),Ut=Ht.FormData===void 0?void 0:Ht.FormData,Wt=e=>{let t;return e&&(Ut&&e instanceof Ut||Ot(e.append)&&((t=yt(e))===`formdata`||t===`object`&&Ot(e.toString)&&e.toString()===`[object FormData]`))},Gt=bt(`URLSearchParams`),[Kt,qt,Jt,Yt]=[`ReadableStream`,`Request`,`Response`,`Headers`].map(bt),Xt=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,``);function Zt(e,t,{allOwnKeys:n=!1}={}){if(e==null)return;let r,i;if(typeof e!=`object`&&(e=[e]),St(e))for(r=0,i=e.length;r0;)if(i=n[r],t===i.toLowerCase())return i;return null}var $t=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:global,en=e=>!Ct(e)&&e!==$t;function tn(){let{caseless:e,skipUndefined:t}=en(this)&&this||{},n={},r=(r,i)=>{if(i===`__proto__`||i===`constructor`||i===`prototype`)return;let a=e&&Qt(n,i)||i;Mt(n[a])&&Mt(r)?n[a]=tn(n[a],r):Mt(r)?n[a]=tn({},r):St(r)?n[a]=r.slice():(!t||!Ct(r))&&(n[a]=r)};for(let e=0,t=arguments.length;e(Zt(t,(t,r)=>{n&&Ot(t)?Object.defineProperty(e,r,{value:mt(t,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,r,{value:t,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:r}),e),rn=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),an=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,`constructor`,{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,`super`,{value:t.prototype}),n&&Object.assign(e.prototype,n)},on=(e,t,n,r)=>{let i,a,o,s={};if(t||={},e==null)return t;do{for(i=Object.getOwnPropertyNames(e),a=i.length;a-- >0;)o=i[a],(!r||r(o,e,t))&&!s[o]&&(t[o]=e[o],s[o]=!0);e=n!==!1&>(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},sn=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;let r=e.indexOf(t,n);return r!==-1&&r===n},cn=e=>{if(!e)return null;if(St(e))return e;let t=e.length;if(!kt(t))return null;let n=Array(t);for(;t-- >0;)n[t]=e[t];return n},ln=(e=>t=>e&&t instanceof e)(typeof Uint8Array<`u`&>(Uint8Array)),un=(e,t)=>{let n=(e&&e[_t]).call(e),r;for(;(r=n.next())&&!r.done;){let n=r.value;t.call(e,n[0],n[1])}},dn=(e,t)=>{let n,r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},fn=bt(`HTMLFormElement`),pn=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(e,t,n){return t.toUpperCase()+n}),mn=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),hn=bt(`RegExp`),gn=(e,t)=>{let n=Object.getOwnPropertyDescriptors(e),r={};Zt(n,(n,i)=>{let a;(a=t(n,i,e))!==!1&&(r[i]=a||n)}),Object.defineProperties(e,r)},_n=e=>{gn(e,(t,n)=>{if(Ot(e)&&[`arguments`,`caller`,`callee`].indexOf(n)!==-1)return!1;let r=e[n];if(Ot(r)){if(t.enumerable=!1,`writable`in t){t.writable=!1;return}t.set||=()=>{throw Error(`Can not rewrite read-only method '`+n+`'`)}}})},vn=(e,t)=>{let n={},r=e=>{e.forEach(e=>{n[e]=!0})};return St(e)?r(e):r(String(e).split(t)),n},yn=()=>{},bn=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function xn(e){return!!(e&&Ot(e.append)&&e[vt]===`FormData`&&e[_t])}var Sn=e=>{let t=Array(10),n=(e,r)=>{if(At(e)){if(t.indexOf(e)>=0)return;if(wt(e))return e;if(!(`toJSON`in e)){t[r]=e;let i=St(e)?[]:{};return Zt(e,(e,t)=>{let a=n(e,r+1);!Ct(a)&&(i[t]=a)}),t[r]=void 0,i}}return e};return n(e,0)},Cn=bt(`AsyncFunction`),wn=e=>e&&(At(e)||Ot(e))&&Ot(e.then)&&Ot(e.catch),Tn=((e,t)=>e?setImmediate:t?((e,t)=>($t.addEventListener(`message`,({source:n,data:r})=>{n===$t&&r===e&&t.length&&t.shift()()},!1),n=>{t.push(n),$t.postMessage(e,`*`)}))(`axios@${Math.random()}`,[]):e=>setTimeout(e))(typeof setImmediate==`function`,Ot($t.postMessage)),M={isArray:St,isArrayBuffer:Tt,isBuffer:wt,isFormData:Wt,isArrayBufferView:Et,isString:Dt,isNumber:kt,isBoolean:jt,isObject:At,isPlainObject:Mt,isEmptyObject:Nt,isReadableStream:Kt,isRequest:qt,isResponse:Jt,isHeaders:Yt,isUndefined:Ct,isDate:Pt,isFile:Ft,isReactNativeBlob:It,isReactNative:Lt,isBlob:Rt,isRegExp:hn,isFunction:Ot,isStream:Bt,isURLSearchParams:Gt,isTypedArray:ln,isFileList:zt,forEach:Zt,merge:tn,extend:nn,trim:Xt,stripBOM:rn,inherits:an,toFlatObject:on,kindOf:yt,kindOfTest:bt,endsWith:sn,toArray:cn,forEachEntry:un,matchAll:dn,isHTMLForm:fn,hasOwnProperty:mn,hasOwnProp:mn,reduceDescriptors:gn,freezeMethods:_n,toObjectSet:vn,toCamelCase:pn,noop:yn,toFiniteNumber:bn,findKey:Qt,global:$t,isContextDefined:en,isSpecCompliantForm:xn,toJSONObject:Sn,isAsyncFn:Cn,isThenable:wn,setImmediate:Tn,asap:typeof queueMicrotask<`u`?queueMicrotask.bind($t):typeof process<`u`&&process.nextTick||Tn,isIterable:e=>e!=null&&Ot(e[_t])},N=class e extends Error{static from(t,n,r,i,a,o){let s=new e(t.message,n||t.code,r,i,a);return s.cause=t,s.name=t.name,t.status!=null&&s.status==null&&(s.status=t.status),o&&Object.assign(s,o),s}constructor(e,t,n,r,i){super(e),Object.defineProperty(this,`message`,{value:e,enumerable:!0,writable:!0,configurable:!0}),this.name=`AxiosError`,this.isAxiosError=!0,t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),i&&(this.response=i,this.status=i.status)}toJSON(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:M.toJSONObject(this.config),code:this.code,status:this.status}}};N.ERR_BAD_OPTION_VALUE=`ERR_BAD_OPTION_VALUE`,N.ERR_BAD_OPTION=`ERR_BAD_OPTION`,N.ECONNABORTED=`ECONNABORTED`,N.ETIMEDOUT=`ETIMEDOUT`,N.ERR_NETWORK=`ERR_NETWORK`,N.ERR_FR_TOO_MANY_REDIRECTS=`ERR_FR_TOO_MANY_REDIRECTS`,N.ERR_DEPRECATED=`ERR_DEPRECATED`,N.ERR_BAD_RESPONSE=`ERR_BAD_RESPONSE`,N.ERR_BAD_REQUEST=`ERR_BAD_REQUEST`,N.ERR_CANCELED=`ERR_CANCELED`,N.ERR_NOT_SUPPORT=`ERR_NOT_SUPPORT`,N.ERR_INVALID_URL=`ERR_INVALID_URL`;function En(e){return M.isPlainObject(e)||M.isArray(e)}function Dn(e){return M.endsWith(e,`[]`)?e.slice(0,-2):e}function On(e,t,n){return e?e.concat(t).map(function(e,t){return e=Dn(e),!n&&t?`[`+e+`]`:e}).join(n?`.`:``):t}function kn(e){return M.isArray(e)&&!e.some(En)}var An=M.toFlatObject(M,{},null,function(e){return/^is[A-Z]/.test(e)});function jn(e,t,n){if(!M.isObject(e))throw TypeError(`target must be an object`);t||=new FormData,n=M.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(e,t){return!M.isUndefined(t[e])});let r=n.metaTokens,i=n.visitor||l,a=n.dots,o=n.indexes,s=(n.Blob||typeof Blob<`u`&&Blob)&&M.isSpecCompliantForm(t);if(!M.isFunction(i))throw TypeError(`visitor must be a function`);function c(e){if(e===null)return``;if(M.isDate(e))return e.toISOString();if(M.isBoolean(e))return e.toString();if(!s&&M.isBlob(e))throw new N(`Blob is not supported. Use a Buffer instead.`);return M.isArrayBuffer(e)||M.isTypedArray(e)?s&&typeof Blob==`function`?new Blob([e]):Buffer.from(e):e}function l(e,n,i){let s=e;if(M.isReactNative(t)&&M.isReactNativeBlob(e))return t.append(On(i,n,a),c(e)),!1;if(e&&!i&&typeof e==`object`){if(M.endsWith(n,`{}`))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(M.isArray(e)&&kn(e)||(M.isFileList(e)||M.endsWith(n,`[]`))&&(s=M.toArray(e)))return n=Dn(n),s.forEach(function(e,r){!(M.isUndefined(e)||e===null)&&t.append(o===!0?On([n],r,a):o===null?n:n+`[]`,c(e))}),!1}return En(e)?!0:(t.append(On(i,n,a),c(e)),!1)}let u=[],d=Object.assign(An,{defaultVisitor:l,convertValue:c,isVisitable:En});function f(e,n){if(!M.isUndefined(e)){if(u.indexOf(e)!==-1)throw Error(`Circular reference detected in `+n.join(`.`));u.push(e),M.forEach(e,function(e,r){(!(M.isUndefined(e)||e===null)&&i.call(t,e,M.isString(r)?r.trim():r,n,d))===!0&&f(e,n?n.concat(r):[r])}),u.pop()}}if(!M.isObject(e))throw TypeError(`data must be an object`);return f(e),t}function Mn(e){let t={"!":`%21`,"'":`%27`,"(":`%28`,")":`%29`,"~":`%7E`,"%20":`+`,"%00":`\0`};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(e){return t[e]})}function Nn(e,t){this._pairs=[],e&&jn(e,this,t)}var Pn=Nn.prototype;Pn.append=function(e,t){this._pairs.push([e,t])},Pn.toString=function(e){let t=e?function(t){return e.call(this,t,Mn)}:Mn;return this._pairs.map(function(e){return t(e[0])+`=`+t(e[1])},``).join(`&`)};function Fn(e){return encodeURIComponent(e).replace(/%3A/gi,`:`).replace(/%24/g,`$`).replace(/%2C/gi,`,`).replace(/%20/g,`+`)}function In(e,t,n){if(!t)return e;let r=n&&n.encode||Fn,i=M.isFunction(n)?{serialize:n}:n,a=i&&i.serialize,o;if(o=a?a(t,i):M.isURLSearchParams(t)?t.toString():new Nn(t,i).toString(r),o){let t=e.indexOf(`#`);t!==-1&&(e=e.slice(0,t)),e+=(e.indexOf(`?`)===-1?`?`:`&`)+o}return e}var Ln=class{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:n?n.synchronous:!1,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&=[]}forEach(e){M.forEach(this.handlers,function(t){t!==null&&e(t)})}},Rn={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0},zn={isBrowser:!0,classes:{URLSearchParams:typeof URLSearchParams<`u`?URLSearchParams:Nn,FormData:typeof FormData<`u`?FormData:null,Blob:typeof Blob<`u`?Blob:null},protocols:[`http`,`https`,`file`,`blob`,`url`,`data`]},Bn=s({hasBrowserEnv:()=>Vn,hasStandardBrowserEnv:()=>Un,hasStandardBrowserWebWorkerEnv:()=>Wn,navigator:()=>Hn,origin:()=>Gn}),Vn=typeof window<`u`&&typeof document<`u`,Hn=typeof navigator==`object`&&navigator||void 0,Un=Vn&&(!Hn||[`ReactNative`,`NativeScript`,`NS`].indexOf(Hn.product)<0),Wn=typeof WorkerGlobalScope<`u`&&self instanceof WorkerGlobalScope&&typeof self.importScripts==`function`,Gn=Vn&&window.location.href||`http://localhost`,Kn={...Bn,...zn};function qn(e,t){return jn(e,new Kn.classes.URLSearchParams,{visitor:function(e,t,n,r){return Kn.isNode&&M.isBuffer(e)?(this.append(t,e.toString(`base64`)),!1):r.defaultVisitor.apply(this,arguments)},...t})}function Jn(e){return M.matchAll(/\w+|\[(\w*)]/g,e).map(e=>e[0]===`[]`?``:e[1]||e[0])}function Yn(e){let t={},n=Object.keys(e),r,i=n.length,a;for(r=0;r=e.length;return a=!a&&M.isArray(r)?r.length:a,s?(M.hasOwnProp(r,a)?r[a]=[r[a],n]:r[a]=n,!o):((!r[a]||!M.isObject(r[a]))&&(r[a]=[]),t(e,n,r[a],i)&&M.isArray(r[a])&&(r[a]=Yn(r[a])),!o)}if(M.isFormData(e)&&M.isFunction(e.entries)){let n={};return M.forEachEntry(e,(e,r)=>{t(Jn(e),r,n,0)}),n}return null}function Zn(e,t,n){if(M.isString(e))try{return(t||JSON.parse)(e),M.trim(e)}catch(e){if(e.name!==`SyntaxError`)throw e}return(n||JSON.stringify)(e)}var Qn={transitional:Rn,adapter:[`xhr`,`http`,`fetch`],transformRequest:[function(e,t){let n=t.getContentType()||``,r=n.indexOf(`application/json`)>-1,i=M.isObject(e);if(i&&M.isHTMLForm(e)&&(e=new FormData(e)),M.isFormData(e))return r?JSON.stringify(Xn(e)):e;if(M.isArrayBuffer(e)||M.isBuffer(e)||M.isStream(e)||M.isFile(e)||M.isBlob(e)||M.isReadableStream(e))return e;if(M.isArrayBufferView(e))return e.buffer;if(M.isURLSearchParams(e))return t.setContentType(`application/x-www-form-urlencoded;charset=utf-8`,!1),e.toString();let a;if(i){if(n.indexOf(`application/x-www-form-urlencoded`)>-1)return qn(e,this.formSerializer).toString();if((a=M.isFileList(e))||n.indexOf(`multipart/form-data`)>-1){let t=this.env&&this.env.FormData;return jn(a?{"files[]":e}:e,t&&new t,this.formSerializer)}}return i||r?(t.setContentType(`application/json`,!1),Zn(e)):e}],transformResponse:[function(e){let t=this.transitional||Qn.transitional,n=t&&t.forcedJSONParsing,r=this.responseType===`json`;if(M.isResponse(e)||M.isReadableStream(e))return e;if(e&&M.isString(e)&&(n&&!this.responseType||r)){let n=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e,this.parseReviver)}catch(e){if(n)throw e.name===`SyntaxError`?N.from(e,N.ERR_BAD_RESPONSE,this,null,this.response):e}}return e}],timeout:0,xsrfCookieName:`XSRF-TOKEN`,xsrfHeaderName:`X-XSRF-TOKEN`,maxContentLength:-1,maxBodyLength:-1,env:{FormData:Kn.classes.FormData,Blob:Kn.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:`application/json, text/plain, */*`,"Content-Type":void 0}}};M.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`],e=>{Qn.headers[e]={}});var $n=M.toObjectSet([`age`,`authorization`,`content-length`,`content-type`,`etag`,`expires`,`from`,`host`,`if-modified-since`,`if-unmodified-since`,`last-modified`,`location`,`max-forwards`,`proxy-authorization`,`referer`,`retry-after`,`user-agent`]),er=e=>{let t={},n,r,i;return e&&e.split(` -`).forEach(function(e){i=e.indexOf(`:`),n=e.substring(0,i).trim().toLowerCase(),r=e.substring(i+1).trim(),!(!n||t[n]&&$n[n])&&(n===`set-cookie`?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+`, `+r:r)}),t},tr=Symbol(`internals`);function nr(e){return e&&String(e).trim().toLowerCase()}function rr(e){return e===!1||e==null?e:M.isArray(e)?e.map(rr):String(e)}function ir(e){let t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g,r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}var ar=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function or(e,t,n,r,i){if(M.isFunction(r))return r.call(this,t,n);if(i&&(t=n),M.isString(t)){if(M.isString(r))return t.indexOf(r)!==-1;if(M.isRegExp(r))return r.test(t)}}function sr(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,t,n)=>t.toUpperCase()+n)}function cr(e,t){let n=M.toCamelCase(` `+t);[`get`,`set`,`has`].forEach(r=>{Object.defineProperty(e,r+n,{value:function(e,n,i){return this[r].call(this,t,e,n,i)},configurable:!0})})}var lr=class{constructor(e){e&&this.set(e)}set(e,t,n){let r=this;function i(e,t,n){let i=nr(t);if(!i)throw Error(`header name must be a non-empty string`);let a=M.findKey(r,i);(!a||r[a]===void 0||n===!0||n===void 0&&r[a]!==!1)&&(r[a||t]=rr(e))}let a=(e,t)=>M.forEach(e,(e,n)=>i(e,n,t));if(M.isPlainObject(e)||e instanceof this.constructor)a(e,t);else if(M.isString(e)&&(e=e.trim())&&!ar(e))a(er(e),t);else if(M.isObject(e)&&M.isIterable(e)){let n={},r,i;for(let t of e){if(!M.isArray(t))throw TypeError(`Object iterator must return a key-value pair`);n[i=t[0]]=(r=n[i])?M.isArray(r)?[...r,t[1]]:[r,t[1]]:t[1]}a(n,t)}else e!=null&&i(t,e,n);return this}get(e,t){if(e=nr(e),e){let n=M.findKey(this,e);if(n){let e=this[n];if(!t)return e;if(t===!0)return ir(e);if(M.isFunction(t))return t.call(this,e,n);if(M.isRegExp(t))return t.exec(e);throw TypeError(`parser must be boolean|regexp|function`)}}}has(e,t){if(e=nr(e),e){let n=M.findKey(this,e);return!!(n&&this[n]!==void 0&&(!t||or(this,this[n],n,t)))}return!1}delete(e,t){let n=this,r=!1;function i(e){if(e=nr(e),e){let i=M.findKey(n,e);i&&(!t||or(n,n[i],i,t))&&(delete n[i],r=!0)}}return M.isArray(e)?e.forEach(i):i(e),r}clear(e){let t=Object.keys(this),n=t.length,r=!1;for(;n--;){let i=t[n];(!e||or(this,this[i],i,e,!0))&&(delete this[i],r=!0)}return r}normalize(e){let t=this,n={};return M.forEach(this,(r,i)=>{let a=M.findKey(n,i);if(a){t[a]=rr(r),delete t[i];return}let o=e?sr(i):String(i).trim();o!==i&&delete t[i],t[o]=rr(r),n[o]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){let t=Object.create(null);return M.forEach(this,(n,r)=>{n!=null&&n!==!1&&(t[r]=e&&M.isArray(n)?n.join(`, `):n)}),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,t])=>e+`: `+t).join(` -`)}getSetCookie(){return this.get(`set-cookie`)||[]}get[Symbol.toStringTag](){return`AxiosHeaders`}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){let n=new this(e);return t.forEach(e=>n.set(e)),n}static accessor(e){let t=(this[tr]=this[tr]={accessors:{}}).accessors,n=this.prototype;function r(e){let r=nr(e);t[r]||(cr(n,e),t[r]=!0)}return M.isArray(e)?e.forEach(r):r(e),this}};lr.accessor([`Content-Type`,`Content-Length`,`Accept`,`Accept-Encoding`,`User-Agent`,`Authorization`]),M.reduceDescriptors(lr.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}}),M.freezeMethods(lr);function ur(e,t){let n=this||Qn,r=t||n,i=lr.from(r.headers),a=r.data;return M.forEach(e,function(e){a=e.call(n,a,i.normalize(),t?t.status:void 0)}),i.normalize(),a}function dr(e){return!!(e&&e.__CANCEL__)}var fr=class extends N{constructor(e,t,n){super(e??`canceled`,N.ERR_CANCELED,t,n),this.name=`CanceledError`,this.__CANCEL__=!0}};function pr(e,t,n){let r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new N(`Request failed with status code `+n.status,[N.ERR_BAD_REQUEST,N.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function mr(e){let t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||``}function hr(e,t){e||=10;let n=Array(e),r=Array(e),i=0,a=0,o;return t=t===void 0?1e3:t,function(s){let c=Date.now(),l=r[a];o||=c,n[i]=s,r[i]=c;let u=a,d=0;for(;u!==i;)d+=n[u++],u%=e;if(i=(i+1)%e,i===a&&(a=(a+1)%e),c-o{n=r,i=null,a&&=(clearTimeout(a),null),e(...t)};return[(...e)=>{let t=Date.now(),s=t-n;s>=r?o(e,t):(i=e,a||=setTimeout(()=>{a=null,o(i)},r-s))},()=>i&&o(i)]}var _r=(e,t,n=3)=>{let r=0,i=hr(50,250);return gr(n=>{let a=n.loaded,o=n.lengthComputable?n.total:void 0,s=a-r,c=i(s),l=a<=o;r=a,e({loaded:a,total:o,progress:o?a/o:void 0,bytes:s,rate:c||void 0,estimated:c&&o&&l?(o-a)/c:void 0,event:n,lengthComputable:o!=null,[t?`download`:`upload`]:!0})},n)},vr=(e,t)=>{let n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},yr=e=>(...t)=>M.asap(()=>e(...t)),br=Kn.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,Kn.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(Kn.origin),Kn.navigator&&/(msie|trident)/i.test(Kn.navigator.userAgent)):()=>!0,xr=Kn.hasStandardBrowserEnv?{write(e,t,n,r,i,a,o){if(typeof document>`u`)return;let s=[`${e}=${encodeURIComponent(t)}`];M.isNumber(n)&&s.push(`expires=${new Date(n).toUTCString()}`),M.isString(r)&&s.push(`path=${r}`),M.isString(i)&&s.push(`domain=${i}`),a===!0&&s.push(`secure`),M.isString(o)&&s.push(`SameSite=${o}`),document.cookie=s.join(`; `)},read(e){if(typeof document>`u`)return null;let t=document.cookie.match(RegExp(`(?:^|; )`+e+`=([^;]*)`));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,``,Date.now()-864e5,`/`)}}:{write(){},read(){return null},remove(){}};function Sr(e){return typeof e==`string`?/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e):!1}function Cr(e,t){return t?e.replace(/\/?\/$/,``)+`/`+t.replace(/^\/+/,``):e}function wr(e,t,n){let r=!Sr(t);return e&&(r||n==0)?Cr(e,t):t}var Tr=e=>e instanceof lr?{...e}:e;function Er(e,t){t||={};let n={};function r(e,t,n,r){return M.isPlainObject(e)&&M.isPlainObject(t)?M.merge.call({caseless:r},e,t):M.isPlainObject(t)?M.merge({},t):M.isArray(t)?t.slice():t}function i(e,t,n,i){if(!M.isUndefined(t))return r(e,t,n,i);if(!M.isUndefined(e))return r(void 0,e,n,i)}function a(e,t){if(!M.isUndefined(t))return r(void 0,t)}function o(e,t){if(!M.isUndefined(t))return r(void 0,t);if(!M.isUndefined(e))return r(void 0,e)}function s(n,i,a){if(a in t)return r(n,i);if(a in e)return r(void 0,n)}let c={url:a,method:a,data:a,baseURL:o,transformRequest:o,transformResponse:o,paramsSerializer:o,timeout:o,timeoutMessage:o,withCredentials:o,withXSRFToken:o,adapter:o,responseType:o,xsrfCookieName:o,xsrfHeaderName:o,onUploadProgress:o,onDownloadProgress:o,decompress:o,maxContentLength:o,maxBodyLength:o,beforeRedirect:o,transport:o,httpAgent:o,httpsAgent:o,cancelToken:o,socketPath:o,responseEncoding:o,validateStatus:s,headers:(e,t,n)=>i(Tr(e),Tr(t),n,!0)};return M.forEach(Object.keys({...e,...t}),function(r){if(r===`__proto__`||r===`constructor`||r===`prototype`)return;let a=M.hasOwnProp(c,r)?c[r]:i,o=a(e[r],t[r],r);M.isUndefined(o)&&a!==s||(n[r]=o)}),n}var Dr=e=>{let t=Er({},e),{data:n,withXSRFToken:r,xsrfHeaderName:i,xsrfCookieName:a,headers:o,auth:s}=t;if(t.headers=o=lr.from(o),t.url=In(wr(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),s&&o.set(`Authorization`,`Basic `+btoa((s.username||``)+`:`+(s.password?unescape(encodeURIComponent(s.password)):``))),M.isFormData(n)){if(Kn.hasStandardBrowserEnv||Kn.hasStandardBrowserWebWorkerEnv)o.setContentType(void 0);else if(M.isFunction(n.getHeaders)){let e=n.getHeaders(),t=[`content-type`,`content-length`];Object.entries(e).forEach(([e,n])=>{t.includes(e.toLowerCase())&&o.set(e,n)})}}if(Kn.hasStandardBrowserEnv&&(r&&M.isFunction(r)&&(r=r(t)),r||r!==!1&&br(t.url))){let e=i&&a&&xr.read(a);e&&o.set(i,e)}return t},Or=typeof XMLHttpRequest<`u`&&function(e){return new Promise(function(t,n){let r=Dr(e),i=r.data,a=lr.from(r.headers).normalize(),{responseType:o,onUploadProgress:s,onDownloadProgress:c}=r,l,u,d,f,p;function m(){f&&f(),p&&p(),r.cancelToken&&r.cancelToken.unsubscribe(l),r.signal&&r.signal.removeEventListener(`abort`,l)}let h=new XMLHttpRequest;h.open(r.method.toUpperCase(),r.url,!0),h.timeout=r.timeout;function g(){if(!h)return;let r=lr.from(`getAllResponseHeaders`in h&&h.getAllResponseHeaders());pr(function(e){t(e),m()},function(e){n(e),m()},{data:!o||o===`text`||o===`json`?h.responseText:h.response,status:h.status,statusText:h.statusText,headers:r,config:e,request:h}),h=null}`onloadend`in h?h.onloadend=g:h.onreadystatechange=function(){!h||h.readyState!==4||h.status===0&&!(h.responseURL&&h.responseURL.indexOf(`file:`)===0)||setTimeout(g)},h.onabort=function(){h&&=(n(new N(`Request aborted`,N.ECONNABORTED,e,h)),null)},h.onerror=function(t){let r=new N(t&&t.message?t.message:`Network Error`,N.ERR_NETWORK,e,h);r.event=t||null,n(r),h=null},h.ontimeout=function(){let t=r.timeout?`timeout of `+r.timeout+`ms exceeded`:`timeout exceeded`,i=r.transitional||Rn;r.timeoutErrorMessage&&(t=r.timeoutErrorMessage),n(new N(t,i.clarifyTimeoutError?N.ETIMEDOUT:N.ECONNABORTED,e,h)),h=null},i===void 0&&a.setContentType(null),`setRequestHeader`in h&&M.forEach(a.toJSON(),function(e,t){h.setRequestHeader(t,e)}),M.isUndefined(r.withCredentials)||(h.withCredentials=!!r.withCredentials),o&&o!==`json`&&(h.responseType=r.responseType),c&&([d,p]=_r(c,!0),h.addEventListener(`progress`,d)),s&&h.upload&&([u,f]=_r(s),h.upload.addEventListener(`progress`,u),h.upload.addEventListener(`loadend`,f)),(r.cancelToken||r.signal)&&(l=t=>{h&&=(n(!t||t.type?new fr(null,e,h):t),h.abort(),null)},r.cancelToken&&r.cancelToken.subscribe(l),r.signal&&(r.signal.aborted?l():r.signal.addEventListener(`abort`,l)));let _=mr(r.url);if(_&&Kn.protocols.indexOf(_)===-1){n(new N(`Unsupported protocol `+_+`:`,N.ERR_BAD_REQUEST,e));return}h.send(i||null)})},kr=(e,t)=>{let{length:n}=e=e?e.filter(Boolean):[];if(t||n){let n=new AbortController,r,i=function(e){if(!r){r=!0,o();let t=e instanceof Error?e:this.reason;n.abort(t instanceof N?t:new fr(t instanceof Error?t.message:t))}},a=t&&setTimeout(()=>{a=null,i(new N(`timeout of ${t}ms exceeded`,N.ETIMEDOUT))},t),o=()=>{e&&=(a&&clearTimeout(a),a=null,e.forEach(e=>{e.unsubscribe?e.unsubscribe(i):e.removeEventListener(`abort`,i)}),null)};e.forEach(e=>e.addEventListener(`abort`,i));let{signal:s}=n;return s.unsubscribe=()=>M.asap(o),s}},Ar=function*(e,t){let n=e.byteLength;if(!t||n{let i=jr(e,t),a=0,o,s=e=>{o||(o=!0,r&&r(e))};return new ReadableStream({async pull(e){try{let{done:t,value:r}=await i.next();if(t){s(),e.close();return}let o=r.byteLength;n&&n(a+=o),e.enqueue(new Uint8Array(r))}catch(e){throw s(e),e}},cancel(e){return s(e),i.return()}},{highWaterMark:2})},Pr=64*1024,{isFunction:Fr}=M,Ir=(({Request:e,Response:t})=>({Request:e,Response:t}))(M.global),{ReadableStream:Lr,TextEncoder:Rr}=M.global,zr=(e,...t)=>{try{return!!e(...t)}catch{return!1}},Br=e=>{e=M.merge.call({skipUndefined:!0},Ir,e);let{fetch:t,Request:n,Response:r}=e,i=t?Fr(t):typeof fetch==`function`,a=Fr(n),o=Fr(r);if(!i)return!1;let s=i&&Fr(Lr),c=i&&(typeof Rr==`function`?(e=>t=>e.encode(t))(new Rr):async e=>new Uint8Array(await new n(e).arrayBuffer())),l=a&&s&&zr(()=>{let e=!1,t=new n(Kn.origin,{body:new Lr,method:`POST`,get duplex(){return e=!0,`half`}}).headers.has(`Content-Type`);return e&&!t}),u=o&&s&&zr(()=>M.isReadableStream(new r(``).body)),d={stream:u&&(e=>e.body)};i&&[`text`,`arrayBuffer`,`blob`,`formData`,`stream`].forEach(e=>{!d[e]&&(d[e]=(t,n)=>{let r=t&&t[e];if(r)return r.call(t);throw new N(`Response type '${e}' is not supported`,N.ERR_NOT_SUPPORT,n)})});let f=async e=>{if(e==null)return 0;if(M.isBlob(e))return e.size;if(M.isSpecCompliantForm(e))return(await new n(Kn.origin,{method:`POST`,body:e}).arrayBuffer()).byteLength;if(M.isArrayBufferView(e)||M.isArrayBuffer(e))return e.byteLength;if(M.isURLSearchParams(e)&&(e+=``),M.isString(e))return(await c(e)).byteLength},p=async(e,t)=>M.toFiniteNumber(e.getContentLength())??f(t);return async e=>{let{url:i,method:o,data:s,signal:c,cancelToken:f,timeout:m,onDownloadProgress:h,onUploadProgress:g,responseType:_,headers:v,withCredentials:y=`same-origin`,fetchOptions:b}=Dr(e),x=t||fetch;_=_?(_+``).toLowerCase():`text`;let S=kr([c,f&&f.toAbortSignal()],m),C=null,w=S&&S.unsubscribe&&(()=>{S.unsubscribe()}),ee;try{if(g&&l&&o!==`get`&&o!==`head`&&(ee=await p(v,s))!==0){let e=new n(i,{method:`POST`,body:s,duplex:`half`}),t;if(M.isFormData(s)&&(t=e.headers.get(`content-type`))&&v.setContentType(t),e.body){let[t,n]=vr(ee,_r(yr(g)));s=Nr(e.body,Pr,t,n)}}M.isString(y)||(y=y?`include`:`omit`);let t=a&&`credentials`in n.prototype,c={...b,signal:S,method:o.toUpperCase(),headers:v.normalize().toJSON(),body:s,duplex:`half`,credentials:t?y:void 0};C=a&&new n(i,c);let f=await(a?x(C,b):x(i,c)),m=u&&(_===`stream`||_===`response`);if(u&&(h||m&&w)){let e={};[`status`,`statusText`,`headers`].forEach(t=>{e[t]=f[t]});let t=M.toFiniteNumber(f.headers.get(`content-length`)),[n,i]=h&&vr(t,_r(yr(h),!0))||[];f=new r(Nr(f.body,Pr,n,()=>{i&&i(),w&&w()}),e)}_||=`text`;let te=await d[M.findKey(d,_)||`text`](f,e);return!m&&w&&w(),await new Promise((t,n)=>{pr(t,n,{data:te,headers:lr.from(f.headers),status:f.status,statusText:f.statusText,config:e,request:C})})}catch(t){throw w&&w(),t&&t.name===`TypeError`&&/Load failed|fetch/i.test(t.message)?Object.assign(new N(`Network Error`,N.ERR_NETWORK,e,C,t&&t.response),{cause:t.cause||t}):N.from(t,t&&t.code,e,C,t&&t.response)}}},Vr=new Map,Hr=e=>{let t=e&&e.env||{},{fetch:n,Request:r,Response:i}=t,a=[r,i,n],o=a.length,s,c,l=Vr;for(;o--;)s=a[o],c=l.get(s),c===void 0&&l.set(s,c=o?new Map:Br(t)),l=c;return c};Hr();var Ur={http:null,xhr:Or,fetch:{get:Hr}};M.forEach(Ur,(e,t)=>{if(e){try{Object.defineProperty(e,`name`,{value:t})}catch{}Object.defineProperty(e,`adapterName`,{value:t})}});var Wr=e=>`- ${e}`,Gr=e=>M.isFunction(e)||e===null||e===!1;function Kr(e,t){e=M.isArray(e)?e:[e];let{length:n}=e,r,i,a={};for(let o=0;o`adapter ${e} `+(t===!1?`is not supported by the environment`:`is not available in the build`));throw new N(`There is no suitable adapter to dispatch the request `+(n?e.length>1?`since : -`+e.map(Wr).join(` -`):` `+Wr(e[0]):`as no adapter specified`),`ERR_NOT_SUPPORT`)}return i}var qr={getAdapter:Kr,adapters:Ur};function Jr(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new fr(null,e)}function Yr(e){return Jr(e),e.headers=lr.from(e.headers),e.data=ur.call(e,e.transformRequest),[`post`,`put`,`patch`].indexOf(e.method)!==-1&&e.headers.setContentType(`application/x-www-form-urlencoded`,!1),qr.getAdapter(e.adapter||Qn.adapter,e)(e).then(function(t){return Jr(e),t.data=ur.call(e,e.transformResponse,t),t.headers=lr.from(t.headers),t},function(t){return dr(t)||(Jr(e),t&&t.response&&(t.response.data=ur.call(e,e.transformResponse,t.response),t.response.headers=lr.from(t.response.headers))),Promise.reject(t)})}var Xr=`1.13.6`,Zr={};[`object`,`boolean`,`number`,`function`,`string`,`symbol`].forEach((e,t)=>{Zr[e]=function(n){return typeof n===e||`a`+(t<1?`n `:` `)+e}});var Qr={};Zr.transitional=function(e,t,n){function r(e,t){return`[Axios v`+Xr+`] Transitional option '`+e+`'`+t+(n?`. `+n:``)}return(n,i,a)=>{if(e===!1)throw new N(r(i,` has been removed`+(t?` in `+t:``)),N.ERR_DEPRECATED);return t&&!Qr[i]&&(Qr[i]=!0,console.warn(r(i,` has been deprecated since v`+t+` and will be removed in the near future`))),e?e(n,i,a):!0}},Zr.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};function $r(e,t,n){if(typeof e!=`object`)throw new N(`options must be an object`,N.ERR_BAD_OPTION_VALUE);let r=Object.keys(e),i=r.length;for(;i-- >0;){let a=r[i],o=t[a];if(o){let t=e[a],n=t===void 0||o(t,a,e);if(n!==!0)throw new N(`option `+a+` must be `+n,N.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new N(`Unknown option `+a,N.ERR_BAD_OPTION)}}var ei={assertOptions:$r,validators:Zr},ti=ei.validators,ni=class{constructor(e){this.defaults=e||{},this.interceptors={request:new Ln,response:new Ln}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=Error();let n=t.stack?t.stack.replace(/^.+\n/,``):``;try{e.stack?n&&!String(e.stack).endsWith(n.replace(/^.+\n.+\n/,``))&&(e.stack+=` -`+n):e.stack=n}catch{}}throw e}}_request(e,t){typeof e==`string`?(t||={},t.url=e):t=e||{},t=Er(this.defaults,t);let{transitional:n,paramsSerializer:r,headers:i}=t;n!==void 0&&ei.assertOptions(n,{silentJSONParsing:ti.transitional(ti.boolean),forcedJSONParsing:ti.transitional(ti.boolean),clarifyTimeoutError:ti.transitional(ti.boolean),legacyInterceptorReqResOrdering:ti.transitional(ti.boolean)},!1),r!=null&&(M.isFunction(r)?t.paramsSerializer={serialize:r}:ei.assertOptions(r,{encode:ti.function,serialize:ti.function},!0)),t.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls===void 0?t.allowAbsoluteUrls=!0:t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls),ei.assertOptions(t,{baseUrl:ti.spelling(`baseURL`),withXsrfToken:ti.spelling(`withXSRFToken`)},!0),t.method=(t.method||this.defaults.method||`get`).toLowerCase();let a=i&&M.merge(i.common,i[t.method]);i&&M.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`,`common`],e=>{delete i[e]}),t.headers=lr.concat(a,i);let o=[],s=!0;this.interceptors.request.forEach(function(e){if(typeof e.runWhen==`function`&&e.runWhen(t)===!1)return;s&&=e.synchronous;let n=t.transitional||Rn;n&&n.legacyInterceptorReqResOrdering?o.unshift(e.fulfilled,e.rejected):o.push(e.fulfilled,e.rejected)});let c=[];this.interceptors.response.forEach(function(e){c.push(e.fulfilled,e.rejected)});let l,u=0,d;if(!s){let e=[Yr.bind(this),void 0];for(e.unshift(...o),e.push(...c),d=e.length,l=Promise.resolve(t);u{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null}),this.promise.then=e=>{let t,r=new Promise(e=>{n.subscribe(e),t=e}).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e(function(e,r,i){n.reason||(n.reason=new fr(e,r,i),t(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;let t=this._listeners.indexOf(e);t!==-1&&this._listeners.splice(t,1)}toAbortSignal(){let e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let t;return{token:new e(function(e){t=e}),cancel:t}}};function ii(e){return function(t){return e.apply(null,t)}}function ai(e){return M.isObject(e)&&e.isAxiosError===!0}var oi={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(oi).forEach(([e,t])=>{oi[t]=e});function si(e){let t=new ni(e),n=mt(ni.prototype.request,t);return M.extend(n,ni.prototype,t,{allOwnKeys:!0}),M.extend(n,t,null,{allOwnKeys:!0}),n.create=function(t){return si(Er(e,t))},n}var P=si(Qn);P.Axios=ni,P.CanceledError=fr,P.CancelToken=ri,P.isCancel=dr,P.VERSION=Xr,P.toFormData=jn,P.AxiosError=N,P.Cancel=P.CanceledError,P.all=function(e){return Promise.all(e)},P.spread=ii,P.isAxiosError=ai,P.mergeConfig=Er,P.AxiosHeaders=lr,P.formToJSON=e=>Xn(M.isHTMLForm(e)?new FormData(e):e),P.getAdapter=qr.getAdapter,P.HttpStatusCode=oi,P.default=P;var ci=l(_()),li=`order-demo-001`;function ui(e,t,n,r,i,a){return{eventId:e,aggregateId:li,aggregateType:`ORDER`,sequenceNumber:t,eventType:n,payload:a,metadata:JSON.stringify({source:`demo`,correlationId:`corr-demo-${t}`}),timestamp:r,globalPosition:i}}function di(){let e=[],t=Date.parse(`2025-01-15T08:00:00.000Z`);for(let n=1;n<=100;n++){let r=new Date(t+n*45e3).toISOString(),i=5e4+n,a,o;if(n===1)a=`ORDER_PLACED`,o={customerId:`cust-77`,channel:`web`,status:`PENDING`,totalCents:0,itemCount:0};else if(n>=2&&n<=48){a=`LINE_ITEM_ADDED`;let e=350+n*73%1200;o={sku:`SKU-${String(1e4+n*17).slice(-4)}`,qty:n%4+1,lineTotalCents:e,lineIndex:n-1}}else if(n>=49&&n<=58)a=`PAYMENT_PROGRESS`,o={paymentId:`pay-chunk-${n}`,amountCents:1500+n*120,balanceCents:Math.max(0,48e3-n*700)};else if(n>=59&&n<=72){let e=[`inventory`,`fraud_check`,`address_verify`,`manual_review`,`carrier_delay`];a=`FULFILLMENT_BLOCKED`,o={reason:e[n%e.length],caseId:`CASE-${n}`,retryAfterMinutes:15+n%45}}else n>=73&&n<=88?(a=`SHIPMENT_EVENT`,o={leg:n-72,carrier:n%3==0?`FAST`:n%3==1?`ECONOMY`:`OVERNIGHT`,status:`IN_TRANSIT`,trackingToken:`trk-${n}${(n*7919).toString(36)}`}):n>=89&&n<=99?(a=`NOTE_APPENDED`,o={author:`agent-${n%6+1}`,noteId:`n-${n}`,preview:`Ops note #${n}: SLA watch / customer ping`}):(a=`REFUND_ISSUED`,o={refundCents:88e3,balanceCents:-12500,reason:`bulk_settlement_adjustment`});e.push(ui(`evt-demo-${n}`,n,a,r,i,JSON.stringify(o)))}return e}var fi=di();function pi(e){return[{code:`NEGATIVE_BALANCE`,severity:`HIGH`,description:`Ledger balance dropped below zero after refund batch`},{code:`REFUND_EXCEEDS_CAPTURE`,severity:`CRITICAL`,description:`Cumulative refunds exceed captured payments for this aggregate`},{code:`DUPLICATE_PAYMENT_CHUNK`,severity:`MEDIUM`,description:`Two payment chunks share the same window and amount fingerprint`},{code:`LINE_ITEM_PRICE_OUTLIER`,severity:`LOW`,description:`Line total deviates >3σ from cohort for this SKU family`},{code:`FULFILLMENT_STALL`,severity:`HIGH`,description:`Order blocked in fulfillment longer than SLA for channel`},{code:`CARRIER_MISMATCH`,severity:`MEDIUM`,description:`Shipment leg carrier differs from preferred routing profile`},{code:`MANUAL_REVIEW_BACKLOG`,severity:`LOW`,description:`Case reopened multiple times without resolution`},{code:`VELOCITY_SPIKE`,severity:`HIGH`,description:`Event rate on this aggregate exceeded rolling baseline`},{code:`ADDRESS_VERIFY_LOOP`,severity:`MEDIUM`,description:`Address verification failed three times with same payload hash`},{code:`INVENTORY_HOLD`,severity:`MEDIUM`,description:`Inventory hold exceeded expected release window`},{code:`FRAUD_SCORE_EDGE`,severity:`LOW`,description:`Fraud score landed in manual-review gray band`},{code:`DISCOUNT_STACK`,severity:`LOW`,description:`Multiple discount signals present without explicit approval event`},{code:`SHIPMENT_GAP`,severity:`HIGH`,description:`Missing scan between expected hub handoffs`},{code:`NOTE_SPAM`,severity:`LOW`,description:`Unusually high operator notes density in short interval`},{code:`PAYMENT_PARTIAL_CLUSTER`,severity:`MEDIUM`,description:`Several partial captures without closing settlement event`},{code:`SKU_QUANTITY_ANOMALY`,severity:`MEDIUM`,description:`Quantity pattern inconsistent with historical order curve`},{code:`CASE_ESCALATION`,severity:`HIGH`,description:`Support case escalated without prior tier-1 closure`},{code:`TRACKING_TOKEN_REUSE`,severity:`CRITICAL`,description:`Tracking token collision across two concurrent legs`},{code:`SLA_BREACH_RISK`,severity:`HIGH`,description:`Projected delivery crosses committed SLA if delay persists`},{code:`SETTLEMENT_BATCH_DRIFT`,severity:`CRITICAL`,description:`Settlement batch totals diverge from summed payment chunks`}].map((t,n)=>{let r=Math.min(100,5+n*5),i=e.find(e=>e.sequenceNumber===r)??e[e.length-1];return{code:t.code,description:t.description,severity:t.severity,aggregateId:li,atSequence:r,triggeringEventType:i.eventType,timestamp:i.timestamp,stateAtAnomaly:{demoIndex:n+1,atSequence:r,code:t.code}}})}var mi=pi(fi);function hi(e,t){let n={...e},r={};try{r=JSON.parse(t.payload||`{}`)}catch{}n._version=t.sequenceNumber,n._lastEventType=t.eventType,n._lastUpdated=t.timestamp;let i=t.eventType.toLowerCase();return i.includes(`created`)||i.includes(`opened`)||i.includes(`placed`)||i.includes(`submitted`)||(i.includes(`deleted`)||i.includes(`closed`)||i.includes(`cancelled`)||i.includes(`rejected`))&&(n.status=`DELETED`),Object.assign(n,r),n}function gi(e,t){let n={};for(let r of Object.keys(t)){let i=e[r],a=t[r];JSON.stringify(i)!==JSON.stringify(a)&&(n[r]={oldValue:i,newValue:a})}for(let r of Object.keys(e))r in t||(n[r]={oldValue:e[r],newValue:void 0});return n}function _i(e){let t=[],n={};for(let r of e){let e={...n};n=hi(n,r);let i={...n};t.push({event:r,stateBefore:e,stateAfter:i,diff:gi(e,i)})}return t}var vi=_i(fi);function yi(e){let t=e.trim().toLowerCase();return t.length<2?!1:t.includes(`demo`)||`order-demo-001`.includes(t)}function bi(e){return yi(e)?[li]:[]}function xi(e){let t=Math.min(Math.max(e,1),500);return[...fi].sort((e,t)=>t.globalPosition-e.globalPosition).slice(0,t)}function Si(e){return e===`order-demo-001`?vi:[]}function Ci(e){let t=Math.min(Math.max(e,1),500);return mi.slice(0,t)}function wi(){return[...fi].sort((e,t)=>t.globalPosition-e.globalPosition).slice(0,40)}function Ti(){return{status:`UP`,version:`demo`,demo:!0}}function Ei(){return!1}var Di=P.create({baseURL:`/api`});function Oi(e){return new Promise(t=>{setTimeout(t,e)})}var ki=async(e,t=20)=>{if(Ei()){await Oi(40);let n=bi(e);try{let r=await Di.get(`/aggregates/search?q=${encodeURIComponent(e)}&limit=${t}`);return[...new Set([...n,...r.data])].slice(0,t)}catch{return n}}return Di.get(`/aggregates/search?q=${encodeURIComponent(e)}&limit=${t}`).then(e=>e.data)},Ai=async e=>Ei()&&e===`order-demo-001`?(await Oi(50),Si(e)):Di.get(`/aggregates/${e}/transitions`).then(e=>e.data),ji=async(e=100)=>Ei()?(await Oi(45),Ci(e)):Di.get(`/anomalies/recent?limit=${e}`).then(e=>e.data),Mi=async(e=50)=>Ei()?(await Oi(35),xi(e)):Di.get(`/events/recent?limit=${e}`).then(e=>e.data),Ni=async()=>Ei()?(await Oi(20),Ti()):Di.get(`/health`).then(e=>e.data);function Pi(e,t){let[n,r]=(0,k.useState)(e);return(0,k.useEffect)(()=>{let n=setTimeout(()=>r(e),t);return()=>clearTimeout(n)},[e,t]),n}function Fi({onSelect:e}){let[t,n]=(0,k.useState)(``),[r,i]=(0,k.useState)(!1),a=(0,k.useRef)(null),o=Pi(t,300),{data:s=[]}=j({queryKey:[`search`,o],queryFn:()=>ki(o),enabled:o.length>=2,staleTime:5e3});(0,k.useEffect)(()=>{let e=e=>{a.current&&!a.current.contains(e.target)&&i(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[]);let c=(0,k.useRef)(null),l=(0,k.useCallback)(()=>{c.current?.focus(),c.current?.select()},[]);(0,k.useEffect)(()=>{document.getElementById(`aggregate-search`)?.addEventListener(`focus`,l)},[l]);let u=t=>{n(t),i(!1),e(t)};return(0,A.jsxs)(`div`,{className:`search-wrapper`,ref:a,children:[(0,A.jsx)(`span`,{className:`search-icon`,children:`🔎`}),(0,A.jsx)(`input`,{id:`aggregate-search`,ref:c,type:`text`,className:`search-input`,placeholder:`Search by aggregate ID (e.g. UUID or stream key)`,value:t,onChange:e=>{n(e.target.value),i(!0)},onFocus:()=>t.length>=2&&i(!0),onKeyDown:e=>{e.key===`Enter`&&t.trim()&&u(t.trim()),e.key===`Escape`&&i(!1)},autoComplete:`off`}),r&&s.length>0&&(0,A.jsx)(`div`,{className:`search-results`,role:`listbox`,children:s.map(e=>(0,A.jsxs)(`button`,{type:`button`,className:`search-result-item`,onClick:()=>u(e),role:`option`,children:[(0,A.jsx)(`span`,{className:`search-result-chevron`,"aria-hidden":!0,children:`→`}),(0,A.jsxs)(`span`,{className:`search-result-body`,children:[(0,A.jsx)(`span`,{className:`search-result-label`,children:`ID`}),(0,A.jsx)(`span`,{className:`search-result-colon`,children:`:`}),(0,A.jsx)(`span`,{className:`search-result-value`,children:e})]})]},e))})]})}function Ii(e){return j({queryKey:[`transitions`,e],queryFn:()=>Ai(e)})}function Li(e){if(typeof e==`number`)return Number.isNaN(e)?new Date:e<0xe8d4a51000?new Date(e*1e3):new Date(e);let t=String(e).trim();if(!t)return new Date;if(t.includes(`T`)||/^\d{4}-\d{2}-\d{2}/.test(t)){let e=Date.parse(t);if(!Number.isNaN(e))return new Date(e)}let n=parseFloat(t);return Number.isNaN(n)?new Date:n<0xe8d4a51000?new Date(n*1e3):new Date(n)}var F=4;function I(e){let t=e.toLowerCase();return t.includes(`created`)||t.includes(`opened`)||t.includes(`placed`)||t.includes(`submitted`)?`created`:t.includes(`deleted`)||t.includes(`closed`)||t.includes(`cancelled`)||t.includes(`rejected`)?`deleted`:t.includes(`completed`)||t.includes(`resolved`)||t.includes(`accepted`)||t.includes(`approved`)||t.includes(`assigned`)?`completed`:t.includes(`failed`)||t.includes(`error`)||t.includes(`blocked`)?`failed`:t.includes(`transfer`)?`transfer`:t.includes(`line_item`)||t.includes(`item`)&&t.includes(`add`)?`item`:t.includes(`payment`)||t.includes(`progress`)?`progress`:`default`}function Ri(e){let t=[],n=0;for(;n=F)t.push({kind:`group`,eventType:r,items:e.slice(n,i),startIndex:n}),n=i;else{for(let r=n;rr(o.sequenceNumber),title:`${o.eventType}\n${Li(o.timestamp).toLocaleString()}`,"aria-current":c?`step`:void 0,"aria-label":`Event ${t}, sequence ${o.sequenceNumber}, ${o.eventType}`,children:[(0,A.jsxs)(`span`,{className:`timeline-step-badge`,children:[`Event `,t]}),(0,A.jsxs)(`span`,{className:`timeline-step-seq`,children:[`seq #`,o.sequenceNumber]}),(0,A.jsx)(`span`,{className:`timeline-step-type`,children:o.eventType}),a&&(0,A.jsx)(`span`,{className:`timeline-anomaly-marker`,title:`Has state changes`,children:`●`})]})}function Vi({aggregateId:e,selectedSequence:t,onSelectEvent:n}){let{data:r,isLoading:i}=Ii(e),[a,o]=(0,k.useState)(null),[s,c]=(0,k.useState)(``),[l,u]=(0,k.useState)(``),d=(0,k.useMemo)(()=>r?.length?Ri(r):[],[r]),f=(0,k.useMemo)(()=>r?.length?[...new Set(r.map(e=>e.event.eventType))].sort():[],[r]),p=(0,k.useMemo)(()=>!l||!r?.length?r:r.filter(e=>e.event.eventType===l),[r,l]),m=(0,k.useMemo)(()=>p?.length?Ri(p):[],[p]),h=l?m:d,g=l?p:r,_=t!=null&&g?.length?g.findIndex(e=>e.event.sequenceNumber===t):-1,v=_>=0?_+1:null,y=g?.[0]?.event.sequenceNumber??0,b=g?.[g.length-1]?.event.sequenceNumber??0,x=(0,k.useMemo)(()=>{if(!a)return null;for(let e of h)if(e.kind===`group`&&zi(e.startIndex,e.items.length)===a)return e;return null},[a,h]);(0,k.useEffect)(()=>{if(!(t==null||_<0)){for(let e of h){if(e.kind!==`group`)continue;let t=e.startIndex+e.items.length-1;if(_>=e.startIndex&&_<=t){o(zi(e.startIndex,e.items.length));return}}o(null)}},[t,_,h]),(0,k.useEffect)(()=>{if(t==null)return;let e=requestAnimationFrame(()=>{let e=document.querySelector(`[data-timeline-seq="${t}"]`),n=document.querySelector(`[data-timeline-group-anchor="1"]`);(e??n)?.scrollIntoView({inline:`center`,block:`nearest`,behavior:`smooth`})});return()=>cancelAnimationFrame(e)},[t,a,h]);let S=(e,t)=>{let n=zi(e,t);o(e=>e===n?null:n)},C=(0,k.useCallback)(e=>{if(!g?.length)return;let t=e.target;if(t.tagName!==`INPUT`){if(e.key===`ArrowLeft`||e.key===`ArrowRight`){e.preventDefault();let t=e.key===`ArrowLeft`?-1:1;if(e.shiftKey){let e=_>=0?h.find(e=>e.kind===`group`?_>=e.startIndex&&_=0&&e{let e=e=>w.current(e);return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);let ee=()=>{let e=parseInt(s,10);!isNaN(e)&&g?.some(t=>t.event.sequenceNumber===e)&&(n(e),c(``))};return i?(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsx)(`div`,{className:`card-title`,children:`⏱ Event sequence`}),(0,A.jsx)(`div`,{className:`skeleton`,style:{height:64}})]}):r?.length?(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsxs)(`div`,{className:`timeline-header-row`,children:[(0,A.jsxs)(`div`,{className:`card-title`,style:{marginBottom:0},children:[`⏱ Event sequence`,(0,A.jsxs)(`span`,{className:`timeline-count-pill`,children:[l?`${g?.length} / ${r.length}`:r.length,` events`]})]}),(0,A.jsxs)(`div`,{className:`timeline-jump-group`,children:[(0,A.jsx)(`input`,{className:`timeline-jump-input`,type:`number`,placeholder:`Jump to seq`,value:s,onChange:e=>c(e.target.value),onKeyDown:e=>e.key===`Enter`&&ee(),"aria-label":`Jump to sequence number`}),(0,A.jsx)(`button`,{type:`button`,className:`timeline-jump-btn`,onClick:ee,children:`↵`})]})]}),f.length>1&&(0,A.jsxs)(`div`,{className:`timeline-filter-chips`,role:`group`,"aria-label":`Filter by event type`,children:[(0,A.jsx)(`button`,{type:`button`,className:`filter-chip ${l?``:`active`}`,onClick:()=>u(``),children:`All`}),f.map(e=>(0,A.jsx)(`button`,{type:`button`,className:`filter-chip ${l===e?`active`:``}`,onClick:()=>u(t=>t===e?``:e),children:e},e))]}),(0,A.jsxs)(`div`,{className:`timeline-rail`,children:[(0,A.jsx)(`div`,{className:`timeline-stepper`,role:`navigation`,"aria-label":`Events in order`,children:(0,A.jsx)(`div`,{className:`timeline-stepper-track`,children:h.map((e,r)=>(0,A.jsxs)(k.Fragment,{children:[r>0&&(0,A.jsx)(`span`,{className:`timeline-step-arrow`,"aria-hidden":!0,children:`→`}),e.kind===`single`?(0,A.jsx)(Bi,{transition:e.transition,stepNumber:e.index+1,selectedSequence:t,onSelectEvent:n,hasDiff:Object.keys(e.transition.diff??{}).length>0}):(0,A.jsx)(Hi,{segment:e,selectedSequence:t,expanded:a===zi(e.startIndex,e.items.length),onToggle:()=>S(e.startIndex,e.items.length)})]},e.kind===`group`?`g-${e.startIndex}`:`s-${e.transition.event.sequenceNumber}`))})}),x&&(0,A.jsxs)(`div`,{className:`timeline-expanded-deck`,children:[(0,A.jsxs)(`div`,{className:`timeline-expanded-head`,children:[(0,A.jsx)(`span`,{className:`timeline-expanded-title`,children:x.eventType}),(0,A.jsxs)(`span`,{className:`timeline-expanded-meta`,children:[x.items.length,` events · steps `,x.startIndex+1,`–`,x.startIndex+x.items.length]}),(0,A.jsx)(`button`,{type:`button`,className:`timeline-expanded-close`,onClick:()=>o(null),children:`Collapse`})]}),(0,A.jsx)(`div`,{className:`timeline-expanded-strip`,children:x.items.map((e,r)=>(0,A.jsxs)(k.Fragment,{children:[r>0&&(0,A.jsx)(`span`,{className:`timeline-step-arrow timeline-step-arrow-compact`,"aria-hidden":!0,children:`→`}),(0,A.jsx)(Bi,{transition:e,stepNumber:x.startIndex+r+1,selectedSequence:t,onSelectEvent:n,compact:!0,hasDiff:Object.keys(e.diff??{}).length>0})]},e.event.sequenceNumber))})]})]}),(0,A.jsx)(`input`,{type:`range`,className:`timeline-slider`,min:y,max:b,value:t??b,onChange:e=>n(Number(e.target.value)),"aria-label":`Scrub event sequence`}),(0,A.jsxs)(`div`,{className:`timeline-info`,children:[(0,A.jsxs)(`span`,{className:`timeline-info-edge`,children:[`First · seq #`,y]}),(0,A.jsx)(`span`,{className:`timeline-info-center`,children:v==null?`Select an event above or drag the scrubber`:(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`strong`,{children:[`Step `,v]}),` of `,g?.length,(0,A.jsxs)(`span`,{className:`timeline-info-muted`,children:[` · sequence #`,t]}),(0,A.jsx)(`br`,{}),(0,A.jsx)(`span`,{className:`timeline-info-type`,children:g?.find(e=>e.event.sequenceNumber===t)?.event.eventType??``})]})}),(0,A.jsxs)(`span`,{className:`timeline-info-edge`,children:[`Last · seq #`,b]})]})]}):(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsx)(`div`,{className:`card-title`,children:`⏱ Event sequence`}),(0,A.jsx)(`p`,{style:{color:`var(--text-muted)`,fontSize:13},children:`No events found for this aggregate.`})]})}function Hi({segment:e,selectedSequence:t,expanded:n,onToggle:r}){let{items:i,startIndex:a,eventType:o}=e,s=i[0].event,c=i[i.length-1].event,l=I(o),u=t!=null&&i.some(e=>e.event.sequenceNumber===t),d=u&&!n;return(0,A.jsxs)(`button`,{type:`button`,className:`timeline-group-chip timeline-step-${l} ${u?`has-selection`:``} ${n?`expanded`:``} ${u&&!n?`active`:``}`,onClick:r,"aria-expanded":n,"data-timeline-group-anchor":d?`1`:void 0,title:`${i.length} × ${o}. Click to ${n?`collapse`:`show every step`}.`,children:[(0,A.jsxs)(`span`,{className:`timeline-group-chip-top`,children:[(0,A.jsxs)(`span`,{className:`timeline-group-count`,children:[`×`,i.length]}),(0,A.jsx)(`span`,{className:`timeline-group-chevron`,"aria-hidden":!0,children:n?`▲`:`▼`})]}),(0,A.jsx)(`span`,{className:`timeline-group-type`,children:o}),(0,A.jsxs)(`span`,{className:`timeline-group-range`,children:[`steps `,a+1,`–`,a+i.length,` · seq #`,s.sequenceNumber,`–#`,c.sequenceNumber]})]})}function Ui(e){return j({queryKey:[`transitions`,e],queryFn:()=>Ai(e)})}function Wi({diff:e}){let t=Object.entries(e),n=t.length>0,[r,i]=(0,k.useState)(`inline`);return n?(0,A.jsxs)(`div`,{className:`diff-panel`,children:[(0,A.jsxs)(`div`,{className:`diff-toolbar`,children:[(0,A.jsxs)(`div`,{className:`diff-toolbar-title`,children:[`Changes`,(0,A.jsxs)(`span`,{className:`diff-count-badge`,children:[t.length,` `,t.length===1?`field`:`fields`,` modified`]})]}),(0,A.jsxs)(`div`,{className:`diff-view-toggle`,role:`group`,"aria-label":`Diff layout`,children:[(0,A.jsx)(`button`,{type:`button`,className:r===`inline`?`active`:``,onClick:()=>i(`inline`),children:`Inline`}),(0,A.jsx)(`button`,{type:`button`,className:r===`split`?`active`:``,onClick:()=>i(`split`),children:`Side by side`})]})]}),(0,A.jsx)(`div`,{className:`diff-body`,children:(0,A.jsx)(`div`,{className:`diff-scroll`,children:r===`inline`?(0,A.jsx)(`div`,{className:`diff-list diff-list-inline`,children:t.map(([e,t],n)=>(0,A.jsxs)(`div`,{className:`diff-row`,children:[(0,A.jsx)(`span`,{className:`diff-line-no`,"aria-hidden":!0,children:n+1}),(0,A.jsxs)(`div`,{className:`diff-row-body`,children:[(0,A.jsx)(`span`,{className:`diff-field`,children:e}),(0,A.jsxs)(`span`,{className:`diff-values-inline`,children:[(0,A.jsx)(`span`,{className:`diff-old`,children:JSON.stringify(t.oldValue)}),(0,A.jsx)(`span`,{className:`diff-arrow`,children:`→`}),(0,A.jsx)(`span`,{className:`diff-new`,children:JSON.stringify(t.newValue)})]})]})]},e))}):(0,A.jsxs)(`div`,{className:`diff-list diff-list-split`,children:[(0,A.jsxs)(`div`,{className:`diff-split-head`,children:[(0,A.jsx)(`span`,{className:`diff-split-label diff-split-old-label`,children:`Before`}),(0,A.jsx)(`span`,{className:`diff-split-label diff-split-new-label`,children:`After`})]}),t.map(([e,t],n)=>(0,A.jsxs)(`div`,{className:`diff-split-row`,children:[(0,A.jsx)(`span`,{className:`diff-line-no`,"aria-hidden":!0,children:n+1}),(0,A.jsxs)(`div`,{className:`diff-split-cells`,children:[(0,A.jsxs)(`div`,{className:`diff-split-cell diff-split-old`,children:[(0,A.jsx)(`span`,{className:`diff-field`,children:e}),(0,A.jsx)(`span`,{className:`diff-cell-value`,children:JSON.stringify(t.oldValue)})]}),(0,A.jsxs)(`div`,{className:`diff-split-cell diff-split-new`,children:[(0,A.jsx)(`span`,{className:`diff-field`,children:e}),(0,A.jsx)(`span`,{className:`diff-cell-value`,children:JSON.stringify(t.newValue)})]})]})]},e))]})})})]}):null}function Gi({open:e,onToggle:t}){return(0,A.jsx)(`button`,{type:`button`,className:`json-tree-toggle`,onClick:e=>{e.stopPropagation(),t()},"aria-expanded":e,"aria-label":e?`Collapse`:`Expand`,children:e?`▼`:`▶`})}function Ki({value:e}){return e===null?(0,A.jsx)(`span`,{className:`json-null`,children:`null`}):typeof e==`boolean`?(0,A.jsx)(`span`,{className:`json-boolean`,children:String(e)}):typeof e==`number`?(0,A.jsx)(`span`,{className:`json-number`,children:e}):(0,A.jsx)(`span`,{className:`json-string`,children:JSON.stringify(e)})}function qi({value:e,changedKeys:t}){return(0,A.jsx)(`div`,{className:`json-tree json-tree-root`,role:`tree`,children:(0,A.jsx)(Ji,{value:e,depth:0,changedKeys:t,keyPath:``})})}function Ji({value:e,depth:t,propertyKey:n,changedKeys:r,keyPath:i=``}){let a=r&&n!==void 0&&r.has(n),o=r&&i&&[...r].some(e=>e.startsWith(i+`.`)),[s,c]=(0,k.useState)(r?t<3||!!a||!!o:t<3),l={paddingLeft:Math.min(t,12)*14},u=a?{background:`rgba(255, 170, 0, 0.12)`,borderRadius:3}:{};if(e===null||typeof e==`boolean`||typeof e==`number`||typeof e==`string`)return(0,A.jsxs)(`div`,{className:`json-tree-line${a?` json-tree-changed`:``}`,style:{...l,...u},children:[n!==void 0&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,A.jsx)(Ki,{value:e})]});let d=e=>i?`${i}.${e}`:e;if(Array.isArray(e))return e.length===0?(0,A.jsxs)(`div`,{className:`json-tree-line`,style:l,children:[n!==void 0&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`[]`})]}):(0,A.jsxs)(`div`,{className:`json-tree-branch`,children:[(0,A.jsxs)(`div`,{className:`json-tree-line${a?` json-tree-changed`:``}`,style:{...l,...u},children:[(0,A.jsx)(Gi,{open:s,onToggle:()=>c(e=>!e)}),n!==void 0&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`[`}),!s&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-ellipsis`,children:[` `,e.length,` items `]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`]`})]})]}),s&&(0,A.jsxs)(A.Fragment,{children:[e.map((e,n)=>(0,A.jsx)(Ji,{value:e,depth:t+1,changedKeys:r,keyPath:d(String(n))},n)),(0,A.jsx)(`div`,{className:`json-tree-line`,style:l,children:(0,A.jsx)(`span`,{className:`json-punct`,children:`]`})})]})]});if(typeof e==`object`){let i=Object.entries(e);return i.length===0?(0,A.jsxs)(`div`,{className:`json-tree-line`,style:l,children:[n!==void 0&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`{}`})]}):(0,A.jsxs)(`div`,{className:`json-tree-branch`,children:[(0,A.jsxs)(`div`,{className:`json-tree-line${a?` json-tree-changed`:``}`,style:{...l,...u},children:[(0,A.jsx)(Gi,{open:s,onToggle:()=>c(e=>!e)}),n!==void 0&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-key`,children:[`"`,n,`"`]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`: `})]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`{`}),!s&&(0,A.jsxs)(A.Fragment,{children:[(0,A.jsxs)(`span`,{className:`json-ellipsis`,children:[` `,i.length,` keys `]}),(0,A.jsx)(`span`,{className:`json-punct`,children:`}`})]})]}),s&&(0,A.jsxs)(A.Fragment,{children:[i.map(([e,n])=>(0,A.jsx)(Ji,{value:n,depth:t+1,propertyKey:e,changedKeys:r,keyPath:d(e)},e)),(0,A.jsx)(`div`,{className:`json-tree-line`,style:l,children:(0,A.jsx)(`span`,{className:`json-punct`,children:`}`})})]})]})}return(0,A.jsx)(`div`,{className:`json-tree-line`,style:l,children:(0,A.jsx)(`span`,{className:`json-unknown`,children:String(e)})})}var Yi=[{id:`changes`,label:`Changes`,emoji:`±`},{id:`before-after`,label:`Before / After`,emoji:`⇄`},{id:`raw`,label:`Raw JSON`,emoji:`{ }`}];function Xi({aggregateId:e,sequence:t,activeTab:n,onTabChange:r}){let{data:i,isLoading:a}=Ui(e),[o,s]=(0,k.useState)(`changes`),c=n??o,l=e=>{s(e),r?.(e)};if(a)return(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsx)(`div`,{className:`card-title`,children:`🔬 State at Event`}),(0,A.jsx)(`div`,{className:`skeleton`,style:{height:120}})]});let u=i?.find(e=>e.event.sequenceNumber===t);if(!u)return null;let{event:d,stateBefore:f,stateAfter:p,diff:m}=u,h=Object.entries(m),g=h.length>0,_=new Set(h.map(([e])=>e));return(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsxs)(`div`,{className:`card-title`,children:[`🔬 State at Event #`,d.sequenceNumber,(0,A.jsx)(`span`,{style:{color:`var(--accent-blue)`,background:`var(--accent-blue-dim)`,padding:`2px 8px`,borderRadius:4,fontSize:12},children:d.eventType}),g&&(0,A.jsxs)(`span`,{className:`diff-count-badge`,children:[h.length,` `,h.length===1?`change`:`changes`]})]}),(0,A.jsx)(`div`,{className:`state-tabs`,role:`tablist`,children:Yi.map(e=>(0,A.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":c===e.id,className:`state-tab ${c===e.id?`active`:``}`,onClick:()=>l(e.id),children:[(0,A.jsx)(`span`,{className:`state-tab-emoji`,"aria-hidden":!0,children:e.emoji}),e.label]},e.id))}),(0,A.jsxs)(`div`,{className:`state-tab-content`,role:`tabpanel`,children:[c===`changes`&&(0,A.jsx)(`div`,{children:g?(0,A.jsx)(Wi,{diff:m}):(0,A.jsx)(`p`,{style:{color:`var(--text-muted)`,marginTop:12,fontSize:13},children:`No field changes at this event.`})}),c===`before-after`&&(0,A.jsxs)(`div`,{className:`state-grid`,style:{marginTop:12},children:[(0,A.jsxs)(`div`,{className:`state-panel state-panel-before`,children:[(0,A.jsx)(`h4`,{children:`Before`}),(0,A.jsx)(qi,{value:f,changedKeys:_})]}),(0,A.jsxs)(`div`,{className:`state-panel state-panel-after`,children:[(0,A.jsx)(`h4`,{children:`After`}),(0,A.jsx)(qi,{value:p,changedKeys:_})]})]}),c===`raw`&&(0,A.jsxs)(`div`,{style:{marginTop:12},children:[(0,A.jsx)(`div`,{className:`json-block`,style:{maxHeight:340},children:JSON.stringify(d,null,2)}),(0,A.jsx)(`button`,{className:`copy-btn`,type:`button`,onClick:()=>navigator.clipboard.writeText(JSON.stringify(d,null,2)),children:`📋 Copy Event JSON`})]})]})]})}var Zi=1e3,Qi=3e4;function $i(e,t,n){let r=n?.enabled??!0,[i,a]=(0,k.useState)(()=>r?`connecting`:`connected`),o=(0,k.useRef)(null),s=(0,k.useRef)(t),c=(0,k.useRef)(Zi);return s.current=t,(0,k.useEffect)(()=>{if(!r)return a(`connected`),()=>{};let t=!1,n,i=()=>{if(t)return;let r=new WebSocket(`${window.location.protocol===`https:`?`wss`:`ws`}://${window.location.host}${e}`);o.current=r,r.onopen=()=>{c.current=Zi,a(`connected`)},r.onclose=()=>{if(a(`disconnected`),!t){let e=c.current;n=setTimeout(()=>{c.current=Math.min(e*2,Qi),i()},e)}},r.onerror=()=>a(`disconnected`),r.onmessage=e=>{try{let t=JSON.parse(e.data);s.current(t)}catch{}}};return i(),()=>{t=!0,clearTimeout(n),o.current?.close()}},[e,r]),i}var ea=(0,k.createContext)(void 0);function ta({children:e}){let[t,n]=(0,k.useState)([]),r=(0,k.useCallback)(e=>{let t=Date.now();n(n=>[...n,{id:t,message:e}]),setTimeout(()=>{n(e=>e.filter(e=>e.id!==t))},4e3)},[]);return(0,A.jsxs)(ea.Provider,{value:{notify:r},children:[e,(0,A.jsx)(`div`,{className:`toast-container`,children:t.map(e=>(0,A.jsx)(`div`,{className:`toast`,children:e.message},e.id))})]})}function na(){let e=(0,k.useContext)(ea);if(!e)throw Error(`useToast must be used within ToastProvider`);return e}function ra(e){let t=e.toLowerCase();return t.includes(`deleted`)||t.includes(`closed`)||t.includes(`cancelled`)||t.includes(`rejected`)?`type-deleted`:t.includes(`withdrawn`)||t.includes(`debit`)?`type-withdrawn`:t.includes(`deposited`)||t.includes(`credit`)?`type-deposited`:t.includes(`created`)||t.includes(`opened`)||t.includes(`placed`)||t.includes(`submitted`)?`type-created`:t.includes(`completed`)||t.includes(`resolved`)||t.includes(`accepted`)||t.includes(`approved`)||t.includes(`assigned`)?`type-completed`:t.includes(`failed`)||t.includes(`error`)?`type-failed`:t.includes(`transfer`)?`type-transfer`:`type-default`}function ia(e){return ra(e)}var aa=100;function oa(e){let t=e.toLowerCase();return t.includes(`deleted`)||t.includes(`closed`)||t.includes(`cancelled`)||t.includes(`rejected`)?`✖`:t.includes(`withdrawn`)||t.includes(`debit`)?`↩`:t.includes(`deposited`)||t.includes(`credit`)?`↪`:t.includes(`created`)||t.includes(`opened`)||t.includes(`placed`)||t.includes(`submitted`)?`✦`:t.includes(`completed`)||t.includes(`resolved`)||t.includes(`accepted`)||t.includes(`approved`)?`✔`:t.includes(`failed`)||t.includes(`error`)?`⚠`:t.includes(`transfer`)?`⇄`:`◆`}function sa(){let e=Ei(),[t,n]=(0,k.useState)(()=>e?wi():[]),[r,i]=(0,k.useState)(!1),a=(0,k.useRef)(null),o=(0,k.useRef)(r);o.current=r;let{notify:s}=na(),c=$i(`/ws/live`,e=>{o.current||n(t=>[...t.slice(-(aa-1)),e])},{enabled:!e}),l=(0,k.useRef)(s);l.current=s;let u=(0,k.useRef)(0);return(0,k.useEffect)(()=>{e||(c===`disconnected`?(u.current++,u.current<=1&&l.current(`Live stream disconnected. Retrying…`)):c===`connected`&&(u.current=0))},[c,e]),(0,k.useEffect)(()=>{!r&&a.current&&(a.current.scrollTop=a.current.scrollHeight)},[t,r]),(0,k.useEffect)(()=>{let e=()=>i(e=>!e);return window.addEventListener(`eventlens:togglestream`,e),()=>window.removeEventListener(`eventlens:togglestream`,e)},[]),(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsxs)(`div`,{className:`live-header`,children:[(0,A.jsx)(`div`,{className:`card-title`,style:{marginBottom:0},children:`📡 Live Event Stream`}),(0,A.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,A.jsxs)(`div`,{className:`live-indicator`,children:[(0,A.jsx)(`span`,{className:`dot ${c===`connected`?`dot-green`:c===`connecting`?`dot-yellow`:`dot-red`}`}),(0,A.jsx)(`span`,{style:{color:c===`connected`?`var(--neon-green)`:`var(--text-muted)`,fontSize:11},children:c===`connected`?r?`Paused`:`Live`:c})]}),(0,A.jsx)(`button`,{className:`pause-btn`,onClick:()=>i(!r),children:r?`▶ Resume`:`⏸ Pause`})]})]}),(0,A.jsxs)(`div`,{className:`event-stream`,ref:a,children:[t.length===0&&(0,A.jsx)(`div`,{style:{color:`var(--text-muted)`,padding:`20px 0`,fontSize:12,fontFamily:`var(--font-mono)`},children:e?`Demo stream (static sample events)`:`Waiting for events…`}),t.map(e=>(0,A.jsxs)(`div`,{className:`event-row ${ia(e.eventType)}`,children:[(0,A.jsx)(`span`,{className:`event-icon`,children:oa(e.eventType)}),(0,A.jsx)(`span`,{className:`event-time`,children:Li(e.timestamp).toLocaleTimeString()}),(0,A.jsx)(`span`,{className:`event-type ${ra(e.eventType)}`,children:e.eventType}),(0,A.jsx)(`span`,{className:`event-agg`,children:e.aggregateId})]},e.eventId))]})]})}function ca(e){switch(e){case`CRITICAL`:return`sev-critical`;case`HIGH`:return`sev-high`;case`MEDIUM`:return`sev-medium`;case`LOW`:return`sev-low`;default:return`sev-low`}}function la(e){switch(e){case`CRITICAL`:return`Critical`;case`HIGH`:return`High`;case`MEDIUM`:return`Warning`;case`LOW`:return`Info`;default:return e}}function L(){return(0,A.jsxs)(`svg`,{viewBox:`0 0 64 64`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,A.jsx)(`defs`,{children:(0,A.jsxs)(`linearGradient`,{id:`shield-grad`,x1:`16`,y1:`8`,x2:`48`,y2:`56`,children:[(0,A.jsx)(`stop`,{offset:`0%`,stopColor:`#00ff88`,stopOpacity:`0.9`}),(0,A.jsx)(`stop`,{offset:`100%`,stopColor:`#00cc66`,stopOpacity:`0.6`})]})}),(0,A.jsx)(`path`,{d:`M32 4 L52 14 L52 32 C52 46 32 58 32 58 C32 58 12 46 12 32 L12 14 Z`,stroke:`url(#shield-grad)`,strokeWidth:`2`,fill:`rgba(0, 255, 136, 0.06)`}),(0,A.jsx)(`path`,{d:`M32 10 L48 18 L48 32 C48 43 32 53 32 53 C32 53 16 43 16 32 L16 18 Z`,stroke:`rgba(0, 255, 136, 0.2)`,strokeWidth:`1`,fill:`none`}),(0,A.jsx)(`polyline`,{points:`22,32 29,40 42,24`,stroke:`#00ff88`,strokeWidth:`3`,strokeLinecap:`round`,strokeLinejoin:`round`,fill:`none`})]})}function ua({color:e}){return(0,A.jsx)(`div`,{className:`gauge-wave`,children:Array.from({length:12},(e,t)=>t).map(t=>(0,A.jsx)(`div`,{className:`gauge-wave-bar ${e}`,style:{animationDelay:`${t*.12}s`}},t))})}function da(){let{data:e,isLoading:t}=j({queryKey:[`anomalies`],queryFn:()=>ji(),refetchInterval:3e4}),n=e&&e.length>0;return(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsxs)(`div`,{className:`card-title anomaly-card-title-row`,children:[(0,A.jsx)(`span`,{className:`anomaly-title-text`,children:`⚠️ Anomaly Detection`}),!t&&n&&(0,A.jsx)(`span`,{className:`anomaly-header-count`,"aria-label":`${e.length} anomalies`,children:e.length})]}),t&&(0,A.jsx)(`div`,{className:`skeleton`,style:{height:120}}),!t&&!n&&(0,A.jsxs)(`div`,{className:`anomaly-panel-inner`,children:[(0,A.jsxs)(`div`,{className:`anomaly-shield`,children:[(0,A.jsx)(`div`,{className:`shield-icon`,children:(0,A.jsx)(L,{})}),(0,A.jsx)(`div`,{className:`shield-text`,children:`No anomalies detected`})]}),(0,A.jsxs)(`div`,{className:`gauge-row`,children:[(0,A.jsxs)(`div`,{className:`gauge-card optimal`,children:[(0,A.jsx)(`div`,{className:`gauge-label`,children:`Data Integrity`}),(0,A.jsx)(`div`,{className:`gauge-value optimal`,children:`OPTIMAL`}),(0,A.jsx)(ua,{color:`green`})]}),(0,A.jsxs)(`div`,{className:`gauge-card baseline`,children:[(0,A.jsx)(`div`,{className:`gauge-label`,children:`Pattern Scan`}),(0,A.jsx)(`div`,{className:`gauge-value baseline`,children:`BASELINE`}),(0,A.jsx)(ua,{color:`cyan`})]}),(0,A.jsxs)(`div`,{className:`gauge-card zero`,children:[(0,A.jsx)(`div`,{className:`gauge-label`,children:`Threat Level`}),(0,A.jsx)(`div`,{className:`gauge-value zero`,children:`ZERO`}),(0,A.jsx)(ua,{color:`green`})]})]})]}),!t&&n&&(0,A.jsx)(`div`,{className:`anomaly-scroll-region`,children:(0,A.jsx)(`div`,{className:`anomaly-list-inner`,children:e.map((e,t)=>(0,A.jsxs)(`details`,{className:`anomaly-card ${e.severity}`,children:[(0,A.jsxs)(`summary`,{className:`anomaly-card-summary`,children:[(0,A.jsx)(`span`,{className:`anomaly-severity-badge ${ca(e.severity)}`,children:la(e.severity)}),(0,A.jsx)(`span`,{className:`anomaly-card-title`,children:e.description}),(0,A.jsx)(`span`,{className:`anomaly-card-chevron`,"aria-hidden":!0,children:`▼`})]}),(0,A.jsxs)(`div`,{className:`anomaly-card-body`,children:[(0,A.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,A.jsx)(`span`,{className:`anomaly-meta-label`,children:`Aggregate`}),(0,A.jsx)(`code`,{className:`anomaly-meta-value`,children:e.aggregateId})]}),(0,A.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,A.jsx)(`span`,{className:`anomaly-meta-label`,children:`Sequence`}),(0,A.jsxs)(`span`,{className:`anomaly-meta-value`,children:[`#`,e.atSequence]})]}),(0,A.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,A.jsx)(`span`,{className:`anomaly-meta-label`,children:`Event type`}),(0,A.jsx)(`span`,{className:`anomaly-meta-value`,children:e.triggeringEventType})]}),(0,A.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,A.jsx)(`span`,{className:`anomaly-meta-label`,children:`When`}),(0,A.jsx)(`span`,{className:`anomaly-meta-value`,children:Li(e.timestamp).toLocaleString()})]}),(0,A.jsxs)(`p`,{className:`anomaly-card-meta`,children:[(0,A.jsx)(`span`,{className:`anomaly-meta-label`,children:`Code`}),(0,A.jsx)(`code`,{className:`anomaly-meta-value`,children:e.code})]})]})]},`${e.aggregateId}-${e.atSequence}-${t}`))})})]})}var fa=[{keys:`← →`,desc:`Navigate events`},{keys:`Shift+← →`,desc:`Jump to group boundary`},{keys:`1 – 3`,desc:`Switch tabs (Changes / ⇄ Before-After / Raw)`},{keys:`Cmd+K`,desc:`Focus search`},{keys:`Space`,desc:`Pause / resume live stream`},{keys:`?`,desc:`Toggle this hint bar`}];function pa(){let[e,t]=(0,k.useState)(!1);return(0,k.useEffect)(()=>{let e=e=>{e.target.tagName!==`INPUT`&&e.key===`?`&&(e.preventDefault(),t(e=>!e))};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]),(0,A.jsx)(`div`,{className:`keyboard-hints ${e?`keyboard-hints--expanded`:``}`,"aria-label":`Keyboard shortcuts`,children:e?(0,A.jsxs)(`div`,{className:`keyboard-hints-grid`,children:[fa.map(e=>(0,A.jsxs)(`div`,{className:`keyboard-hint-row`,children:[(0,A.jsx)(`kbd`,{className:`keyboard-key`,children:e.keys}),(0,A.jsx)(`span`,{className:`keyboard-hint-desc`,children:e.desc})]},e.keys)),(0,A.jsx)(`button`,{type:`button`,className:`keyboard-hints-close`,onClick:()=>t(!1),"aria-label":`Close shortcuts`,children:`✕ Close`})]}):(0,A.jsxs)(`div`,{className:`keyboard-hints-bar`,children:[(0,A.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,A.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`← →`}),` Navigate`]}),(0,A.jsx)(`span`,{className:`keyboard-hints-sep`,children:`·`}),(0,A.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,A.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`1–3`}),` Tabs`]}),(0,A.jsx)(`span`,{className:`keyboard-hints-sep`,children:`·`}),(0,A.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,A.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`Space`}),` Pause stream`]}),(0,A.jsx)(`span`,{className:`keyboard-hints-sep`,children:`·`}),(0,A.jsxs)(`span`,{className:`keyboard-hints-item`,children:[(0,A.jsx)(`kbd`,{className:`keyboard-key-mini`,children:`?`}),` All shortcuts`]})]})})}function ma(){return(0,A.jsxs)(`svg`,{viewBox:`0 0 40 40`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:[(0,A.jsx)(`defs`,{children:(0,A.jsxs)(`linearGradient`,{id:`lens-grad`,x1:`0`,y1:`0`,x2:`40`,y2:`40`,children:[(0,A.jsx)(`stop`,{offset:`0%`,stopColor:`#00f0ff`}),(0,A.jsx)(`stop`,{offset:`100%`,stopColor:`#ff00e5`})]})}),(0,A.jsx)(`circle`,{cx:`18`,cy:`18`,r:`11`,stroke:`url(#lens-grad)`,strokeWidth:`2.5`,fill:`none`}),(0,A.jsx)(`circle`,{cx:`18`,cy:`18`,r:`6`,stroke:`#00f0ff`,strokeWidth:`1`,fill:`none`,opacity:`0.5`}),(0,A.jsx)(`line`,{x1:`26`,y1:`26`,x2:`36`,y2:`36`,stroke:`url(#lens-grad)`,strokeWidth:`3`,strokeLinecap:`round`}),(0,A.jsx)(`polygon`,{points:`18,12 21,18 18,24 15,18`,fill:`#00f0ff`,opacity:`0.3`})]})}function ha(){return(0,A.jsx)(`div`,{className:`mini-wave`,children:[6,12,8,16,10,14,7,11,15,9].map((e,t)=>(0,A.jsx)(`div`,{className:`mini-wave-bar`,style:{height:e,animationDelay:`${t*.1}s`}},t))})}function ga({isUp:e}){let[t,n]=(0,k.useState)(0),[r,i]=(0,k.useState)(null),a=(0,k.useRef)(void 0);return(0,k.useEffect)(()=>{let e=Date.now();return a.current=setInterval(()=>{n(Math.floor((Date.now()-e)/1e3))},1e3),()=>clearInterval(a.current)},[]),(0,k.useEffect)(()=>{let e=()=>{Mi(500).then(e=>i(e.length)).catch(()=>{})};e();let t=setInterval(e,15e3);return()=>clearInterval(t)},[]),(0,A.jsxs)(`div`,{className:`conn-stats`,children:[(0,A.jsx)(ha,{}),(0,A.jsxs)(`div`,{className:`conn-stat`,children:[(0,A.jsx)(`span`,{className:`conn-stat-label`,children:`API`}),(0,A.jsx)(`span`,{className:`conn-stat-value ${e?`green`:``}`,children:e?`Healthy`:`Down`})]}),(0,A.jsxs)(`div`,{className:`conn-stat`,children:[(0,A.jsx)(`span`,{className:`conn-stat-label`,children:`Events`}),(0,A.jsx)(`span`,{className:`conn-stat-value`,children:r??`...`})]}),(0,A.jsxs)(`div`,{className:`conn-stat`,children:[(0,A.jsx)(`span`,{className:`conn-stat-label`,children:`Uptime`}),(0,A.jsx)(`span`,{className:`conn-stat-value green`,children:(e=>{let t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60;return t>0?`${t}h ${n}m`:n>0?`${n}m ${r}s`:`${r}s`})(t)})]})]})}function _a({aggregateId:e,sequence:t,totalEvents:n}){let{data:r}=j({queryKey:[`transitions`,e],queryFn:()=>Ai(e),staleTime:3e4}),i=r?.find(e=>e.event.sequenceNumber===t);if(!i)return null;let{event:a,diff:o}=i,s=Object.keys(o).length,c=r?r.findIndex(e=>e.event.sequenceNumber===t)+1:null;return(0,A.jsxs)(`div`,{className:`event-summary-bar`,children:[(0,A.jsxs)(`div`,{className:`event-summary-left`,children:[(0,A.jsx)(`span`,{className:`event-summary-type`,children:a.eventType}),(0,A.jsxs)(`span`,{className:`event-summary-meta`,children:[`seq #`,t,c!==null&&` · step ${c} of ${n}`,` · `,Li(a.timestamp).toLocaleTimeString()]})]}),s>0&&(0,A.jsxs)(`span`,{className:`event-summary-changes`,children:[s,` `,s===1?`field`:`fields`,` changed`]})]})}function va(){let[e,t]=(0,k.useState)(null),[n,r]=(0,k.useState)(null),[i,a]=(0,k.useState)(`changes`);(0,k.useEffect)(()=>{let e=e=>{let t=e.detail;t&&a(t)};return window.addEventListener(`eventlens:switchtab`,e),()=>window.removeEventListener(`eventlens:switchtab`,e)},[]),(0,k.useEffect)(()=>{let e=new URLSearchParams(window.location.search),n=e.get(`aggregateId`),i=e.get(`seq`),o=e.get(`tab`);if(n&&t(n),i!==null){let e=Number(i);Number.isNaN(e)||r(e)}o&&[`changes`,`before-after`,`raw`].includes(o)&&a(o)},[]),(0,k.useEffect)(()=>{let t=new URLSearchParams(window.location.search);e?t.set(`aggregateId`,e):t.delete(`aggregateId`),n==null?t.delete(`seq`):t.set(`seq`,String(n)),t.set(`tab`,i);let r=t.toString(),a=r?`${window.location.pathname}?${r}`:window.location.pathname;window.history.replaceState(null,``,a)},[e,n,i]);let{data:o}=j({queryKey:[`health`],queryFn:Ni,refetchInterval:3e4}),s=o?.status===`UP`,c=e=>{t(e),r(null)},{data:l}=j({queryKey:[`transitions`,e],queryFn:()=>Ai(e),enabled:!!e,staleTime:3e4}),u=l?.length??0;return(0,A.jsxs)(`div`,{className:`app`,children:[(0,A.jsxs)(`header`,{className:`app-header`,children:[(0,A.jsxs)(`div`,{className:`brand`,children:[(0,A.jsx)(`div`,{className:`brand-logo`,children:(0,A.jsx)(ma,{})}),(0,A.jsxs)(`div`,{children:[(0,A.jsx)(`div`,{className:`brand-name`,children:`EventLens`}),(0,A.jsx)(`div`,{className:`brand-sub`,children:`Event Store Visual Debugger`})]})]}),(0,A.jsx)(`div`,{className:`header-title`,children:`EventLens`}),(0,A.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:20},children:[(0,A.jsx)(ga,{isUp:s}),(0,A.jsxs)(`div`,{className:`header-status`,children:[(0,A.jsx)(`span`,{className:`dot ${s?`dot-green`:`dot-red`}`}),(0,A.jsx)(`span`,{className:`status-text ${s?``:`offline`}`,children:s?`Connected`:o?.status??`Connecting`})]})]})]}),(0,A.jsxs)(`main`,{className:`app-main`,children:[Ei()&&(0,A.jsxs)(`div`,{className:`demo-banner`,role:`status`,children:[`Demo mode (frontend only): API calls are stubbed with sample data. Search`,` `,(0,A.jsx)(`code`,{children:`order-demo-001`}),` or `,(0,A.jsx)(`code`,{children:`demo`}),` to load the sample aggregate.`]}),(0,A.jsxs)(`div`,{className:`card card--dropdown-host`,children:[(0,A.jsx)(`div`,{className:`card-title`,children:`⚡ Search Aggregates`}),(0,A.jsx)(Fi,{onSelect:c}),e&&(0,A.jsxs)(`div`,{style:{marginTop:10,fontSize:12,color:`var(--text-muted)`,fontFamily:`var(--font-mono)`},children:[`Viewing: `,(0,A.jsx)(`span`,{style:{color:`var(--neon-cyan)`,textShadow:`0 0 6px rgba(0,240,255,0.3)`},children:e}),(0,A.jsx)(`button`,{onClick:()=>t(null),style:{marginLeft:12,background:`none`,border:`none`,color:`var(--text-muted)`,cursor:`pointer`,fontFamily:`var(--font-mono)`},children:`× clear`})]})]}),e&&(0,A.jsx)(Vi,{aggregateId:e,selectedSequence:n,onSelectEvent:r}),e&&n!==null&&(0,A.jsx)(_a,{aggregateId:e,sequence:n,totalEvents:u}),e&&n!==null&&(0,A.jsx)(Xi,{aggregateId:e,sequence:n,activeTab:i,onTabChange:a}),(0,A.jsxs)(`div`,{className:`bottom-grid`,children:[(0,A.jsx)(sa,{}),(0,A.jsx)(da,{})]})]}),(0,A.jsx)(pa,{})]})}var ya=class extends k.Component{state={hasError:!1};static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e,t){console.error(`UI error boundary caught:`,e,t)}render(){return this.state.hasError?(0,A.jsx)(`div`,{className:`app`,children:(0,A.jsx)(`main`,{className:`app-main`,children:(0,A.jsxs)(`div`,{className:`card`,children:[(0,A.jsx)(`div`,{className:`card-title`,children:`Something went wrong`}),(0,A.jsx)(`p`,{style:{color:`var(--text-muted)`,fontSize:13},children:`The UI hit an unexpected error. Try refreshing the page. If the problem persists, check the browser console and server logs.`})]})})}):this.props.children}},ba=new Ye({defaultOptions:{queries:{staleTime:3e4,retry:2}}});ci.createRoot(document.getElementById(`root`)).render((0,A.jsx)(k.StrictMode,{children:(0,A.jsx)(et,{client:ba,children:(0,A.jsx)(ta,{children:(0,A.jsx)(ya,{children:(0,A.jsx)(va,{})})})})})); \ No newline at end of file diff --git a/eventlens-api/src/main/resources/web/assets/index-Cw0Fu2da.css b/eventlens-api/src/main/resources/web/assets/index-Cw0Fu2da.css new file mode 100644 index 0000000..67a6046 --- /dev/null +++ b/eventlens-api/src/main/resources/web/assets/index-Cw0Fu2da.css @@ -0,0 +1 @@ +:root{--bg-base:#050508;--bg-surface:#0a0c14;--bg-raised:#0f1220;--bg-elevated:#161b2e;--bg-panel:linear-gradient(145deg, #0c0f1a 0%, #0a0d18 50%, #080b14 100%);--border:#1a2040;--border-muted:#121830;--border-glow:#00f0ff26;--text-primary:#e8eef8;--text-secondary:#94a3c0;--text-muted:#5a6a8a;--neon-cyan:#00f0ff;--neon-cyan-dim:#00f0ff14;--neon-cyan-mid:#00f0ff40;--neon-magenta:#ff00e5;--neon-magenta-dim:#ff00e514;--neon-green:#0f8;--neon-green-dim:#00ff881a;--neon-amber:#fa0;--neon-amber-dim:#ffaa001a;--neon-red:#f35;--neon-red-dim:#ff33551a;--neon-purple:#a855f7;--accent-blue:#4f9cf9;--accent-blue-dim:#4f9cf926;--accent-green:var(--neon-green);--accent-red:var(--neon-red);--accent-yellow:var(--neon-amber);--accent-purple:var(--neon-purple);--font-sans:"Inter", system-ui, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", monospace;--font-display:"Orbitron", var(--font-sans);--radius:6px;--radius-lg:10px;--shadow:0 4px 24px #0009;--shadow-neon:0 0 20px #00f0ff14, 0 0 60px #00f0ff08;--transition:.2s ease;--bottom-panel-scroll-height:280px}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{font-size:15px}body{font-family:var(--font-sans);background:var(--bg-base);color:var(--text-primary);-webkit-font-smoothing:antialiased;min-height:100vh;line-height:1.65}body:after{content:"";pointer-events:none;z-index:9999;background:repeating-linear-gradient(0deg,#0000,#0000 2px,#00f0ff04 2px 4px);position:fixed;inset:0}.app{flex-direction:column;min-height:100vh;display:flex}.app-header{--header-pad-x:24px;--header-pad-right:48px;--header-control-h:34px;padding:0 var(--header-pad-right) 0 var(--header-pad-x);border-bottom:1px solid var(--border);z-index:100;background:linear-gradient(#0d1020 0%,#080b14 100%);grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;column-gap:clamp(12px,2vw,20px);height:64px;display:grid;position:sticky;top:0;box-shadow:0 2px 20px #00000080,inset 0 -1px #00f0ff0f}.brand{justify-self:start;align-items:center;gap:12px;min-width:0;display:flex}.brand-logo{justify-content:center;align-items:center;width:36px;height:36px;display:flex;position:relative}.brand-logo svg{width:36px;height:36px;filter:drop-shadow(0 0 6px var(--neon-cyan)) drop-shadow(0 0 12px #00f0ff4d)}.brand-name{font-family:var(--font-display);letter-spacing:1.5px;color:var(--text-primary);text-transform:uppercase;font-size:15px;font-weight:700}.brand-sub{color:var(--text-muted);letter-spacing:.5px;font-size:10px}.header-title{font-family:var(--font-display);letter-spacing:3px;text-transform:uppercase;background:linear-gradient(135deg, var(--neon-cyan), #4facfe, var(--neon-magenta));-webkit-text-fill-color:transparent;filter:drop-shadow(0 0 8px #00f0ff66);text-align:center;-webkit-background-clip:text;background-clip:text;flex-shrink:0;margin:0;font-size:20px;font-weight:800;line-height:1}.header-center{flex-flow:row;justify-content:center;justify-self:center;align-items:center;gap:clamp(10px,1.5vw,16px);min-width:0;display:flex}.header-demo-pill{box-sizing:border-box;height:var(--header-control-h);background:var(--neon-amber-dim);color:var(--neon-amber);font-family:var(--font-mono);white-space:nowrap;border:1px solid #ffaa0059;border-radius:999px;flex-shrink:0;justify-content:center;align-items:center;padding:0 12px;font-size:11px;line-height:1;display:inline-flex}.header-actions{flex-direction:row;justify-content:flex-end;justify-self:end;align-items:center;gap:clamp(10px,1.2vw,16px);min-width:0;display:flex}.header-actions .conn-stats,.header-actions .header-status{flex-shrink:0}.workspace-datasource{flex-direction:column;gap:6px;min-width:0;margin:0;display:flex}.workspace-datasource-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;font-size:10px;line-height:1}.workspace-datasource-select{box-sizing:border-box;appearance:none;width:100%;height:34px;color:var(--text-primary);font-family:var(--font-mono);cursor:pointer;transition:border-color var(--transition), box-shadow var(--transition);background-color:#0c1020f2;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3c0' d='M3 4.5 6 8l3-3.5'/%3E%3C/svg%3E");background-position:right 10px center;background-repeat:no-repeat;border:1px solid #ffffff24;border-radius:8px;padding:0 32px 0 12px;font-size:12px;line-height:1;box-shadow:inset 0 1px #00f0ff0f}.workspace-datasource-select:hover{border-color:#00f0ff40}.workspace-datasource-select:focus{border-color:var(--neon-cyan-mid);outline:none;box-shadow:0 0 0 1px #00f0ff33}.header-status{font-size:12px;font-family:var(--font-sans);letter-spacing:.3px;align-items:center;gap:8px;display:flex}.header-status .status-text{color:var(--neon-green);text-shadow:0 0 8px #00ff8880}.header-status .status-text.offline{color:var(--neon-red);text-shadow:0 0 8px #ff335580}.dot{border-radius:50%;width:8px;height:8px}.dot-green{background:var(--neon-green);box-shadow:0 0 6px var(--neon-green), 0 0 12px #0f86;animation:2s infinite pulse-neon}.dot-red{background:var(--neon-red);box-shadow:0 0 6px var(--neon-red)}.dot-yellow{background:var(--neon-amber);box-shadow:0 0 6px var(--neon-amber);animation:1.5s infinite pulse-neon}@keyframes pulse-neon{0%,to{opacity:1}50%{opacity:.4}}.app-main{flex-direction:column;flex:1;gap:16px;width:100%;max-width:1440px;margin:0 auto;padding:20px;display:flex}.card{background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow), var(--shadow-neon);padding:20px;position:relative;overflow:hidden}.card.card--dropdown-host{z-index:30;overflow:visible}.card:before{content:"";background:linear-gradient(90deg, transparent, var(--neon-cyan-mid), transparent);height:1px;position:absolute;top:0;left:0;right:0}.card:after{content:"";background:linear-gradient(90deg,#0000,#ff00e51a,#0000);height:1px;position:absolute;bottom:0;left:0;right:0}.card-title{font-family:var(--font-sans);color:var(--neon-cyan);letter-spacing:.2px;text-shadow:0 0 6px #00f0ff33;align-items:center;gap:8px;margin-bottom:16px;font-size:13px;font-weight:600;display:flex}.control-ribbon{padding:14px 18px}.control-ribbon-top{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:14px;display:flex}.control-ribbon-title{margin-bottom:4px}.control-ribbon-subtitle{color:var(--text-muted);font-size:12px}.control-ribbon-nav{align-items:center;gap:12px;font-size:13px;display:inline-flex}.control-ribbon-nav a{text-decoration:none}.control-ribbon-nav a[aria-current=page]{text-underline-offset:2px;text-decoration:underline}.control-panel{gap:12px;padding:16px 18px;display:grid}.control-panel-grid{grid-template-columns:minmax(220px,360px) minmax(0,1fr);align-items:end;gap:12px;display:grid}.control-field,.control-field--search{min-width:0}.control-field-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:11px;display:block}.control-select{width:100%;color:var(--text-primary);font-family:var(--font-mono);transition:border-color var(--transition), box-shadow var(--transition);background:#0d1123eb;border:1px solid #ffffff24;border-radius:10px;outline:none;padding:10px 12px;font-size:12px}.control-select:focus{border-color:var(--neon-cyan-mid);box-shadow:0 0 14px #00f0ff26}.datasource-pills{flex-wrap:wrap;gap:8px;display:flex}.datasource-pill{border:1px solid var(--border);font-size:11px;font-family:var(--font-mono);border-radius:999px;padding:4px 8px}.selection-summary{color:var(--text-muted);font-size:12px;font-family:var(--font-mono);margin-top:2px}.selection-clear-btn{color:var(--text-muted);cursor:pointer;font-family:var(--font-mono);background:0 0;border:none;margin-left:12px}.selection-clear-btn:hover{color:var(--neon-cyan)}.workspace-dock{z-index:110;border:1px solid var(--border-muted);pointer-events:auto;border-right:none;border-radius:10px 0 0 10px;flex-direction:row;align-items:center;width:auto;height:auto;max-height:min(72vh,100vh - 112px);transition:box-shadow .2s;display:flex;position:fixed;inset:50% 0 auto auto;overflow:hidden;transform:translateY(-50%);box-shadow:-6px 4px 22px #0000006b}.workspace-dock--open{box-shadow:-8px 6px 28px #0000007a,0 0 0 1px #00f0ff14}.workspace-dock-handle{border:none;border-left:1px solid var(--border-muted);width:36px;height:36px;color:var(--neon-cyan);font-family:var(--font-mono);cursor:pointer;transition:background var(--transition), color var(--transition);background:linear-gradient(#0f1324 0%,#0a0e18 100%);flex-direction:row;flex-shrink:0;justify-content:center;align-items:center;padding:0;display:flex}.workspace-dock-handle:hover{color:var(--text-primary);background:linear-gradient(#141a30 0%,#0d1220 100%)}.workspace-dock-handle:focus-visible{outline:2px solid var(--neon-cyan);outline-offset:-2px}.workspace-dock-chevron{font-size:15px;font-weight:700;line-height:1}.workspace-dock-panel{border-left:1px solid var(--border);background:linear-gradient(145deg,#0c101c 0%,#080c14 100%);flex-direction:column;flex:0 auto;gap:8px;width:min(252px,100vw - 48px);min-width:0;max-height:min(72vh,100vh - 112px);padding:10px 12px 12px 14px;display:flex;overflow:hidden auto}.workspace-dock-panel[hidden]{display:none!important}.workspace-dock-title{font-family:var(--font-sans);color:var(--neon-cyan);letter-spacing:.2px;font-size:12px;font-weight:600}.workspace-dock-scrim{z-index:109;cursor:pointer;background:#03050c73;border:none;margin:0;padding:0;position:fixed;inset:64px 0 36px}.workspace-sidebar-kpis{border:1px solid var(--border-muted);border-radius:var(--radius);background:#080b148c;gap:6px;padding:8px 10px;display:grid}.workspace-kpi-row{color:var(--text-muted);font-size:11px;font-family:var(--font-mono);justify-content:space-between;gap:8px;display:flex}.workspace-kpi-row strong{color:var(--text-primary)}.workspace-sidebar-links{color:var(--text-secondary);gap:6px;font-size:12px;display:grid}.workspace-content{gap:16px;display:grid}.search-panel{width:100%;min-width:0}.selection-clear-btn:focus-visible,.control-ribbon-nav a:focus-visible,.control-select:focus-visible,.workspace-datasource-select:focus-visible{outline:2px solid var(--neon-cyan);outline-offset:2px}.plugin-dashboard{gap:14px;display:grid}.plugin-cards-grid{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;display:grid}.plugin-cards-grid--dense{grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}.plugin-card{border:1px solid var(--border-muted);border-radius:var(--radius-lg);background:linear-gradient(140deg,#101527f2,#0a0e1cf2);padding:12px 14px;box-shadow:inset 0 1px #ffffff05}.plugin-card--interactive{transition:border-color var(--transition), box-shadow var(--transition), transform var(--transition)}.plugin-card--interactive:hover{border-color:var(--neon-cyan-mid);transform:translateY(-1px);box-shadow:0 0 16px #00f0ff1a}.plugin-card--interactive:focus-within{border-color:var(--neon-cyan-mid);box-shadow:0 0 0 2px #00f0ff26}.plugin-card-head{justify-content:space-between;align-items:center;gap:10px;display:flex}.plugin-pill{border:1px solid var(--border);font-size:11px;font-family:var(--font-mono);border-radius:999px;padding:2px 8px}.plugin-card-meta{color:var(--text-muted);font-size:12px;font-family:var(--font-mono);overflow-wrap:anywhere;margin-top:8px}.plugin-card-detail{color:var(--text-secondary);margin-top:8px;font-size:12px;line-height:1.55}.search-wrapper{position:relative}.search-input{background:var(--bg-raised);border:1px solid var(--border);border-radius:var(--radius);width:100%;color:var(--text-primary);font-family:var(--font-mono);transition:border-color var(--transition), box-shadow var(--transition);outline:none;padding:14px 16px 14px 44px;font-size:13px}.search-input:focus{border-color:var(--neon-cyan);box-shadow:0 0 14px #00f0ff2e}.search-input::placeholder{color:var(--text-muted)}.search-icon{color:var(--neon-cyan);pointer-events:none;filter:drop-shadow(0 0 4px #00f0ff80);font-size:16px;position:absolute;top:50%;left:14px;transform:translateY(-50%)}.search-results{background:var(--bg-raised);border:1px solid var(--neon-cyan-mid);border-radius:var(--radius);z-index:500;max-height:min(55vh,420px);position:absolute;top:calc(100% + 6px);left:0;right:0;overflow:hidden auto;box-shadow:0 12px 40px #000000a6,0 0 24px #00f0ff14}.search-result-item{cursor:pointer;transition:background var(--transition);font-family:var(--font-mono);color:var(--text-primary);text-align:left;background:0 0;border:none;align-items:center;gap:10px;width:100%;padding:10px 16px;font-size:13px;display:flex}.search-result-item:hover{background:var(--bg-elevated);box-shadow:inset 3px 0 0 var(--neon-cyan)}.search-result-item+.search-result-item{border-top:1px solid var(--border-muted)}.search-result-chevron{color:var(--text-muted);flex-shrink:0;padding-right:4px}.search-result-body{flex-wrap:wrap;flex:1;align-items:baseline;gap:0 6px;min-width:0;display:flex}.search-result-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;flex-shrink:0;font-size:10px}.search-result-colon{color:var(--text-muted);flex-shrink:0;margin-right:2px}.search-result-value{overflow-wrap:anywhere;word-break:break-word;flex:1;min-width:0}.conn-stats{font-family:var(--font-mono);color:var(--text-secondary);align-items:center;gap:16px;font-size:11px;display:flex}.conn-stat{flex-direction:column;align-items:flex-end;gap:1px;display:flex}.conn-stat-label{font-family:var(--font-sans);letter-spacing:.3px;color:var(--text-muted);font-size:9px}.conn-stat-value{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff66;font-size:12px}.conn-stat-value.green{color:var(--neon-green);text-shadow:0 0 6px #0f86}.conn-stat-value.amber{color:var(--neon-amber);text-shadow:0 0 6px #fa06}.conn-stat--metric .conn-stat-value,.conn-stat-value--uptime{text-align:right;font-variant-numeric:tabular-nums;min-width:6.5ch;display:inline-block}.mini-wave{align-items:flex-end;gap:1px;height:24px;display:flex}.mini-wave-bar{background:var(--neon-cyan);border-radius:1px;width:3px;animation:1.2s ease-in-out infinite wave-pulse;box-shadow:0 0 4px #00f0ff4d}@keyframes wave-pulse{0%,to{transform:scaleY(.3)}50%{transform:scaleY(1)}}.timeline-count-pill{color:var(--accent-blue);font-family:var(--font-mono);background:var(--accent-blue-dim);border:1px solid #4f9cf940;border-radius:999px;padding:2px 10px;font-size:11px}.timeline-hint{color:var(--text-muted);max-width:52rem;margin:-8px 0 12px;font-size:12px;line-height:1.5}.timeline-rail{border:1px solid var(--border-muted);border-radius:var(--radius-lg);background:var(--bg-base);position:relative;box-shadow:inset 0 1px #00f0ff0a}.timeline-stepper{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base);padding:12px 14px 14px;overflow:auto hidden;-webkit-mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000);mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}.timeline-stepper-track{flex-wrap:nowrap;align-items:center;gap:4px;width:max-content;min-height:88px;display:flex}.timeline-step-arrow{color:var(--text-muted);opacity:.7;flex-shrink:0;align-self:center;padding:0 2px;font-size:11px}.timeline-step-arrow-compact{padding:0 1px;font-size:9px}.timeline-step{border-radius:var(--radius);border:1px solid var(--border-muted);background:var(--bg-raised);cursor:pointer;text-align:left;min-width:118px;max-width:200px;font-family:var(--font-sans);transition:border-color var(--transition), box-shadow var(--transition), transform var(--transition);flex-direction:column;flex-shrink:0;align-items:flex-start;gap:4px;padding:10px 14px;display:flex}.timeline-step-compact{gap:2px;min-width:96px;max-width:140px;padding:8px 10px}.timeline-step-compact .timeline-step-badge{font-size:8px}.timeline-step-compact .timeline-step-type{-webkit-line-clamp:1;font-size:10px}.timeline-step:hover{border-color:var(--neon-cyan-mid);transform:translateY(-1px)}.timeline-step.active{box-shadow:0 0 0 2px var(--neon-cyan-dim), 0 0 16px #00f0ff1f;border-color:#00f0ff8c}.timeline-step-badge{font-family:var(--font-sans);letter-spacing:.3px;color:var(--neon-cyan);font-size:10px;font-weight:600}.timeline-step-seq{color:var(--text-muted);font-size:10px;font-family:var(--font-mono)}.timeline-step-type{color:var(--text-secondary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;line-height:1.4;display:-webkit-box;overflow:hidden}.timeline-step-created{border-left:3px solid var(--neon-green)}.timeline-step-deleted{border-left:3px solid var(--neon-red)}.timeline-step-completed{border-left:3px solid var(--neon-green)}.timeline-step-failed{border-left:3px solid var(--neon-red)}.timeline-step-transfer{border-left:3px solid var(--neon-amber)}.timeline-step-item{border-left:3px solid var(--neon-purple)}.timeline-step-progress{border-left:3px solid #38bdf8}.timeline-step-default{border-left:3px solid var(--neon-cyan)}.timeline-group-chip{border-radius:var(--radius);border:1px solid var(--border-muted);background:linear-gradient(145deg, var(--bg-elevated) 0%, var(--bg-raised) 100%);cursor:pointer;text-align:left;min-width:140px;max-width:240px;font-family:var(--font-sans);transition:border-color var(--transition), box-shadow var(--transition), transform var(--transition);flex-direction:column;flex-shrink:0;align-items:flex-start;gap:4px;padding:10px 14px;display:flex}.timeline-group-chip:hover{border-color:var(--neon-cyan-mid);transform:translateY(-1px)}.timeline-group-chip.active{box-shadow:0 0 0 2px var(--neon-cyan-dim), 0 0 16px #00f0ff1f;border-color:#00f0ff8c}.timeline-group-chip.expanded{border-color:#00f0ff59}.timeline-group-chip-top{justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.timeline-group-count{font-family:var(--font-display);letter-spacing:.5px;color:var(--neon-cyan);font-size:16px;font-weight:800;line-height:1}.timeline-group-chevron{color:var(--text-muted);font-size:10px}.timeline-group-chip .timeline-group-type{color:var(--text-primary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}.timeline-group-range{color:var(--text-muted);font-size:10px;font-family:var(--font-mono);line-height:1.4}.timeline-expanded-deck{border-top:1px solid var(--border-muted);background:#00000040;padding:10px 14px 14px}.timeline-expanded-head{flex-wrap:wrap;align-items:center;gap:10px 16px;margin-bottom:10px;display:flex}.timeline-expanded-title{font-family:var(--font-sans);letter-spacing:.3px;color:var(--neon-cyan);font-size:12px;font-weight:700}.timeline-expanded-meta{font-family:var(--font-mono);color:var(--text-muted);flex:1;font-size:11px}.timeline-expanded-close{border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-raised);color:var(--text-secondary);font-family:var(--font-mono);cursor:pointer;transition:border-color var(--transition), color var(--transition);margin-left:auto;padding:4px 12px;font-size:11px}.timeline-expanded-close:hover{border-color:var(--neon-cyan-mid);color:var(--neon-cyan)}.timeline-expanded-strip{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base);flex-wrap:nowrap;align-items:center;gap:4px;width:100%;padding-bottom:6px;display:flex;overflow-x:auto}.timeline-slider{width:100%;accent-color:var(--neon-cyan);cursor:pointer;margin-top:4px}.timeline-info{color:var(--text-muted);font-size:12px;font-family:var(--font-mono);grid-template-columns:minmax(0,1fr) minmax(0,2.2fr) minmax(0,1fr);align-items:start;gap:12px;margin-top:12px;display:grid}.timeline-info-edge{color:var(--text-muted);font-size:11px}.timeline-info-center{text-align:center;color:var(--text-secondary);line-height:1.55}.timeline-info-center strong{color:var(--neon-cyan);font-weight:600}.timeline-info-muted{color:var(--text-muted);font-weight:400}.timeline-info-type{color:var(--text-primary);font-size:11px}.state-grid{grid-template-columns:1fr 1fr;gap:16px;display:grid}.state-panel h4{font-family:var(--font-display);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px;font-size:10px;font-weight:600}.state-panel-before h4{color:var(--text-muted)}.state-panel-after h4{color:var(--neon-green);text-shadow:0 0 6px #00ff884d}.json-block{background:var(--bg-base);border:1px solid var(--border-muted);border-radius:var(--radius);font-family:var(--font-mono);max-height:260px;color:var(--text-secondary);white-space:pre;padding:12px;font-size:12px;line-height:1.7;overflow:auto}.json-tree-root{background:var(--bg-base);border:1px solid var(--border-muted);border-radius:var(--radius);max-height:320px;font-family:var(--font-mono);color:var(--text-secondary);padding:10px 12px;font-size:13px;line-height:1.75;overflow:auto}.json-tree-line{flex-wrap:wrap;align-items:baseline;gap:2px 0;min-height:1.5em;display:flex}.json-tree-toggle{width:22px;height:22px;color:var(--text-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;flex-shrink:0;margin-right:4px;padding:0;font-size:10px;line-height:1}.json-tree-toggle:hover{color:var(--neon-cyan);background:var(--neon-cyan-dim)}.json-key{color:#7dd3fc}.json-string{color:#86efac}.json-number{color:#fcd34d}.json-boolean{color:#c4b5fd}.json-null{color:var(--text-muted);font-style:italic}.json-punct{color:var(--text-muted)}.json-ellipsis{color:var(--text-muted);font-size:11px;font-style:italic}.json-unknown{color:var(--neon-amber)}.diff-panel{border-top:1px solid var(--border-muted);margin-top:20px;padding-top:16px}.diff-toolbar{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;display:flex}.diff-toolbar-title{font-family:var(--font-sans);color:var(--neon-cyan);letter-spacing:.2px;font-size:13px;font-weight:600}.diff-view-toggle{border-radius:var(--radius);border:1px solid var(--border);font-family:var(--font-mono);font-size:11px;display:flex;overflow:hidden}.diff-view-toggle button{background:var(--bg-raised);color:var(--text-muted);cursor:pointer;transition:background var(--transition), color var(--transition);border:none;padding:6px 14px}.diff-view-toggle button+button{border-left:1px solid var(--border)}.diff-view-toggle button:hover{color:var(--text-secondary)}.diff-view-toggle button.active{background:var(--neon-cyan-dim);color:var(--neon-cyan)}.diff-body{align-items:stretch;gap:10px;display:flex}.diff-minimap{border:1px solid var(--border-muted);background:var(--bg-base);border-radius:4px;flex-direction:column;flex-shrink:0;width:10px;max-height:280px;display:flex;overflow:hidden}.diff-minimap-chunk{background:var(--border);cursor:pointer;min-height:8px;transition:background var(--transition);border:none;flex:1;margin:0;padding:0}.diff-minimap-chunk:hover,.diff-minimap-chunk:focus-visible{background:var(--neon-cyan-mid);outline:none}.diff-scroll{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base);flex:1;min-width:0;max-height:280px;overflow:auto}.diff-list{flex-direction:column;gap:0;display:flex}.diff-row{font-family:var(--font-mono);background:var(--bg-raised);border-left:3px solid var(--neon-cyan);border-bottom:1px solid var(--border-muted);border-radius:0;align-items:stretch;gap:0;padding:0;font-size:12px;line-height:1.65;display:flex}.diff-row:last-child{border-radius:0 0 var(--radius) var(--radius);border-bottom:none}.diff-row:first-child{border-radius:var(--radius) var(--radius) 0 0}.diff-line-no{text-align:right;width:36px;color:var(--text-muted);background:var(--bg-base);border-right:1px solid var(--border-muted);-webkit-user-select:none;user-select:none;flex-shrink:0;padding:10px 6px;font-size:10px}.diff-row-body{flex-wrap:wrap;flex:1;align-items:baseline;gap:8px 12px;padding:10px 12px;display:flex}.diff-field{color:var(--text-primary);min-width:6rem;font-weight:700}.diff-values-inline{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.diff-old{color:var(--neon-red);font-weight:400;text-decoration:line-through}.diff-arrow{color:var(--text-muted);flex-shrink:0}.diff-new{color:var(--neon-green);text-shadow:0 0 4px #00ff8840;font-weight:400}.diff-split-head{font-family:var(--font-sans);letter-spacing:.3px;color:var(--text-muted);border-bottom:1px solid var(--border);background:var(--bg-raised);z-index:1;grid-template-columns:1fr 1fr;gap:8px;padding:8px 12px 8px 48px;font-size:11px;display:grid;position:sticky;top:0}.diff-split-old-label{color:var(--neon-red)}.diff-split-new-label{color:var(--neon-green)}.diff-split-row{border-bottom:1px solid var(--border-muted);align-items:stretch;gap:0;display:flex}.diff-split-row:last-child{border-bottom:none}.diff-split-cells{flex:1;grid-template-columns:1fr 1fr;gap:8px;min-width:0;padding:8px 12px 8px 0;display:grid}.diff-split-cell{border-radius:var(--radius);background:var(--bg-base);border:1px solid var(--border-muted);min-width:0;padding:8px 10px}.diff-split-cell .diff-field{margin-bottom:6px;font-size:11px;display:block}.diff-cell-value{overflow-wrap:anywhere;word-break:break-word;font-size:12px;font-weight:400;line-height:1.6}.diff-split-old .diff-cell-value{color:var(--neon-red);opacity:.9}.diff-split-new .diff-cell-value{color:var(--neon-green)}.event-meta{color:var(--text-muted);grid-template-columns:repeat(2,1fr);gap:10px 16px;margin-top:20px;font-size:12px;display:grid}.event-meta-bar{background:var(--bg-raised);border:1px solid var(--border-muted);border-radius:var(--radius);padding:14px 16px;box-shadow:inset 0 1px #00f0ff0a}.event-meta-time{font-family:var(--font-mono);color:var(--text-secondary);font-size:12px;font-weight:500}.event-meta-id{font-family:var(--font-mono);color:var(--text-primary);overflow-wrap:anywhere;word-break:break-word;grid-column:1/-1;font-size:11px;line-height:1.5}.event-meta-extra{font-family:var(--font-mono);color:var(--text-secondary);font-size:11px}@media (width<=900px){.diff-split-cells{grid-template-columns:1fr}.diff-split-head{display:none}}.copy-btn{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-secondary);cursor:pointer;transition:all var(--transition);font-size:12px;font-family:var(--font-mono);margin-top:12px;padding:6px 14px}.copy-btn:hover{background:var(--bg-raised);color:var(--neon-cyan);border-color:var(--neon-cyan);box-shadow:0 0 10px #00f0ff1a}.live-header{justify-content:space-between;align-items:center;margin-bottom:12px;display:flex}.live-indicator{font-family:var(--font-sans);letter-spacing:.2px;align-items:center;gap:6px;font-size:11px;display:flex}.pause-btn{background:var(--bg-elevated);border:1px solid var(--border);color:var(--text-secondary);font-family:var(--font-sans);cursor:pointer;transition:all var(--transition);border-radius:4px;padding:5px 12px;font-size:11px}.pause-btn:hover{background:var(--bg-raised);border-color:var(--neon-cyan);color:var(--neon-cyan);box-shadow:0 0 8px #00f0ff1a}.event-stream{height:var(--bottom-panel-scroll-height);min-height:0;font-family:var(--font-mono);flex-direction:column;gap:3px;padding-right:4px;font-size:12px;display:flex;overflow-y:auto}.event-stream,.anomaly-scroll-region,.search-results{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base)}.event-stream::-webkit-scrollbar{width:6px}.anomaly-scroll-region::-webkit-scrollbar{width:6px}.search-results::-webkit-scrollbar{width:6px}.event-stream::-webkit-scrollbar-track{background:var(--bg-base)}.anomaly-scroll-region::-webkit-scrollbar-track{background:var(--bg-base)}.search-results::-webkit-scrollbar-track{background:var(--bg-base)}.event-stream::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.anomaly-scroll-region::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.search-results::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.event-stream::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.anomaly-scroll-region::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.search-results::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.event-row{transition:all var(--transition);cursor:default;background:#ffffff03;border-left:3px solid #0000;border-radius:4px;align-items:center;gap:12px;padding:6px 10px;display:flex}.event-row:hover{background:var(--bg-elevated)}.event-row.type-withdrawn{border-left-color:var(--neon-magenta)}.event-row.type-deposited{border-left-color:var(--neon-cyan)}.event-row.type-created{border-left-color:var(--neon-green)}.event-row.type-transfer{border-left-color:var(--neon-amber)}.event-row.type-deleted{border-left-color:var(--neon-red)}.event-row.type-completed{border-left-color:var(--neon-green)}.event-row.type-failed{border-left-color:var(--neon-red)}.event-row.type-default{border-left-color:var(--neon-purple)}.event-time{color:var(--text-muted);flex-shrink:0;width:75px;font-size:11px}.event-type{flex-shrink:0;width:180px;font-weight:500}.event-agg{color:var(--text-secondary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.event-icon{text-align:center;flex-shrink:0;width:20px;font-size:14px}.type-created{color:var(--neon-green);text-shadow:0 0 6px #00ff884d}.type-deleted{color:var(--neon-red);text-shadow:0 0 6px #ff33554d}.type-completed{color:var(--neon-green);text-shadow:0 0 6px #00ff884d}.type-failed{color:var(--neon-red);text-shadow:0 0 6px #ff33554d}.type-transfer{color:var(--neon-amber);text-shadow:0 0 6px #ffaa004d}.type-default{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff4d}.type-withdrawn{color:var(--neon-magenta);text-shadow:0 0 6px #ff00e54d}.type-deposited{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff4d}.anomaly-panel-inner{position:relative}.anomaly-shield{background:radial-gradient(#00ff880f 0%,#0000 70%);border:1px solid #00ff8826;border-radius:12px;flex-direction:column;justify-content:center;align-items:center;margin-bottom:20px;padding:30px 20px;display:flex}.shield-icon{justify-content:center;align-items:center;width:64px;height:64px;margin-bottom:12px;display:flex;position:relative}.shield-icon svg{filter:drop-shadow(0 0 12px #00ff8880)drop-shadow(0 0 24px #0f83);width:64px;height:64px}.shield-icon:after{content:"";border:1px solid #00ff8826;border-radius:50%;animation:3s ease-in-out infinite shield-pulse;position:absolute;inset:-8px}@keyframes shield-pulse{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.8;transform:scale(1.15)}}.shield-text{font-family:var(--font-display);letter-spacing:2px;text-transform:uppercase;color:var(--neon-green);text-shadow:0 0 8px #0f86;font-size:13px;font-weight:600}.gauge-row{grid-template-columns:1fr 1fr 1fr;gap:12px;display:grid}.gauge-card{background:var(--bg-raised);border:1px solid var(--border);border-radius:var(--radius);text-align:center;padding:12px;position:relative;overflow:hidden}.gauge-card:before{content:"";height:2px;position:absolute;bottom:0;left:0;right:0}.gauge-card.optimal:before{background:var(--neon-green);box-shadow:0 0 8px #0f86}.gauge-card.baseline:before{background:var(--neon-cyan);box-shadow:0 0 8px #00f0ff66}.gauge-card.zero:before{background:var(--neon-green);box-shadow:0 0 8px #0f86}.gauge-label{font-family:var(--font-sans);letter-spacing:.2px;color:var(--text-muted);margin-bottom:6px;font-size:10px;font-weight:500}.gauge-value{font-family:var(--font-display);letter-spacing:1px;font-size:13px;font-weight:700}.gauge-value.optimal{color:var(--neon-green);text-shadow:0 0 6px #0f86}.gauge-value.baseline{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff66}.gauge-value.zero{color:var(--neon-green);text-shadow:0 0 6px #0f86}.gauge-wave{justify-content:center;align-items:flex-end;gap:2px;height:20px;margin-top:6px;display:flex}.gauge-wave-bar{border-radius:1px;width:2px;animation:1.5s ease-in-out infinite gauge-wave-anim}.gauge-wave-bar.green{background:var(--neon-green);box-shadow:0 0 3px #0f86}.gauge-wave-bar.cyan{background:var(--neon-cyan);box-shadow:0 0 3px #00f0ff66}@keyframes gauge-wave-anim{0%,to{height:4px}50%{height:16px}}.anomaly-scroll-region{box-sizing:border-box;height:var(--bottom-panel-scroll-height);max-height:var(--bottom-panel-scroll-height);z-index:1;min-height:0;padding-right:4px;position:relative;overflow:hidden auto}.anomaly-list-inner{flex-direction:column;gap:10px;display:flex}.anomaly-card-title-row{flex-wrap:wrap;width:100%}.anomaly-title-text{flex:1}.anomaly-header-count{min-width:28px;height:28px;font-family:var(--font-display);color:var(--bg-base);background:var(--neon-amber);border-radius:999px;justify-content:center;align-items:center;margin-left:auto;padding:0 10px;font-size:11px;font-weight:700;display:inline-flex;box-shadow:0 0 12px #ffaa0059}.anomaly-card{border:1px solid var(--border-muted);border-radius:var(--radius-lg);background:var(--bg-raised);transition:border-color var(--transition), box-shadow var(--transition);overflow:hidden}.anomaly-card.CRITICAL{border-left:4px solid #f44}.anomaly-card.HIGH{border-left:4px solid var(--neon-red)}.anomaly-card.MEDIUM{border-left:4px solid var(--neon-amber)}.anomaly-card.LOW{border-left:4px solid var(--neon-cyan)}.anomaly-card-summary{cursor:pointer;font-family:var(--font-sans);flex-wrap:wrap;align-items:center;gap:10px 12px;padding:14px 16px;list-style:none;display:flex}.anomaly-card-summary::-webkit-details-marker{display:none}.anomaly-severity-badge{font-family:var(--font-display);letter-spacing:1px;text-transform:uppercase;border-radius:4px;flex-shrink:0;padding:4px 10px;font-size:9px;font-weight:700}.anomaly-severity-badge.sev-critical{color:#f66;background:#f443;border:1px solid #ff444459}.anomaly-severity-badge.sev-high{background:var(--neon-red-dim);color:var(--neon-red);border:1px solid #ff335559}.anomaly-severity-badge.sev-medium{background:var(--neon-amber-dim);color:var(--neon-amber);border:1px solid #ffaa0059}.anomaly-severity-badge.sev-low{background:var(--neon-cyan-dim);color:var(--neon-cyan);border:1px solid #00f0ff40}.anomaly-card-title{min-width:0;color:var(--text-primary);flex:1;font-size:15px;font-weight:600;line-height:1.45}.anomaly-card-chevron{color:var(--text-muted);transition:transform var(--transition);flex-shrink:0;font-size:10px}.anomaly-card[open] .anomaly-card-chevron{transform:rotate(-180deg)}.anomaly-card-body{border-top:1px solid var(--border-muted);background:#0003;padding:0 16px 16px}.anomaly-card-meta{flex-wrap:wrap;align-items:baseline;gap:8px 12px;margin-top:12px;font-size:13px;line-height:1.5;display:flex}.anomaly-card-meta:first-child{margin-top:12px}.anomaly-meta-label{font-family:var(--font-sans);letter-spacing:.2px;color:var(--text-muted);min-width:72px;font-size:10px}.anomaly-meta-value{font-family:var(--font-mono);color:var(--text-secondary);overflow-wrap:anywhere}.no-anomalies{color:var(--neon-green);align-items:center;gap:8px;padding:20px 0;font-size:13px;display:flex}.bottom-grid{grid-template-columns:1fr 1fr;align-items:start;gap:16px;display:grid}.bottom-grid>.card{flex-direction:column;align-items:stretch;min-width:0;min-height:0;display:flex}.bottom-grid>.card>.card-title,.bottom-grid>.card>.live-header,.bottom-grid .anomaly-scroll-region,.bottom-grid .event-stream{flex-shrink:0}.skeleton{background:linear-gradient(90deg, var(--bg-raised) 25%, var(--bg-elevated) 50%, var(--bg-raised) 75%);border-radius:var(--radius);background-size:200% 100%;animation:1.4s infinite shimmer}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.toast-container{z-index:999;flex-direction:column;gap:8px;display:flex;position:fixed;bottom:24px;right:24px}.toast{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-primary);box-shadow:var(--shadow);border-left:3px solid var(--neon-amber);padding:10px 16px;font-size:13px;animation:.2s slideIn}.toast.error{border-left-color:var(--neon-red)}.toast.success{border-left-color:var(--neon-green)}@keyframes slideIn{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.demo-banner{font-size:13px;font-family:var(--font-mono);color:var(--neon-amber);background:var(--neon-amber-dim);border-radius:var(--radius);border:1px solid #ffaa0059;margin:0 0 16px;padding:10px 16px}.demo-banner code{color:var(--neon-cyan)}@media (width<=900px){.state-grid,.bottom-grid{grid-template-columns:1fr}.header-center .header-demo-pill{display:none}.workspace-dock-panel{width:min(240px,88vw)}.gauge-row,.control-panel-grid{grid-template-columns:1fr}.timeline-info{text-align:center;grid-template-columns:1fr}.timeline-info-edge{display:none}}.state-tabs{border-bottom:1px solid var(--border);gap:2px;margin-bottom:16px;padding-bottom:0;display:flex}.state-tab{color:var(--text-muted);font-family:var(--font-sans);cursor:pointer;transition:color var(--transition), border-color var(--transition);border-radius:var(--radius) var(--radius) 0 0;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-1px;padding:8px 16px;font-size:13px;font-weight:500;display:flex}.state-tab:hover{color:var(--text-secondary)}.state-tab.active{color:var(--neon-cyan);border-bottom-color:var(--neon-cyan);background:var(--neon-cyan-dim);text-shadow:0 0 8px #00f0ff33}.state-tab-emoji{font-size:14px}.state-tab-content{min-height:80px}.summary-tab{padding-top:4px}.summary-changes{flex-direction:column;gap:8px;display:flex}.summary-changes-header{color:var(--text-muted);border-bottom:1px solid var(--border-muted);margin-bottom:4px;padding-bottom:6px;font-size:12px;font-weight:600}.summary-change-row{border-radius:var(--radius);background:var(--bg-raised);border:1px solid var(--border-muted);border-left:3px solid var(--neon-amber);font-family:var(--font-mono);transition:background var(--transition);flex-wrap:wrap;align-items:baseline;gap:6px 10px;padding:9px 12px;font-size:13px;display:flex}.summary-change-row:hover{background:var(--bg-elevated)}.summary-change-field{color:var(--text-primary);min-width:5rem;font-weight:700;font-family:var(--font-sans)}.summary-change-old{color:var(--neon-red);opacity:.9;text-decoration:line-through}.summary-change-arrow{color:var(--text-muted);flex-shrink:0}.summary-change-new{color:var(--neon-green);text-shadow:0 0 4px #0f83;font-weight:600}.summary-no-changes{color:var(--neon-green);align-items:center;gap:12px;padding:20px 0;font-size:14px;display:flex}.event-summary-bar{background:linear-gradient(145deg, var(--bg-elevated) 0%, var(--bg-raised) 100%);border:1px solid var(--border);border-left:3px solid var(--neon-cyan);border-radius:var(--radius);justify-content:space-between;align-items:center;gap:12px;margin-bottom:4px;padding:10px 20px;display:flex;box-shadow:0 0 16px #00f0ff0d}.event-summary-left{flex-wrap:wrap;align-items:baseline;gap:6px 12px;display:flex}.event-summary-type{font-family:var(--font-sans);color:var(--text-primary);font-size:14px;font-weight:600}.event-summary-meta{font-family:var(--font-mono);color:var(--text-muted);font-size:12px}.event-summary-changes{color:var(--neon-amber);font-size:12px;font-family:var(--font-sans);background:#ffaa001f;border:1px solid #ffaa004d;border-radius:999px;flex-shrink:0;padding:3px 10px;font-weight:600}.diff-count-badge{color:var(--neon-amber);font-size:11px;font-family:var(--font-sans);background:#ffaa001f;border:1px solid #ffaa004d;border-radius:999px;align-items:center;margin-left:6px;padding:2px 8px;font-weight:500;display:inline-flex}.diff-summary-view{flex-direction:column;gap:6px;display:flex}.diff-summary-row{border-radius:var(--radius);background:var(--bg-raised);border:1px solid var(--border-muted);border-left:3px solid var(--neon-cyan);font-family:var(--font-mono);transition:background var(--transition);flex-wrap:wrap;align-items:baseline;gap:6px 10px;padding:9px 12px;font-size:13px;display:flex}.diff-summary-row:hover{background:var(--bg-elevated)}.diff-summary-field{color:var(--text-primary);min-width:6rem;font-weight:700;font-family:var(--font-sans)}.diff-summary-old{color:var(--neon-red);opacity:.9;text-decoration:line-through}.diff-summary-arrow{color:var(--text-muted);flex-shrink:0}.diff-summary-new{color:var(--neon-green);text-shadow:0 0 4px #0f83;font-weight:600}.diff-jump-next{border-radius:var(--radius);border:1px solid var(--border);color:var(--text-muted);font-family:var(--font-mono);cursor:pointer;transition:color var(--transition), border-color var(--transition);background:0 0;margin-left:auto;padding:2px 8px;font-size:10px}.diff-jump-next:hover{color:var(--neon-cyan);border-color:var(--neon-cyan-mid)}.json-tree-changed{border-radius:3px;background:#ffaa001a!important}.timeline-header-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.timeline-jump-group{border:1px solid var(--border);border-radius:var(--radius);align-items:center;gap:0;display:flex;overflow:hidden}.timeline-jump-input{background:var(--bg-raised);border:none;border-right:1px solid var(--border);width:100px;color:var(--text-primary);font-family:var(--font-mono);outline:none;padding:5px 8px;font-size:12px}.timeline-jump-input:focus{border-right-color:var(--neon-cyan);background:var(--bg-elevated)}.timeline-jump-input::placeholder{color:var(--text-muted)}.timeline-jump-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.timeline-jump-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.timeline-jump-input[type=number]{-moz-appearance:textfield}.timeline-jump-btn{background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;transition:background var(--transition), color var(--transition);border:none;padding:5px 10px;font-size:14px}.timeline-jump-btn:hover{background:var(--neon-cyan-dim);color:var(--neon-cyan)}.timeline-filter-chips{flex-wrap:wrap;gap:6px;margin-bottom:10px;display:flex}.filter-chip{border:1px solid var(--border);background:var(--bg-raised);color:var(--text-muted);font-family:var(--font-sans);cursor:pointer;transition:all var(--transition);white-space:nowrap;text-overflow:ellipsis;border-radius:999px;max-width:160px;padding:3px 10px;font-size:11px;overflow:hidden}.filter-chip:hover{border-color:var(--neon-cyan-mid);color:var(--text-secondary)}.filter-chip.active{background:var(--neon-cyan-dim);color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff33;border-color:#00f0ff66}.timeline-anomaly-marker{color:var(--neon-amber);text-shadow:0 0 4px #fa09;align-self:flex-start;font-size:7px;line-height:1}.keyboard-hints{z-index:200;border-top:1px solid var(--border);-webkit-backdrop-filter:blur(12px);background:#080b14eb;position:fixed;bottom:0;left:0;right:0}.keyboard-hints-bar{font-family:var(--font-mono);color:var(--text-muted);flex-wrap:wrap;justify-content:center;align-items:center;gap:8px;padding:6px 24px;font-size:11px;display:flex}.keyboard-hints-item{align-items:center;gap:5px;display:flex}.keyboard-hints-sep{color:var(--border);font-size:14px;line-height:1}.keyboard-hints-grid{flex-wrap:wrap;justify-content:center;gap:10px 24px;padding:14px 24px;display:flex}.keyboard-hint-row{font-family:var(--font-mono);color:var(--text-secondary);align-items:center;gap:10px;font-size:12px;display:flex}.keyboard-hint-desc{color:var(--text-muted)}.keyboard-key{border:1px solid var(--border);border-bottom:2px solid var(--border);background:var(--bg-elevated);color:var(--neon-cyan);font-family:var(--font-mono);white-space:nowrap;border-radius:4px;align-items:center;padding:2px 8px;font-size:11px;display:inline-flex}.keyboard-key-mini{border:1px solid var(--border);background:var(--bg-elevated);color:var(--neon-cyan);font-family:var(--font-mono);white-space:nowrap;border-radius:3px;align-items:center;padding:1px 5px;font-size:10px;display:inline-flex}.keyboard-hints-close{border-radius:var(--radius);border:1px solid var(--border);color:var(--text-muted);font-family:var(--font-mono);cursor:pointer;transition:color var(--transition), border-color var(--transition);background:0 0;padding:4px 12px;font-size:11px}.keyboard-hints-close:hover{color:var(--neon-cyan);border-color:var(--neon-cyan-mid)}.app-main{padding-bottom:40px} diff --git a/eventlens-api/src/main/resources/web/assets/index-DKlW5VNn.css b/eventlens-api/src/main/resources/web/assets/index-DKlW5VNn.css deleted file mode 100644 index 0fb361a..0000000 --- a/eventlens-api/src/main/resources/web/assets/index-DKlW5VNn.css +++ /dev/null @@ -1 +0,0 @@ -:root{--bg-base:#050508;--bg-surface:#0a0c14;--bg-raised:#0f1220;--bg-elevated:#161b2e;--bg-panel:linear-gradient(145deg, #0c0f1a 0%, #0a0d18 50%, #080b14 100%);--border:#1a2040;--border-muted:#121830;--border-glow:#00f0ff26;--text-primary:#e8eef8;--text-secondary:#94a3c0;--text-muted:#5a6a8a;--neon-cyan:#00f0ff;--neon-cyan-dim:#00f0ff14;--neon-cyan-mid:#00f0ff40;--neon-magenta:#ff00e5;--neon-magenta-dim:#ff00e514;--neon-green:#0f8;--neon-green-dim:#00ff881a;--neon-amber:#fa0;--neon-amber-dim:#ffaa001a;--neon-red:#f35;--neon-red-dim:#ff33551a;--neon-purple:#a855f7;--accent-blue:#4f9cf9;--accent-blue-dim:#4f9cf926;--accent-green:var(--neon-green);--accent-red:var(--neon-red);--accent-yellow:var(--neon-amber);--accent-purple:var(--neon-purple);--font-sans:"Inter", system-ui, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", monospace;--font-display:"Orbitron", var(--font-sans);--radius:6px;--radius-lg:10px;--shadow:0 4px 24px #0009;--shadow-neon:0 0 20px #00f0ff14, 0 0 60px #00f0ff08;--transition:.2s ease;--bottom-panel-scroll-height:280px}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{font-size:15px}body{font-family:var(--font-sans);background:var(--bg-base);color:var(--text-primary);-webkit-font-smoothing:antialiased;min-height:100vh;line-height:1.65}body:after{content:"";pointer-events:none;z-index:9999;background:repeating-linear-gradient(0deg,#0000,#0000 2px,#00f0ff04 2px 4px);position:fixed;inset:0}.app{flex-direction:column;min-height:100vh;display:flex}.app-header{border-bottom:1px solid var(--border);z-index:100;background:linear-gradient(#0d1020 0%,#080b14 100%);justify-content:space-between;align-items:center;height:64px;padding:0 28px;display:flex;position:sticky;top:0;box-shadow:0 2px 20px #00000080,inset 0 -1px #00f0ff0f}.brand{align-items:center;gap:12px;display:flex}.brand-logo{justify-content:center;align-items:center;width:36px;height:36px;display:flex;position:relative}.brand-logo svg{width:36px;height:36px;filter:drop-shadow(0 0 6px var(--neon-cyan)) drop-shadow(0 0 12px #00f0ff4d)}.brand-name{font-family:var(--font-display);letter-spacing:1.5px;color:var(--text-primary);text-transform:uppercase;font-size:15px;font-weight:700}.brand-sub{color:var(--text-muted);letter-spacing:.5px;font-size:10px}.header-title{font-family:var(--font-display);letter-spacing:4px;text-transform:uppercase;background:linear-gradient(135deg, var(--neon-cyan), #4facfe, var(--neon-magenta));-webkit-text-fill-color:transparent;filter:drop-shadow(0 0 8px #00f0ff66);text-align:center;-webkit-background-clip:text;background-clip:text;font-size:22px;font-weight:800}.header-status{font-size:12px;font-family:var(--font-sans);letter-spacing:.3px;align-items:center;gap:8px;display:flex}.header-status .status-text{color:var(--neon-green);text-shadow:0 0 8px #00ff8880}.header-status .status-text.offline{color:var(--neon-red);text-shadow:0 0 8px #ff335580}.dot{border-radius:50%;width:8px;height:8px}.dot-green{background:var(--neon-green);box-shadow:0 0 6px var(--neon-green), 0 0 12px #0f86;animation:2s infinite pulse-neon}.dot-red{background:var(--neon-red);box-shadow:0 0 6px var(--neon-red)}.dot-yellow{background:var(--neon-amber);box-shadow:0 0 6px var(--neon-amber);animation:1.5s infinite pulse-neon}@keyframes pulse-neon{0%,to{opacity:1}50%{opacity:.4}}.app-main{flex-direction:column;flex:1;gap:16px;width:100%;max-width:1440px;margin:0 auto;padding:20px;display:flex}.card{background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow), var(--shadow-neon);padding:20px;position:relative;overflow:hidden}.card.card--dropdown-host{z-index:30;overflow:visible}.card:before{content:"";background:linear-gradient(90deg, transparent, var(--neon-cyan-mid), transparent);height:1px;position:absolute;top:0;left:0;right:0}.card:after{content:"";background:linear-gradient(90deg,#0000,#ff00e51a,#0000);height:1px;position:absolute;bottom:0;left:0;right:0}.card-title{font-family:var(--font-sans);color:var(--neon-cyan);letter-spacing:.2px;text-shadow:0 0 6px #00f0ff33;align-items:center;gap:8px;margin-bottom:16px;font-size:13px;font-weight:600;display:flex}.search-wrapper{position:relative}.search-input{background:var(--bg-raised);border:1px solid var(--border);border-radius:var(--radius);width:100%;color:var(--text-primary);font-family:var(--font-mono);transition:border-color var(--transition), box-shadow var(--transition);outline:none;padding:14px 16px 14px 44px;font-size:13px}.search-input:focus{border-color:var(--neon-cyan);box-shadow:0 0 14px #00f0ff2e}.search-input::placeholder{color:var(--text-muted)}.search-icon{color:var(--neon-cyan);pointer-events:none;filter:drop-shadow(0 0 4px #00f0ff80);font-size:16px;position:absolute;top:50%;left:14px;transform:translateY(-50%)}.search-results{background:var(--bg-raised);border:1px solid var(--neon-cyan-mid);border-radius:var(--radius);z-index:500;max-height:min(55vh,420px);position:absolute;top:calc(100% + 6px);left:0;right:0;overflow:hidden auto;box-shadow:0 12px 40px #000000a6,0 0 24px #00f0ff14}.search-result-item{cursor:pointer;transition:background var(--transition);font-family:var(--font-mono);color:var(--text-primary);text-align:left;background:0 0;border:none;align-items:center;gap:10px;width:100%;padding:10px 16px;font-size:13px;display:flex}.search-result-item:hover{background:var(--bg-elevated);box-shadow:inset 3px 0 0 var(--neon-cyan)}.search-result-item+.search-result-item{border-top:1px solid var(--border-muted)}.search-result-chevron{color:var(--text-muted);flex-shrink:0;padding-right:4px}.search-result-body{flex-wrap:wrap;flex:1;align-items:baseline;gap:0 6px;min-width:0;display:flex}.search-result-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;flex-shrink:0;font-size:10px}.search-result-colon{color:var(--text-muted);flex-shrink:0;margin-right:2px}.search-result-value{overflow-wrap:anywhere;word-break:break-word;flex:1;min-width:0}.conn-stats{font-family:var(--font-mono);color:var(--text-secondary);align-items:center;gap:16px;font-size:11px;display:flex}.conn-stat{flex-direction:column;align-items:flex-end;gap:1px;display:flex}.conn-stat-label{font-family:var(--font-sans);letter-spacing:.3px;color:var(--text-muted);font-size:9px}.conn-stat-value{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff66;font-size:12px}.conn-stat-value.green{color:var(--neon-green);text-shadow:0 0 6px #0f86}.conn-stat-value.amber{color:var(--neon-amber);text-shadow:0 0 6px #fa06}.mini-wave{align-items:flex-end;gap:1px;height:24px;display:flex}.mini-wave-bar{background:var(--neon-cyan);border-radius:1px;width:3px;animation:1.2s ease-in-out infinite wave-pulse;box-shadow:0 0 4px #00f0ff4d}@keyframes wave-pulse{0%,to{transform:scaleY(.3)}50%{transform:scaleY(1)}}.timeline-count-pill{color:var(--accent-blue);font-family:var(--font-mono);background:var(--accent-blue-dim);border:1px solid #4f9cf940;border-radius:999px;padding:2px 10px;font-size:11px}.timeline-hint{color:var(--text-muted);max-width:52rem;margin:-8px 0 12px;font-size:12px;line-height:1.5}.timeline-rail{border:1px solid var(--border-muted);border-radius:var(--radius-lg);background:var(--bg-base);position:relative;box-shadow:inset 0 1px #00f0ff0a}.timeline-stepper{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base);padding:12px 14px 14px;overflow:auto hidden;-webkit-mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000);mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}.timeline-stepper-track{flex-wrap:nowrap;align-items:center;gap:4px;width:max-content;min-height:88px;display:flex}.timeline-step-arrow{color:var(--text-muted);opacity:.7;flex-shrink:0;align-self:center;padding:0 2px;font-size:11px}.timeline-step-arrow-compact{padding:0 1px;font-size:9px}.timeline-step{border-radius:var(--radius);border:1px solid var(--border-muted);background:var(--bg-raised);cursor:pointer;text-align:left;min-width:118px;max-width:200px;font-family:var(--font-sans);transition:border-color var(--transition), box-shadow var(--transition), transform var(--transition);flex-direction:column;flex-shrink:0;align-items:flex-start;gap:4px;padding:10px 14px;display:flex}.timeline-step-compact{gap:2px;min-width:96px;max-width:140px;padding:8px 10px}.timeline-step-compact .timeline-step-badge{font-size:8px}.timeline-step-compact .timeline-step-type{-webkit-line-clamp:1;font-size:10px}.timeline-step:hover{border-color:var(--neon-cyan-mid);transform:translateY(-1px)}.timeline-step.active{box-shadow:0 0 0 2px var(--neon-cyan-dim), 0 0 16px #00f0ff1f;border-color:#00f0ff8c}.timeline-step-badge{font-family:var(--font-sans);letter-spacing:.3px;color:var(--neon-cyan);font-size:10px;font-weight:600}.timeline-step-seq{color:var(--text-muted);font-size:10px;font-family:var(--font-mono)}.timeline-step-type{color:var(--text-secondary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;line-height:1.4;display:-webkit-box;overflow:hidden}.timeline-step-created{border-left:3px solid var(--neon-green)}.timeline-step-deleted{border-left:3px solid var(--neon-red)}.timeline-step-completed{border-left:3px solid var(--neon-green)}.timeline-step-failed{border-left:3px solid var(--neon-red)}.timeline-step-transfer{border-left:3px solid var(--neon-amber)}.timeline-step-item{border-left:3px solid var(--neon-purple)}.timeline-step-progress{border-left:3px solid #38bdf8}.timeline-step-default{border-left:3px solid var(--neon-cyan)}.timeline-group-chip{border-radius:var(--radius);border:1px solid var(--border-muted);background:linear-gradient(145deg, var(--bg-elevated) 0%, var(--bg-raised) 100%);cursor:pointer;text-align:left;min-width:140px;max-width:240px;font-family:var(--font-sans);transition:border-color var(--transition), box-shadow var(--transition), transform var(--transition);flex-direction:column;flex-shrink:0;align-items:flex-start;gap:4px;padding:10px 14px;display:flex}.timeline-group-chip:hover{border-color:var(--neon-cyan-mid);transform:translateY(-1px)}.timeline-group-chip.active{box-shadow:0 0 0 2px var(--neon-cyan-dim), 0 0 16px #00f0ff1f;border-color:#00f0ff8c}.timeline-group-chip.expanded{border-color:#00f0ff59}.timeline-group-chip-top{justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.timeline-group-count{font-family:var(--font-display);letter-spacing:.5px;color:var(--neon-cyan);font-size:16px;font-weight:800;line-height:1}.timeline-group-chevron{color:var(--text-muted);font-size:10px}.timeline-group-chip .timeline-group-type{color:var(--text-primary);-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:12px;font-weight:600;line-height:1.35;display:-webkit-box;overflow:hidden}.timeline-group-range{color:var(--text-muted);font-size:10px;font-family:var(--font-mono);line-height:1.4}.timeline-expanded-deck{border-top:1px solid var(--border-muted);background:#00000040;padding:10px 14px 14px}.timeline-expanded-head{flex-wrap:wrap;align-items:center;gap:10px 16px;margin-bottom:10px;display:flex}.timeline-expanded-title{font-family:var(--font-sans);letter-spacing:.3px;color:var(--neon-cyan);font-size:12px;font-weight:700}.timeline-expanded-meta{font-family:var(--font-mono);color:var(--text-muted);flex:1;font-size:11px}.timeline-expanded-close{border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-raised);color:var(--text-secondary);font-family:var(--font-mono);cursor:pointer;transition:border-color var(--transition), color var(--transition);margin-left:auto;padding:4px 12px;font-size:11px}.timeline-expanded-close:hover{border-color:var(--neon-cyan-mid);color:var(--neon-cyan)}.timeline-expanded-strip{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base);flex-wrap:nowrap;align-items:center;gap:4px;width:100%;padding-bottom:6px;display:flex;overflow-x:auto}.timeline-slider{width:100%;accent-color:var(--neon-cyan);cursor:pointer;margin-top:4px}.timeline-info{color:var(--text-muted);font-size:12px;font-family:var(--font-mono);grid-template-columns:minmax(0,1fr) minmax(0,2.2fr) minmax(0,1fr);align-items:start;gap:12px;margin-top:12px;display:grid}.timeline-info-edge{color:var(--text-muted);font-size:11px}.timeline-info-center{text-align:center;color:var(--text-secondary);line-height:1.55}.timeline-info-center strong{color:var(--neon-cyan);font-weight:600}.timeline-info-muted{color:var(--text-muted);font-weight:400}.timeline-info-type{color:var(--text-primary);font-size:11px}.state-grid{grid-template-columns:1fr 1fr;gap:16px;display:grid}.state-panel h4{font-family:var(--font-display);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px;font-size:10px;font-weight:600}.state-panel-before h4{color:var(--text-muted)}.state-panel-after h4{color:var(--neon-green);text-shadow:0 0 6px #00ff884d}.json-block{background:var(--bg-base);border:1px solid var(--border-muted);border-radius:var(--radius);font-family:var(--font-mono);max-height:260px;color:var(--text-secondary);white-space:pre;padding:12px;font-size:12px;line-height:1.7;overflow:auto}.json-tree-root{background:var(--bg-base);border:1px solid var(--border-muted);border-radius:var(--radius);max-height:320px;font-family:var(--font-mono);color:var(--text-secondary);padding:10px 12px;font-size:13px;line-height:1.75;overflow:auto}.json-tree-line{flex-wrap:wrap;align-items:baseline;gap:2px 0;min-height:1.5em;display:flex}.json-tree-toggle{width:22px;height:22px;color:var(--text-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;flex-shrink:0;margin-right:4px;padding:0;font-size:10px;line-height:1}.json-tree-toggle:hover{color:var(--neon-cyan);background:var(--neon-cyan-dim)}.json-key{color:#7dd3fc}.json-string{color:#86efac}.json-number{color:#fcd34d}.json-boolean{color:#c4b5fd}.json-null{color:var(--text-muted);font-style:italic}.json-punct{color:var(--text-muted)}.json-ellipsis{color:var(--text-muted);font-size:11px;font-style:italic}.json-unknown{color:var(--neon-amber)}.diff-panel{border-top:1px solid var(--border-muted);margin-top:20px;padding-top:16px}.diff-toolbar{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;display:flex}.diff-toolbar-title{font-family:var(--font-sans);color:var(--neon-cyan);letter-spacing:.2px;font-size:13px;font-weight:600}.diff-view-toggle{border-radius:var(--radius);border:1px solid var(--border);font-family:var(--font-mono);font-size:11px;display:flex;overflow:hidden}.diff-view-toggle button{background:var(--bg-raised);color:var(--text-muted);cursor:pointer;transition:background var(--transition), color var(--transition);border:none;padding:6px 14px}.diff-view-toggle button+button{border-left:1px solid var(--border)}.diff-view-toggle button:hover{color:var(--text-secondary)}.diff-view-toggle button.active{background:var(--neon-cyan-dim);color:var(--neon-cyan)}.diff-body{align-items:stretch;gap:10px;display:flex}.diff-minimap{border:1px solid var(--border-muted);background:var(--bg-base);border-radius:4px;flex-direction:column;flex-shrink:0;width:10px;max-height:280px;display:flex;overflow:hidden}.diff-minimap-chunk{background:var(--border);cursor:pointer;min-height:8px;transition:background var(--transition);border:none;flex:1;margin:0;padding:0}.diff-minimap-chunk:hover,.diff-minimap-chunk:focus-visible{background:var(--neon-cyan-mid);outline:none}.diff-scroll{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base);flex:1;min-width:0;max-height:280px;overflow:auto}.diff-list{flex-direction:column;gap:0;display:flex}.diff-row{font-family:var(--font-mono);background:var(--bg-raised);border-left:3px solid var(--neon-cyan);border-bottom:1px solid var(--border-muted);border-radius:0;align-items:stretch;gap:0;padding:0;font-size:12px;line-height:1.65;display:flex}.diff-row:last-child{border-radius:0 0 var(--radius) var(--radius);border-bottom:none}.diff-row:first-child{border-radius:var(--radius) var(--radius) 0 0}.diff-line-no{text-align:right;width:36px;color:var(--text-muted);background:var(--bg-base);border-right:1px solid var(--border-muted);-webkit-user-select:none;user-select:none;flex-shrink:0;padding:10px 6px;font-size:10px}.diff-row-body{flex-wrap:wrap;flex:1;align-items:baseline;gap:8px 12px;padding:10px 12px;display:flex}.diff-field{color:var(--text-primary);min-width:6rem;font-weight:700}.diff-values-inline{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.diff-old{color:var(--neon-red);font-weight:400;text-decoration:line-through}.diff-arrow{color:var(--text-muted);flex-shrink:0}.diff-new{color:var(--neon-green);text-shadow:0 0 4px #00ff8840;font-weight:400}.diff-split-head{font-family:var(--font-sans);letter-spacing:.3px;color:var(--text-muted);border-bottom:1px solid var(--border);background:var(--bg-raised);z-index:1;grid-template-columns:1fr 1fr;gap:8px;padding:8px 12px 8px 48px;font-size:11px;display:grid;position:sticky;top:0}.diff-split-old-label{color:var(--neon-red)}.diff-split-new-label{color:var(--neon-green)}.diff-split-row{border-bottom:1px solid var(--border-muted);align-items:stretch;gap:0;display:flex}.diff-split-row:last-child{border-bottom:none}.diff-split-cells{flex:1;grid-template-columns:1fr 1fr;gap:8px;min-width:0;padding:8px 12px 8px 0;display:grid}.diff-split-cell{border-radius:var(--radius);background:var(--bg-base);border:1px solid var(--border-muted);min-width:0;padding:8px 10px}.diff-split-cell .diff-field{margin-bottom:6px;font-size:11px;display:block}.diff-cell-value{overflow-wrap:anywhere;word-break:break-word;font-size:12px;font-weight:400;line-height:1.6}.diff-split-old .diff-cell-value{color:var(--neon-red);opacity:.9}.diff-split-new .diff-cell-value{color:var(--neon-green)}.event-meta{color:var(--text-muted);grid-template-columns:repeat(2,1fr);gap:10px 16px;margin-top:20px;font-size:12px;display:grid}.event-meta-bar{background:var(--bg-raised);border:1px solid var(--border-muted);border-radius:var(--radius);padding:14px 16px;box-shadow:inset 0 1px #00f0ff0a}.event-meta-time{font-family:var(--font-mono);color:var(--text-secondary);font-size:12px;font-weight:500}.event-meta-id{font-family:var(--font-mono);color:var(--text-primary);overflow-wrap:anywhere;word-break:break-word;grid-column:1/-1;font-size:11px;line-height:1.5}.event-meta-extra{font-family:var(--font-mono);color:var(--text-secondary);font-size:11px}@media (width<=900px){.diff-split-cells{grid-template-columns:1fr}.diff-split-head{display:none}}.copy-btn{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-secondary);cursor:pointer;transition:all var(--transition);font-size:12px;font-family:var(--font-mono);margin-top:12px;padding:6px 14px}.copy-btn:hover{background:var(--bg-raised);color:var(--neon-cyan);border-color:var(--neon-cyan);box-shadow:0 0 10px #00f0ff1a}.live-header{justify-content:space-between;align-items:center;margin-bottom:12px;display:flex}.live-indicator{font-family:var(--font-sans);letter-spacing:.2px;align-items:center;gap:6px;font-size:11px;display:flex}.pause-btn{background:var(--bg-elevated);border:1px solid var(--border);color:var(--text-secondary);font-family:var(--font-sans);cursor:pointer;transition:all var(--transition);border-radius:4px;padding:5px 12px;font-size:11px}.pause-btn:hover{background:var(--bg-raised);border-color:var(--neon-cyan);color:var(--neon-cyan);box-shadow:0 0 8px #00f0ff1a}.event-stream{height:var(--bottom-panel-scroll-height);min-height:0;font-family:var(--font-mono);flex-direction:column;gap:3px;padding-right:4px;font-size:12px;display:flex;overflow-y:auto}.event-stream,.anomaly-scroll-region,.search-results{scrollbar-width:thin;scrollbar-color:var(--border) var(--bg-base)}.event-stream::-webkit-scrollbar{width:6px}.anomaly-scroll-region::-webkit-scrollbar{width:6px}.search-results::-webkit-scrollbar{width:6px}.event-stream::-webkit-scrollbar-track{background:var(--bg-base)}.anomaly-scroll-region::-webkit-scrollbar-track{background:var(--bg-base)}.search-results::-webkit-scrollbar-track{background:var(--bg-base)}.event-stream::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.anomaly-scroll-region::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.search-results::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.event-stream::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.anomaly-scroll-region::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.search-results::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.event-row{transition:all var(--transition);cursor:default;background:#ffffff03;border-left:3px solid #0000;border-radius:4px;align-items:center;gap:12px;padding:6px 10px;display:flex}.event-row:hover{background:var(--bg-elevated)}.event-row.type-withdrawn{border-left-color:var(--neon-magenta)}.event-row.type-deposited{border-left-color:var(--neon-cyan)}.event-row.type-created{border-left-color:var(--neon-green)}.event-row.type-transfer{border-left-color:var(--neon-amber)}.event-row.type-deleted{border-left-color:var(--neon-red)}.event-row.type-completed{border-left-color:var(--neon-green)}.event-row.type-failed{border-left-color:var(--neon-red)}.event-row.type-default{border-left-color:var(--neon-purple)}.event-time{color:var(--text-muted);flex-shrink:0;width:75px;font-size:11px}.event-type{flex-shrink:0;width:180px;font-weight:500}.event-agg{color:var(--text-secondary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.event-icon{text-align:center;flex-shrink:0;width:20px;font-size:14px}.type-created{color:var(--neon-green);text-shadow:0 0 6px #00ff884d}.type-deleted{color:var(--neon-red);text-shadow:0 0 6px #ff33554d}.type-completed{color:var(--neon-green);text-shadow:0 0 6px #00ff884d}.type-failed{color:var(--neon-red);text-shadow:0 0 6px #ff33554d}.type-transfer{color:var(--neon-amber);text-shadow:0 0 6px #ffaa004d}.type-default{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff4d}.type-withdrawn{color:var(--neon-magenta);text-shadow:0 0 6px #ff00e54d}.type-deposited{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff4d}.anomaly-panel-inner{position:relative}.anomaly-shield{background:radial-gradient(#00ff880f 0%,#0000 70%);border:1px solid #00ff8826;border-radius:12px;flex-direction:column;justify-content:center;align-items:center;margin-bottom:20px;padding:30px 20px;display:flex}.shield-icon{justify-content:center;align-items:center;width:64px;height:64px;margin-bottom:12px;display:flex;position:relative}.shield-icon svg{filter:drop-shadow(0 0 12px #00ff8880)drop-shadow(0 0 24px #0f83);width:64px;height:64px}.shield-icon:after{content:"";border:1px solid #00ff8826;border-radius:50%;animation:3s ease-in-out infinite shield-pulse;position:absolute;inset:-8px}@keyframes shield-pulse{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.8;transform:scale(1.15)}}.shield-text{font-family:var(--font-display);letter-spacing:2px;text-transform:uppercase;color:var(--neon-green);text-shadow:0 0 8px #0f86;font-size:13px;font-weight:600}.gauge-row{grid-template-columns:1fr 1fr 1fr;gap:12px;display:grid}.gauge-card{background:var(--bg-raised);border:1px solid var(--border);border-radius:var(--radius);text-align:center;padding:12px;position:relative;overflow:hidden}.gauge-card:before{content:"";height:2px;position:absolute;bottom:0;left:0;right:0}.gauge-card.optimal:before{background:var(--neon-green);box-shadow:0 0 8px #0f86}.gauge-card.baseline:before{background:var(--neon-cyan);box-shadow:0 0 8px #00f0ff66}.gauge-card.zero:before{background:var(--neon-green);box-shadow:0 0 8px #0f86}.gauge-label{font-family:var(--font-sans);letter-spacing:.2px;color:var(--text-muted);margin-bottom:6px;font-size:10px;font-weight:500}.gauge-value{font-family:var(--font-display);letter-spacing:1px;font-size:13px;font-weight:700}.gauge-value.optimal{color:var(--neon-green);text-shadow:0 0 6px #0f86}.gauge-value.baseline{color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff66}.gauge-value.zero{color:var(--neon-green);text-shadow:0 0 6px #0f86}.gauge-wave{justify-content:center;align-items:flex-end;gap:2px;height:20px;margin-top:6px;display:flex}.gauge-wave-bar{border-radius:1px;width:2px;animation:1.5s ease-in-out infinite gauge-wave-anim}.gauge-wave-bar.green{background:var(--neon-green);box-shadow:0 0 3px #0f86}.gauge-wave-bar.cyan{background:var(--neon-cyan);box-shadow:0 0 3px #00f0ff66}@keyframes gauge-wave-anim{0%,to{height:4px}50%{height:16px}}.anomaly-scroll-region{box-sizing:border-box;height:var(--bottom-panel-scroll-height);max-height:var(--bottom-panel-scroll-height);z-index:1;min-height:0;padding-right:4px;position:relative;overflow:hidden auto}.anomaly-list-inner{flex-direction:column;gap:10px;display:flex}.anomaly-card-title-row{flex-wrap:wrap;width:100%}.anomaly-title-text{flex:1}.anomaly-header-count{min-width:28px;height:28px;font-family:var(--font-display);color:var(--bg-base);background:var(--neon-amber);border-radius:999px;justify-content:center;align-items:center;margin-left:auto;padding:0 10px;font-size:11px;font-weight:700;display:inline-flex;box-shadow:0 0 12px #ffaa0059}.anomaly-card{border:1px solid var(--border-muted);border-radius:var(--radius-lg);background:var(--bg-raised);transition:border-color var(--transition), box-shadow var(--transition);overflow:hidden}.anomaly-card.CRITICAL{border-left:4px solid #f44}.anomaly-card.HIGH{border-left:4px solid var(--neon-red)}.anomaly-card.MEDIUM{border-left:4px solid var(--neon-amber)}.anomaly-card.LOW{border-left:4px solid var(--neon-cyan)}.anomaly-card-summary{cursor:pointer;font-family:var(--font-sans);flex-wrap:wrap;align-items:center;gap:10px 12px;padding:14px 16px;list-style:none;display:flex}.anomaly-card-summary::-webkit-details-marker{display:none}.anomaly-severity-badge{font-family:var(--font-display);letter-spacing:1px;text-transform:uppercase;border-radius:4px;flex-shrink:0;padding:4px 10px;font-size:9px;font-weight:700}.anomaly-severity-badge.sev-critical{color:#f66;background:#f443;border:1px solid #ff444459}.anomaly-severity-badge.sev-high{background:var(--neon-red-dim);color:var(--neon-red);border:1px solid #ff335559}.anomaly-severity-badge.sev-medium{background:var(--neon-amber-dim);color:var(--neon-amber);border:1px solid #ffaa0059}.anomaly-severity-badge.sev-low{background:var(--neon-cyan-dim);color:var(--neon-cyan);border:1px solid #00f0ff40}.anomaly-card-title{min-width:0;color:var(--text-primary);flex:1;font-size:15px;font-weight:600;line-height:1.45}.anomaly-card-chevron{color:var(--text-muted);transition:transform var(--transition);flex-shrink:0;font-size:10px}.anomaly-card[open] .anomaly-card-chevron{transform:rotate(-180deg)}.anomaly-card-body{border-top:1px solid var(--border-muted);background:#0003;padding:0 16px 16px}.anomaly-card-meta{flex-wrap:wrap;align-items:baseline;gap:8px 12px;margin-top:12px;font-size:13px;line-height:1.5;display:flex}.anomaly-card-meta:first-child{margin-top:12px}.anomaly-meta-label{font-family:var(--font-sans);letter-spacing:.2px;color:var(--text-muted);min-width:72px;font-size:10px}.anomaly-meta-value{font-family:var(--font-mono);color:var(--text-secondary);overflow-wrap:anywhere}.no-anomalies{color:var(--neon-green);align-items:center;gap:8px;padding:20px 0;font-size:13px;display:flex}.bottom-grid{grid-template-columns:1fr 1fr;align-items:start;gap:16px;display:grid}.bottom-grid>.card{flex-direction:column;align-items:stretch;min-width:0;min-height:0;display:flex}.bottom-grid>.card>.card-title,.bottom-grid>.card>.live-header,.bottom-grid .anomaly-scroll-region,.bottom-grid .event-stream{flex-shrink:0}.skeleton{background:linear-gradient(90deg, var(--bg-raised) 25%, var(--bg-elevated) 50%, var(--bg-raised) 75%);border-radius:var(--radius);background-size:200% 100%;animation:1.4s infinite shimmer}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.toast-container{z-index:999;flex-direction:column;gap:8px;display:flex;position:fixed;bottom:24px;right:24px}.toast{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-primary);box-shadow:var(--shadow);border-left:3px solid var(--neon-amber);padding:10px 16px;font-size:13px;animation:.2s slideIn}.toast.error{border-left-color:var(--neon-red)}.toast.success{border-left-color:var(--neon-green)}@keyframes slideIn{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#00f0ff4d}.demo-banner{font-size:13px;font-family:var(--font-mono);color:var(--neon-amber);background:var(--neon-amber-dim);border-radius:var(--radius);border:1px solid #ffaa0059;margin:0 0 16px;padding:10px 16px}.demo-banner code{color:var(--neon-cyan)}@media (width<=900px){.state-grid,.bottom-grid{grid-template-columns:1fr}.header-title{display:none}.gauge-row{grid-template-columns:1fr}.timeline-info{text-align:center;grid-template-columns:1fr}.timeline-info-edge{display:none}}.state-tabs{border-bottom:1px solid var(--border);gap:2px;margin-bottom:16px;padding-bottom:0;display:flex}.state-tab{color:var(--text-muted);font-family:var(--font-sans);cursor:pointer;transition:color var(--transition), border-color var(--transition);border-radius:var(--radius) var(--radius) 0 0;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-1px;padding:8px 16px;font-size:13px;font-weight:500;display:flex}.state-tab:hover{color:var(--text-secondary)}.state-tab.active{color:var(--neon-cyan);border-bottom-color:var(--neon-cyan);background:var(--neon-cyan-dim);text-shadow:0 0 8px #00f0ff33}.state-tab-emoji{font-size:14px}.state-tab-content{min-height:80px}.summary-tab{padding-top:4px}.summary-changes{flex-direction:column;gap:8px;display:flex}.summary-changes-header{color:var(--text-muted);border-bottom:1px solid var(--border-muted);margin-bottom:4px;padding-bottom:6px;font-size:12px;font-weight:600}.summary-change-row{border-radius:var(--radius);background:var(--bg-raised);border:1px solid var(--border-muted);border-left:3px solid var(--neon-amber);font-family:var(--font-mono);transition:background var(--transition);flex-wrap:wrap;align-items:baseline;gap:6px 10px;padding:9px 12px;font-size:13px;display:flex}.summary-change-row:hover{background:var(--bg-elevated)}.summary-change-field{color:var(--text-primary);min-width:5rem;font-weight:700;font-family:var(--font-sans)}.summary-change-old{color:var(--neon-red);opacity:.9;text-decoration:line-through}.summary-change-arrow{color:var(--text-muted);flex-shrink:0}.summary-change-new{color:var(--neon-green);text-shadow:0 0 4px #0f83;font-weight:600}.summary-no-changes{color:var(--neon-green);align-items:center;gap:12px;padding:20px 0;font-size:14px;display:flex}.event-summary-bar{background:linear-gradient(145deg, var(--bg-elevated) 0%, var(--bg-raised) 100%);border:1px solid var(--border);border-left:3px solid var(--neon-cyan);border-radius:var(--radius);justify-content:space-between;align-items:center;gap:12px;margin-bottom:4px;padding:10px 20px;display:flex;box-shadow:0 0 16px #00f0ff0d}.event-summary-left{flex-wrap:wrap;align-items:baseline;gap:6px 12px;display:flex}.event-summary-type{font-family:var(--font-sans);color:var(--text-primary);font-size:14px;font-weight:600}.event-summary-meta{font-family:var(--font-mono);color:var(--text-muted);font-size:12px}.event-summary-changes{color:var(--neon-amber);font-size:12px;font-family:var(--font-sans);background:#ffaa001f;border:1px solid #ffaa004d;border-radius:999px;flex-shrink:0;padding:3px 10px;font-weight:600}.diff-count-badge{color:var(--neon-amber);font-size:11px;font-family:var(--font-sans);background:#ffaa001f;border:1px solid #ffaa004d;border-radius:999px;align-items:center;margin-left:6px;padding:2px 8px;font-weight:500;display:inline-flex}.diff-summary-view{flex-direction:column;gap:6px;display:flex}.diff-summary-row{border-radius:var(--radius);background:var(--bg-raised);border:1px solid var(--border-muted);border-left:3px solid var(--neon-cyan);font-family:var(--font-mono);transition:background var(--transition);flex-wrap:wrap;align-items:baseline;gap:6px 10px;padding:9px 12px;font-size:13px;display:flex}.diff-summary-row:hover{background:var(--bg-elevated)}.diff-summary-field{color:var(--text-primary);min-width:6rem;font-weight:700;font-family:var(--font-sans)}.diff-summary-old{color:var(--neon-red);opacity:.9;text-decoration:line-through}.diff-summary-arrow{color:var(--text-muted);flex-shrink:0}.diff-summary-new{color:var(--neon-green);text-shadow:0 0 4px #0f83;font-weight:600}.diff-jump-next{border-radius:var(--radius);border:1px solid var(--border);color:var(--text-muted);font-family:var(--font-mono);cursor:pointer;transition:color var(--transition), border-color var(--transition);background:0 0;margin-left:auto;padding:2px 8px;font-size:10px}.diff-jump-next:hover{color:var(--neon-cyan);border-color:var(--neon-cyan-mid)}.json-tree-changed{border-radius:3px;background:#ffaa001a!important}.timeline-header-row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.timeline-jump-group{border:1px solid var(--border);border-radius:var(--radius);align-items:center;gap:0;display:flex;overflow:hidden}.timeline-jump-input{background:var(--bg-raised);border:none;border-right:1px solid var(--border);width:100px;color:var(--text-primary);font-family:var(--font-mono);outline:none;padding:5px 8px;font-size:12px}.timeline-jump-input:focus{border-right-color:var(--neon-cyan);background:var(--bg-elevated)}.timeline-jump-input::placeholder{color:var(--text-muted)}.timeline-jump-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.timeline-jump-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.timeline-jump-input[type=number]{-moz-appearance:textfield}.timeline-jump-btn{background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;transition:background var(--transition), color var(--transition);border:none;padding:5px 10px;font-size:14px}.timeline-jump-btn:hover{background:var(--neon-cyan-dim);color:var(--neon-cyan)}.timeline-filter-chips{flex-wrap:wrap;gap:6px;margin-bottom:10px;display:flex}.filter-chip{border:1px solid var(--border);background:var(--bg-raised);color:var(--text-muted);font-family:var(--font-sans);cursor:pointer;transition:all var(--transition);white-space:nowrap;text-overflow:ellipsis;border-radius:999px;max-width:160px;padding:3px 10px;font-size:11px;overflow:hidden}.filter-chip:hover{border-color:var(--neon-cyan-mid);color:var(--text-secondary)}.filter-chip.active{background:var(--neon-cyan-dim);color:var(--neon-cyan);text-shadow:0 0 6px #00f0ff33;border-color:#00f0ff66}.timeline-anomaly-marker{color:var(--neon-amber);text-shadow:0 0 4px #fa09;align-self:flex-start;font-size:7px;line-height:1}.keyboard-hints{z-index:200;border-top:1px solid var(--border);-webkit-backdrop-filter:blur(12px);background:#080b14eb;position:fixed;bottom:0;left:0;right:0}.keyboard-hints-bar{font-family:var(--font-mono);color:var(--text-muted);flex-wrap:wrap;justify-content:center;align-items:center;gap:8px;padding:6px 24px;font-size:11px;display:flex}.keyboard-hints-item{align-items:center;gap:5px;display:flex}.keyboard-hints-sep{color:var(--border);font-size:14px;line-height:1}.keyboard-hints-grid{flex-wrap:wrap;justify-content:center;gap:10px 24px;padding:14px 24px;display:flex}.keyboard-hint-row{font-family:var(--font-mono);color:var(--text-secondary);align-items:center;gap:10px;font-size:12px;display:flex}.keyboard-hint-desc{color:var(--text-muted)}.keyboard-key{border:1px solid var(--border);border-bottom:2px solid var(--border);background:var(--bg-elevated);color:var(--neon-cyan);font-family:var(--font-mono);white-space:nowrap;border-radius:4px;align-items:center;padding:2px 8px;font-size:11px;display:inline-flex}.keyboard-key-mini{border:1px solid var(--border);background:var(--bg-elevated);color:var(--neon-cyan);font-family:var(--font-mono);white-space:nowrap;border-radius:3px;align-items:center;padding:1px 5px;font-size:10px;display:inline-flex}.keyboard-hints-close{border-radius:var(--radius);border:1px solid var(--border);color:var(--text-muted);font-family:var(--font-mono);cursor:pointer;transition:color var(--transition), border-color var(--transition);background:0 0;padding:4px 12px;font-size:11px}.keyboard-hints-close:hover{color:var(--neon-cyan);border-color:var(--neon-cyan-mid)}.app-main{padding-bottom:40px} diff --git a/eventlens-api/src/main/resources/web/index.html b/eventlens-api/src/main/resources/web/index.html index 149ed97..ac066fb 100644 --- a/eventlens-api/src/main/resources/web/index.html +++ b/eventlens-api/src/main/resources/web/index.html @@ -9,8 +9,8 @@ - - + +

diff --git a/eventlens-api/src/test/java/io/eventlens/api/SecurityHeadersTest.java b/eventlens-api/src/test/java/io/eventlens/api/SecurityHeadersTest.java index 36b0bc0..5656f5f 100644 --- a/eventlens-api/src/test/java/io/eventlens/api/SecurityHeadersTest.java +++ b/eventlens-api/src/test/java/io/eventlens/api/SecurityHeadersTest.java @@ -4,6 +4,7 @@ import io.eventlens.core.aggregator.ReducerRegistry; import io.eventlens.core.engine.*; import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.plugin.PluginManager; import io.eventlens.core.spi.EventStoreReader; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -89,7 +90,7 @@ public List searchAggregates(String query, int limit) { var exportEngine = new ExportEngine(reader, replayEngine); var diffEngine = new DiffEngine(replayEngine); - server = new EventLensServer(cfg, reader, replayEngine, bisectEngine, anomalyDetector, exportEngine, diffEngine); + server = new EventLensServer(cfg, reader, replayEngine, new ReducerRegistry(), new PluginManager(30), "default", bisectEngine, anomalyDetector, exportEngine, diffEngine); server.start(); var client = HttpClient.newHttpClient(); @@ -136,7 +137,7 @@ void forwardedHttpsAddsHsts() throws Exception { var exportEngine = new ExportEngine(reader, replayEngine); var diffEngine = new DiffEngine(replayEngine); - server = new EventLensServer(cfg, reader, replayEngine, bisectEngine, anomalyDetector, exportEngine, diffEngine); + server = new EventLensServer(cfg, reader, replayEngine, new ReducerRegistry(), new PluginManager(30), "default", bisectEngine, anomalyDetector, exportEngine, diffEngine); server.start(); var client = HttpClient.newHttpClient(); @@ -180,7 +181,7 @@ void rateLimitReturns429AndRetryAfter() throws Exception { var exportEngine = new ExportEngine(reader, replayEngine); var diffEngine = new DiffEngine(replayEngine); - server = new EventLensServer(cfg, reader, replayEngine, bisectEngine, anomalyDetector, exportEngine, diffEngine); + server = new EventLensServer(cfg, reader, replayEngine, new ReducerRegistry(), new PluginManager(30), "default", bisectEngine, anomalyDetector, exportEngine, diffEngine); server.start(); var client = HttpClient.newHttpClient(); @@ -227,7 +228,7 @@ void corsRejectsNonAllowlistedOrigin() throws Exception { var exportEngine = new ExportEngine(reader, replayEngine); var diffEngine = new DiffEngine(replayEngine); - server = new EventLensServer(cfg, reader, replayEngine, bisectEngine, anomalyDetector, exportEngine, diffEngine); + server = new EventLensServer(cfg, reader, replayEngine, new ReducerRegistry(), new PluginManager(30), "default", bisectEngine, anomalyDetector, exportEngine, diffEngine); server.start(); var client = HttpClient.newHttpClient(); @@ -248,3 +249,4 @@ private static int freePort() throws Exception { } } + diff --git a/eventlens-api/src/test/java/io/eventlens/api/SourceAwarePanelsIntegrationTest.java b/eventlens-api/src/test/java/io/eventlens/api/SourceAwarePanelsIntegrationTest.java new file mode 100644 index 0000000..98ccbcd --- /dev/null +++ b/eventlens-api/src/test/java/io/eventlens/api/SourceAwarePanelsIntegrationTest.java @@ -0,0 +1,363 @@ +package io.eventlens.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.eventlens.core.EventLensConfig; +import io.eventlens.core.aggregator.ReducerRegistry; +import io.eventlens.core.engine.AnomalyDetector; +import io.eventlens.core.engine.BisectEngine; +import io.eventlens.core.engine.DiffEngine; +import io.eventlens.core.engine.ExportEngine; +import io.eventlens.core.engine.ReplayEngine; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.plugin.PluginManager; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.spi.Event; +import io.eventlens.spi.EventQuery; +import io.eventlens.spi.EventQueryResult; +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.spi.HealthStatus; +import io.eventlens.spi.StreamAdapterPlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.net.ServerSocket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.WebSocket; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +class SourceAwarePanelsIntegrationTest { + + private static final ObjectMapper JSON = new ObjectMapper().findAndRegisterModules(); + + private EventLensServer server; + private PluginManager pluginManager; + + @AfterEach + void tearDown() { + if (server != null) { + server.stop(); + } + if (pluginManager != null) { + pluginManager.close(); + } + } + + @Test + void anomalyEndpointUsesSelectedSource() throws Exception { + TestEventSourcePlugin primary = new TestEventSourcePlugin("pg-primary", anomalyEvents("PG-AGG")); + TestEventSourcePlugin legacy = new TestEventSourcePlugin("mysql-alt", anomalyEvents("MYSQL-AGG")); + startServer(primary, legacy, null, Map.of()); + + JsonNode defaultAnomalies = getJson("/api/v1/anomalies/recent?limit=100"); + JsonNode mysqlAnomalies = getJson("/api/v1/anomalies/recent?limit=100&source=mysql-alt"); + + assertThat(defaultAnomalies).isNotEmpty(); + assertThat(mysqlAnomalies).isNotEmpty(); + assertThat(defaultAnomalies.get(0).path("aggregateId").asText()).isEqualTo("PG-AGG"); + assertThat(mysqlAnomalies.get(0).path("aggregateId").asText()).isEqualTo("MYSQL-AGG"); + } + + @Test + void websocketStreamsMappedSourceAndShowsPlaceholderForSourceWithoutStream() throws Exception { + TestEventSourcePlugin primary = new TestEventSourcePlugin("pg-primary", List.of()); + TestEventSourcePlugin legacy = new TestEventSourcePlugin("mysql-alt", List.of()); + TestStreamAdapterPlugin pgStream = new TestStreamAdapterPlugin(); + startServer(primary, legacy, pgStream, Map.of("pg-primary", "pg-stream", "mysql-alt", "")); + + CompletableFuture liveMessage = new CompletableFuture<>(); + WebSocket liveSocket = openSocket("/ws/live?source=pg-primary", liveMessage); + pgStream.emit(new Event( + "evt-live", + "PG-AGG", + "BankAccount", + 99, + "LiveArrived", + JSON.readTree("{\"note\":\"streamed\"}"), + JSON.readTree("{\"source\":\"pg\"}"), + Instant.parse("2026-03-24T12:00:00Z"), + 99 + )); + + String streamed = liveMessage.get(5, TimeUnit.SECONDS); + assertThat(streamed).contains("\"eventType\":\"LiveArrived\""); + + CompletableFuture placeholderMessage = new CompletableFuture<>(); + WebSocket noStreamSocket = openSocket("/ws/live?source=mysql-alt", placeholderMessage); + String placeholder = placeholderMessage.get(5, TimeUnit.SECONDS); + assertThat(placeholder).contains("\"type\":\"NO_LIVE_STREAM\""); + assertThat(placeholder).contains("\"source\":\"mysql-alt\""); + + liveSocket.sendClose(WebSocket.NORMAL_CLOSURE, "done").join(); + noStreamSocket.sendClose(WebSocket.NORMAL_CLOSURE, "done").join(); + } + + private void startServer( + TestEventSourcePlugin primary, + TestEventSourcePlugin legacy, + TestStreamAdapterPlugin stream, + Map bindings) throws Exception { + pluginManager = new PluginManager(30); + pluginManager.registerEventSource("pg-primary", primary, Map.of()); + pluginManager.registerEventSource("mysql-alt", legacy, Map.of()); + if (stream != null) { + pluginManager.registerStreamAdapter("pg-stream", stream, Map.of("bootstrapServers", "unused", "topic", "unused")); + } + + EventStoreReader defaultReader = primary; + ReducerRegistry reducers = new ReducerRegistry(); + ReplayEngine replayEngine = new ReplayEngine(defaultReader, reducers); + EventLensConfig config = new EventLensConfig(); + config.getServer().setPort(freePort()); + config.getServer().getAuth().setEnabled(false); + config.getServer().getSecurity().getRateLimit().setEnabled(false); + config.getAudit().setEnabled(false); + + var bisectEngine = new BisectEngine(replayEngine, defaultReader); + var anomalyDetector = new AnomalyDetector(defaultReader, replayEngine, config.getAnomaly()); + var exportEngine = new ExportEngine(defaultReader, replayEngine); + var diffEngine = new DiffEngine(replayEngine); + + server = new EventLensServer( + config, + defaultReader, + replayEngine, + reducers, + pluginManager, + "pg-primary", + bisectEngine, + anomalyDetector, + exportEngine, + diffEngine, + bindings + ); + server.start(); + } + + private JsonNode getJson(String pathAndQuery) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + server.getApp().port() + pathAndQuery)) + .GET() + .build(); + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + assertThat(response.statusCode()).isEqualTo(200); + return JSON.readTree(response.body()); + } + + private WebSocket openSocket(String pathAndQuery, CompletableFuture firstMessage) { + return HttpClient.newHttpClient() + .newWebSocketBuilder() + .buildAsync( + URI.create("ws://localhost:" + server.getApp().port() + pathAndQuery), + new FirstMessageListener(firstMessage)) + .join(); + } + + private static int freePort() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private static List anomalyEvents(String aggregateId) { + List events = new ArrayList<>(); + for (int i = 1; i <= 100; i++) { + events.add(new StoredEvent( + "evt-" + aggregateId + "-" + i, + aggregateId, + "BankAccount", + i, + i == 1 ? "AccountCreated" : "MoneyDeposited", + "{\"balance\":" + i + "}", + "{}", + Instant.parse("2026-03-24T10:00:00Z").plusSeconds(i), + i + )); + } + return events; + } + + private static final class TestEventSourcePlugin implements EventSourcePlugin, EventStoreReader { + private final String instanceId; + private final List events; + + private TestEventSourcePlugin(String instanceId, List events) { + this.instanceId = instanceId; + this.events = events.stream() + .sorted(Comparator.comparingLong(StoredEvent::sequenceNumber)) + .toList(); + } + + @Override + public String typeId() { + return "test-source"; + } + + @Override + public String displayName() { + return "Test Source " + instanceId; + } + + @Override + public void initialize(String instanceId, Map config) { + } + + @Override + public EventQueryResult query(EventQuery query) { + return new EventQueryResult(List.of(), false, null); + } + + @Override + public HealthStatus healthCheck() { + return HealthStatus.up(); + } + + @Override + public List getEvents(String aggregateId) { + return events.stream().filter(event -> event.aggregateId().equals(aggregateId)).toList(); + } + + @Override + public List getEvents(String aggregateId, int limit, int offset) { + return getEvents(aggregateId).stream().skip(offset).limit(limit).toList(); + } + + @Override + public List getEventsAfterSequence(String aggregateId, long afterSequence, int limit) { + return getEvents(aggregateId).stream() + .filter(event -> event.sequenceNumber() > afterSequence) + .limit(limit) + .toList(); + } + + @Override + public List getEventsUpTo(String aggregateId, long maxSequence) { + return getEvents(aggregateId).stream().filter(event -> event.sequenceNumber() <= maxSequence).toList(); + } + + @Override + public List findAggregateIds(String aggregateType, int limit, int offset) { + return events.stream() + .filter(event -> event.aggregateType().equals(aggregateType)) + .map(StoredEvent::aggregateId) + .distinct() + .skip(offset) + .limit(limit) + .toList(); + } + + @Override + public List getRecentEvents(int limit) { + return events.stream() + .sorted(Comparator.comparingLong(StoredEvent::globalPosition).reversed()) + .limit(limit) + .toList(); + } + + @Override + public List getEventsAfter(long globalPosition, int limit) { + return events.stream() + .filter(event -> event.globalPosition() > globalPosition) + .limit(limit) + .toList(); + } + + @Override + public long countEvents(String aggregateId) { + return getEvents(aggregateId).size(); + } + + @Override + public List getAggregateTypes() { + return events.stream().map(StoredEvent::aggregateType).distinct().toList(); + } + + @Override + public List searchAggregates(String query, int limit) { + return events.stream() + .map(StoredEvent::aggregateId) + .filter(id -> id.contains(query)) + .distinct() + .limit(limit) + .toList(); + } + } + + private static final class TestStreamAdapterPlugin implements StreamAdapterPlugin { + private final AtomicReference> listener = new AtomicReference<>(); + + @Override + public String typeId() { + return "test-stream"; + } + + @Override + public String displayName() { + return "Test Stream"; + } + + @Override + public void initialize(String instanceId, Map config) { + } + + @Override + public void subscribe(java.util.function.Consumer listener) { + this.listener.set(listener); + } + + @Override + public void unsubscribe() { + listener.set(null); + } + + @Override + public HealthStatus healthCheck() { + return HealthStatus.up(); + } + + private void emit(Event event) { + var activeListener = listener.get(); + if (activeListener != null) { + activeListener.accept(event); + } + } + } + + private static final class FirstMessageListener implements WebSocket.Listener { + private final CompletableFuture firstMessage; + private final StringBuilder text = new StringBuilder(); + + private FirstMessageListener(CompletableFuture firstMessage) { + this.firstMessage = firstMessage; + } + + @Override + public void onOpen(WebSocket webSocket) { + webSocket.request(1); + WebSocket.Listener.super.onOpen(webSocket); + } + + @Override + public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { + text.append(data); + if (last && !firstMessage.isDone()) { + firstMessage.complete(text.toString()); + } + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/eventlens-api/src/test/java/io/eventlens/api/V4ReadinessApiE2ETest.java b/eventlens-api/src/test/java/io/eventlens/api/V4ReadinessApiE2ETest.java new file mode 100644 index 0000000..eb83b9a --- /dev/null +++ b/eventlens-api/src/test/java/io/eventlens/api/V4ReadinessApiE2ETest.java @@ -0,0 +1,338 @@ +package io.eventlens.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.eventlens.core.EventLensConfig; +import io.eventlens.core.aggregator.ReducerRegistry; +import io.eventlens.core.engine.AnomalyDetector; +import io.eventlens.core.engine.BisectEngine; +import io.eventlens.core.engine.DiffEngine; +import io.eventlens.core.engine.ExportEngine; +import io.eventlens.core.engine.ReplayEngine; +import io.eventlens.core.plugin.PluginManager; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.mysql.MySqlEventSourcePlugin; +import io.eventlens.pg.PostgresEventSourcePlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.net.ServerSocket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers(disabledWithoutDocker = true) +class V4ReadinessApiE2ETest { + + private static final String AGGREGATE_ID = "SHARED-001"; + private static final ObjectMapper JSON = new ObjectMapper().findAndRegisterModules(); + + @Container + @SuppressWarnings("resource") + private final PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine") + .withDatabaseName("eventlens_pg"); + + @Container + @SuppressWarnings("resource") + private final MySQLContainer mysql = new MySQLContainer<>("mysql:8.4") + .withDatabaseName("eventlens_mysql"); + + private RunningSystem running; + + @AfterEach + void tearDown() { + if (running != null) { + running.close(); + } + } + + @Test + void multiSourceSwitchingReturnsDifferentTimelineData() throws Exception { + running = startSystem(); + + JsonNode postgresTimeline = getJson("/api/v1/aggregates/" + AGGREGATE_ID + "/timeline"); + JsonNode mysqlTimeline = getJson("/api/v1/aggregates/" + AGGREGATE_ID + "/timeline?source=mysql-alt"); + + assertThat(postgresTimeline.path("aggregateType").asText()).isEqualTo("BankAccount"); + assertThat(postgresTimeline.path("events")).hasSize(2); + assertThat(postgresTimeline.at("/events/0/eventType").asText()).isEqualTo("AccountCreated"); + assertThat(postgresTimeline.at("/events/1/payload").asText()).contains("postgres-note"); + + assertThat(mysqlTimeline.path("aggregateType").asText()).isEqualTo("Order"); + assertThat(mysqlTimeline.path("events")).hasSize(2); + assertThat(mysqlTimeline.at("/events/0/eventType").asText()).isEqualTo("OrderCreated"); + assertThat(mysqlTimeline.at("/events/1/payload").asText()).contains("mysql-note"); + + assertThat(postgresTimeline.path("aggregateType").asText()) + .isNotEqualTo(mysqlTimeline.path("aggregateType").asText()); + assertThat(postgresTimeline.at("/events/1/payload").asText()) + .isNotEqualTo(mysqlTimeline.at("/events/1/payload").asText()); + } + + @Test + void pluginFailureIsolationKeepsHealthySourceServing() throws Exception { + running = startSystem(); + + mysql.stop(); + + JsonNode datasources = waitForDatasourceListStatus("mysql-alt", "degraded"); + assertThat(statusFor(datasources, "pg-primary")).isEqualTo("ready"); + assertThat(statusFor(datasources, "mysql-alt")).isEqualTo("degraded"); + + JsonNode postgresTimeline = getJson("/api/v1/aggregates/" + AGGREGATE_ID + "/timeline"); + assertThat(postgresTimeline.path("aggregateType").asText()).isEqualTo("BankAccount"); + assertThat(postgresTimeline.at("/events/1/payload").asText()).contains("postgres-note"); + + JsonNode mysqlHealth = getJson("/api/v1/datasources/mysql-alt/health"); + assertThat(mysqlHealth.path("status").asText()).isEqualTo("degraded"); + assertThat(mysqlHealth.at("/health/state").asText()).isEqualTo("down"); + } + + @Test + void lazyPayloadRoundTripReturnsMetadataThenFullPayload() throws Exception { + running = startSystem(); + + JsonNode metadataTimeline = getJson("/api/v1/aggregates/" + AGGREGATE_ID + "/timeline?fields=metadata"); + JsonNode fullTransitions = getJson("/api/v1/aggregates/" + AGGREGATE_ID + "/transitions"); + + assertThat(metadataTimeline.at("/events/0/payload").isNull()).isTrue(); + assertThat(metadataTimeline.at("/events/1/payload").isNull()).isTrue(); + + JsonNode selectedEvent = transitionEvent(fullTransitions, 2); + assertThat(selectedEvent).isNotNull(); + assertThat(selectedEvent.path("payload").isTextual()).isTrue(); + assertThat(selectedEvent.path("payload").asText()).contains("postgres-note"); + } + + private RunningSystem startSystem() throws Exception { + createPostgresSchema(); + createMySqlSchema(); + seedPostgres(); + seedMySql(); + + PluginManager pluginManager = new PluginManager(1); + pluginManager.registerEventSource("pg-primary", new PostgresEventSourcePlugin(), Map.of( + "jdbcUrl", postgres.getJdbcUrl(), + "username", postgres.getUsername(), + "password", postgres.getPassword(), + "tableName", "event_store" + )); + pluginManager.registerEventSource("mysql-alt", new MySqlEventSourcePlugin(), Map.of( + "jdbcUrl", mysql.getJdbcUrl(), + "username", mysql.getUsername(), + "password", mysql.getPassword(), + "tableName", "event_store" + )); + pluginManager.startHealthChecks(); + + EventStoreReader defaultReader = (EventStoreReader) pluginManager.getEventSource("pg-primary").orElseThrow(); + ReducerRegistry reducers = new ReducerRegistry(); + ReplayEngine replayEngine = new ReplayEngine(defaultReader, reducers); + EventLensConfig config = new EventLensConfig(); + config.getServer().setPort(freePort()); + config.getServer().getAuth().setEnabled(false); + config.getServer().getSecurity().getRateLimit().setEnabled(false); + config.getAudit().setEnabled(false); + + var bisectEngine = new BisectEngine(replayEngine, defaultReader); + var anomalyDetector = new AnomalyDetector(defaultReader, replayEngine, config.getAnomaly()); + var exportEngine = new ExportEngine(defaultReader, replayEngine); + var diffEngine = new DiffEngine(replayEngine); + + EventLensServer server = new EventLensServer( + config, + defaultReader, + replayEngine, + reducers, + pluginManager, + "pg-primary", + bisectEngine, + anomalyDetector, + exportEngine, + diffEngine + ); + server.start(); + + return new RunningSystem(server, pluginManager, config.getServer().getPort()); + } + + private void createPostgresSchema() throws Exception { + try (Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute(""" + DROP TABLE IF EXISTS event_store; + CREATE TABLE event_store ( + event_id UUID PRIMARY KEY, + aggregate_id VARCHAR(255) NOT NULL, + aggregate_type VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + event_type VARCHAR(255) NOT NULL, + payload JSONB NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + timestamp TIMESTAMPTZ NOT NULL, + global_position BIGINT GENERATED ALWAYS AS IDENTITY UNIQUE, + UNIQUE (aggregate_id, sequence_number) + ) + """); + } + } + + private void createMySqlSchema() throws Exception { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE IF EXISTS event_store"); + stmt.execute(""" + CREATE TABLE event_store ( + event_id VARCHAR(64) PRIMARY KEY, + aggregate_id VARCHAR(255) NOT NULL, + aggregate_type VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + event_type VARCHAR(255) NOT NULL, + payload JSON NOT NULL, + metadata JSON NULL, + timestamp TIMESTAMP NOT NULL, + global_position BIGINT NOT NULL AUTO_INCREMENT UNIQUE, + UNIQUE KEY uq_aggregate_sequence (aggregate_id, sequence_number) + ) + """); + } + } + + private void seedPostgres() throws Exception { + insertPostgres(1, "AccountCreated", """ + {"owner":"Alice","balance":0,"note":"postgres-note-created"} + """, Instant.parse("2026-03-24T10:00:00Z")); + insertPostgres(2, "MoneyDeposited", """ + {"amount":125,"balance":125,"note":"postgres-note-deposit"} + """, Instant.parse("2026-03-24T10:05:00Z")); + } + + private void seedMySql() throws Exception { + insertMySql(1, "OrderCreated", """ + {"customer":"Bob","status":"created","note":"mysql-note-created"} + """, Instant.parse("2026-03-24T11:00:00Z")); + insertMySql(2, "OrderApproved", """ + {"status":"approved","approver":"ops","note":"mysql-note-approved"} + """, Instant.parse("2026-03-24T11:05:00Z")); + } + + private void insertPostgres(long sequence, String eventType, String payload, Instant timestamp) throws Exception { + try (Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO event_store ( + event_id, aggregate_id, aggregate_type, sequence_number, event_type, payload, metadata, timestamp + ) VALUES (?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?) + """)) { + ps.setObject(1, UUID.randomUUID()); + ps.setString(2, AGGREGATE_ID); + ps.setString(3, "BankAccount"); + ps.setLong(4, sequence); + ps.setString(5, eventType); + ps.setString(6, payload.strip()); + ps.setString(7, "{\"source\":\"postgres\"}"); + ps.setObject(8, java.sql.Timestamp.from(timestamp)); + ps.executeUpdate(); + } + } + + private void insertMySql(long sequence, String eventType, String payload, Instant timestamp) throws Exception { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO event_store ( + event_id, aggregate_id, aggregate_type, sequence_number, event_type, payload, metadata, timestamp + ) VALUES (?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?) + """)) { + ps.setString(1, UUID.randomUUID().toString()); + ps.setString(2, AGGREGATE_ID); + ps.setString(3, "Order"); + ps.setLong(4, sequence); + ps.setString(5, eventType); + ps.setString(6, payload.strip()); + ps.setString(7, "{\"source\":\"mysql\"}"); + ps.setObject(8, java.sql.Timestamp.from(timestamp)); + ps.executeUpdate(); + } + } + + private JsonNode getJson(String pathAndQuery) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(running.baseUrl() + pathAndQuery)) + .timeout(Duration.ofSeconds(15)) + .GET() + .build(); + HttpResponse response = running.client().send(request, HttpResponse.BodyHandlers.ofString()); + assertThat(response.statusCode()) + .withFailMessage("Expected 200 for %s but got %s with body %s", pathAndQuery, response.statusCode(), response.body()) + .isEqualTo(200); + return JSON.readTree(response.body()); + } + + private JsonNode waitForDatasourceListStatus(String datasourceId, String expectedStatus) throws Exception { + Instant deadline = Instant.now().plusSeconds(15); + JsonNode last = null; + while (Instant.now().isBefore(deadline)) { + last = getJson("/api/v1/datasources"); + if (expectedStatus.equals(statusFor(last, datasourceId))) { + return last; + } + Thread.sleep(250); + } + return last; + } + + private String statusFor(JsonNode datasources, String datasourceId) { + for (JsonNode datasource : datasources) { + if (datasourceId.equals(datasource.path("id").asText())) { + return datasource.path("status").asText(); + } + } + return ""; + } + + private JsonNode transitionEvent(JsonNode transitions, long sequence) { + for (JsonNode transition : transitions) { + JsonNode event = transition.path("event"); + if (event.path("sequenceNumber").asLong() == sequence) { + return event; + } + } + return null; + } + + private static int freePort() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private record RunningSystem(EventLensServer server, PluginManager pluginManager, int port) implements AutoCloseable { + private HttpClient client() { + return HttpClient.newHttpClient(); + } + + private String baseUrl() { + return "http://localhost:" + port; + } + + @Override + public void close() { + server.stop(); + pluginManager.close(); + } + } +} diff --git a/eventlens-api/src/test/java/io/eventlens/api/cache/QueryResultCacheBenchmarkTest.java b/eventlens-api/src/test/java/io/eventlens/api/cache/QueryResultCacheBenchmarkTest.java new file mode 100644 index 0000000..67a46e1 --- /dev/null +++ b/eventlens-api/src/test/java/io/eventlens/api/cache/QueryResultCacheBenchmarkTest.java @@ -0,0 +1,30 @@ +package io.eventlens.api.cache; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +class QueryResultCacheBenchmarkTest { + + @Test + void repeatedQueriesProduceCacheHitRatioAboveFiftyPercent() { + QueryResultCache cache = new QueryResultCache(true, 128); + AtomicInteger supplierCalls = new AtomicInteger(); + + for (int i = 0; i < 100; i++) { + String key = i < 80 ? "timeline:ACC-001" : "timeline:ACC-002"; + cache.getOrCompute("timeline", key, Duration.ofSeconds(30), () -> { + supplierCalls.incrementAndGet(); + return "value:" + key; + }); + } + + assertThat(supplierCalls.get()).isEqualTo(2); + assertThat(cache.hitRatio()) + .withFailMessage("Expected cache hit ratio > 0.50 but was %.2f", cache.hitRatio()) + .isGreaterThan(0.50); + } +} diff --git a/eventlens-api/src/test/java/io/eventlens/api/routes/TimelineMetadataPayloadBenchmarkTest.java b/eventlens-api/src/test/java/io/eventlens/api/routes/TimelineMetadataPayloadBenchmarkTest.java new file mode 100644 index 0000000..ef17096 --- /dev/null +++ b/eventlens-api/src/test/java/io/eventlens/api/routes/TimelineMetadataPayloadBenchmarkTest.java @@ -0,0 +1,68 @@ +package io.eventlens.api.routes; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.eventlens.api.cache.QueryResultCache; +import io.eventlens.core.EventLensConfig; +import io.eventlens.core.model.AggregateTimeline; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.pii.PiiMasker; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class TimelineMetadataPayloadBenchmarkTest { + + private static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); + + @Test + void metadataOnlyShapeReducesSerializedPayloadByAtLeastSeventyPercent() throws Exception { + AggregateTimeline fullTimeline = new AggregateTimeline( + "ACC-001", + "BankAccount", + syntheticEvents(), + syntheticEvents().size()); + + TimelineRoutes routes = new TimelineRoutes( + null, + null, + new PiiMasker(new EventLensConfig.PiiConfig()), + new QueryResultCache(false, 1), + Duration.ofSeconds(1)); + + Method metadataOnly = TimelineRoutes.class.getDeclaredMethod("metadataOnly", AggregateTimeline.class); + metadataOnly.setAccessible(true); + AggregateTimeline metadataTimeline = (AggregateTimeline) metadataOnly.invoke(routes, fullTimeline); + + int fullBytes = MAPPER.writeValueAsString(fullTimeline).getBytes(StandardCharsets.UTF_8).length; + int metadataBytes = MAPPER.writeValueAsString(metadataTimeline).getBytes(StandardCharsets.UTF_8).length; + double reduction = 1.0 - ((double) metadataBytes / (double) fullBytes); + + assertThat(reduction) + .withFailMessage("Expected metadata-only reduction > 0.70 but was %.2f (full=%s bytes, metadata=%s bytes)", reduction, fullBytes, metadataBytes) + .isGreaterThan(0.70); + } + + private static List syntheticEvents() { + String largePayload = "{\"description\":\"" + "x".repeat(4096) + "\",\"nested\":{\"trace\":\"" + "y".repeat(4096) + "\"}}"; + String metadata = "{\"source\":\"benchmark\",\"correlationId\":\"corr-123\"}"; + return java.util.stream.IntStream.rangeClosed(1, 25) + .mapToObj(i -> new StoredEvent( + "evt-" + i, + "ACC-001", + "BankAccount", + i, + i == 1 ? "AccountCreated" : "MoneyDeposited", + largePayload, + metadata, + Instant.parse("2026-01-01T00:00:00Z").plusSeconds(i), + i + )) + .toList(); + } +} diff --git a/eventlens-app/build.gradle.kts b/eventlens-app/build.gradle.kts index 344a2fe..1b71271 100644 --- a/eventlens-app/build.gradle.kts +++ b/eventlens-app/build.gradle.kts @@ -6,8 +6,9 @@ plugins { dependencies { implementation(project(":eventlens-core")) - implementation(project(":eventlens-pg")) - implementation(project(":eventlens-kafka")) + implementation(project(":eventlens-source-postgres")) + implementation(project(":eventlens-source-mysql")) + implementation(project(":eventlens-stream-kafka")) implementation(project(":eventlens-api")) implementation(project(":eventlens-cli")) implementation("ch.qos.logback:logback-classic:1.5.32") @@ -24,7 +25,6 @@ tasks.shadowJar { archiveVersion.set("") mergeServiceFiles() - // Ensure the fat JAR runs with preview features manifest { attributes( "Main-Class" to "io.eventlens.EventLensMain", @@ -33,7 +33,6 @@ tasks.shadowJar { } } -// Make 'build' also produce the shadow jar tasks.named("build") { dependsOn(tasks.shadowJar) } diff --git a/eventlens-cli/build.gradle.kts b/eventlens-cli/build.gradle.kts index 305a8e8..09ce891 100644 --- a/eventlens-cli/build.gradle.kts +++ b/eventlens-cli/build.gradle.kts @@ -1,7 +1,9 @@ dependencies { implementation(project(":eventlens-core")) - implementation(project(":eventlens-pg")) - implementation(project(":eventlens-kafka")) + implementation(project(":eventlens-spi")) + implementation(project(":eventlens-source-postgres")) + implementation(project(":eventlens-source-mysql")) + implementation(project(":eventlens-stream-kafka")) implementation(project(":eventlens-api")) implementation("io.javalin:javalin:7.1.0") implementation("info.picocli:picocli:4.7.7") diff --git a/eventlens-cli/src/main/java/io/eventlens/cli/ServeCommand.java b/eventlens-cli/src/main/java/io/eventlens/cli/ServeCommand.java index 74452e7..f321446 100644 --- a/eventlens-cli/src/main/java/io/eventlens/cli/ServeCommand.java +++ b/eventlens-cli/src/main/java/io/eventlens/cli/ServeCommand.java @@ -1,21 +1,35 @@ package io.eventlens.cli; import io.eventlens.api.EventLensServer; -import io.eventlens.api.websocket.LiveTailWebSocket; -import io.eventlens.core.ConfigValidator; import io.eventlens.core.ConfigLoader; +import io.eventlens.core.ConfigValidator; import io.eventlens.core.EventLensConfig; -import io.eventlens.core.aggregator.*; +import io.eventlens.core.aggregator.ClasspathReducerLoader; +import io.eventlens.core.aggregator.ReducerRegistry; import io.eventlens.core.audit.AuditLogger; -import io.eventlens.core.engine.*; +import io.eventlens.core.engine.AnomalyDetector; +import io.eventlens.core.engine.BisectEngine; +import io.eventlens.core.engine.DiffEngine; +import io.eventlens.core.engine.ExportEngine; +import io.eventlens.core.engine.ReplayEngine; +import io.eventlens.core.plugin.PluginDiscovery; +import io.eventlens.core.plugin.PluginManager; +import io.eventlens.core.spi.EventStoreReader; import io.eventlens.core.spi.ResilientEventStoreReader; -import io.eventlens.kafka.*; -import io.eventlens.pg.*; +import io.eventlens.mysql.MySqlEventSourcePlugin; +import io.eventlens.kafka.KafkaStreamAdapterPlugin; +import io.eventlens.pg.PostgresEventSourcePlugin; +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.spi.StreamAdapterPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import picocli.CommandLine.Command; import picocli.CommandLine.Option; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + @Command(name = "serve", description = "Start the EventLens web server") public class ServeCommand implements Runnable { @@ -50,86 +64,168 @@ public class ServeCommand implements Runnable { @Override public void run() { - EventLensConfig config = configPath != null - ? ConfigLoader.load(configPath) - : ConfigLoader.load(); - - // CLI flags override config file - if (port != null) - config.getServer().setPort(port); - if (dbUrl != null) - config.getDatasource().setUrl(dbUrl); - if (dbUser != null) - config.getDatasource().setUsername(dbUser); - if (dbPassword != null) - config.getDatasource().setPassword(dbPassword); - if (tableName != null) - config.getDatasource().setTable(tableName); + EventLensConfig config = configPath != null ? ConfigLoader.load(configPath) : ConfigLoader.load(); + + if (port != null) config.getServer().setPort(port); + if (dbUrl != null) config.getDatasource().setUrl(dbUrl); + if (dbUser != null) config.getDatasource().setUsername(dbUser); + if (dbPassword != null) config.getDatasource().setPassword(dbPassword); + if (tableName != null) config.getDatasource().setTable(tableName); ConfigValidator.validateOrThrow(config); - var pgConfig = new PgConfig( - config.getDatasource().getUrl(), - config.getDatasource().getUsername(), - config.getDatasource().getPassword(), - config.getDatasource().getTable(), - config.getDatasource().getColumns(), // Fix 1: pass column overrides - config.getDatasource().getPool(), - config.getDatasource().getQueryTimeoutSeconds()); - - var rawReader = new PgEventStoreReader(pgConfig); - var reader = new ResilientEventStoreReader(rawReader); - var registry = new ReducerRegistry(); + PluginManager pluginManager = new PluginManager(config.getPlugins().getHealthCheckIntervalSeconds()); + PluginDiscovery.DiscoveryResult discovered = new PluginDiscovery().discoverFromClasspath() + .merge(new PluginDiscovery().discoverFromDirectory(config.getPlugins().getDirectory())); - // Load custom reducers from classpath JARs + registerDatasources(config, pluginManager, discovered); + registerStreams(config, pluginManager, discovered); + pluginManager.startHealthChecks(); + + String primaryDatasourceId = selectPrimaryDatasourceId(config, pluginManager); + EventStoreReader sourceReader = selectReader(primaryDatasourceId, pluginManager); + var reader = new ResilientEventStoreReader(sourceReader); + var registry = new ReducerRegistry(); if (classpathJars != null && !classpathJars.isEmpty()) { - var loader = new ClasspathReducerLoader(); - loader.loadAll(registry, classpathJars, config.getReplay().getReducers()); + new ClasspathReducerLoader().loadAll(registry, classpathJars, config.getReplay().getReducers()); } var replayEngine = new ReplayEngine(reader, registry); var bisectEngine = new BisectEngine(replayEngine, reader); - var anomalyDetector = new AnomalyDetector(reader, replayEngine, config.getAnomaly()); // Fix 11 + var anomalyDetector = new AnomalyDetector(reader, replayEngine, config.getAnomaly()); var exportEngine = new ExportEngine(reader, replayEngine); var diffEngine = new DiffEngine(replayEngine); - // Kafka is optional — graceful degradation to PG polling - KafkaLiveTail kafkaTail = null; - String brokers = kafkaBrokers != null ? kafkaBrokers - : (config.getKafka() != null ? config.getKafka().getBootstrapServers() : null); - String topic = kafkaTopic != null ? kafkaTopic - : (config.getKafka() != null ? config.getKafka().getTopic() : null); - - if (brokers != null && topic != null) { + var server = new EventLensServer( + config, + reader, + replayEngine, + registry, + pluginManager, + primaryDatasourceId, + bisectEngine, + anomalyDetector, + exportEngine, + diffEngine, + datasourceStreamBindings(config)); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { - kafkaTail = new KafkaLiveTail(new KafkaConfig(brokers, topic)); - log.info("Kafka consumer connected to topic: {}", topic); + pluginManager.close(); } catch (Exception e) { - log.warn("Kafka unavailable ({}) — using PostgreSQL polling fallback", e.getMessage()); + log.warn("Failed to close plugin manager cleanly", e); } - } - - var server = new EventLensServer(config, reader, replayEngine, - bisectEngine, anomalyDetector, exportEngine, diffEngine); - // LiveTailWebSocket is wired and configured inside EventLensServer (v2). - // We still need a reference here for Kafka listener wiring. - var auditLogger = new AuditLogger(config.getAudit().isEnabled()); - var liveTail = new LiveTailWebSocket(reader, auditLogger); - - if (kafkaTail != null) { - kafkaTail.addListener(liveTail::broadcast); - kafkaTail.start(); - } else { - liveTail.startPolling(); - } + }, "eventlens-plugin-shutdown")); server.start(); - // Block the main thread to keep the JVM alive (Javalin runs on daemon threads) try { Thread.currentThread().join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } + + private void registerDatasources(EventLensConfig config, PluginManager pluginManager, PluginDiscovery.DiscoveryResult discovered) { + for (EventLensConfig.DatasourceInstanceConfig ds : config.getDatasourcesOrLegacy()) { + if (ds == null || !ds.isEnabled()) continue; + // Always create a fresh plugin instance per datasource to avoid shared state. + // The discovered list is only used to verify the type is supported. + boolean supported = discovered.eventSources().stream() + .anyMatch(candidate -> ds.getType().equalsIgnoreCase(candidate.typeId())); + EventSourcePlugin plugin = supported || isBuiltinType(ds.getType()) + ? createBuiltinDatasource(ds.getType()) + : discovered.eventSources().stream() + .filter(candidate -> ds.getType().equalsIgnoreCase(candidate.typeId())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Unsupported datasource type: " + ds.getType())); + pluginManager.registerEventSource(ds.getId(), plugin, datasourceConfig(ds)); + } + } + + private boolean isBuiltinType(String type) { + return "postgres".equalsIgnoreCase(type) || "mysql".equalsIgnoreCase(type); + } + + private void registerStreams(EventLensConfig config, PluginManager pluginManager, PluginDiscovery.DiscoveryResult discovered) { + List streams = config.getStreamsOrLegacy(); + for (int i = 0; i < streams.size(); i++) { + EventLensConfig.StreamInstanceConfig stream = streams.get(i); + if (stream == null || !stream.isEnabled()) continue; + if (i == 0 && kafkaBrokers != null) stream.setBootstrapServers(kafkaBrokers); + if (i == 0 && kafkaTopic != null) stream.setTopic(kafkaTopic); + StreamAdapterPlugin plugin = discovered.streamAdapters().stream() + .filter(candidate -> stream.getType().equalsIgnoreCase(candidate.typeId())) + .findFirst() + .orElseGet(() -> createBuiltinStream(stream.getType())); + try { + pluginManager.registerStreamAdapter(stream.getId(), plugin, Map.of( + "bootstrapServers", stream.getBootstrapServers(), + "topic", stream.getTopic())); + } catch (Exception e) { + log.warn("Stream '{}' unavailable ({}). Skipping.", stream.getId(), e.getMessage()); + } + } + } + + private String selectPrimaryDatasourceId(EventLensConfig config, PluginManager pluginManager) { + for (EventLensConfig.DatasourceInstanceConfig ds : config.getDatasourcesOrLegacy()) { + var plugin = pluginManager.getEventSource(ds.getId()).orElse(null); + if (plugin instanceof EventStoreReader) { + return ds.getId(); + } + } + return pluginManager.listByType(io.eventlens.core.plugin.PluginInstance.PluginType.EVENT_SOURCE).stream() + .filter(instance -> instance.plugin() instanceof EventStoreReader) + .findFirst() + .map(io.eventlens.core.plugin.PluginInstance::instanceId) + .orElseThrow(() -> new IllegalStateException("No ready event source plugin found")); + } + + private EventStoreReader selectReader(String datasourceId, PluginManager pluginManager) { + return pluginManager.getEventSource(datasourceId) + .filter(EventStoreReader.class::isInstance) + .map(EventStoreReader.class::cast) + .orElseThrow(() -> new IllegalStateException("No ready event source plugin found for id: " + datasourceId)); + } + + private Map datasourceConfig(EventLensConfig.DatasourceInstanceConfig ds) { + Map sourceConfig = new HashMap<>(); + sourceConfig.put("jdbcUrl", ds.getUrl()); + sourceConfig.put("username", ds.getUsername()); + sourceConfig.put("password", ds.getPassword()); + sourceConfig.put("tableName", ds.getTable()); + sourceConfig.put("columnOverrides", ds.getColumns()); + sourceConfig.put("pool", ds.getPool()); + sourceConfig.put("queryTimeoutSeconds", ds.getQueryTimeoutSeconds()); + return sourceConfig; + } + + private Map datasourceStreamBindings(EventLensConfig config) { + Map bindings = new HashMap<>(); + for (EventLensConfig.DatasourceInstanceConfig datasource : config.getDatasourcesOrLegacy()) { + if (datasource == null || !datasource.isEnabled()) continue; + if (datasource.getStreamId() != null) { + bindings.put(datasource.getId(), datasource.getStreamId()); + } + } + return bindings; + } + + private EventSourcePlugin createBuiltinDatasource(String type) { + return switch (type.toLowerCase()) { + case "postgres" -> new PostgresEventSourcePlugin(); + case "mysql" -> new MySqlEventSourcePlugin(); + default -> throw new IllegalArgumentException("Unsupported datasource type: " + type); + }; + } + + private StreamAdapterPlugin createBuiltinStream(String type) { + return switch (type.toLowerCase()) { + case "kafka" -> new KafkaStreamAdapterPlugin(); + default -> throw new IllegalArgumentException("Unsupported stream type: " + type); + }; + } } + diff --git a/eventlens-core/build.gradle.kts b/eventlens-core/build.gradle.kts index ee490d6..ffc5872 100644 --- a/eventlens-core/build.gradle.kts +++ b/eventlens-core/build.gradle.kts @@ -1,4 +1,5 @@ dependencies { + implementation(project(":eventlens-spi")) implementation("com.fasterxml.jackson.core:jackson-databind:2.21.2") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.2") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.2") diff --git a/eventlens-core/src/main/java/io/eventlens/core/ConfigLoader.java b/eventlens-core/src/main/java/io/eventlens/core/ConfigLoader.java index d766de3..d132b20 100644 --- a/eventlens-core/src/main/java/io/eventlens/core/ConfigLoader.java +++ b/eventlens-core/src/main/java/io/eventlens/core/ConfigLoader.java @@ -1,8 +1,8 @@ package io.eventlens.core; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -15,18 +15,7 @@ import java.io.IOException; /** - * Discovers and loads the EventLens configuration file from well-known - * locations. - * - *

- * Search order: - *

    - *
  1. {@code eventlens.yaml} (working directory)
  2. - *
  3. {@code eventlens.yml} (working directory)
  4. - *
  5. {@code ~/.eventlens/config.yaml}
  6. - *
  7. {@code /etc/eventlens/config.yaml}
  8. - *
- * Falls back to sensible defaults when no file is found. + * Discovers and loads the EventLens configuration file from well-known locations. */ public class ConfigLoader { @@ -56,13 +45,10 @@ public static EventLensConfig load() { } } } - log.warn("No config file found — using defaults. Searched: {}", String.join(", ", CONFIG_PATHS)); + log.warn("No config file found - using defaults. Searched: {}", String.join(", ", CONFIG_PATHS)); return new EventLensConfig(); } - /** - * Load a config from a specific path (e.g. from a --config CLI flag). - */ public static EventLensConfig load(String path) { File file = new File(path); if (!file.exists()) { @@ -83,7 +69,8 @@ private static EventLensConfig readAndInterpolate(File file) throws IOException throw new ConfigurationException("Config file is empty: " + file.getAbsolutePath()); } interpolateNode(root); - return YAML_MAPPER.treeToValue(root, EventLensConfig.class); + JsonNode normalized = ConfigMigrator.normalize(root, log); + return YAML_MAPPER.treeToValue(normalized, EventLensConfig.class); } private static void interpolateNode(JsonNode node) { diff --git a/eventlens-core/src/main/java/io/eventlens/core/ConfigMigrator.java b/eventlens-core/src/main/java/io/eventlens/core/ConfigMigrator.java new file mode 100644 index 0000000..a3327cb --- /dev/null +++ b/eventlens-core/src/main/java/io/eventlens/core/ConfigMigrator.java @@ -0,0 +1,94 @@ +package io.eventlens.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.eventlens.core.exception.ConfigurationException; +import org.slf4j.Logger; + +public final class ConfigMigrator { + + private ConfigMigrator() { + } + + public static JsonNode normalize(JsonNode root, Logger log) { + if (!(root instanceof ObjectNode objectRoot)) { + return root; + } + + boolean hasLegacyDatasource = objectRoot.has("datasource"); + boolean hasPluralDatasources = objectRoot.has("datasources"); + boolean hasLegacyKafka = objectRoot.has("kafka"); + boolean hasPluralStreams = objectRoot.has("streams"); + + if (hasLegacyDatasource && hasPluralDatasources) { + throw new ConfigurationException("Config cannot contain both 'datasource' and 'datasources'. Choose one format."); + } + if (hasLegacyKafka && hasPluralStreams) { + throw new ConfigurationException("Config cannot contain both 'kafka' and 'streams'. Choose one format."); + } + + if (hasLegacyDatasource && !hasPluralDatasources) { + ArrayNode datasources = objectRoot.putArray("datasources"); + ObjectNode migrated = datasources.addObject(); + migrated.put("id", "default"); + migrated.put("type", "postgres"); + copyObjectFields(objectRoot.withObject("datasource"), migrated); + log.warn("Using deprecated v2 'datasource' config format. Migrate to 'datasources[]'."); + } + + if (hasLegacyKafka && !hasPluralStreams) { + ArrayNode streams = objectRoot.putArray("streams"); + ObjectNode migrated = streams.addObject(); + migrated.put("id", "default-kafka"); + migrated.put("type", "kafka"); + copyObjectFields(objectRoot.withObject("kafka"), migrated); + log.warn("Using deprecated v2 'kafka' config format. Migrate to 'streams[]'."); + } + + if (!objectRoot.has("datasource") && hasPluralDatasources && objectRoot.get("datasources").isArray() && !objectRoot.get("datasources").isEmpty()) { + JsonNode first = objectRoot.get("datasources").get(0); + if (first instanceof ObjectNode firstObject) { + ObjectNode legacy = objectRoot.putObject("datasource"); + copyKnownDatasourceFields(firstObject, legacy); + } + } + + if (!objectRoot.has("kafka") && hasPluralStreams && objectRoot.get("streams").isArray()) { + for (JsonNode stream : objectRoot.withArray("streams")) { + if (stream instanceof ObjectNode streamObject && "kafka".equals(streamObject.path("type").asText("kafka"))) { + ObjectNode legacy = objectRoot.putObject("kafka"); + copyKnownStreamFields(streamObject, legacy); + break; + } + } + } + + return objectRoot; + } + + private static void copyObjectFields(ObjectNode from, ObjectNode to) { + from.properties().forEach(entry -> to.set(entry.getKey(), entry.getValue().deepCopy())); + } + + private static void copyKnownDatasourceFields(ObjectNode from, ObjectNode to) { + copyIfPresent(from, to, "url"); + copyIfPresent(from, to, "username"); + copyIfPresent(from, to, "password"); + copyIfPresent(from, to, "table"); + copyIfPresent(from, to, "columns"); + copyIfPresent(from, to, "pool"); + copyIfPresent(from, to, "query-timeout-seconds"); + } + + private static void copyKnownStreamFields(ObjectNode from, ObjectNode to) { + copyIfPresent(from, to, "bootstrap-servers"); + copyIfPresent(from, to, "topic"); + } + + private static void copyIfPresent(ObjectNode from, ObjectNode to, String field) { + if (from.has(field)) { + to.set(field, from.get(field).deepCopy()); + } + } +} diff --git a/eventlens-core/src/main/java/io/eventlens/core/ConfigValidator.java b/eventlens-core/src/main/java/io/eventlens/core/ConfigValidator.java index 96a9ecc..1b81a5d 100644 --- a/eventlens-core/src/main/java/io/eventlens/core/ConfigValidator.java +++ b/eventlens-core/src/main/java/io/eventlens/core/ConfigValidator.java @@ -30,7 +30,6 @@ public static List validate(EventLensConfig config) { return issues; } - // --- Server --- var server = config.getServer(); if (server == null) { issues.add(error("server", "Required")); @@ -44,7 +43,6 @@ public static List validate(EventLensConfig config) { issues.add(warning("server.allowed-origins", "Empty allowlist will block browser access")); } - // --- Auth --- var auth = server.getAuth(); if (auth == null) { issues.add(error("server.auth", "Required")); @@ -75,16 +73,13 @@ public static List validate(EventLensConfig config) { } } - // --- Rate limiting --- if (server != null && server.getSecurity() != null && server.getSecurity().getRateLimit() != null) { var rl = server.getSecurity().getRateLimit(); if (rl.getRequestsPerMinute() < 1 || rl.getRequestsPerMinute() > 10_000) { - issues.add(error("server.security.rate-limit.requests-per-minute", - "Must be between 1 and 10000")); + issues.add(error("server.security.rate-limit.requests-per-minute", "Must be between 1 and 10000")); } if (rl.getBurst() < 1 || rl.getBurst() > 10_000) { - issues.add(error("server.security.rate-limit.burst", - "Must be between 1 and 10000")); + issues.add(error("server.security.rate-limit.burst", "Must be between 1 and 10000")); } if (rl.getBurst() > rl.getRequestsPerMinute() * 10L) { issues.add(warning("server.security.rate-limit.burst", @@ -92,20 +87,80 @@ public static List validate(EventLensConfig config) { } } - // --- Datasource --- + validateLegacyDatasource(config, issues); + validateDatasourceInstances(config.getDatasourcesOrLegacy(), issues); + validateStreamInstances(config.getStreamsOrLegacy(), issues); + + var anomaly = config.getAnomaly(); + if (anomaly != null && anomaly.getRules() != null) { + for (int i = 0; i < anomaly.getRules().size(); i++) { + var rule = anomaly.getRules().get(i); + String base = "anomaly.rules[%d]".formatted(i); + if (rule == null) { + issues.add(error(base, "Rule is null")); + continue; + } + if (isBlank(rule.getCode())) { + issues.add(error(base + ".code", "Required")); + } + if (isBlank(rule.getCondition())) { + issues.add(error(base + ".condition", "Required for rule '%s'".formatted(rule.getCode()))); + } else { + try { + BisectEngine.parseCondition(rule.getCondition()); + } catch (Exception e) { + issues.add(error(base + ".condition", + "Unparseable condition for rule '%s': %s".formatted(rule.getCode(), e.getMessage()))); + } + } + } + } + + return issues; + } + + private static void validateLegacyDatasource(EventLensConfig config, List issues) { var ds = config.getDatasource(); if (ds == null) { issues.add(error("datasource", "Required")); - } else { + return; + } + if (isBlank(ds.getUrl())) { + issues.add(error("datasource.url", "Required")); + } else if (!ds.getUrl().startsWith("jdbc:postgresql://")) { + String prefix = ds.getUrl().substring(0, Math.min(30, ds.getUrl().length())); + issues.add(error("datasource.url", "Must be a PostgreSQL JDBC URL (got: %s...)".formatted(prefix))); + } + if (isBlank(ds.getUsername())) { + issues.add(error("datasource.username", "Required")); + } + } + + private static void validateDatasourceInstances(List datasources, List issues) { + if (datasources == null || datasources.isEmpty()) { + issues.add(error("datasources", "At least one datasource is required")); + return; + } + for (int i = 0; i < datasources.size(); i++) { + var ds = datasources.get(i); + String base = "datasources[%d]".formatted(i); + if (ds == null) { + issues.add(error(base, "Datasource is null")); + continue; + } + if (isBlank(ds.getId())) { + issues.add(error(base + ".id", "Required")); + } + if (isBlank(ds.getType())) { + issues.add(error(base + ".type", "Required")); + } if (isBlank(ds.getUrl())) { - issues.add(error("datasource.url", "Required")); - } else if (!ds.getUrl().startsWith("jdbc:postgresql://")) { - String prefix = ds.getUrl().substring(0, Math.min(30, ds.getUrl().length())); - issues.add(error("datasource.url", - "Must be a PostgreSQL JDBC URL (got: %s...)".formatted(prefix))); + issues.add(error(base + ".url", "Required")); + } else if (!isSupportedDatasourceUrl(ds.getType(), ds.getUrl())) { + issues.add(error(base + ".url", "URL must match datasource type '%s'".formatted(ds.getType()))); } if (isBlank(ds.getUsername())) { - issues.add(error("datasource.username", "Required")); + issues.add(error(base + ".username", "Required")); } if (ds.getColumns() != null && ds.getColumns().hasAnyOverride()) { try { @@ -121,73 +176,71 @@ public static List validate(EventLensConfig config) { Map.entry("global-position", ds.getColumns().getGlobalPosition()) )); } catch (ConfigurationException e) { - issues.add(error("datasource.columns", e.getMessage())); - } - } - - // --- HikariCP pool sizing (elastic pooling) --- - var pool = ds.getPool(); - if (pool != null) { - if (pool.getMaximumPoolSize() < 1 || pool.getMaximumPoolSize() > 200) { - issues.add(error("datasource.pool.maximum-pool-size", "Must be between 1 and 200")); - } - if (pool.getMinimumIdle() < 0 || pool.getMinimumIdle() > 200) { - issues.add(error("datasource.pool.minimum-idle", "Must be between 0 and 200")); - } - if (pool.getMinimumIdle() > pool.getMaximumPoolSize()) { - issues.add(error("datasource.pool.minimum-idle", - "Must be <= maximum-pool-size")); - } else if (pool.getMinimumIdle() == pool.getMaximumPoolSize() && pool.getMaximumPoolSize() > 10) { - issues.add(warning("datasource.pool.minimum-idle", - "minimum-idle equals maximum-pool-size; this keeps all connections warm but can waste RAM. Consider elastic pooling (e.g. minimum-idle=5, maximum-pool-size=50).")); + issues.add(error(base + ".columns", e.getMessage())); } } - + validatePool(base + ".pool", ds.getPool(), issues); if (ds.getQueryTimeoutSeconds() < 1 || ds.getQueryTimeoutSeconds() > 600) { - issues.add(error("datasource.query-timeout-seconds", "Must be between 1 and 600")); + issues.add(error(base + ".query-timeout-seconds", "Must be between 1 and 600")); } } + } - // --- Kafka (optional) --- - var kafka = config.getKafka(); - if (kafka != null) { - if (isBlank(kafka.getBootstrapServers())) { - issues.add(error("kafka.bootstrap-servers", "Required when kafka section is present")); + private static void validateStreamInstances(List streams, List issues) { + if (streams == null) { + return; + } + for (int i = 0; i < streams.size(); i++) { + var stream = streams.get(i); + String base = "streams[%d]".formatted(i); + if (stream == null) { + issues.add(error(base, "Stream is null")); + continue; } - if (isBlank(kafka.getTopic())) { - issues.add(error("kafka.topic", "Required when kafka section is present")); + if (isBlank(stream.getId())) { + issues.add(error(base + ".id", "Required")); } - } - - // --- Anomaly rules --- - var anomaly = config.getAnomaly(); - if (anomaly != null && anomaly.getRules() != null) { - for (int i = 0; i < anomaly.getRules().size(); i++) { - var rule = anomaly.getRules().get(i); - String base = "anomaly.rules[%d]".formatted(i); - if (rule == null) { - issues.add(error(base, "Rule is null")); - continue; + if (isBlank(stream.getType())) { + issues.add(error(base + ".type", "Required")); + } + if ("kafka".equals(stream.getType())) { + if (isBlank(stream.getBootstrapServers())) { + issues.add(error(base + ".bootstrap-servers", "Required for kafka streams")); } - if (isBlank(rule.getCode())) { - issues.add(error(base + ".code", "Required")); - } - if (isBlank(rule.getCondition())) { - issues.add(error(base + ".condition", - "Required for rule '%s'".formatted(rule.getCode()))); - } else { - try { - BisectEngine.parseCondition(rule.getCondition()); - } catch (Exception e) { - issues.add(error(base + ".condition", - "Unparseable condition for rule '%s': %s" - .formatted(rule.getCode(), e.getMessage()))); - } + if (isBlank(stream.getTopic())) { + issues.add(error(base + ".topic", "Required for kafka streams")); } } } + } - return issues; + private static void validatePool(String base, EventLensConfig.PoolConfig pool, List issues) { + if (pool == null) { + return; + } + if (pool.getMaximumPoolSize() < 1 || pool.getMaximumPoolSize() > 200) { + issues.add(error(base + ".maximum-pool-size", "Must be between 1 and 200")); + } + if (pool.getMinimumIdle() < 0 || pool.getMinimumIdle() > 200) { + issues.add(error(base + ".minimum-idle", "Must be between 0 and 200")); + } + if (pool.getMinimumIdle() > pool.getMaximumPoolSize()) { + issues.add(error(base + ".minimum-idle", "Must be <= maximum-pool-size")); + } else if (pool.getMinimumIdle() == pool.getMaximumPoolSize() && pool.getMaximumPoolSize() > 10) { + issues.add(warning(base + ".minimum-idle", + "minimum-idle equals maximum-pool-size; this keeps all connections warm but can waste RAM. Consider elastic pooling (e.g. minimum-idle=5, maximum-pool-size=50).")); + } + } + + private static boolean isSupportedDatasourceUrl(String type, String url) { + if (type == null || url == null) { + return false; + } + return switch (type.toLowerCase()) { + case "postgres" -> url.startsWith("jdbc:postgresql://"); + case "mysql" -> url.startsWith("jdbc:mysql://"); + default -> url.startsWith("jdbc:"); + }; } public static void validateOrThrow(EventLensConfig config) { @@ -198,25 +251,15 @@ public static void validateOrThrow(EventLensConfig config) { StringBuilder sb = new StringBuilder(); sb.append("EventLens configuration validation failed.\n\n"); for (ValidationError i : issues) { - String prefix = i.severity() == ValidationError.Severity.ERROR ? "✗" : "⚠"; + String prefix = i.severity() == ValidationError.Severity.ERROR ? "x" : "!"; sb.append(" ").append(prefix).append(" ").append(i.path()).append(": ").append(i.message()).append("\n"); } sb.append("\n").append(errors).append(" error(s). EventLens will not start.\n"); sb.append("Fix the errors above in your eventlens.yaml and restart.\n"); - throw new ConfigurationException(sb.toString()); } - private static ValidationError error(String path, String message) { - return new ValidationError(path, message, ValidationError.Severity.ERROR); - } - - private static ValidationError warning(String path, String message) { - return new ValidationError(path, message, ValidationError.Severity.WARNING); - } - - private static boolean isBlank(String s) { - return s == null || s.isBlank(); - } + private static ValidationError error(String path, String message) { return new ValidationError(path, message, ValidationError.Severity.ERROR); } + private static ValidationError warning(String path, String message) { return new ValidationError(path, message, ValidationError.Severity.WARNING); } + private static boolean isBlank(String s) { return s == null || s.isBlank(); } } - diff --git a/eventlens-core/src/main/java/io/eventlens/core/EventLensConfig.java b/eventlens-core/src/main/java/io/eventlens/core/EventLensConfig.java index 1e62caa..4f04cdf 100644 --- a/eventlens-core/src/main/java/io/eventlens/core/EventLensConfig.java +++ b/eventlens-core/src/main/java/io/eventlens/core/EventLensConfig.java @@ -11,99 +11,81 @@ public class EventLensConfig { private ServerConfig server = new ServerConfig(); private DatasourceConfig datasource = new DatasourceConfig(); - private KafkaConfig kafka; // null = Kafka disabled + private KafkaConfig kafka; + private List datasources = List.of(); + private List streams = List.of(); private ReplayConfig replay = new ReplayConfig(); private AnomalyConfig anomaly = new AnomalyConfig(); private UiConfig ui = new UiConfig(); private AuditConfig audit = new AuditConfig(); private DataProtectionConfig dataProtection = new DataProtectionConfig(); private ExportConfig export = new ExportConfig(); + private PluginsConfig plugins = new PluginsConfig(); + private QueryCacheConfig queryCache = new QueryCacheConfig(); private String version = "2.0.0"; - // ── Getters / Setters ────────────────────────────────────────────── + public ServerConfig getServer() { return server; } + public void setServer(ServerConfig server) { this.server = server; } - public ServerConfig getServer() { - return server; - } - - public void setServer(ServerConfig s) { - this.server = s; - } - - public DatasourceConfig getDatasource() { - return datasource; - } - - public void setDatasource(DatasourceConfig d) { - this.datasource = d; - } - - public KafkaConfig getKafka() { - return kafka; - } - - public void setKafka(KafkaConfig k) { - this.kafka = k; - } + public DatasourceConfig getDatasource() { return datasource; } + public void setDatasource(DatasourceConfig datasource) { this.datasource = datasource; } - public ReplayConfig getReplay() { - return replay; - } + public KafkaConfig getKafka() { return kafka; } + public void setKafka(KafkaConfig kafka) { this.kafka = kafka; } - public void setReplay(ReplayConfig r) { - this.replay = r; - } + public List getDatasources() { return datasources; } + public void setDatasources(List datasources) { this.datasources = datasources == null ? List.of() : datasources; } - public AnomalyConfig getAnomaly() { - return anomaly; - } + public List getStreams() { return streams; } + public void setStreams(List streams) { this.streams = streams == null ? List.of() : streams; } - public void setAnomaly(AnomalyConfig a) { - this.anomaly = a; - } + public ReplayConfig getReplay() { return replay; } + public void setReplay(ReplayConfig replay) { this.replay = replay; } - public UiConfig getUi() { - return ui; - } + public AnomalyConfig getAnomaly() { return anomaly; } + public void setAnomaly(AnomalyConfig anomaly) { this.anomaly = anomaly; } - public void setUi(UiConfig u) { - this.ui = u; - } + public UiConfig getUi() { return ui; } + public void setUi(UiConfig ui) { this.ui = ui; } - public AuditConfig getAudit() { - return audit; - } + public AuditConfig getAudit() { return audit; } + public void setAudit(AuditConfig audit) { this.audit = audit; } - public void setAudit(AuditConfig a) { - this.audit = a; - } + public DataProtectionConfig getDataProtection() { return dataProtection; } + public void setDataProtection(DataProtectionConfig dataProtection) { this.dataProtection = dataProtection; } - public DataProtectionConfig getDataProtection() { - return dataProtection; - } + public ExportConfig getExport() { return export; } + public void setExport(ExportConfig export) { this.export = export; } - public void setDataProtection(DataProtectionConfig dp) { - this.dataProtection = dp; - } + public PluginsConfig getPlugins() { return plugins; } + public void setPlugins(PluginsConfig plugins) { this.plugins = plugins; } - public ExportConfig getExport() { - return export; - } + public QueryCacheConfig getQueryCache() { return queryCache; } + public void setQueryCache(QueryCacheConfig queryCache) { this.queryCache = queryCache; } - public void setExport(ExportConfig export) { - this.export = export; - } + public String getVersion() { return version; } + public void setVersion(String version) { this.version = version; } - public String getVersion() { - return version; + public List getDatasourcesOrLegacy() { + if (datasources != null && !datasources.isEmpty()) { + return datasources; + } + if (datasource == null || datasource.getUrl() == null || datasource.getUrl().isBlank()) { + return List.of(); + } + return List.of(DatasourceInstanceConfig.fromLegacy("default", "postgres", datasource)); } - public void setVersion(String version) { - this.version = version; + public List getStreamsOrLegacy() { + if (streams != null && !streams.isEmpty()) { + return streams; + } + if (kafka == null || kafka.getBootstrapServers() == null || kafka.getBootstrapServers().isBlank()) { + return List.of(); + } + return List.of(StreamInstanceConfig.fromLegacy("default-kafka", "kafka", kafka)); } - // ── Nested configs ───────────────────────────────────────────────── - public static class ServerConfig { private int port = 9090; private List allowedOrigins = List.of("http://localhost:5173", "http://localhost:9090"); @@ -111,182 +93,100 @@ public static class ServerConfig { private SecurityConfig security = new SecurityConfig(); private int corsMaxAgeSeconds = 600; - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public List getAllowedOrigins() { - return allowedOrigins; - } - - public void setAllowedOrigins(List o) { - this.allowedOrigins = o; - } - - public AuthConfig getAuth() { - return auth; - } - - public void setAuth(AuthConfig auth) { - this.auth = auth; - } - - public SecurityConfig getSecurity() { - return security; - } - - public void setSecurity(SecurityConfig security) { - this.security = security; - } - - public int getCorsMaxAgeSeconds() { - return corsMaxAgeSeconds; - } - - public void setCorsMaxAgeSeconds(int corsMaxAgeSeconds) { - this.corsMaxAgeSeconds = corsMaxAgeSeconds; - } + public int getPort() { return port; } + public void setPort(int port) { this.port = port; } + public List getAllowedOrigins() { return allowedOrigins; } + public void setAllowedOrigins(List allowedOrigins) { this.allowedOrigins = allowedOrigins; } + public AuthConfig getAuth() { return auth; } + public void setAuth(AuthConfig auth) { this.auth = auth; } + public SecurityConfig getSecurity() { return security; } + public void setSecurity(SecurityConfig security) { this.security = security; } + public int getCorsMaxAgeSeconds() { return corsMaxAgeSeconds; } + public void setCorsMaxAgeSeconds(int corsMaxAgeSeconds) { this.corsMaxAgeSeconds = corsMaxAgeSeconds; } } public static class AuthConfig { private boolean enabled = false; private String username = "admin"; private String password = "changeme"; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getUsername() { - return username; - } - - public void setUsername(String u) { - this.username = u; - } - - public String getPassword() { - return password; - } - - public void setPassword(String p) { - this.password = p; - } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } } public static class SecurityConfig { private RateLimitConfig rateLimit = new RateLimitConfig(); - - public RateLimitConfig getRateLimit() { - return rateLimit; - } - - public void setRateLimit(RateLimitConfig rateLimit) { - this.rateLimit = rateLimit; - } + public RateLimitConfig getRateLimit() { return rateLimit; } + public void setRateLimit(RateLimitConfig rateLimit) { this.rateLimit = rateLimit; } } public static class RateLimitConfig { private boolean enabled = false; private int requestsPerMinute = 120; private int burst = 20; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public int getRequestsPerMinute() { - return requestsPerMinute; - } - - public void setRequestsPerMinute(int requestsPerMinute) { - this.requestsPerMinute = requestsPerMinute; - } - - public int getBurst() { - return burst; - } - - public void setBurst(int burst) { - this.burst = burst; - } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public int getRequestsPerMinute() { return requestsPerMinute; } + public void setRequestsPerMinute(int requestsPerMinute) { this.requestsPerMinute = requestsPerMinute; } + public int getBurst() { return burst; } + public void setBurst(int burst) { this.burst = burst; } } public static class DatasourceConfig { private String url = "jdbc:postgresql://localhost:5432/eventlens_dev"; private String username = "postgres"; private String password = ""; - private String table; // null = auto-detect + private String table; private ColumnMappingConfig columns = new ColumnMappingConfig(); private PoolConfig pool = new PoolConfig(); private int queryTimeoutSeconds = 30; - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getUsername() { - return username; - } - - public void setUsername(String u) { - this.username = u; - } - - public String getPassword() { - return password; - } - - public void setPassword(String p) { - this.password = p; - } - - public String getTable() { - return table; - } - - public void setTable(String table) { - this.table = table; - } - - public ColumnMappingConfig getColumns() { - return columns; - } - - public void setColumns(ColumnMappingConfig columns) { - this.columns = columns; - } - - public PoolConfig getPool() { - return pool; - } - - public void setPool(PoolConfig pool) { - this.pool = pool; - } - - public int getQueryTimeoutSeconds() { - return queryTimeoutSeconds; - } + public String getUrl() { return url; } + public void setUrl(String url) { this.url = url; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getTable() { return table; } + public void setTable(String table) { this.table = table; } + public ColumnMappingConfig getColumns() { return columns; } + public void setColumns(ColumnMappingConfig columns) { this.columns = columns; } + public PoolConfig getPool() { return pool; } + public void setPool(PoolConfig pool) { this.pool = pool; } + public int getQueryTimeoutSeconds() { return queryTimeoutSeconds; } + public void setQueryTimeoutSeconds(int queryTimeoutSeconds) { this.queryTimeoutSeconds = queryTimeoutSeconds; } + } + + public static class DatasourceInstanceConfig extends DatasourceConfig { + private String id = "default"; + private String type = "postgres"; + private String streamId; + private boolean enabled = true; - public void setQueryTimeoutSeconds(int queryTimeoutSeconds) { - this.queryTimeoutSeconds = queryTimeoutSeconds; + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getStreamId() { return streamId; } + public void setStreamId(String streamId) { this.streamId = streamId; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public static DatasourceInstanceConfig fromLegacy(String id, String type, DatasourceConfig legacy) { + DatasourceInstanceConfig config = new DatasourceInstanceConfig(); + config.setId(id); + config.setType(type); + config.setUrl(legacy.getUrl()); + config.setUsername(legacy.getUsername()); + config.setPassword(legacy.getPassword()); + config.setTable(legacy.getTable()); + config.setColumns(legacy.getColumns()); + config.setPool(legacy.getPool()); + config.setQueryTimeoutSeconds(legacy.getQueryTimeoutSeconds()); + return config; } } @@ -297,121 +197,59 @@ public static class PoolConfig { private long idleTimeoutMs = 300_000; private long maxLifetimeMs = 900_000; private long leakDetectionThresholdMs = 30_000; - - public int getMaximumPoolSize() { - return maximumPoolSize; - } - - public void setMaximumPoolSize(int maximumPoolSize) { - this.maximumPoolSize = maximumPoolSize; - } - - public int getMinimumIdle() { - return minimumIdle; - } - - public void setMinimumIdle(int minimumIdle) { - this.minimumIdle = minimumIdle; - } - - public long getConnectionTimeoutMs() { - return connectionTimeoutMs; - } - - public void setConnectionTimeoutMs(long connectionTimeoutMs) { - this.connectionTimeoutMs = connectionTimeoutMs; - } - - public long getIdleTimeoutMs() { - return idleTimeoutMs; - } - - public void setIdleTimeoutMs(long idleTimeoutMs) { - this.idleTimeoutMs = idleTimeoutMs; - } - - public long getMaxLifetimeMs() { - return maxLifetimeMs; - } - - public void setMaxLifetimeMs(long maxLifetimeMs) { - this.maxLifetimeMs = maxLifetimeMs; - } - - public long getLeakDetectionThresholdMs() { - return leakDetectionThresholdMs; - } - - public void setLeakDetectionThresholdMs(long leakDetectionThresholdMs) { - this.leakDetectionThresholdMs = leakDetectionThresholdMs; - } + public int getMaximumPoolSize() { return maximumPoolSize; } + public void setMaximumPoolSize(int maximumPoolSize) { this.maximumPoolSize = maximumPoolSize; } + public int getMinimumIdle() { return minimumIdle; } + public void setMinimumIdle(int minimumIdle) { this.minimumIdle = minimumIdle; } + public long getConnectionTimeoutMs() { return connectionTimeoutMs; } + public void setConnectionTimeoutMs(long connectionTimeoutMs) { this.connectionTimeoutMs = connectionTimeoutMs; } + public long getIdleTimeoutMs() { return idleTimeoutMs; } + public void setIdleTimeoutMs(long idleTimeoutMs) { this.idleTimeoutMs = idleTimeoutMs; } + public long getMaxLifetimeMs() { return maxLifetimeMs; } + public void setMaxLifetimeMs(long maxLifetimeMs) { this.maxLifetimeMs = maxLifetimeMs; } + public long getLeakDetectionThresholdMs() { return leakDetectionThresholdMs; } + public void setLeakDetectionThresholdMs(long leakDetectionThresholdMs) { this.leakDetectionThresholdMs = leakDetectionThresholdMs; } + } + + public static class PluginsConfig { + private String directory = "./plugins"; + private int healthCheckIntervalSeconds = 30; + public String getDirectory() { return directory; } + public void setDirectory(String directory) { this.directory = directory; } + public int getHealthCheckIntervalSeconds() { return healthCheckIntervalSeconds; } + public void setHealthCheckIntervalSeconds(int healthCheckIntervalSeconds) { this.healthCheckIntervalSeconds = healthCheckIntervalSeconds; } } - // ── 2.6 Async Export ───────────────────────────────────────────────── - public static class ExportConfig { private String directory = "./exports"; private int maxConcurrent = 2; private int maxEventsPerExport = 100_000; private int expireAfterSeconds = 3_600; + public String getDirectory() { return directory; } + public void setDirectory(String directory) { this.directory = directory; } + public int getMaxConcurrent() { return maxConcurrent; } + public void setMaxConcurrent(int maxConcurrent) { this.maxConcurrent = maxConcurrent; } + public int getMaxEventsPerExport() { return maxEventsPerExport; } + public void setMaxEventsPerExport(int maxEventsPerExport) { this.maxEventsPerExport = maxEventsPerExport; } + public int getExpireAfterSeconds() { return expireAfterSeconds; } + public void setExpireAfterSeconds(int expireAfterSeconds) { this.expireAfterSeconds = expireAfterSeconds; } + } - public String getDirectory() { - return directory; - } - - public void setDirectory(String directory) { - this.directory = directory; - } - - public int getMaxConcurrent() { - return maxConcurrent; - } - - public void setMaxConcurrent(int maxConcurrent) { - this.maxConcurrent = maxConcurrent; - } - - public int getMaxEventsPerExport() { - return maxEventsPerExport; - } - - public void setMaxEventsPerExport(int maxEventsPerExport) { - this.maxEventsPerExport = maxEventsPerExport; - } - - public int getExpireAfterSeconds() { - return expireAfterSeconds; - } - - public void setExpireAfterSeconds(int expireAfterSeconds) { - this.expireAfterSeconds = expireAfterSeconds; - } + public static class QueryCacheConfig { + private boolean enabled = true; + private int maxEntries = 512; + private int searchTtlSeconds = 15; + private int timelineTtlSeconds = 10; + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public int getMaxEntries() { return maxEntries; } + public void setMaxEntries(int maxEntries) { this.maxEntries = maxEntries; } + public int getSearchTtlSeconds() { return searchTtlSeconds; } + public void setSearchTtlSeconds(int searchTtlSeconds) { this.searchTtlSeconds = searchTtlSeconds; } + public int getTimelineTtlSeconds() { return timelineTtlSeconds; } + public void setTimelineTtlSeconds(int timelineTtlSeconds) { this.timelineTtlSeconds = timelineTtlSeconds; } } - /** - * Fix 1: Explicit column name overrides for projects whose event store schema - * does not match EventLens's auto-detection candidates. - * - *

- * Usage in eventlens.yaml: - * - *

-     * datasource:
-     *   table: my_events
-     *   columns:
-     *     event-id: uid              # default: event_id / id / uid
-     *     aggregate-id: account_id  # default: aggregate_id / stream_id / entity_id
-     *     aggregate-type: kind       # default: aggregate_type / type (optional)
-     *     sequence: revision         # default: sequence_number / version / seq
-     *     event-type: event_name     # default: event_type / type_name
-     *     payload: body              # default: payload / data / event_data
-     *     metadata: headers          # default: metadata / meta (optional)
-     *     timestamp: created_at      # default: timestamp / occurred_at / created_at
-     *     global-position: log_seq   # default: global_position / global_seq (optional)
-     * 
- * - * Any field left null falls back to auto-detection from the table metadata. - */ public static class ColumnMappingConfig { private String eventId; private String aggregateId; @@ -422,159 +260,78 @@ public static class ColumnMappingConfig { private String metadata; private String timestamp; private String globalPosition; - - public String getEventId() { - return eventId; - } - - public void setEventId(String v) { - this.eventId = v; - } - - public String getAggregateId() { - return aggregateId; - } - - public void setAggregateId(String v) { - this.aggregateId = v; - } - - public String getAggregateType() { - return aggregateType; - } - - public void setAggregateType(String v) { - this.aggregateType = v; - } - - public String getSequence() { - return sequence; - } - - public void setSequence(String v) { - this.sequence = v; - } - - public String getEventType() { - return eventType; - } - - public void setEventType(String v) { - this.eventType = v; - } - - public String getPayload() { - return payload; - } - - public void setPayload(String v) { - this.payload = v; - } - - public String getMetadata() { - return metadata; - } - - public void setMetadata(String v) { - this.metadata = v; - } - - public String getTimestamp() { - return timestamp; - } - - public void setTimestamp(String v) { - this.timestamp = v; - } - - public String getGlobalPosition() { - return globalPosition; - } - - public void setGlobalPosition(String v) { - this.globalPosition = v; - } - - /** Returns true if any column override has been set by the user. */ + public String getEventId() { return eventId; } + public void setEventId(String eventId) { this.eventId = eventId; } + public String getAggregateId() { return aggregateId; } + public void setAggregateId(String aggregateId) { this.aggregateId = aggregateId; } + public String getAggregateType() { return aggregateType; } + public void setAggregateType(String aggregateType) { this.aggregateType = aggregateType; } + public String getSequence() { return sequence; } + public void setSequence(String sequence) { this.sequence = sequence; } + public String getEventType() { return eventType; } + public void setEventType(String eventType) { this.eventType = eventType; } + public String getPayload() { return payload; } + public void setPayload(String payload) { this.payload = payload; } + public String getMetadata() { return metadata; } + public void setMetadata(String metadata) { this.metadata = metadata; } + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } + public String getGlobalPosition() { return globalPosition; } + public void setGlobalPosition(String globalPosition) { this.globalPosition = globalPosition; } public boolean hasAnyOverride() { - return eventId != null || aggregateId != null || aggregateType != null - || sequence != null || eventType != null || payload != null - || metadata != null || timestamp != null || globalPosition != null; + return eventId != null || aggregateId != null || aggregateType != null || sequence != null + || eventType != null || payload != null || metadata != null || timestamp != null || globalPosition != null; } } public static class KafkaConfig { private String bootstrapServers; private String topic = "domain-events"; + public String getBootstrapServers() { return bootstrapServers; } + public void setBootstrapServers(String bootstrapServers) { this.bootstrapServers = bootstrapServers; } + public String getTopic() { return topic; } + public void setTopic(String topic) { this.topic = topic; } + } - public String getBootstrapServers() { - return bootstrapServers; - } - - public void setBootstrapServers(String b) { - this.bootstrapServers = b; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; + public static class StreamInstanceConfig extends KafkaConfig { + private String id = "default-kafka"; + private String type = "kafka"; + private boolean enabled = true; + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public static StreamInstanceConfig fromLegacy(String id, String type, KafkaConfig legacy) { + StreamInstanceConfig config = new StreamInstanceConfig(); + config.setId(id); + config.setType(type); + config.setBootstrapServers(legacy.getBootstrapServers()); + config.setTopic(legacy.getTopic()); + return config; } } public static class ReplayConfig { private String defaultReducer = "generic"; private Map reducers = Map.of(); - - public String getDefaultReducer() { - return defaultReducer; - } - - public void setDefaultReducer(String d) { - this.defaultReducer = d; - } - - public Map getReducers() { - return reducers; - } - - public void setReducers(Map r) { - this.reducers = r; - } + public String getDefaultReducer() { return defaultReducer; } + public void setDefaultReducer(String defaultReducer) { this.defaultReducer = defaultReducer; } + public Map getReducers() { return reducers; } + public void setReducers(Map reducers) { this.reducers = reducers; } } public static class AnomalyConfig { private int scanIntervalSeconds = 60; - // Fix 11: cap how many aggregates scanRecent() will process to prevent O(n²) - // blowup private int maxAggregatesPerScan = 20; private List rules = List.of(); - - public int getScanIntervalSeconds() { - return scanIntervalSeconds; - } - - public void setScanIntervalSeconds(int s) { - this.scanIntervalSeconds = s; - } - - public int getMaxAggregatesPerScan() { - return maxAggregatesPerScan; - } - - public void setMaxAggregatesPerScan(int v) { - this.maxAggregatesPerScan = v; - } - - public List getRules() { - return rules; - } - - public void setRules(List r) { - this.rules = r; - } + public int getScanIntervalSeconds() { return scanIntervalSeconds; } + public void setScanIntervalSeconds(int scanIntervalSeconds) { this.scanIntervalSeconds = scanIntervalSeconds; } + public int getMaxAggregatesPerScan() { return maxAggregatesPerScan; } + public void setMaxAggregatesPerScan(int maxAggregatesPerScan) { this.maxAggregatesPerScan = maxAggregatesPerScan; } + public List getRules() { return rules; } + public void setRules(List rules) { this.rules = rules; } } public static class AnomalyRuleConfig { @@ -582,116 +339,47 @@ public static class AnomalyRuleConfig { private String condition; private String severity = "MEDIUM"; private String description; - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - public String getCondition() { - return condition; - } - - public void setCondition(String c) { - this.condition = c; - } - - public String getSeverity() { - return severity; - } - - public void setSeverity(String s) { - this.severity = s; - } - - public String getDescription() { - return description; - } - - public void setDescription(String d) { - this.description = d; - } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + public String getCondition() { return condition; } + public void setCondition(String condition) { this.condition = condition; } + public String getSeverity() { return severity; } + public void setSeverity(String severity) { this.severity = severity; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } } public static class UiConfig { private String theme = "dark"; - - public String getTheme() { - return theme; - } - - public void setTheme(String theme) { - this.theme = theme; - } + public String getTheme() { return theme; } + public void setTheme(String theme) { this.theme = theme; } } - // ── 1.8 Audit Logging ─────────────────────────────────────────────── - public static class AuditConfig { - /** Enable writing structured audit entries to logs/audit.log. */ private boolean enabled = true; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } } - // ── 1.9 PII Masking ───────────────────────────────────────────────── - public static class DataProtectionConfig { private PiiConfig pii = new PiiConfig(); - - public PiiConfig getPii() { - return pii; - } - - public void setPii(PiiConfig pii) { - this.pii = pii; - } + public PiiConfig getPii() { return pii; } + public void setPii(PiiConfig pii) { this.pii = pii; } } public static class PiiConfig { - /** Enable PII masking on event payloads in API responses. */ private boolean enabled = false; private List patterns = defaultPatterns(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public List getPatterns() { - return patterns; - } - - public void setPatterns(List patterns) { - this.patterns = patterns; - } - + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public List getPatterns() { return patterns; } + public void setPatterns(List patterns) { this.patterns = patterns; } private static List defaultPatterns() { var list = new ArrayList(); - list.add(new PiiPatternConfig("email", - "[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}", - "***@***.***")); - list.add(new PiiPatternConfig("credit-card", - "\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b", - "****-****-****-****")); - list.add(new PiiPatternConfig("phone", - "\\+?[1-9]\\d{7,14}", - "***-***-****")); - list.add(new PiiPatternConfig("ssn", - "\\d{3}-\\d{2}-\\d{4}", - "***-**-****")); + list.add(new PiiPatternConfig("email", "[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}", "***@***.***")); + list.add(new PiiPatternConfig("credit-card", "\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b", "****-****-****-****")); + list.add(new PiiPatternConfig("phone", "\\+?[1-9]\\d{7,14}", "***-***-****")); + list.add(new PiiPatternConfig("ssn", "\\d{3}-\\d{2}-\\d{4}", "***-**-****")); return list; } } @@ -700,23 +388,18 @@ public static class PiiPatternConfig { private String name; private String regex; private String mask; - - /** No-arg constructor required by SnakeYAML / Jackson. */ public PiiPatternConfig() {} - public PiiPatternConfig(String name, String regex, String mask) { - this.name = name; + this.name = name; this.regex = regex; - this.mask = mask; + this.mask = mask; } - - public String getName() { return name; } + public String getName() { return name; } public void setName(String name) { this.name = name; } - public String getRegex() { return regex; } public void setRegex(String regex) { this.regex = regex; } - - public String getMask() { return mask; } + public String getMask() { return mask; } public void setMask(String mask) { this.mask = mask; } } } + diff --git a/eventlens-core/src/main/java/io/eventlens/core/plugin/DatasourceListingModel.java b/eventlens-core/src/main/java/io/eventlens/core/plugin/DatasourceListingModel.java new file mode 100644 index 0000000..0b4a0e8 --- /dev/null +++ b/eventlens-core/src/main/java/io/eventlens/core/plugin/DatasourceListingModel.java @@ -0,0 +1,41 @@ +package io.eventlens.core.plugin; + +import java.util.List; + +/** + * API model for datasource listing endpoint. + */ +public record DatasourceListingModel( + String id, + String displayName, + String status, + String healthMessage, + List capabilities +) { + public static DatasourceListingModel from(PluginInstance instance) { + if (instance.pluginType() != PluginInstance.PluginType.EVENT_SOURCE) { + throw new IllegalArgumentException("Instance is not an event source: " + instance.instanceId()); + } + + String status = switch (instance.lifecycle()) { + case READY -> "ready"; + case DEGRADED -> "degraded"; + case FAILED -> "failed"; + case INITIALIZING -> "initializing"; + case DISCOVERED -> "discovered"; + case STOPPED -> "stopped"; + }; + + String healthMessage = instance.health() != null + ? java.util.Objects.toString(instance.health().message(), "") + : "Health not yet checked"; + + return new DatasourceListingModel( + instance.instanceId(), + instance.displayName(), + status, + healthMessage, + List.of() // Capabilities will be populated in later phases + ); + } +} diff --git a/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginDiscovery.java b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginDiscovery.java new file mode 100644 index 0000000..e8955bb --- /dev/null +++ b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginDiscovery.java @@ -0,0 +1,119 @@ +package io.eventlens.core.plugin; + +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.spi.ReducerPlugin; +import io.eventlens.spi.StreamAdapterPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; + +/** + * Discovers plugins from classpath (ServiceLoader) and external JARs. + */ +public class PluginDiscovery { + + private static final Logger log = LoggerFactory.getLogger(PluginDiscovery.class); + + /** + * Discover all plugins from classpath using ServiceLoader. + */ + public DiscoveryResult discoverFromClasspath() { + List sources = new ArrayList<>(); + List streams = new ArrayList<>(); + List reducers = new ArrayList<>(); + + ServiceLoader.load(EventSourcePlugin.class).forEach(sources::add); + ServiceLoader.load(StreamAdapterPlugin.class).forEach(streams::add); + ServiceLoader.load(ReducerPlugin.class).forEach(reducers::add); + + log.info("Discovered from classpath: {} event sources, {} stream adapters, {} reducers", + sources.size(), streams.size(), reducers.size()); + + return new DiscoveryResult(sources, streams, reducers); + } + + /** + * Discover plugins from external JAR directory. + */ + public DiscoveryResult discoverFromDirectory(String directoryPath) { + File dir = new File(directoryPath); + if (!dir.exists()) { + log.warn("Plugin directory does not exist: {}", directoryPath); + return DiscoveryResult.empty(); + } + + if (!dir.isDirectory()) { + log.warn("Plugin path is not a directory: {}", directoryPath); + return DiscoveryResult.empty(); + } + + File[] jarFiles = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".jar")); + if (jarFiles == null || jarFiles.length == 0) { + log.info("No JAR files found in plugin directory: {}", directoryPath); + return DiscoveryResult.empty(); + } + + URL[] urls = Arrays.stream(jarFiles) + .map(f -> { + try { + return f.toURI().toURL(); + } catch (Exception e) { + log.warn("Failed to convert JAR to URL: {}", f.getAbsolutePath(), e); + return null; + } + }) + .filter(Objects::nonNull) + .toArray(URL[]::new); + + if (urls.length == 0) { + log.warn("No valid JARs found in plugin directory: {}", directoryPath); + return DiscoveryResult.empty(); + } + + List sources = new ArrayList<>(); + List streams = new ArrayList<>(); + List reducers = new ArrayList<>(); + + try (URLClassLoader loader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader())) { + ServiceLoader.load(EventSourcePlugin.class, loader).forEach(sources::add); + ServiceLoader.load(StreamAdapterPlugin.class, loader).forEach(streams::add); + ServiceLoader.load(ReducerPlugin.class, loader).forEach(reducers::add); + + log.info("Discovered from {}: {} event sources, {} stream adapters, {} reducers", + directoryPath, sources.size(), streams.size(), reducers.size()); + + } catch (Exception e) { + log.error("Failed to load plugins from directory: {}", directoryPath, e); + return DiscoveryResult.empty(); + } + + return new DiscoveryResult(sources, streams, reducers); + } + + public record DiscoveryResult( + List eventSources, + List streamAdapters, + List reducers + ) { + public static DiscoveryResult empty() { + return new DiscoveryResult(List.of(), List.of(), List.of()); + } + + public DiscoveryResult merge(DiscoveryResult other) { + List mergedSources = new ArrayList<>(eventSources); + mergedSources.addAll(other.eventSources); + + List mergedStreams = new ArrayList<>(streamAdapters); + mergedStreams.addAll(other.streamAdapters); + + List mergedReducers = new ArrayList<>(reducers); + mergedReducers.addAll(other.reducers); + + return new DiscoveryResult(mergedSources, mergedStreams, mergedReducers); + } + } +} diff --git a/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginInstance.java b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginInstance.java new file mode 100644 index 0000000..9884480 --- /dev/null +++ b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginInstance.java @@ -0,0 +1,48 @@ +package io.eventlens.core.plugin; + +import io.eventlens.spi.HealthStatus; +import io.eventlens.spi.PluginLifecycle; + +import java.time.Instant; + +/** + * Represents a loaded plugin instance with its lifecycle state and health. + */ +public record PluginInstance( + String instanceId, + String typeId, + String displayName, + PluginType pluginType, + Object plugin, + PluginLifecycle lifecycle, + HealthStatus health, + Instant lastHealthCheck, + String failureReason +) { + public enum PluginType { + EVENT_SOURCE, + STREAM_ADAPTER, + REDUCER + } + + public PluginInstance withLifecycle(PluginLifecycle newLifecycle) { + return new PluginInstance(instanceId, typeId, displayName, pluginType, plugin, + newLifecycle, health, lastHealthCheck, failureReason); + } + + public PluginInstance withHealth(HealthStatus newHealth, Instant checkTime) { + PluginLifecycle newLifecycle = lifecycle; + if (newHealth.state() == HealthStatus.State.DOWN && lifecycle == PluginLifecycle.READY) { + newLifecycle = PluginLifecycle.DEGRADED; + } else if (newHealth.state() == HealthStatus.State.UP && lifecycle == PluginLifecycle.DEGRADED) { + newLifecycle = PluginLifecycle.READY; + } + return new PluginInstance(instanceId, typeId, displayName, pluginType, plugin, + newLifecycle, newHealth, checkTime, failureReason); + } + + public PluginInstance withFailure(String reason) { + return new PluginInstance(instanceId, typeId, displayName, pluginType, plugin, + PluginLifecycle.FAILED, health, lastHealthCheck, reason); + } +} diff --git a/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginListingModel.java b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginListingModel.java new file mode 100644 index 0000000..7665496 --- /dev/null +++ b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginListingModel.java @@ -0,0 +1,45 @@ +package io.eventlens.core.plugin; + +import io.eventlens.spi.HealthStatus; +import io.eventlens.spi.PluginLifecycle; + +import java.time.Instant; + +/** + * API model for plugin listing endpoint. + */ +public record PluginListingModel( + String instanceId, + String typeId, + String displayName, + String pluginType, + String lifecycle, + HealthStatusModel health, + Instant lastHealthCheck, + String failureReason +) { + public static PluginListingModel from(PluginInstance instance) { + return new PluginListingModel( + instance.instanceId(), + instance.typeId(), + instance.displayName(), + instance.pluginType().name().toLowerCase(), + instance.lifecycle().name().toLowerCase(), + HealthStatusModel.from(instance.health()), + instance.lastHealthCheck(), + instance.failureReason() + ); + } + + public record HealthStatusModel( + String state, + String message + ) { + public static HealthStatusModel from(HealthStatus health) { + return new HealthStatusModel( + health.state().name().toLowerCase(), + health.message() + ); + } + } +} diff --git a/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginManager.java b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginManager.java new file mode 100644 index 0000000..a198841 --- /dev/null +++ b/eventlens-core/src/main/java/io/eventlens/core/plugin/PluginManager.java @@ -0,0 +1,303 @@ +package io.eventlens.core.plugin; + +import io.eventlens.spi.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.*; + +/** + * Manages plugin lifecycle, health tracking, and safe shutdown. + */ +public class PluginManager implements AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(PluginManager.class); + + private final Map instances = new ConcurrentHashMap<>(); + private final ScheduledExecutorService healthScheduler; + private final int healthCheckIntervalSeconds; + private volatile boolean running = false; + + public PluginManager(int healthCheckIntervalSeconds) { + this.healthCheckIntervalSeconds = healthCheckIntervalSeconds; + this.healthScheduler = Executors.newScheduledThreadPool(1, r -> { + Thread t = new Thread(r, "plugin-health-checker"); + t.setDaemon(true); + return t; + }); + } + + /** + * Initialize and register an event source plugin. + */ + public void registerEventSource(String instanceId, EventSourcePlugin plugin, Map config) { + registerPlugin(instanceId, plugin, PluginInstance.PluginType.EVENT_SOURCE, config, + plugin::initialize, plugin::healthCheck); + } + + /** + * Initialize and register a stream adapter plugin. + */ + public void registerStreamAdapter(String instanceId, StreamAdapterPlugin plugin, Map config) { + registerPlugin(instanceId, plugin, PluginInstance.PluginType.STREAM_ADAPTER, config, + plugin::initialize, plugin::healthCheck); + } + + /** + * Register a reducer plugin (no initialization needed). + */ + public void registerReducer(String instanceId, ReducerPlugin plugin) { + String compatError = SpiVersions.checkCompatibility(plugin.typeId(), plugin.spiVersion()).orElse(null); + if (compatError != null) { + log.error("Reducer plugin '{}' failed SPI version check: {}", instanceId, compatError); + PluginInstance failed = new PluginInstance( + instanceId, plugin.typeId(), plugin.displayName(), + PluginInstance.PluginType.REDUCER, plugin, + PluginLifecycle.FAILED, HealthStatus.down(compatError), + Instant.now(), compatError + ); + instances.put(instanceId, failed); + return; + } + + PluginInstance instance = new PluginInstance( + instanceId, plugin.typeId(), plugin.displayName(), + PluginInstance.PluginType.REDUCER, plugin, + PluginLifecycle.READY, HealthStatus.up(), + Instant.now(), null + ); + instances.put(instanceId, instance); + log.info("Registered reducer plugin: {} ({})", instanceId, plugin.displayName()); + } + + private void registerPlugin( + String instanceId, + Object plugin, + PluginInstance.PluginType type, + Map config, + InitFunction initFn, + HealthFunction healthFn + ) { + String typeId = getTypeId(plugin); + String displayName = getDisplayName(plugin); + int spiVersion = getSpiVersion(plugin); + + // SPI version check + String compatError = SpiVersions.checkCompatibility(typeId, spiVersion).orElse(null); + if (compatError != null) { + log.error("Plugin '{}' failed SPI version check: {}", instanceId, compatError); + PluginInstance failed = new PluginInstance( + instanceId, typeId, displayName, type, plugin, + PluginLifecycle.FAILED, HealthStatus.down(compatError), + Instant.now(), compatError + ); + instances.put(instanceId, failed); + return; + } + + // Mark as initializing + PluginInstance initializing = new PluginInstance( + instanceId, typeId, displayName, type, plugin, + PluginLifecycle.INITIALIZING, HealthStatus.up(), + Instant.now(), null + ); + instances.put(instanceId, initializing); + + try { + initFn.initialize(instanceId, config); + HealthStatus health = healthFn.check(); + PluginLifecycle lifecycle = health.state() == HealthStatus.State.UP + ? PluginLifecycle.READY + : PluginLifecycle.DEGRADED; + + PluginInstance ready = new PluginInstance( + instanceId, typeId, displayName, type, plugin, + lifecycle, health, Instant.now(), null + ); + instances.put(instanceId, ready); + log.info("Initialized plugin: {} ({}) - {}", instanceId, displayName, lifecycle); + + } catch (Exception e) { + log.error("Failed to initialize plugin: {}", instanceId, e); + PluginInstance failed = initializing.withFailure(e.getMessage()); + instances.put(instanceId, failed); + } + } + + /** + * Start the health check scheduler. + */ + public void startHealthChecks() { + if (running) { + log.warn("Health checks already running"); + return; + } + running = true; + healthScheduler.scheduleAtFixedRate( + this::refreshAllHealth, + healthCheckIntervalSeconds, + healthCheckIntervalSeconds, + TimeUnit.SECONDS + ); + log.info("Started health check scheduler (interval: {}s)", healthCheckIntervalSeconds); + } + + private void refreshAllHealth() { + instances.values().stream() + .filter(i -> i.lifecycle() == PluginLifecycle.READY || i.lifecycle() == PluginLifecycle.DEGRADED) + .forEach(this::refreshHealth); + } + + private void refreshHealth(PluginInstance instance) { + try { + HealthStatus health = switch (instance.pluginType()) { + case EVENT_SOURCE -> ((EventSourcePlugin) instance.plugin()).healthCheck(); + case STREAM_ADAPTER -> ((StreamAdapterPlugin) instance.plugin()).healthCheck(); + case REDUCER -> HealthStatus.up(); // Reducers don't have health checks + }; + + PluginInstance updated = instance.withHealth(health, Instant.now()); + instances.put(instance.instanceId(), updated); + + if (updated.lifecycle() != instance.lifecycle()) { + log.info("Plugin '{}' transitioned: {} -> {}", + instance.instanceId(), instance.lifecycle(), updated.lifecycle()); + } + + } catch (Exception e) { + log.error("Health check failed for plugin: {}", instance.instanceId(), e); + HealthStatus down = HealthStatus.down("Health check threw exception: " + e.getMessage()); + PluginInstance degraded = instance.withHealth(down, Instant.now()); + instances.put(instance.instanceId(), degraded); + } + } + + /** + * Get all registered plugin instances. + */ + public List listAll() { + return List.copyOf(instances.values()); + } + + /** + * Get plugin instances by type. + */ + public List listByType(PluginInstance.PluginType type) { + return instances.values().stream() + .filter(i -> i.pluginType() == type) + .toList(); + } + + /** + * Get a specific plugin instance. + */ + public Optional getInstance(String instanceId) { + return Optional.ofNullable(instances.get(instanceId)); + } + + public Optional getEventSource(String instanceId) { + return getInstance(instanceId) + .filter(i -> i.pluginType() == PluginInstance.PluginType.EVENT_SOURCE) + .filter(i -> i.lifecycle() == PluginLifecycle.READY || i.lifecycle() == PluginLifecycle.DEGRADED) + .map(i -> (EventSourcePlugin) i.plugin()); + } + + public Optional getStreamAdapter(String instanceId) { + return getInstance(instanceId) + .filter(i -> i.pluginType() == PluginInstance.PluginType.STREAM_ADAPTER) + .filter(i -> i.lifecycle() == PluginLifecycle.READY || i.lifecycle() == PluginLifecycle.DEGRADED) + .map(i -> (StreamAdapterPlugin) i.plugin()); + } + + public Optional getFirstReadyEventSource() { + return instances.values().stream() + .filter(i -> i.pluginType() == PluginInstance.PluginType.EVENT_SOURCE) + .filter(i -> i.lifecycle() == PluginLifecycle.READY || i.lifecycle() == PluginLifecycle.DEGRADED) + .map(i -> (EventSourcePlugin) i.plugin()) + .findFirst(); + } + + public Optional getFirstReadyStreamAdapter() { + return instances.values().stream() + .filter(i -> i.pluginType() == PluginInstance.PluginType.STREAM_ADAPTER) + .filter(i -> i.lifecycle() == PluginLifecycle.READY || i.lifecycle() == PluginLifecycle.DEGRADED) + .map(i -> (StreamAdapterPlugin) i.plugin()) + .findFirst(); + } + + @Override + public void close() { + log.info("Shutting down plugin manager..."); + running = false; + + // Stop health checks + healthScheduler.shutdown(); + try { + if (!healthScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + healthScheduler.shutdownNow(); + } + } catch (InterruptedException e) { + healthScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + + // Shutdown plugins: streams first, then sources + shutdownPluginsByType(PluginInstance.PluginType.STREAM_ADAPTER); + shutdownPluginsByType(PluginInstance.PluginType.EVENT_SOURCE); + + log.info("Plugin manager shutdown complete"); + } + + private void shutdownPluginsByType(PluginInstance.PluginType type) { + instances.values().stream() + .filter(i -> i.pluginType() == type) + .forEach(instance -> { + try { + if (instance.plugin() instanceof AutoCloseable closeable) { + closeable.close(); + PluginInstance stopped = instance.withLifecycle(PluginLifecycle.STOPPED); + instances.put(instance.instanceId(), stopped); + log.info("Closed plugin: {}", instance.instanceId()); + } + } catch (Exception e) { + log.error("Failed to close plugin: {}", instance.instanceId(), e); + } + }); + } + + // Helper methods + private String getTypeId(Object plugin) { + if (plugin instanceof EventSourcePlugin p) return p.typeId(); + if (plugin instanceof StreamAdapterPlugin p) return p.typeId(); + if (plugin instanceof ReducerPlugin p) return p.typeId(); + return "unknown"; + } + + private String getDisplayName(Object plugin) { + if (plugin instanceof EventSourcePlugin p) return p.displayName(); + if (plugin instanceof StreamAdapterPlugin p) return p.displayName(); + if (plugin instanceof ReducerPlugin p) return p.displayName(); + return "Unknown"; + } + + private int getSpiVersion(Object plugin) { + if (plugin instanceof EventSourcePlugin p) return p.spiVersion(); + if (plugin instanceof StreamAdapterPlugin p) return p.spiVersion(); + if (plugin instanceof ReducerPlugin p) return p.spiVersion(); + return 0; + } + + @FunctionalInterface + private interface InitFunction { + void initialize(String instanceId, Map config); + } + + @FunctionalInterface + private interface HealthFunction { + HealthStatus check(); + } +} + + diff --git a/eventlens-core/src/main/resources/eventlens-example.yaml b/eventlens-core/src/main/resources/eventlens-example.yaml new file mode 100644 index 0000000..5469847 --- /dev/null +++ b/eventlens-core/src/main/resources/eventlens-example.yaml @@ -0,0 +1,112 @@ +# EventLens Configuration Example +# v2 legacy keys are still supported, but v3 plural source/stream blocks are preferred. + +version: "3.0.0" + +server: + port: 9090 + allowed-origins: + - http://localhost:5173 + - http://localhost:9090 + cors-max-age-seconds: 600 + auth: + enabled: false + username: admin + password: changeme + security: + rate-limit: + enabled: false + requests-per-minute: 120 + burst: 20 + +plugins: + directory: ./plugins + health-check-interval-seconds: 30 + +query-cache: + enabled: true + max-entries: 512 + search-ttl-seconds: 15 + timeline-ttl-seconds: 10 + +# Preferred v3 multi-source format +datasources: + - id: default + type: postgres + stream-id: default-kafka + url: jdbc:postgresql://localhost:5432/eventlens_dev + username: postgres + password: "" + table: null + query-timeout-seconds: 30 + columns: + event-id: null + aggregate-id: null + aggregate-type: null + sequence: null + event-type: null + payload: null + metadata: null + timestamp: null + global-position: null + pool: + maximum-pool-size: 10 + minimum-idle: 2 + connection-timeout-ms: 5000 + idle-timeout-ms: 300000 + max-lifetime-ms: 900000 + leak-detection-threshold-ms: 30000 + + - id: reporting-mysql + type: mysql + stream-id: "" + url: jdbc:mysql://localhost:3306/eventlens_reporting + username: root + password: "" + table: event_store + query-timeout-seconds: 30 + +streams: + - id: default-kafka + type: kafka + bootstrap-servers: localhost:9092 + topic: domain-events + +replay: + default-reducer: generic + reducers: {} + +anomaly: + scan-interval-seconds: 60 + max-aggregates-per-scan: 20 + rules: [] + +ui: + theme: dark + +audit: + enabled: true + +data-protection: + pii: + enabled: false + patterns: + - name: email + regex: "[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}" + mask: "***@***.***" + - name: credit-card + regex: "\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b" + mask: "****-****-****-****" + - name: phone + regex: "\\+?[1-9]\\d{7,14}" + mask: "***-***-****" + - name: ssn + regex: "\\d{3}-\\d{2}-\\d{4}" + mask: "***-**-****" + +export: + directory: ./exports + max-concurrent: 2 + max-events-per-export: 100000 + expire-after-seconds: 3600 + diff --git a/eventlens-core/src/test/java/io/eventlens/core/ConfigLoaderTest.java b/eventlens-core/src/test/java/io/eventlens/core/ConfigLoaderTest.java new file mode 100644 index 0000000..09076bc --- /dev/null +++ b/eventlens-core/src/test/java/io/eventlens/core/ConfigLoaderTest.java @@ -0,0 +1,79 @@ +package io.eventlens.core; + +import io.eventlens.core.exception.ConfigurationException; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ConfigLoaderTest { + + @Test + void loadsLegacyConfigIntoNormalizedLists() throws Exception { + Path file = Files.createTempFile("eventlens-legacy", ".yaml"); + Files.writeString(file, """ + datasource: + url: jdbc:postgresql://localhost:5432/eventlens + username: postgres + password: secret + kafka: + bootstrap-servers: localhost:9092 + topic: domain-events + """); + + EventLensConfig config = ConfigLoader.load(file.toString()); + + assertThat(config.getDatasourcesOrLegacy()).hasSize(1); + assertThat(config.getDatasourcesOrLegacy().get(0).getType()).isEqualTo("postgres"); + assertThat(config.getStreamsOrLegacy()).hasSize(1); + assertThat(config.getStreamsOrLegacy().get(0).getType()).isEqualTo("kafka"); + } + + @Test + void loadsPluralConfigWithoutLegacyKeys() throws Exception { + Path file = Files.createTempFile("eventlens-v3", ".yaml"); + Files.writeString(file, """ + datasources: + - id: orders-mysql + type: mysql + url: jdbc:mysql://localhost:3306/orders + username: root + password: secret + streams: + - id: events-kafka + type: kafka + bootstrap-servers: localhost:9092 + topic: domain-events + """); + + EventLensConfig config = ConfigLoader.load(file.toString()); + + assertThat(config.getDatasources()).hasSize(1); + assertThat(config.getDatasources().get(0).getType()).isEqualTo("mysql"); + assertThat(config.getDatasource().getUrl()).isEqualTo("jdbc:mysql://localhost:3306/orders"); + assertThat(config.getStreams()).hasSize(1); + assertThat(config.getKafka().getBootstrapServers()).isEqualTo("localhost:9092"); + } + + @Test + void rejectsMixedLegacyAndPluralConfig() throws Exception { + Path file = Files.createTempFile("eventlens-mixed", ".yaml"); + Files.writeString(file, """ + datasource: + url: jdbc:postgresql://localhost:5432/eventlens + username: postgres + datasources: + - id: pg + type: postgres + url: jdbc:postgresql://localhost:5432/eventlens + username: postgres + """); + + assertThatThrownBy(() -> ConfigLoader.load(file.toString())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("both 'datasource' and 'datasources'"); + } +} diff --git a/eventlens-core/src/test/java/io/eventlens/core/ConfigValidatorTest.java b/eventlens-core/src/test/java/io/eventlens/core/ConfigValidatorTest.java index bdbd402..2d53c37 100644 --- a/eventlens-core/src/test/java/io/eventlens/core/ConfigValidatorTest.java +++ b/eventlens-core/src/test/java/io/eventlens/core/ConfigValidatorTest.java @@ -12,8 +12,7 @@ class ConfigValidatorTest { void validDefaultConfigHasNoErrors() { var cfg = new EventLensConfig(); var issues = ConfigValidator.validate(cfg); - assertThat(issues.stream().filter(i -> i.severity() == ConfigValidator.ValidationError.Severity.ERROR).toList()) - .isEmpty(); + assertThat(issues.stream().filter(i -> i.severity() == ConfigValidator.ValidationError.Severity.ERROR).toList()).isEmpty(); } @Test @@ -30,7 +29,7 @@ void authEnabledRequiresStrongPassword() { } @Test - void datasourceUrlMustBePostgresJdbc() { + void legacyDatasourceUrlMustBePostgresJdbc() { var cfg = new EventLensConfig(); cfg.getDatasource().setUrl("jdbc:mysql://localhost/db"); @@ -38,5 +37,18 @@ void datasourceUrlMustBePostgresJdbc() { assertThat(issues.stream().anyMatch(i -> i.path().equals("datasource.url") && i.severity() == ConfigValidator.ValidationError.Severity.ERROR)).isTrue(); } -} + @Test + void pluralMysqlDatasourceIsValid() { + var cfg = new EventLensConfig(); + var mysql = new EventLensConfig.DatasourceInstanceConfig(); + mysql.setId("mysql-main"); + mysql.setType("mysql"); + mysql.setUrl("jdbc:mysql://localhost:3306/eventlens"); + mysql.setUsername("root"); + cfg.setDatasources(java.util.List.of(mysql)); + + var issues = ConfigValidator.validate(cfg); + assertThat(issues.stream().filter(i -> i.severity() == ConfigValidator.ValidationError.Severity.ERROR && i.path().startsWith("datasources[0]")).toList()).isEmpty(); + } +} diff --git a/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginDiscoveryExternalJarTest.java b/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginDiscoveryExternalJarTest.java new file mode 100644 index 0000000..c2b7ff2 --- /dev/null +++ b/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginDiscoveryExternalJarTest.java @@ -0,0 +1,122 @@ +package io.eventlens.core.plugin; + +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import static org.assertj.core.api.Assertions.assertThat; + +class PluginDiscoveryExternalJarTest { + + @Test + void discoversEventSourcePluginFromExternalJar() throws Exception { + Path pluginDir = Files.createTempDirectory("eventlens-plugin-dir"); + Path classesDir = Files.createTempDirectory("eventlens-plugin-classes"); + Path sourceFile = writeSourceFile(pluginDir); + + compileDummyPlugin(sourceFile, classesDir); + Path jarFile = createPluginJar(pluginDir, classesDir); + + PluginDiscovery.DiscoveryResult result = new PluginDiscovery().discoverFromDirectory(pluginDir.toString()); + + assertThat(Files.exists(jarFile)).isTrue(); + assertThat(result.eventSources()).hasSize(1); + assertThat(result.eventSources().getFirst().typeId()).isEqualTo("dummy-external"); + assertThat(result.streamAdapters()).isEmpty(); + } + + private static Path writeSourceFile(Path pluginDir) throws IOException { + Path sourceDir = Files.createDirectories(pluginDir.resolve("src/testplugins")); + Path sourceFile = sourceDir.resolve("DummyExternalEventSourcePlugin.java"); + Files.writeString(sourceFile, """ + package testplugins; + + import io.eventlens.spi.Event; + import io.eventlens.spi.EventQuery; + import io.eventlens.spi.EventQueryResult; + import io.eventlens.spi.EventSourcePlugin; + import io.eventlens.spi.HealthStatus; + + import java.time.Instant; + import java.util.List; + import java.util.Map; + + public final class DummyExternalEventSourcePlugin implements EventSourcePlugin { + @Override + public String typeId() { + return \"dummy-external\"; + } + + @Override + public String displayName() { + return \"Dummy External Plugin\"; + } + + @Override + public void initialize(String instanceId, Map config) { + } + + @Override + public EventQueryResult query(EventQuery query) { + Event event = new Event( + \"evt-1\", + \"ACC-001\", + \"BankAccount\", + 1, + \"AccountCreated\", + com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(), + com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(), + Instant.parse(\"2026-01-01T00:00:00Z\"), + 1 + ); + return new EventQueryResult(List.of(event), false, null); + } + + @Override + public HealthStatus healthCheck() { + return HealthStatus.up(); + } + } + """, StandardCharsets.UTF_8); + return sourceFile; + } + + private static void compileDummyPlugin(Path sourceFile, Path classesDir) throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertThat(compiler).isNotNull(); + + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, List.of(classesDir)); + var compilationUnits = fileManager.getJavaFileObjectsFromFiles(List.of(sourceFile.toFile())); + String classpath = System.getProperty("java.class.path"); + Boolean success = compiler.getTask(null, fileManager, null, List.of("-classpath", classpath), null, compilationUnits).call(); + assertThat(success).isTrue(); + } + } + + private static Path createPluginJar(Path pluginDir, Path classesDir) throws IOException { + Path jarFile = pluginDir.resolve("dummy-external-plugin.jar"); + try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(jarFile))) { + Path classFile = classesDir.resolve(Path.of("testplugins", "DummyExternalEventSourcePlugin.class")); + jar.putNextEntry(new JarEntry("testplugins/DummyExternalEventSourcePlugin.class")); + jar.write(Files.readAllBytes(classFile)); + jar.closeEntry(); + + jar.putNextEntry(new JarEntry("META-INF/services/io.eventlens.spi.EventSourcePlugin")); + jar.write("testplugins.DummyExternalEventSourcePlugin\n".getBytes(StandardCharsets.UTF_8)); + jar.closeEntry(); + } + return jarFile; + } +} + diff --git a/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginManagerTest.java b/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginManagerTest.java new file mode 100644 index 0000000..a806caf --- /dev/null +++ b/eventlens-core/src/test/java/io/eventlens/core/plugin/PluginManagerTest.java @@ -0,0 +1,226 @@ +package io.eventlens.core.plugin; + +import io.eventlens.spi.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class PluginManagerTest { + + private PluginManager manager; + + @AfterEach + void cleanup() { + if (manager != null) { + manager.close(); + } + } + + @Test + void testRegisterEventSource_Success() { + manager = new PluginManager(60); + TestEventSourcePlugin plugin = new TestEventSourcePlugin(true); + + manager.registerEventSource("test-source", plugin, Map.of()); + + List instances = manager.listAll(); + assertEquals(1, instances.size()); + + PluginInstance instance = instances.get(0); + assertEquals("test-source", instance.instanceId()); + assertEquals("test-datasource", instance.typeId()); + assertEquals(PluginLifecycle.READY, instance.lifecycle()); + assertEquals(HealthStatus.State.UP, instance.health().state()); + } + + @Test + void testRegisterEventSource_InitFailure() { + manager = new PluginManager(60); + TestEventSourcePlugin plugin = new TestEventSourcePlugin(false); + + manager.registerEventSource("failing-source", plugin, Map.of()); + + PluginInstance instance = manager.getInstance("failing-source").orElseThrow(); + assertEquals(PluginLifecycle.FAILED, instance.lifecycle()); + assertNotNull(instance.failureReason()); + } + + @Test + void testRegisterReducer() { + manager = new PluginManager(60); + TestReducerPlugin plugin = new TestReducerPlugin(); + + manager.registerReducer("test-reducer", plugin); + + PluginInstance instance = manager.getInstance("test-reducer").orElseThrow(); + assertEquals(PluginLifecycle.READY, instance.lifecycle()); + assertEquals(PluginInstance.PluginType.REDUCER, instance.pluginType()); + } + + @Test + void testHealthTransition_ReadyToDegraded() throws InterruptedException { + manager = new PluginManager(1); // 1 second health check + TestEventSourcePlugin plugin = new TestEventSourcePlugin(true); + + manager.registerEventSource("test-source", plugin, Map.of()); + manager.startHealthChecks(); + + // Initially ready + PluginInstance initial = manager.getInstance("test-source").orElseThrow(); + assertEquals(PluginLifecycle.READY, initial.lifecycle()); + + // Make health check fail + plugin.setHealthy(false); + Thread.sleep(1500); // Wait for health check + + PluginInstance degraded = manager.getInstance("test-source").orElseThrow(); + assertEquals(PluginLifecycle.DEGRADED, degraded.lifecycle()); + assertEquals(HealthStatus.State.DOWN, degraded.health().state()); + } + + @Test + void testHealthTransition_DegradedToReady() throws InterruptedException { + manager = new PluginManager(1); + TestEventSourcePlugin plugin = new TestEventSourcePlugin(true); // Start healthy + + manager.registerEventSource("test-source", plugin, Map.of()); + + // Make it unhealthy before starting health checks + plugin.setHealthy(false); + manager.startHealthChecks(); + + Thread.sleep(1500); // Wait for first health check to make it degraded + + // Should be degraded now + PluginInstance degraded = manager.getInstance("test-source").orElseThrow(); + assertEquals(PluginLifecycle.DEGRADED, degraded.lifecycle()); + + // Make health check succeed + plugin.setHealthy(true); + Thread.sleep(1500); + + PluginInstance ready = manager.getInstance("test-source").orElseThrow(); + assertEquals(PluginLifecycle.READY, ready.lifecycle()); + assertEquals(HealthStatus.State.UP, ready.health().state()); + } + + @Test + void testListByType() { + manager = new PluginManager(60); + manager.registerEventSource("source1", new TestEventSourcePlugin(true), Map.of()); + manager.registerStreamAdapter("stream1", new TestStreamAdapterPlugin(true), Map.of()); + manager.registerReducer("reducer1", new TestReducerPlugin()); + + List sources = manager.listByType(PluginInstance.PluginType.EVENT_SOURCE); + List streams = manager.listByType(PluginInstance.PluginType.STREAM_ADAPTER); + List reducers = manager.listByType(PluginInstance.PluginType.REDUCER); + + assertEquals(1, sources.size()); + assertEquals(1, streams.size()); + assertEquals(1, reducers.size()); + } + + // Test plugins + static class TestEventSourcePlugin implements EventSourcePlugin { + private boolean healthy; + + TestEventSourcePlugin(boolean healthy) { + this.healthy = healthy; + } + + void setHealthy(boolean healthy) { + this.healthy = healthy; + } + + @Override + public String typeId() { + return "test-datasource"; + } + + @Override + public String displayName() { + return "Test Datasource"; + } + + @Override + public void initialize(String instanceId, Map config) { + if (!healthy) { + throw new RuntimeException("Initialization failed"); + } + } + + @Override + public EventQueryResult query(EventQuery query) { + return new EventQueryResult(List.of(), false, null); + } + + @Override + public HealthStatus healthCheck() { + return healthy ? HealthStatus.up() : HealthStatus.down("Unhealthy"); + } + } + + static class TestStreamAdapterPlugin implements StreamAdapterPlugin { + private boolean healthy; + + TestStreamAdapterPlugin(boolean healthy) { + this.healthy = healthy; + } + + @Override + public String typeId() { + return "test-stream"; + } + + @Override + public String displayName() { + return "Test Stream"; + } + + @Override + public void initialize(String instanceId, Map config) { + if (!healthy) { + throw new RuntimeException("Initialization failed"); + } + } + + @Override + public void subscribe(java.util.function.Consumer listener) { + } + + @Override + public void unsubscribe() { + } + + @Override + public HealthStatus healthCheck() { + return healthy ? HealthStatus.up() : HealthStatus.down("Unhealthy"); + } + } + + static class TestReducerPlugin implements ReducerPlugin { + @Override + public String typeId() { + return "test-reducer"; + } + + @Override + public String displayName() { + return "Test Reducer"; + } + + @Override + public String aggregateType() { + return "TestAggregate"; + } + + @Override + public com.fasterxml.jackson.databind.JsonNode reduce(List events) { + return com.fasterxml.jackson.databind.node.NullNode.getInstance(); + } + } +} diff --git a/eventlens-plugin-test/build.gradle.kts b/eventlens-plugin-test/build.gradle.kts new file mode 100644 index 0000000..2086e09 --- /dev/null +++ b/eventlens-plugin-test/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + `java-library` +} + +dependencies { + api(project(":eventlens-spi")) + api("org.junit.jupiter:junit-jupiter-api:6.0.3") + api("org.assertj:assertj-core:3.27.7") +} diff --git a/eventlens-plugin-test/src/main/java/io/eventlens/test/CanonicalEventSet.java b/eventlens-plugin-test/src/main/java/io/eventlens/test/CanonicalEventSet.java new file mode 100644 index 0000000..923d60e --- /dev/null +++ b/eventlens-plugin-test/src/main/java/io/eventlens/test/CanonicalEventSet.java @@ -0,0 +1,33 @@ +package io.eventlens.test; + +import java.util.List; + +public final class CanonicalEventSet { + + public static final String PRIMARY_AGGREGATE_ID = "ACC-001"; + public static final String SECONDARY_AGGREGATE_ID = "ACC-002"; + public static final String TERTIARY_AGGREGATE_ID = "ORD-001"; + public static final String SEARCH_TERM = "ACC"; + + private CanonicalEventSet() { + } + + public static List defaultEvents() { + return List.of( + new SeedEvent(PRIMARY_AGGREGATE_ID, "BankAccount", 1, "AccountCreated", "{\"balance\":0}", "{\"source\":\"contract\"}"), + new SeedEvent(PRIMARY_AGGREGATE_ID, "BankAccount", 2, "MoneyDeposited", "{\"amount\":100}", "{\"source\":\"contract\"}"), + new SeedEvent(PRIMARY_AGGREGATE_ID, "BankAccount", 3, "MoneyWithdrawn", "{\"amount\":40}", "{\"source\":\"contract\"}"), + new SeedEvent(SECONDARY_AGGREGATE_ID, "BankAccount", 1, "AccountCreated", "{\"balance\":50}", "{\"source\":\"contract\"}"), + new SeedEvent(TERTIARY_AGGREGATE_ID, "Order", 1, "OrderCreated", "{\"total\":99}", "{\"source\":\"contract\"}") + ); + } + + public record SeedEvent( + String aggregateId, + String aggregateType, + long sequenceNumber, + String eventType, + String payload, + String metadata) { + } +} diff --git a/eventlens-plugin-test/src/main/java/io/eventlens/test/EventSourcePluginTestKit.java b/eventlens-plugin-test/src/main/java/io/eventlens/test/EventSourcePluginTestKit.java new file mode 100644 index 0000000..65f8610 --- /dev/null +++ b/eventlens-plugin-test/src/main/java/io/eventlens/test/EventSourcePluginTestKit.java @@ -0,0 +1,105 @@ +package io.eventlens.test; + +import io.eventlens.spi.EventQuery; +import io.eventlens.spi.EventSourcePlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class EventSourcePluginTestKit { + + protected EventSourcePlugin plugin; + + @BeforeEach + void initializePluginUnderTest() throws Exception { + plugin = createPlugin(); + seedCanonicalEvents(); + } + + @AfterEach + void cleanupPluginUnderTest() throws Exception { + if (plugin != null) { + plugin.close(); + } + cleanupStore(); + } + + protected abstract EventSourcePlugin createPlugin() throws Exception; + + protected abstract void seedCanonicalEvents() throws Exception; + + protected abstract void cleanupStore() throws Exception; + + @Test + void healthCheckReportsUpForSeededPlugin() { + assertThat(plugin.healthCheck().state().name()).isEqualTo("UP"); + } + + @Test + void timelineQueryReturnsEventsInSequenceOrder() { + var result = plugin.query(EventQuery.builder(EventQuery.QueryType.TIMELINE) + .aggregateId(CanonicalEventSet.PRIMARY_AGGREGATE_ID) + .limit(10) + .build()); + + PluginAssertions.assertOrderedSequence(result, 1L, 2L, 3L); + PluginAssertions.assertEventTypes(result.events(), "AccountCreated", "MoneyDeposited", "MoneyWithdrawn"); + assertThat(result.hasMore()).isFalse(); + assertThat(result.nextCursor()).isNull(); + } + + @Test + void timelineCursorPaginationReturnsNextWindowWithoutDuplicates() { + var firstPage = plugin.query(EventQuery.builder(EventQuery.QueryType.TIMELINE) + .aggregateId(CanonicalEventSet.PRIMARY_AGGREGATE_ID) + .limit(2) + .build()); + PluginAssertions.assertOrderedSequence(firstPage, 1L, 2L); + assertThat(firstPage.hasMore()).isTrue(); + assertThat(firstPage.nextCursor()).isNotBlank(); + + var secondPage = plugin.query(EventQuery.builder(EventQuery.QueryType.TIMELINE) + .aggregateId(CanonicalEventSet.PRIMARY_AGGREGATE_ID) + .cursor(firstPage.nextCursor()) + .limit(2) + .build()); + PluginAssertions.assertOrderedSequence(secondPage, 3L); + } + + @Test + void metadataOnlyTimelineOmitsPayloadButKeepsMetadata() { + var result = plugin.query(EventQuery.builder(EventQuery.QueryType.TIMELINE) + .aggregateId(CanonicalEventSet.PRIMARY_AGGREGATE_ID) + .fields(EventQuery.Fields.METADATA) + .limit(10) + .build()); + + assertThat(result.events()).isNotEmpty(); + result.events().forEach(PluginAssertions::assertMetadataOnly); + } + + @Test + void searchQueryReturnsMatchingAggregateSummaries() { + var result = plugin.query(EventQuery.builder(EventQuery.QueryType.SEARCH) + .aggregateId(CanonicalEventSet.SEARCH_TERM) + .limit(10) + .build()); + + PluginAssertions.assertAggregateIdsPresent(result, CanonicalEventSet.PRIMARY_AGGREGATE_ID, CanonicalEventSet.SECONDARY_AGGREGATE_ID); + assertThat(result.hasMore()).isFalse(); + } + + @Test + void emptyTimelineReturnsEmptyResult() { + var result = plugin.query(EventQuery.builder(EventQuery.QueryType.TIMELINE) + .aggregateId("UNKNOWN-AGGREGATE") + .limit(10) + .build()); + + assertThat(result.events()).isEmpty(); + assertThat(result.hasMore()).isFalse(); + assertThat(result.nextCursor()).isNull(); + } +} diff --git a/eventlens-plugin-test/src/main/java/io/eventlens/test/PluginAssertions.java b/eventlens-plugin-test/src/main/java/io/eventlens/test/PluginAssertions.java new file mode 100644 index 0000000..f972a88 --- /dev/null +++ b/eventlens-plugin-test/src/main/java/io/eventlens/test/PluginAssertions.java @@ -0,0 +1,36 @@ +package io.eventlens.test; + +import io.eventlens.spi.Event; +import io.eventlens.spi.EventQueryResult; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class PluginAssertions { + + private PluginAssertions() { + } + + public static void assertOrderedSequence(EventQueryResult result, long... expectedSequenceNumbers) { + Long[] boxed = java.util.Arrays.stream(expectedSequenceNumbers).boxed().toArray(Long[]::new); + assertThat(result.events()).extracting(Event::sequenceNumber).containsExactly(boxed); + } + + public static void assertMetadataOnly(Event event) { + assertThat(event.payload()).isNotNull(); + assertThat(event.payload().isObject()).isTrue(); + assertThat(event.payload().size()).isZero(); + assertThat(event.metadata()).isNotNull(); + assertThat(event.metadata().isObject()).isTrue(); + } + + public static void assertAggregateIdsPresent(EventQueryResult result, String... aggregateIds) { + assertThat(result.events()).extracting(Event::aggregateId).contains(aggregateIds); + } + + public static void assertEventTypes(List events, String... eventTypes) { + assertThat(events).extracting(Event::eventType).contains(eventTypes); + } +} + diff --git a/eventlens-plugin-test/src/main/java/io/eventlens/test/StreamAdapterPluginTestKit.java b/eventlens-plugin-test/src/main/java/io/eventlens/test/StreamAdapterPluginTestKit.java new file mode 100644 index 0000000..107ee1b --- /dev/null +++ b/eventlens-plugin-test/src/main/java/io/eventlens/test/StreamAdapterPluginTestKit.java @@ -0,0 +1,73 @@ +package io.eventlens.test; + +import io.eventlens.spi.Event; +import io.eventlens.spi.StreamAdapterPlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class StreamAdapterPluginTestKit { + + protected StreamAdapterPlugin plugin; + + @BeforeEach + void initializePluginUnderTest() throws Exception { + plugin = createPlugin(); + } + + @AfterEach + void cleanupPluginUnderTest() throws Exception { + if (plugin != null) { + plugin.close(); + } + cleanupStream(); + } + + protected abstract StreamAdapterPlugin createPlugin() throws Exception; + + protected abstract void emitCanonicalEvents() throws Exception; + + protected void cleanupStream() throws Exception { + } + + protected int expectedEventCount() { + return 2; + } + + protected Duration awaitTimeout() { + return Duration.ofSeconds(30); + } + + @Test + void healthCheckReportsUpForInitializedPlugin() { + assertThat(plugin.healthCheck().state().name()).isEqualTo("UP"); + } + + @Test + void subscribeReceivesCanonicalEvents() throws Exception { + CountDownLatch latch = new CountDownLatch(expectedEventCount()); + List received = new CopyOnWriteArrayList<>(); + + plugin.subscribe(event -> { + received.add(event); + if (latch.getCount() > 0) { + latch.countDown(); + } + }); + + emitCanonicalEvents(); + + assertThat(latch.await(awaitTimeout().toSeconds(), TimeUnit.SECONDS)).isTrue(); + assertThat(received).hasSizeGreaterThanOrEqualTo(expectedEventCount()); + assertThat(received).extracting(Event::aggregateId).contains(CanonicalEventSet.PRIMARY_AGGREGATE_ID); + plugin.unsubscribe(); + } +} diff --git a/eventlens-source-mysql/build.gradle.kts b/eventlens-source-mysql/build.gradle.kts new file mode 100644 index 0000000..4341d40 --- /dev/null +++ b/eventlens-source-mysql/build.gradle.kts @@ -0,0 +1,12 @@ +dependencies { + implementation(project(":eventlens-core")) + implementation(project(":eventlens-spi")) + implementation("com.fasterxml.jackson.core:jackson-databind:2.21.1") + implementation("com.zaxxer:HikariCP:7.0.2") + implementation("com.mysql:mysql-connector-j:9.3.0") + + testImplementation(project(":eventlens-plugin-test")) + testImplementation("org.testcontainers:junit-jupiter:1.21.4") + testImplementation("org.testcontainers:mysql:1.21.4") +} + diff --git a/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlConfig.java b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlConfig.java new file mode 100644 index 0000000..f4e1b4c --- /dev/null +++ b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlConfig.java @@ -0,0 +1,14 @@ +package io.eventlens.mysql; + +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.EventLensConfig.PoolConfig; + +public record MySqlConfig( + String jdbcUrl, + String username, + String password, + String tableName, + ColumnMappingConfig columnOverrides, + PoolConfig pool, + int queryTimeoutSeconds) { +} diff --git a/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlEventSourcePlugin.java b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlEventSourcePlugin.java new file mode 100644 index 0000000..6225a4b --- /dev/null +++ b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlEventSourcePlugin.java @@ -0,0 +1,97 @@ +package io.eventlens.mysql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.EventLensConfig.PoolConfig; +import io.eventlens.core.JsonUtil; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.spi.Event; +import io.eventlens.spi.EventQuery; +import io.eventlens.spi.EventQueryResult; +import io.eventlens.spi.EventSourceCapabilities; +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.spi.HealthStatus; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class MySqlEventSourcePlugin implements EventSourcePlugin, EventStoreReader { + + private volatile MySqlEventStoreReader reader; + + @Override public String typeId() { return "mysql"; } + @Override public String displayName() { return "MySQL Event Store"; } + + @Override + public void initialize(String instanceId, Map config) { + this.reader = new MySqlEventStoreReader(new MySqlConfig( + requireString(config, "jdbcUrl"), + requireString(config, "username"), + Objects.toString(config.getOrDefault("password", ""), ""), + blankToNull(Objects.toString(config.get("tableName"), null)), + config.get("columnOverrides") instanceof ColumnMappingConfig columnOverrides ? columnOverrides : new ColumnMappingConfig(), + config.get("pool") instanceof PoolConfig pool ? pool : new PoolConfig(), + config.get("queryTimeoutSeconds") instanceof Number n ? n.intValue() : 30)); + } + + @Override public EventSourceCapabilities capabilities() { return new EventSourceCapabilities(true, true, true, true, Set.of("aggregate_id", "aggregate_type", "event_type", "timestamp")); } + + @Override + public EventQueryResult query(EventQuery query) { + MySqlEventStoreReader activeReader = requireReader(); + if (query.type() == EventQuery.QueryType.TIMELINE) { + List events = query.cursor() != null && !query.cursor().isBlank() + ? activeReader.getEventsAfterSequence(query.aggregateId(), Long.parseLong(query.cursor()), query.limit() + 1) + : activeReader.getEvents(query.aggregateId(), query.limit() + 1, 0); + boolean hasMore = events.size() > query.limit(); + List page = hasMore ? events.subList(0, query.limit()) : events; + String nextCursor = hasMore && !page.isEmpty() ? Long.toString(page.get(page.size() - 1).sequenceNumber()) : null; + return new EventQueryResult(page.stream().map(event -> toSpiEvent(event, query.fields())).toList(), hasMore, nextCursor); + } + String searchTerm = query.aggregateId() != null ? query.aggregateId() : ""; + List ids = activeReader.searchAggregates(searchTerm, query.limit()); + List events = ids.stream().map(id -> activeReader.getEvents(id, 1, 0)).filter(list -> !list.isEmpty()).map(list -> toSpiEvent(list.get(0), query.fields())).toList(); + return new EventQueryResult(events, false, null); + } + + @Override + public HealthStatus healthCheck() { + try { requireReader().getAggregateTypes(); return HealthStatus.up(); } + catch (Exception e) { return HealthStatus.down(e.getMessage() != null ? e.getMessage() : "mysql health check failed"); } + } + + @Override public void close() { MySqlEventStoreReader activeReader = reader; reader = null; if (activeReader != null) activeReader.close(); } + @Override public List getEvents(String aggregateId) { return requireReader().getEvents(aggregateId); } + @Override public List getEvents(String aggregateId, int limit, int offset) { return requireReader().getEvents(aggregateId, limit, offset); } + @Override public List getEventsAfterSequence(String aggregateId, long afterSequence, int limit) { return requireReader().getEventsAfterSequence(aggregateId, afterSequence, limit); } + @Override public List getEventsUpTo(String aggregateId, long maxSequence) { return requireReader().getEventsUpTo(aggregateId, maxSequence); } + @Override public List findAggregateIds(String aggregateType, int limit, int offset) { return requireReader().findAggregateIds(aggregateType, limit, offset); } + @Override public List getRecentEvents(int limit) { return requireReader().getRecentEvents(limit); } + @Override public List getEventsAfter(long globalPosition, int limit) { return requireReader().getEventsAfter(globalPosition, limit); } + @Override public long countEvents(String aggregateId) { return requireReader().countEvents(aggregateId); } + @Override public List getAggregateTypes() { return requireReader().getAggregateTypes(); } + @Override public List searchAggregates(String query, int limit) { return requireReader().searchAggregates(query, limit); } + + private MySqlEventStoreReader requireReader() { + MySqlEventStoreReader activeReader = reader; + if (activeReader == null) throw new IllegalStateException("MySQL plugin is not initialized"); + return activeReader; + } + + private static Event toSpiEvent(StoredEvent event, EventQuery.Fields fields) { + return new Event(event.eventId(), event.aggregateId(), event.aggregateType(), event.sequenceNumber(), event.eventType(), fields == EventQuery.Fields.METADATA ? emptyObject() : parseJson(event.payload()), parseJson(event.metadata()), event.timestamp(), event.globalPosition()); + } + + private static JsonNode parseJson(String json) { + try { return JsonUtil.mapper().readTree(json == null || json.isBlank() ? "{}" : json); } + catch (Exception e) { ObjectNode fallback = JsonUtil.mapper().createObjectNode(); fallback.put("raw", json == null ? "" : json); return fallback; } + } + + private static ObjectNode emptyObject() { return JsonUtil.mapper().createObjectNode(); } + private static String requireString(Map config, String key) { Object value = config.get(key); if (value == null || value.toString().isBlank()) throw new IllegalArgumentException("Missing required mysql config: " + key); return value.toString(); } + private static String blankToNull(String value) { return value == null || value.isBlank() ? null : value; } +} diff --git a/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlEventStoreReader.java b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlEventStoreReader.java new file mode 100644 index 0000000..e0e9903 --- /dev/null +++ b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlEventStoreReader.java @@ -0,0 +1,225 @@ +package io.eventlens.mysql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.exception.EventStoreException; +import io.eventlens.core.exception.QueryTimeoutException; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.mysql.MySqlSchemaDetector.DetectedSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class MySqlEventStoreReader implements EventStoreReader, AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(MySqlEventStoreReader.class); + + private final HikariDataSource dataSource; + private final DetectedSchema schema; + private final int queryTimeoutSeconds; + + public MySqlEventStoreReader(MySqlConfig config) { + HikariConfig hc = new HikariConfig(); + hc.setJdbcUrl(config.jdbcUrl()); + hc.setUsername(config.username()); + hc.setPassword(config.password()); + var pool = config.pool(); + if (pool != null) { + hc.setMaximumPoolSize(pool.getMaximumPoolSize()); + hc.setMinimumIdle(pool.getMinimumIdle()); + hc.setConnectionTimeout(pool.getConnectionTimeoutMs()); + hc.setIdleTimeout(pool.getIdleTimeoutMs()); + hc.setMaxLifetime(pool.getMaxLifetimeMs()); + hc.setLeakDetectionThreshold(pool.getLeakDetectionThresholdMs()); + } + hc.setReadOnly(true); + hc.setPoolName("eventlens-mysql"); + this.dataSource = new HikariDataSource(hc); + this.queryTimeoutSeconds = Math.max(1, config.queryTimeoutSeconds()); + + var detector = new MySqlSchemaDetector(); + var overrides = config.columnOverrides() != null ? config.columnOverrides() : new ColumnMappingConfig(); + this.schema = config.tableName() != null && !config.tableName().isBlank() + ? detector.detectForTable(config.tableName(), dataSource, overrides) + : detector.detect(dataSource, overrides); + } + + @Override public List getEvents(String aggregateId) { return getEvents(aggregateId, Integer.MAX_VALUE, 0); } + + @Override + public List getEvents(String aggregateId, int limit, int offset) { + String sql = "SELECT * FROM %s WHERE %s = ? ORDER BY %s ASC LIMIT ? OFFSET ?".formatted(q(schema.tableName()), q(schema.aggregateIdColumn()), q(schema.sequenceColumn())); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ps.setInt(2, limit); + ps.setInt(3, offset); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to read events for aggregate: " + aggregateId, e); } + } + + @Override + public List getEventsAfterSequence(String aggregateId, long afterSequence, int limit) { + String seqCol = q(schema.sequenceColumn()); + String sql = "SELECT * FROM %s WHERE %s = ? AND %s > ? ORDER BY %s ASC LIMIT ?".formatted(q(schema.tableName()), q(schema.aggregateIdColumn()), seqCol, seqCol); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ps.setLong(2, afterSequence); + ps.setInt(3, limit); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to read events after sequence " + afterSequence, e); } + } + + @Override + public List getEventsUpTo(String aggregateId, long maxSequence) { + String seqCol = q(schema.sequenceColumn()); + String sql = "SELECT * FROM %s WHERE %s = ? AND %s <= ? ORDER BY %s ASC".formatted(q(schema.tableName()), q(schema.aggregateIdColumn()), seqCol, seqCol); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ps.setLong(2, maxSequence); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to read events up to sequence " + maxSequence, e); } + } + + @Override + public List findAggregateIds(String aggregateType, int limit, int offset) { + String aggCol = q(schema.aggregateIdColumn()); + final String sql; + if (schema.aggregateTypeColumn() != null) { + sql = "SELECT DISTINCT %s FROM %s WHERE %s = ? ORDER BY %s LIMIT ? OFFSET ?".formatted(aggCol, q(schema.tableName()), q(schema.aggregateTypeColumn()), aggCol); + } else { + sql = "SELECT DISTINCT %s FROM %s ORDER BY %s LIMIT ? OFFSET ?".formatted(aggCol, q(schema.tableName()), aggCol); + } + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + if (schema.aggregateTypeColumn() != null) { ps.setString(1, aggregateType); ps.setInt(2, limit); ps.setInt(3, offset); } + else { ps.setInt(1, limit); ps.setInt(2, offset); } + return extractFirstColumn(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to find aggregate IDs for type: " + aggregateType, e); } + } + + @Override + public List getRecentEvents(int limit) { + String orderCol = schema.globalPositionColumn() != null ? schema.globalPositionColumn() : schema.timestampColumn() != null ? schema.timestampColumn() : schema.eventIdColumn(); + String sql = "SELECT * FROM %s ORDER BY %s DESC LIMIT ?".formatted(q(schema.tableName()), q(orderCol)); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setInt(1, limit); + List results = mapResults(ps.executeQuery()); + Collections.reverse(results); + return results; + } catch (SQLException e) { throw mapException("Failed to read recent events", e); } + } + + @Override + public List getEventsAfter(long globalPosition, int limit) { + String positionColumn = schema.globalPositionColumn() != null ? schema.globalPositionColumn() : schema.eventIdColumn(); + String posCol = q(positionColumn); + String sql = "SELECT * FROM %s WHERE %s > ? ORDER BY %s ASC LIMIT ?".formatted(q(schema.tableName()), posCol, posCol); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setLong(1, globalPosition); + ps.setInt(2, limit); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to poll events after position " + globalPosition, e); } + } + + @Override + public long countEvents(String aggregateId) { + String sql = "SELECT COUNT(*) FROM %s WHERE %s = ?".formatted(q(schema.tableName()), q(schema.aggregateIdColumn())); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ResultSet rs = ps.executeQuery(); + return rs.next() ? rs.getLong(1) : 0; + } catch (SQLException e) { throw mapException("Failed to count events for: " + aggregateId, e); } + } + + @Override + public List getAggregateTypes() { + if (schema.aggregateTypeColumn() == null) return List.of(); + String typeCol = q(schema.aggregateTypeColumn()); + String sql = "SELECT DISTINCT %s FROM %s ORDER BY %s".formatted(typeCol, q(schema.tableName()), typeCol); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + return extractFirstColumn(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to get aggregate types", e); } + } + + @Override + public List searchAggregates(String query, int limit) { + String aggCol = q(schema.aggregateIdColumn()); + String sql = "SELECT DISTINCT %s FROM %s WHERE LOWER(%s) LIKE LOWER(?) ORDER BY %s LIMIT ?".formatted(aggCol, q(schema.tableName()), aggCol, aggCol); + try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, "%" + query + "%"); + ps.setInt(2, limit); + return extractFirstColumn(ps.executeQuery()); + } catch (SQLException e) { throw mapException("Failed to search aggregates", e); } + } + + @Override public void close() { if (dataSource != null && !dataSource.isClosed()) dataSource.close(); } + + private RuntimeException mapException(String message, SQLException e) { + if ("HY000".equals(e.getSQLState()) && e.getMessage() != null && e.getMessage().contains("maximum statement execution time")) { + return new QueryTimeoutException(queryTimeoutSeconds, "Query exceeded timeout", e); + } + return new EventStoreException(message, e); + } + + private static String q(String identifier) { return "`" + identifier.replace("`", "``") + "`"; } + + private List mapResults(ResultSet rs) throws SQLException { + List events = new ArrayList<>(); + while (rs.next()) { + events.add(new StoredEvent( + Objects.toString(rs.getObject(schema.eventIdColumn()), ""), + Objects.toString(rs.getObject(schema.aggregateIdColumn()), ""), + schema.aggregateTypeColumn() != null ? Objects.toString(rs.getObject(schema.aggregateTypeColumn()), "unknown") : "unknown", + rs.getLong(schema.sequenceColumn()), + rs.getString(schema.eventTypeColumn()), + rs.getString(schema.payloadColumn()), + safeGetString(rs, schema.metadataColumn(), "{}"), + safeGetInstant(rs, schema.timestampColumn()), + schema.globalPositionColumn() != null ? rs.getLong(schema.globalPositionColumn()) : safeGetLong(rs, schema.eventIdColumn()))); + } + return events; + } + + private String safeGetString(ResultSet rs, String columnName, String fallback) { + if (columnName == null) return fallback; + try { String value = rs.getString(columnName); return value != null ? value : fallback; } + catch (SQLException e) { log.debug("Could not read optional column '{}': {}", columnName, e.getMessage()); return fallback; } + } + + private long safeGetLong(ResultSet rs, String columnName) { + try { return rs.getLong(columnName); } + catch (SQLException e) { try { return Long.parseLong(Objects.toString(rs.getObject(columnName), "0")); } catch (Exception ignored) { return 0; } } + } + + private Instant safeGetInstant(ResultSet rs, String columnName) { + if (columnName == null) return Instant.EPOCH; + try { Timestamp ts = rs.getTimestamp(columnName); return ts != null ? ts.toInstant() : Instant.EPOCH; } + catch (SQLException e) { return Instant.EPOCH; } + } + + private List extractFirstColumn(ResultSet rs) throws SQLException { + List result = new ArrayList<>(); + while (rs.next()) result.add(Objects.toString(rs.getObject(1), "")); + return result; + } +} diff --git a/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlSchemaDetector.java b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlSchemaDetector.java new file mode 100644 index 0000000..ce45cea --- /dev/null +++ b/eventlens-source-mysql/src/main/java/io/eventlens/mysql/MySqlSchemaDetector.java @@ -0,0 +1,105 @@ +package io.eventlens.mysql; + +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.exception.SchemaDetectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class MySqlSchemaDetector { + + private static final Logger log = LoggerFactory.getLogger(MySqlSchemaDetector.class); + private static final List CANDIDATE_TABLES = List.of( + "event_store", "events", "domain_events", "stored_events", + "event_log", "aggregate_events", "es_events", + "es_event", "mt_events", "domain_event_entry", + "event_journal", "outbox_event"); + + public record DetectedSchema( + String tableName, + String eventIdColumn, + String aggregateIdColumn, + String aggregateTypeColumn, + String sequenceColumn, + String eventTypeColumn, + String payloadColumn, + String metadataColumn, + String timestampColumn, + String globalPositionColumn + ) {} + + public DetectedSchema detect(DataSource dataSource, ColumnMappingConfig overrides) { + try (Connection conn = dataSource.getConnection()) { + var meta = conn.getMetaData(); + for (String candidate : CANDIDATE_TABLES) { + try (ResultSet rs = meta.getColumns(conn.getCatalog(), null, candidate, null)) { + if (rs.next()) { + log.info("Auto-detected MySQL event store table: '{}'", candidate); + return detectColumns(candidate, conn, overrides); + } + } + } + throw new SchemaDetectionException("No event store table found. Searched: " + CANDIDATE_TABLES); + } catch (SQLException e) { + throw new SchemaDetectionException("Schema detection failed", e); + } + } + + public DetectedSchema detectForTable(String tableName, DataSource dataSource, ColumnMappingConfig overrides) { + try (Connection conn = dataSource.getConnection()) { + try (ResultSet rs = conn.getMetaData().getColumns(conn.getCatalog(), null, tableName, null)) { + if (!rs.next()) { + throw new SchemaDetectionException("Table or view '" + tableName + "' not found in the database."); + } + } + return detectColumns(tableName, conn, overrides); + } catch (SchemaDetectionException e) { + throw e; + } catch (SQLException e) { + throw new SchemaDetectionException("Schema detection failed for table '" + tableName + "'", e); + } + } + + private DetectedSchema detectColumns(String table, Connection conn, ColumnMappingConfig overrides) throws SQLException { + Map columns = new LinkedHashMap<>(); + try (ResultSet rs = conn.getMetaData().getColumns(conn.getCatalog(), null, table, null)) { + while (rs.next()) { + columns.put(rs.getString("COLUMN_NAME").toLowerCase(), rs.getString("TYPE_NAME")); + } + } + return new DetectedSchema( + table, + overrides.getEventId() != null ? overrides.getEventId() : findColumn(columns, table, "event_id", "id", "uid"), + overrides.getAggregateId() != null ? overrides.getAggregateId() : findColumn(columns, table, "aggregate_id", "stream_id", "entity_id", "stream_key"), + overrides.getAggregateType() != null ? overrides.getAggregateType() : findColumnOrNull(columns, "aggregate_type", "stream_type", "entity_type"), + overrides.getSequence() != null ? overrides.getSequence() : findColumn(columns, table, "sequence_number", "version", "seq", "position", "event_number", "revision"), + overrides.getEventType() != null ? overrides.getEventType() : findColumn(columns, table, "event_type", "type_name", "event_name", "type"), + overrides.getPayload() != null ? overrides.getPayload() : findColumn(columns, table, "payload", "data", "event_data", "body", "json_data", "json_payload", "event_body", "event_payload"), + overrides.getMetadata() != null ? overrides.getMetadata() : findColumnOrNull(columns, "metadata", "meta", "headers"), + overrides.getTimestamp() != null ? overrides.getTimestamp() : findColumnOrNull(columns, "timestamp", "occurred_at", "event_timestamp", "created_at", "inserted_at"), + overrides.getGlobalPosition() != null ? overrides.getGlobalPosition() : findColumnOrNull(columns, "global_position", "global_seq", "log_position", "seq_id", "transaction_id") + ); + } + + private String findColumn(Map columns, String table, String... candidates) { + for (String candidate : candidates) { + if (columns.containsKey(candidate)) return candidate; + } + throw new SchemaDetectionException("Cannot detect required column in table '" + table + "'. Tried: " + Arrays.toString(candidates)); + } + + private String findColumnOrNull(Map columns, String... candidates) { + for (String candidate : candidates) { + if (columns.containsKey(candidate)) return candidate; + } + return null; + } +} diff --git a/eventlens-source-mysql/src/main/resources/META-INF/services/io.eventlens.spi.EventSourcePlugin b/eventlens-source-mysql/src/main/resources/META-INF/services/io.eventlens.spi.EventSourcePlugin new file mode 100644 index 0000000..fcce56c --- /dev/null +++ b/eventlens-source-mysql/src/main/resources/META-INF/services/io.eventlens.spi.EventSourcePlugin @@ -0,0 +1 @@ +io.eventlens.mysql.MySqlEventSourcePlugin diff --git a/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventSourcePluginContractTest.java b/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventSourcePluginContractTest.java new file mode 100644 index 0000000..ff2a60d --- /dev/null +++ b/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventSourcePluginContractTest.java @@ -0,0 +1,86 @@ +package io.eventlens.mysql; + +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.test.CanonicalEventSet; +import io.eventlens.test.EventSourcePluginTestKit; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.Map; +import java.util.UUID; + +@Testcontainers(disabledWithoutDocker = true) +class MySqlEventSourcePluginContractTest extends EventSourcePluginTestKit { + + @Container + @SuppressWarnings("resource") + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.4") + .withDatabaseName("eventlens_contract_test"); + + @Override + protected EventSourcePlugin createPlugin() throws Exception { + ensureSchema(); + var plugin = new MySqlEventSourcePlugin(); + plugin.initialize("contract-mysql", Map.of( + "jdbcUrl", mysql.getJdbcUrl(), + "username", mysql.getUsername(), + "password", mysql.getPassword(), + "tableName", "event_store" + )); + return plugin; + } + + @Override + protected void seedCanonicalEvents() throws Exception { + for (var event : CanonicalEventSet.defaultEvents()) { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO event_store (event_id, aggregate_id, aggregate_type, sequence_number, event_type, payload, metadata) + VALUES (?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON)) + """)) { + ps.setString(1, UUID.randomUUID().toString()); + ps.setString(2, event.aggregateId()); + ps.setString(3, event.aggregateType()); + ps.setLong(4, event.sequenceNumber()); + ps.setString(5, event.eventType()); + ps.setString(6, event.payload()); + ps.setString(7, event.metadata()); + ps.executeUpdate(); + } + } + } + + @Override + protected void cleanupStore() throws Exception { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute("TRUNCATE TABLE event_store"); + } + } + + private static void ensureSchema() throws Exception { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE IF NOT EXISTS event_store ( + event_id VARCHAR(64) PRIMARY KEY, + aggregate_id VARCHAR(255) NOT NULL, + aggregate_type VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + event_type VARCHAR(255) NOT NULL, + payload JSON NOT NULL, + metadata JSON NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + global_position BIGINT NOT NULL AUTO_INCREMENT UNIQUE, + UNIQUE KEY uq_aggregate_sequence (aggregate_id, sequence_number) + ) + """); + stmt.execute("TRUNCATE TABLE event_store"); + } + } +} diff --git a/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventStoreReaderIntegrationTest.java b/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventStoreReaderIntegrationTest.java new file mode 100644 index 0000000..ba25aa8 --- /dev/null +++ b/eventlens-source-mysql/src/test/java/io/eventlens/mysql/MySqlEventStoreReaderIntegrationTest.java @@ -0,0 +1,95 @@ +package io.eventlens.mysql; + +import io.eventlens.core.model.StoredEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers(disabledWithoutDocker = true) +class MySqlEventStoreReaderIntegrationTest { + + @Container + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.4").withDatabaseName("eventlens_test"); + + private MySqlEventStoreReader reader; + + @BeforeEach + void setUp() throws Exception { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE IF NOT EXISTS event_store ( + event_id VARCHAR(64) PRIMARY KEY, + aggregate_id VARCHAR(255) NOT NULL, + aggregate_type VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + event_type VARCHAR(255) NOT NULL, + payload JSON NOT NULL, + metadata JSON NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + global_position BIGINT NOT NULL AUTO_INCREMENT UNIQUE, + UNIQUE KEY uq_aggregate_sequence (aggregate_id, sequence_number) + ) + """); + stmt.execute("TRUNCATE TABLE event_store"); + } + reader = new MySqlEventStoreReader(new MySqlConfig(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword(), "event_store", null, null, 30)); + } + + @AfterEach + void tearDown() { + if (reader != null) { + reader.close(); + } + } + + @Test + void getEventsReturnsOrderedEvents() throws Exception { + insert("ACC-001", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-001", "BankAccount", 2, "MoneyDeposited", "{\"amount\":100}"); + insert("ACC-001", "BankAccount", 3, "MoneyDeposited", "{\"amount\":50}"); + + List events = reader.getEvents("ACC-001"); + assertThat(events).hasSize(3); + assertThat(events.get(0).sequenceNumber()).isEqualTo(1); + assertThat(events.get(2).sequenceNumber()).isEqualTo(3); + } + + @Test + void searchAggregatesFindsPartialMatch() throws Exception { + insert("ACC-001", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-002", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ORD-001", "Order", 1, "OrderCreated", "{\"total\":50}"); + + assertThat(reader.searchAggregates("acc", 10)).containsExactlyInAnyOrder("ACC-001", "ACC-002"); + } + + private void insert(String aggId, String aggType, long seq, String eventType, String payload) throws Exception { + try (Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO event_store (event_id, aggregate_id, aggregate_type, sequence_number, event_type, payload, metadata) + VALUES (?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON)) + """)) { + ps.setString(1, UUID.randomUUID().toString()); + ps.setString(2, aggId); + ps.setString(3, aggType); + ps.setLong(4, seq); + ps.setString(5, eventType); + ps.setString(6, payload); + ps.setString(7, "{}"); + ps.executeUpdate(); + } + } +} diff --git a/eventlens-source-postgres/build.gradle.kts b/eventlens-source-postgres/build.gradle.kts new file mode 100644 index 0000000..bad534c --- /dev/null +++ b/eventlens-source-postgres/build.gradle.kts @@ -0,0 +1,12 @@ +dependencies { + implementation(project(":eventlens-core")) + implementation(project(":eventlens-spi")) + implementation("com.fasterxml.jackson.core:jackson-databind:2.21.1") + implementation("com.zaxxer:HikariCP:7.0.2") + implementation("org.postgresql:postgresql:42.7.10") + + testImplementation(project(":eventlens-plugin-test")) + testImplementation("org.testcontainers:junit-jupiter:1.21.4") + testImplementation("org.testcontainers:postgresql:1.21.4") +} + diff --git a/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgConfig.java b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgConfig.java new file mode 100644 index 0000000..0897976 --- /dev/null +++ b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgConfig.java @@ -0,0 +1,18 @@ +package io.eventlens.pg; + +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.EventLensConfig.PoolConfig; + +public record PgConfig( + String jdbcUrl, + String username, + String password, + String tableName, + ColumnMappingConfig columnOverrides, + PoolConfig pool, + int queryTimeoutSeconds) { + + public PgConfig(String jdbcUrl, String username, String password, String tableName) { + this(jdbcUrl, username, password, tableName, new ColumnMappingConfig(), new PoolConfig(), 30); + } +} diff --git a/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgEventStoreReader.java b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgEventStoreReader.java new file mode 100644 index 0000000..9e75cd2 --- /dev/null +++ b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgEventStoreReader.java @@ -0,0 +1,432 @@ +package io.eventlens.pg; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.exception.QueryTimeoutException; +import io.eventlens.core.exception.EventStoreException; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.pg.PgSchemaDetector.DetectedSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.time.Instant; +import java.util.*; + +/** + * PostgreSQL-backed implementation of {@link EventStoreReader}. + * + *

+ * Uses HikariCP in read-only mode — it will never write to your event + * store. Schema is auto-detected from database metadata unless overridden in + * config. Column name mappings can be explicitly set via + * {@code datasource.columns} in eventlens.yaml. + */ +public class PgEventStoreReader implements EventStoreReader, AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(PgEventStoreReader.class); + + private final HikariDataSource dataSource; + private final DetectedSchema schema; + private final int queryTimeoutSeconds; + + public PgEventStoreReader(PgConfig config) { + HikariConfig hc = new HikariConfig(); + hc.setJdbcUrl(config.jdbcUrl()); + hc.setUsername(config.username()); + hc.setPassword(config.password()); + var pool = config.pool(); + if (pool != null) { + hc.setMaximumPoolSize(pool.getMaximumPoolSize()); + hc.setMinimumIdle(pool.getMinimumIdle()); + hc.setConnectionTimeout(pool.getConnectionTimeoutMs()); + hc.setIdleTimeout(pool.getIdleTimeoutMs()); + hc.setMaxLifetime(pool.getMaxLifetimeMs()); + hc.setLeakDetectionThreshold(pool.getLeakDetectionThresholdMs()); + } + hc.setReadOnly(true); // CRITICAL: read-only + if (pool == null) { + hc.setConnectionTimeout(5_000); + } + hc.setPoolName("eventlens-pg"); + this.dataSource = new HikariDataSource(hc); + this.queryTimeoutSeconds = Math.max(1, config.queryTimeoutSeconds()); + log.info("Connected to PostgreSQL: {}", config.jdbcUrl()); + + var detector = new PgSchemaDetector(); + var overrides = config.columnOverrides() != null ? config.columnOverrides() : new ColumnMappingConfig(); + + if (config.tableName() != null && !config.tableName().isBlank()) { + // Fix 2: use detectForTable instead of the hardcoded buildManualSchema() + // so we still read real DB metadata even when the table is manually specified. + this.schema = detector.detectForTable(config.tableName(), dataSource, overrides); + } else { + this.schema = detector.detect(dataSource, overrides); + } + } + + @Override + public List getEvents(String aggregateId) { + return getEvents(aggregateId, Integer.MAX_VALUE, 0); + } + + @Override + public List getEvents(String aggregateId, int limit, int offset) { + String table = quoteIdentifier(schema.tableName()); + String aggCol = quoteIdentifier(schema.aggregateIdColumn()); + String seqCol = quoteIdentifier(schema.sequenceColumn()); + String sql = String.format( + "SELECT * FROM %s WHERE %s = ? ORDER BY %s ASC LIMIT ? OFFSET ?", + table, aggCol, seqCol); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ps.setInt(2, limit); + ps.setInt(3, offset); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to read events for aggregate: " + aggregateId, e); + } + } + + @Override + public List getEventsAfterSequence(String aggregateId, long afterSequence, int limit) { + String table = quoteIdentifier(schema.tableName()); + String aggCol = quoteIdentifier(schema.aggregateIdColumn()); + String seqCol = quoteIdentifier(schema.sequenceColumn()); + String sql = String.format( + "SELECT * FROM %s WHERE %s = ? AND %s > ? ORDER BY %s ASC LIMIT ?", + table, aggCol, seqCol, seqCol); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ps.setLong(2, afterSequence); + ps.setInt(3, limit); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to read events after sequence " + afterSequence + " for aggregate: " + aggregateId, e); + } + } + + @Override + public List getEventsUpTo(String aggregateId, long maxSequence) { + String table = quoteIdentifier(schema.tableName()); + String aggCol = quoteIdentifier(schema.aggregateIdColumn()); + String seqCol = quoteIdentifier(schema.sequenceColumn()); + String sql = String.format( + "SELECT * FROM %s WHERE %s = ? AND %s <= ? ORDER BY %s ASC", + table, aggCol, seqCol, seqCol); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ps.setLong(2, maxSequence); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to read events up to sequence " + maxSequence, e); + } + } + + @Override + public List findAggregateIds(String aggregateType, int limit, int offset) { + String table = quoteIdentifier(schema.tableName()); + String aggCol = quoteIdentifier(schema.aggregateIdColumn()); + final String sql; + if (schema.aggregateTypeColumn() != null) { + String typeCol = quoteIdentifier(schema.aggregateTypeColumn()); + sql = String.format( + "SELECT DISTINCT %s FROM %s WHERE %s = ? ORDER BY %s LIMIT ? OFFSET ?", + aggCol, table, typeCol, aggCol); + } else { + log.debug("No aggregate type column detected; returning all aggregate IDs (type filter ignored)"); + sql = String.format( + "SELECT DISTINCT %s FROM %s ORDER BY %s LIMIT ? OFFSET ?", + aggCol, table, aggCol); + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + if (schema.aggregateTypeColumn() != null) { + ps.setString(1, aggregateType); + ps.setInt(2, limit); + ps.setInt(3, offset); + } else { + ps.setInt(1, limit); + ps.setInt(2, offset); + } + return extractFirstColumn(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to find aggregate IDs for type: " + aggregateType, e); + } + } + + @Override + public List getRecentEvents(int limit) { + String orderCol = schema.globalPositionColumn() != null + ? schema.globalPositionColumn() + : schema.timestampColumn() != null + ? schema.timestampColumn() + : schema.eventIdColumn(); + String table = quoteIdentifier(schema.tableName()); + String orderColQ = quoteIdentifier(orderCol); + String sql = String.format( + "SELECT * FROM %s ORDER BY %s DESC LIMIT ?", + table, orderColQ); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setInt(1, limit); + List results = mapResults(ps.executeQuery()); + Collections.reverse(results); // oldest first for display + return results; + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to read recent events", e); + } + } + + @Override + public List getEventsAfter(long globalPosition, int limit) { + String posColumn = schema.globalPositionColumn() != null + ? schema.globalPositionColumn() + : schema.eventIdColumn(); + String table = quoteIdentifier(schema.tableName()); + String posColQ = quoteIdentifier(posColumn); + String sql = String.format( + "SELECT * FROM %s WHERE %s > ? ORDER BY %s ASC LIMIT ?", + table, posColQ, posColQ); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setLong(1, globalPosition); + ps.setInt(2, limit); + return mapResults(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to poll events after position " + globalPosition, e); + } + } + + @Override + public long countEvents(String aggregateId) { + String table = quoteIdentifier(schema.tableName()); + String aggCol = quoteIdentifier(schema.aggregateIdColumn()); + String sql = String.format( + "SELECT COUNT(*) FROM %s WHERE %s = ?", + table, aggCol); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, aggregateId); + ResultSet rs = ps.executeQuery(); + return rs.next() ? rs.getLong(1) : 0; + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to count events for: " + aggregateId, e); + } + } + + @Override + public List getAggregateTypes() { + if (schema.aggregateTypeColumn() == null) + return List.of(); + String table = quoteIdentifier(schema.tableName()); + String typeCol = quoteIdentifier(schema.aggregateTypeColumn()); + String sql = String.format( + "SELECT DISTINCT %s FROM %s ORDER BY %s", + typeCol, table, typeCol); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + return extractFirstColumn(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to get aggregate types", e); + } + } + + @Override + public List searchAggregates(String query, int limit) { + String table = quoteIdentifier(schema.tableName()); + String aggCol = quoteIdentifier(schema.aggregateIdColumn()); + String sql = String.format( + "SELECT DISTINCT %s FROM %s WHERE %s ILIKE ? ORDER BY %s LIMIT ?", + aggCol, table, aggCol, aggCol); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setQueryTimeout(queryTimeoutSeconds); + ps.setString(1, "%" + query + "%"); + ps.setInt(2, limit); + return extractFirstColumn(ps.executeQuery()); + } catch (SQLException e) { + if (isQueryCanceled(e)) { + throw new QueryTimeoutException( + queryTimeoutSeconds, + "Query exceeded %ds timeout. Consider narrowing your search or adding indexes." + .formatted(queryTimeoutSeconds), + e); + } + throw new EventStoreException("Failed to search aggregates", e); + } + } + + private static boolean isQueryCanceled(SQLException e) { + return "57014".equals(e.getSQLState()); + } + + public void close() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + log.info("PostgreSQL connection pool closed"); + } + } + + /** + * Quote a PostgreSQL identifier to prevent SQL injection and handle reserved words. + * Escapes double quotes inside the identifier by doubling them. + */ + private static String quoteIdentifier(String identifier) { + if (identifier == null || identifier.isEmpty()) + throw new IllegalArgumentException("Identifier cannot be null or empty"); + return "\"" + identifier.replace("\"", "\"\"") + "\""; + } + + // ── Private helpers ────────────────────────────────────────────────────── + + private List mapResults(ResultSet rs) throws SQLException { + List events = new ArrayList<>(); + while (rs.next()) { + events.add(new StoredEvent( + // Fix 4 + Fix 6: use Objects.toString(getObject()) instead of + // UUID.fromString(getString()). Handles UUID, BIGSERIAL, ULID, String equally. + Objects.toString(rs.getObject(schema.eventIdColumn()), ""), + Objects.toString(rs.getObject(schema.aggregateIdColumn()), ""), + schema.aggregateTypeColumn() != null + ? Objects.toString(rs.getObject(schema.aggregateTypeColumn()), "unknown") + : "unknown", + rs.getLong(schema.sequenceColumn()), + rs.getString(schema.eventTypeColumn()), + rs.getString(schema.payloadColumn()), + // Fix 8: gracefully handle missing or null metadata column + safeGetString(rs, schema.metadataColumn(), "{}"), + // Fix 8: gracefully handle null timestamp (toInstant on null NPE) + safeGetInstant(rs, schema.timestampColumn()), + schema.globalPositionColumn() != null + ? rs.getLong(schema.globalPositionColumn()) + : safeGetLong(rs, schema.eventIdColumn()))); + } + return events; + } + + /** + * Fix 8: safely read a string column — returns fallback if column is null, + * column name is null (optional column not detected), or value is SQL NULL. + */ + private String safeGetString(ResultSet rs, String colName, String fallback) { + if (colName == null) + return fallback; + try { + String val = rs.getString(colName); + return val != null ? val : fallback; + } catch (SQLException e) { + log.debug("Could not read optional column '{}': {}", colName, e.getMessage()); + return fallback; + } + } + + /** + * Safely read a long column — returns 0 if the column value cannot be + * parsed as a long (e.g. UUID primary keys). Used as fallback global + * position when the event_id is BIGSERIAL. + */ + private long safeGetLong(ResultSet rs, String colName) { + try { + return rs.getLong(colName); + } catch (SQLException e) { + try { + return Long.parseLong(Objects.toString(rs.getObject(colName), "0")); + } catch (Exception ignored) { + return 0; + } + } + } + + /** + * Safely read a timestamp column — returns Instant.EPOCH if the column + * name is null (not detected), the value is SQL NULL, or the read fails. + */ + private Instant safeGetInstant(ResultSet rs, String colName) { + if (colName == null) return Instant.EPOCH; + try { + Timestamp ts = rs.getTimestamp(colName); + return ts != null ? ts.toInstant() : Instant.EPOCH; + } catch (SQLException e) { + log.debug("Could not read timestamp column '{}': {}", colName, e.getMessage()); + return Instant.EPOCH; + } + } + + private List extractFirstColumn(ResultSet rs) throws SQLException { + List result = new ArrayList<>(); + while (rs.next()) + result.add(Objects.toString(rs.getObject(1), "")); + return result; + } +} diff --git a/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgSchemaDetector.java b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgSchemaDetector.java new file mode 100644 index 0000000..493703b --- /dev/null +++ b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PgSchemaDetector.java @@ -0,0 +1,130 @@ +package io.eventlens.pg; + +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.exception.SchemaDetectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class PgSchemaDetector { + + private static final Logger log = LoggerFactory.getLogger(PgSchemaDetector.class); + + private static final List CANDIDATE_TABLES = List.of( + "event_store", "events", "domain_events", "stored_events", + "event_log", "aggregate_events", "es_events", + "es_event", "mt_events", "domain_event_entry", + "event_journal", "outbox_event"); + + public record DetectedSchema( + String tableName, + String eventIdColumn, + String aggregateIdColumn, + String aggregateTypeColumn, + String sequenceColumn, + String eventTypeColumn, + String payloadColumn, + String metadataColumn, + String timestampColumn, + String globalPositionColumn + ) { + } + + public DetectedSchema detect(DataSource dataSource, ColumnMappingConfig overrides) { + try (Connection conn = dataSource.getConnection()) { + var meta = conn.getMetaData(); + for (String candidate : CANDIDATE_TABLES) { + try (ResultSet rs = meta.getColumns(null, null, candidate, null)) { + if (rs.next()) { + log.info("Auto-detected event store table: '{}'", candidate); + return detectColumns(candidate, conn, overrides); + } + } + } + throw new SchemaDetectionException( + "No event store table found. Searched: " + CANDIDATE_TABLES + + ". Use --table flag or datasource.table config to specify manually."); + } catch (SQLException e) { + throw new SchemaDetectionException("Schema detection failed", e); + } + } + + public DetectedSchema detect(DataSource dataSource) { + return detect(dataSource, new ColumnMappingConfig()); + } + + public DetectedSchema detectForTable(String tableName, DataSource dataSource, ColumnMappingConfig overrides) { + try (Connection conn = dataSource.getConnection()) { + try (ResultSet rs = conn.getMetaData().getColumns(null, null, tableName, null)) { + if (!rs.next()) { + throw new SchemaDetectionException( + "Table or view '" + tableName + "' not found in the database. Check the datasource.table config value."); + } + } + log.info("Using manually configured table/view: '{}'", tableName); + return detectColumns(tableName, conn, overrides); + } catch (SchemaDetectionException e) { + throw e; + } catch (SQLException e) { + throw new SchemaDetectionException("Schema detection failed for table '" + tableName + "'", e); + } + } + + private DetectedSchema detectColumns(String table, Connection conn, ColumnMappingConfig overrides) throws SQLException { + Map columns = new LinkedHashMap<>(); + try (ResultSet rs = conn.getMetaData().getColumns(null, null, table, null)) { + while (rs.next()) { + columns.put(rs.getString("COLUMN_NAME").toLowerCase(), rs.getString("TYPE_NAME")); + } + } + log.debug("Columns for table '{}': {}", table, columns.keySet()); + + var detected = new DetectedSchema( + table, + overrides.getEventId() != null ? overrides.getEventId() : findColumn(columns, table, "event_id", "id", "uid"), + overrides.getAggregateId() != null ? overrides.getAggregateId() : findColumn(columns, table, "aggregate_id", "stream_id", "entity_id", "stream_key"), + overrides.getAggregateType() != null ? overrides.getAggregateType() : findColumnOrNull(columns, "aggregate_type", "stream_type", "entity_type"), + overrides.getSequence() != null ? overrides.getSequence() : findColumn(columns, table, "sequence_number", "version", "seq", "position", "event_number", "revision"), + overrides.getEventType() != null ? overrides.getEventType() : findColumn(columns, table, "event_type", "type_name", "event_name", "type"), + overrides.getPayload() != null ? overrides.getPayload() : findColumn(columns, table, "payload", "data", "event_data", "body", "json_data", "json_payload", "event_body", "event_payload"), + overrides.getMetadata() != null ? overrides.getMetadata() : findColumnOrNull(columns, "metadata", "meta", "headers"), + overrides.getTimestamp() != null ? overrides.getTimestamp() : findColumnOrNull(columns, "timestamp", "occurred_at", "event_timestamp", "created_at", "inserted_at"), + overrides.getGlobalPosition() != null ? overrides.getGlobalPosition() : findColumnOrNull(columns, "global_position", "global_seq", "log_position", "seq_id", "transaction_id")); + + if (detected.timestampColumn() == null) { + log.warn("No timestamp column detected in '{}'; events will use epoch as timestamp. Override with datasource.columns.timestamp in eventlens.yaml.", table); + } + if (detected.globalPositionColumn() == null) { + log.info("No global_position column in '{}'; live tail will use '{}' as surrogate.", table, detected.eventIdColumn()); + } + return detected; + } + + private String findColumn(Map columns, String table, String... candidates) { + for (String candidate : candidates) { + if (columns.containsKey(candidate)) { + return candidate; + } + } + throw new SchemaDetectionException( + "Cannot detect required column in table '" + table + "'. Tried: " + Arrays.toString(candidates) + + ". Use datasource.columns config to specify the column name explicitly."); + } + + private String findColumnOrNull(Map columns, String... candidates) { + for (String candidate : candidates) { + if (columns.containsKey(candidate)) { + return candidate; + } + } + return null; + } +} diff --git a/eventlens-source-postgres/src/main/java/io/eventlens/pg/PostgresEventSourcePlugin.java b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PostgresEventSourcePlugin.java new file mode 100644 index 0000000..188b513 --- /dev/null +++ b/eventlens-source-postgres/src/main/java/io/eventlens/pg/PostgresEventSourcePlugin.java @@ -0,0 +1,202 @@ +package io.eventlens.pg; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.eventlens.core.EventLensConfig.ColumnMappingConfig; +import io.eventlens.core.EventLensConfig.PoolConfig; +import io.eventlens.core.JsonUtil; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.core.spi.EventStoreReader; +import io.eventlens.spi.Event; +import io.eventlens.spi.EventQuery; +import io.eventlens.spi.EventQueryResult; +import io.eventlens.spi.EventSourceCapabilities; +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.spi.HealthStatus; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class PostgresEventSourcePlugin implements EventSourcePlugin, EventStoreReader { + + private volatile PgEventStoreReader reader; + private volatile String instanceId; + + @Override + public String typeId() { + return "postgres"; + } + + @Override + public String displayName() { + return "PostgreSQL Event Store"; + } + + @Override + public void initialize(String instanceId, Map config) { + this.instanceId = instanceId; + this.reader = new PgEventStoreReader(new PgConfig( + requireString(config, "jdbcUrl"), + requireString(config, "username"), + Objects.toString(config.getOrDefault("password", ""), ""), + blankToNull(Objects.toString(config.get("tableName"), null)), + config.get("columnOverrides") instanceof ColumnMappingConfig columnOverrides ? columnOverrides : new ColumnMappingConfig(), + config.get("pool") instanceof PoolConfig pool ? pool : new PoolConfig(), + config.get("queryTimeoutSeconds") instanceof Number n ? n.intValue() : 30)); + } + + @Override + public EventSourceCapabilities capabilities() { + return new EventSourceCapabilities(true, true, true, true, + Set.of("aggregate_id", "aggregate_type", "event_type", "timestamp")); + } + + @Override + public EventQueryResult query(EventQuery query) { + PgEventStoreReader activeReader = requireReader(); + if (query.type() == EventQuery.QueryType.TIMELINE) { + List events; + if (query.cursor() != null && !query.cursor().isBlank()) { + long afterSequence = Long.parseLong(query.cursor()); + events = activeReader.getEventsAfterSequence(query.aggregateId(), afterSequence, query.limit() + 1); + } else { + events = activeReader.getEvents(query.aggregateId(), query.limit() + 1, 0); + } + boolean hasMore = events.size() > query.limit(); + List page = hasMore ? events.subList(0, query.limit()) : events; + String nextCursor = hasMore && !page.isEmpty() ? Long.toString(page.get(page.size() - 1).sequenceNumber()) : null; + return new EventQueryResult(page.stream().map(event -> toSpiEvent(event, query.fields())).toList(), hasMore, nextCursor); + } + + String searchTerm = query.aggregateId() != null ? query.aggregateId() : ""; + List ids = activeReader.searchAggregates(searchTerm, query.limit()); + List events = ids.stream() + .map(id -> activeReader.getEvents(id, 1, 0)) + .filter(list -> !list.isEmpty()) + .map(list -> toSpiEvent(list.get(0), query.fields())) + .toList(); + return new EventQueryResult(events, false, null); + } + + @Override + public HealthStatus healthCheck() { + try { + requireReader().getAggregateTypes(); + return HealthStatus.up(); + } catch (Exception e) { + return HealthStatus.down(e.getMessage() != null ? e.getMessage() : "postgres health check failed"); + } + } + + @Override + public void close() { + PgEventStoreReader activeReader = this.reader; + this.reader = null; + if (activeReader != null) { + activeReader.close(); + } + } + + @Override + public List getEvents(String aggregateId) { + return requireReader().getEvents(aggregateId); + } + + @Override + public List getEvents(String aggregateId, int limit, int offset) { + return requireReader().getEvents(aggregateId, limit, offset); + } + + @Override + public List getEventsAfterSequence(String aggregateId, long afterSequence, int limit) { + return requireReader().getEventsAfterSequence(aggregateId, afterSequence, limit); + } + + @Override + public List getEventsUpTo(String aggregateId, long maxSequence) { + return requireReader().getEventsUpTo(aggregateId, maxSequence); + } + + @Override + public List findAggregateIds(String aggregateType, int limit, int offset) { + return requireReader().findAggregateIds(aggregateType, limit, offset); + } + + @Override + public List getRecentEvents(int limit) { + return requireReader().getRecentEvents(limit); + } + + @Override + public List getEventsAfter(long globalPosition, int limit) { + return requireReader().getEventsAfter(globalPosition, limit); + } + + @Override + public long countEvents(String aggregateId) { + return requireReader().countEvents(aggregateId); + } + + @Override + public List getAggregateTypes() { + return requireReader().getAggregateTypes(); + } + + @Override + public List searchAggregates(String query, int limit) { + return requireReader().searchAggregates(query, limit); + } + + public String instanceId() { + return instanceId; + } + + private PgEventStoreReader requireReader() { + PgEventStoreReader activeReader = reader; + if (activeReader == null) { + throw new IllegalStateException("Postgres plugin is not initialized"); + } + return activeReader; + } + + private static Event toSpiEvent(StoredEvent event, EventQuery.Fields fields) { + return new Event( + event.eventId(), + event.aggregateId(), + event.aggregateType(), + event.sequenceNumber(), + event.eventType(), + fields == EventQuery.Fields.METADATA ? emptyObject() : parseJson(event.payload()), + parseJson(event.metadata()), + event.timestamp(), + event.globalPosition()); + } + + private static JsonNode parseJson(String json) { + try { + return JsonUtil.mapper().readTree(json == null || json.isBlank() ? "{}" : json); + } catch (Exception e) { + ObjectNode fallback = JsonUtil.mapper().createObjectNode(); + fallback.put("raw", json == null ? "" : json); + return fallback; + } + } + + private static ObjectNode emptyObject() { + return JsonUtil.mapper().createObjectNode(); + } + + private static String requireString(Map config, String key) { + Object value = config.get(key); + if (value == null || value.toString().isBlank()) { + throw new IllegalArgumentException("Missing required postgres config: " + key); + } + return value.toString(); + } + + private static String blankToNull(String value) { + return value == null || value.isBlank() ? null : value; + } +} diff --git a/eventlens-source-postgres/src/main/resources/META-INF/services/io.eventlens.spi.EventSourcePlugin b/eventlens-source-postgres/src/main/resources/META-INF/services/io.eventlens.spi.EventSourcePlugin new file mode 100644 index 0000000..dae2f21 --- /dev/null +++ b/eventlens-source-postgres/src/main/resources/META-INF/services/io.eventlens.spi.EventSourcePlugin @@ -0,0 +1 @@ +io.eventlens.pg.PostgresEventSourcePlugin diff --git a/eventlens-source-postgres/src/test/java/io/eventlens/pg/PgEventStoreReaderIntegrationTest.java b/eventlens-source-postgres/src/test/java/io/eventlens/pg/PgEventStoreReaderIntegrationTest.java new file mode 100644 index 0000000..78b40c7 --- /dev/null +++ b/eventlens-source-postgres/src/test/java/io/eventlens/pg/PgEventStoreReaderIntegrationTest.java @@ -0,0 +1,160 @@ +package io.eventlens.pg; + +import io.eventlens.core.model.StoredEvent; +import org.junit.jupiter.api.*; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.*; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@Testcontainers(disabledWithoutDocker = true) +class PgEventStoreReaderIntegrationTest { + + @Container + @SuppressWarnings("resource") + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine") + .withDatabaseName("eventlens_test"); + + private PgEventStoreReader reader; + + @BeforeAll + static void createSchema() throws Exception { + try (Connection conn = DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE event_store ( + event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + aggregate_id VARCHAR(255) NOT NULL, + aggregate_type VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + event_type VARCHAR(255) NOT NULL, + payload JSONB NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + global_position BIGSERIAL, + UNIQUE (aggregate_id, sequence_number) + ) + """); + } + } + + @BeforeEach + void setUp() { + var config = new PgConfig(postgres.getJdbcUrl(), postgres.getUsername(), + postgres.getPassword(), "event_store"); + reader = new PgEventStoreReader(config); + } + + @AfterEach + void tearDown() throws Exception { + try (Connection conn = DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute("TRUNCATE event_store RESTART IDENTITY"); + } + reader.close(); + } + + @Test + void schemaDetectorFindsEventStoreTable() { + // Just constructing the reader exercises the schema detector + assertThat(reader).isNotNull(); + } + + @Test + void getEventsReturnsOrderedEvents() throws Exception { + insert("ACC-001", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-001", "BankAccount", 2, "MoneyDeposited", "{\"amount\":100}"); + insert("ACC-001", "BankAccount", 3, "MoneyDeposited", "{\"amount\":50}"); + + List events = reader.getEvents("ACC-001"); + + assertThat(events).hasSize(3); + assertThat(events.get(0).sequenceNumber()).isEqualTo(1); + assertThat(events.get(0).eventType()).isEqualTo("AccountCreated"); + assertThat(events.get(2).sequenceNumber()).isEqualTo(3); + } + + @Test + void getEventsUpToRespectsMaxSequence() throws Exception { + insert("ACC-002", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-002", "BankAccount", 2, "MoneyDeposited", "{\"amount\":100}"); + insert("ACC-002", "BankAccount", 3, "MoneyWithdrawn", "{\"amount\":30}"); + + List events = reader.getEventsUpTo("ACC-002", 2); + + assertThat(events).hasSize(2); + assertThat(events.getLast().sequenceNumber()).isEqualTo(2); + } + + @Test + void searchAggregatesFindsPartialMatch() throws Exception { + insert("ACC-001", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-002", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ORD-001", "Order", 1, "OrderCreated", "{\"total\":50}"); + + List results = reader.searchAggregates("ACC", 10); + + assertThat(results).containsExactlyInAnyOrder("ACC-001", "ACC-002"); + } + + @Test + void countEventsReturnsCorrectCount() throws Exception { + insert("ACC-003", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-003", "BankAccount", 2, "MoneyDeposited", "{\"amount\":200}"); + + assertThat(reader.countEvents("ACC-003")).isEqualTo(2); + assertThat(reader.countEvents("NON-EXISTENT")).isEqualTo(0); + } + + @Test + void getEventsWithPaginationReturnsWindowedSlice() throws Exception { + insert("ACC-004", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ACC-004", "BankAccount", 2, "MoneyDeposited", "{\"amount\":100}"); + insert("ACC-004", "BankAccount", 3, "MoneyDeposited", "{\"amount\":50}"); + insert("ACC-004", "BankAccount", 4, "MoneyWithdrawn", "{\"amount\":20}"); + + // limit=2, offset=1 -> should return sequence 2 and 3 + List window = reader.getEvents("ACC-004", 2, 1); + + assertThat(window).hasSize(2); + assertThat(window.get(0).sequenceNumber()).isEqualTo(2); + assertThat(window.get(1).sequenceNumber()).isEqualTo(3); + } + + @Test + void getAggregateTypesReturnsDistinctTypes() throws Exception { + insert("ACC-001", "BankAccount", 1, "AccountCreated", "{\"balance\":0}"); + insert("ORD-001", "Order", 1, "OrderCreated", "{\"total\":50}"); + + List types = reader.getAggregateTypes(); + + assertThat(types).containsExactlyInAnyOrder("BankAccount", "Order"); + } + + // ── Test helper ────────────────────────────────────────────────────────── + + private void insert(String aggId, String aggType, long seq, + String eventType, String payload) throws Exception { + try (Connection conn = DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO event_store (aggregate_id, aggregate_type, sequence_number, + event_type, payload) + VALUES (?, ?, ?, ?, ?::jsonb) + """)) { + ps.setString(1, aggId); + ps.setString(2, aggType); + ps.setLong(3, seq); + ps.setString(4, eventType); + ps.setString(5, payload); + ps.executeUpdate(); + } + } +} + diff --git a/eventlens-source-postgres/src/test/java/io/eventlens/pg/PostgresEventSourcePluginContractTest.java b/eventlens-source-postgres/src/test/java/io/eventlens/pg/PostgresEventSourcePluginContractTest.java new file mode 100644 index 0000000..a17a124 --- /dev/null +++ b/eventlens-source-postgres/src/test/java/io/eventlens/pg/PostgresEventSourcePluginContractTest.java @@ -0,0 +1,85 @@ +package io.eventlens.pg; + +import io.eventlens.spi.EventSourcePlugin; +import io.eventlens.test.CanonicalEventSet; +import io.eventlens.test.EventSourcePluginTestKit; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.Map; + +@Testcontainers(disabledWithoutDocker = true) +class PostgresEventSourcePluginContractTest extends EventSourcePluginTestKit { + + @Container + @SuppressWarnings("resource") + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine") + .withDatabaseName("eventlens_contract_test"); + + @BeforeAll + static void createSchema() throws Exception { + try (Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto"); + stmt.execute(""" + CREATE TABLE IF NOT EXISTS event_store ( + event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + aggregate_id VARCHAR(255) NOT NULL, + aggregate_type VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + event_type VARCHAR(255) NOT NULL, + payload JSONB NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + global_position BIGSERIAL, + UNIQUE (aggregate_id, sequence_number) + ) + """); + } + } + + @Override + protected EventSourcePlugin createPlugin() { + var plugin = new PostgresEventSourcePlugin(); + plugin.initialize("contract-postgres", Map.of( + "jdbcUrl", postgres.getJdbcUrl(), + "username", postgres.getUsername(), + "password", postgres.getPassword(), + "tableName", "event_store" + )); + return plugin; + } + + @Override + protected void seedCanonicalEvents() throws Exception { + for (var event : CanonicalEventSet.defaultEvents()) { + try (Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO event_store (aggregate_id, aggregate_type, sequence_number, event_type, payload, metadata) + VALUES (?, ?, ?, ?, ?::jsonb, ?::jsonb) + """)) { + ps.setString(1, event.aggregateId()); + ps.setString(2, event.aggregateType()); + ps.setLong(3, event.sequenceNumber()); + ps.setString(4, event.eventType()); + ps.setString(5, event.payload()); + ps.setString(6, event.metadata()); + ps.executeUpdate(); + } + } + } + + @Override + protected void cleanupStore() throws Exception { + try (Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + Statement stmt = conn.createStatement()) { + stmt.execute("TRUNCATE event_store RESTART IDENTITY"); + } + } +} diff --git a/eventlens-spi/README.md b/eventlens-spi/README.md new file mode 100644 index 0000000..6662cee --- /dev/null +++ b/eventlens-spi/README.md @@ -0,0 +1,19 @@ +# eventlens-spi + +Stable plugin contract for EventLens v3. + +## Rules + +- This module is a dependency leaf. +- It must not depend on runtime modules (`eventlens-core`, source plugins, stream plugins). +- Keep only interfaces, records, enums, and compatibility helpers. + +## SPI Versioning + +- Adding a `default` method to an interface is backward compatible. +- Adding a new interface is backward compatible. +- Adding a required method to an interface requires an SPI major bump. +- Changing a method signature requires an SPI major bump. +- Removing a method requires an SPI major bump. + +Compatibility checks are centralized in `SpiVersions`. diff --git a/eventlens-spi/build.gradle.kts b/eventlens-spi/build.gradle.kts new file mode 100644 index 0000000..e5c2631 --- /dev/null +++ b/eventlens-spi/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `java-library` + `maven-publish` +} + +java { + withSourcesJar() + withJavadocJar() +} + +dependencies { + api("com.fasterxml.jackson.core:jackson-databind:2.17.2") + testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2") +} + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + pom { + name.set("eventlens-spi") + description.set("Stable plugin contract for EventLens v3") + } + } + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/Event.java b/eventlens-spi/src/main/java/io/eventlens/spi/Event.java new file mode 100644 index 0000000..ddf30bb --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/Event.java @@ -0,0 +1,18 @@ +package io.eventlens.spi; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.time.Instant; + +public record Event( + String eventId, + String aggregateId, + String aggregateType, + long sequenceNumber, + String eventType, + JsonNode payload, + JsonNode metadata, + Instant timestamp, + long globalPosition +) { +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/EventQuery.java b/eventlens-spi/src/main/java/io/eventlens/spi/EventQuery.java new file mode 100644 index 0000000..6b8b853 --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/EventQuery.java @@ -0,0 +1,113 @@ +package io.eventlens.spi; + +import java.time.Instant; +import java.util.Objects; + +public record EventQuery( + QueryType type, + String aggregateId, + String aggregateType, + String eventType, + Instant from, + Instant to, + int limit, + String cursor, + Fields fields +) { + public enum QueryType { + TIMELINE, + SEARCH + } + + public enum Fields { + ALL, + METADATA + } + + public EventQuery { + Objects.requireNonNull(type, "type"); + if (limit <= 0) { + throw new IllegalArgumentException("limit must be > 0"); + } + if (from != null && to != null && from.isAfter(to)) { + throw new IllegalArgumentException("from must be <= to"); + } + if (fields == null) { + fields = Fields.ALL; + } + } + + public static Builder builder(QueryType type) { + return new Builder(type); + } + + public static final class Builder { + private final QueryType type; + private String aggregateId; + private String aggregateType; + private String eventType; + private Instant from; + private Instant to; + private int limit = 100; + private String cursor; + private Fields fields = Fields.ALL; + + private Builder(QueryType type) { + this.type = Objects.requireNonNull(type, "type"); + } + + public Builder aggregateId(String aggregateId) { + this.aggregateId = aggregateId; + return this; + } + + public Builder aggregateType(String aggregateType) { + this.aggregateType = aggregateType; + return this; + } + + public Builder eventType(String eventType) { + this.eventType = eventType; + return this; + } + + public Builder from(Instant from) { + this.from = from; + return this; + } + + public Builder to(Instant to) { + this.to = to; + return this; + } + + public Builder limit(int limit) { + this.limit = limit; + return this; + } + + public Builder cursor(String cursor) { + this.cursor = cursor; + return this; + } + + public Builder fields(Fields fields) { + this.fields = fields; + return this; + } + + public EventQuery build() { + return new EventQuery( + type, + aggregateId, + aggregateType, + eventType, + from, + to, + limit, + cursor, + fields + ); + } + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/EventQueryResult.java b/eventlens-spi/src/main/java/io/eventlens/spi/EventQueryResult.java new file mode 100644 index 0000000..3b089a4 --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/EventQueryResult.java @@ -0,0 +1,17 @@ +package io.eventlens.spi; + +import java.util.List; + +public record EventQueryResult( + List events, + boolean hasMore, + String nextCursor +) { + public EventQueryResult { + events = List.copyOf(events == null ? List.of() : events); + } + + public static EventQueryResult empty() { + return new EventQueryResult(List.of(), false, null); + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/EventSourceCapabilities.java b/eventlens-spi/src/main/java/io/eventlens/spi/EventSourceCapabilities.java new file mode 100644 index 0000000..aabe74d --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/EventSourceCapabilities.java @@ -0,0 +1,25 @@ +package io.eventlens.spi; + +import java.util.Set; + +public record EventSourceCapabilities( + boolean supportsCursorPagination, + boolean supportsFullTextSearch, + boolean supportsGlobalOrdering, + boolean supportsTimeRangeFilter, + Set filterableFields +) { + public EventSourceCapabilities { + filterableFields = Set.copyOf(filterableFields == null ? Set.of() : filterableFields); + } + + public static EventSourceCapabilities basic() { + return new EventSourceCapabilities( + true, + false, + true, + true, + Set.of("aggregate_id", "aggregate_type", "event_type", "timestamp") + ); + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/EventSourcePlugin.java b/eventlens-spi/src/main/java/io/eventlens/spi/EventSourcePlugin.java new file mode 100644 index 0000000..0ed4bea --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/EventSourcePlugin.java @@ -0,0 +1,35 @@ +package io.eventlens.spi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +import java.util.Map; + +public interface EventSourcePlugin extends AutoCloseable { + String typeId(); + + String displayName(); + + default int spiVersion() { + return SpiVersions.CURRENT; + } + + void initialize(String instanceId, Map config); + + EventQueryResult query(EventQuery query); + + default EventSourceCapabilities capabilities() { + return EventSourceCapabilities.basic(); + } + + HealthStatus healthCheck(); + + default JsonNode configSchema() { + return NullNode.getInstance(); + } + + @Override + default void close() { + // Optional for plugins with no resources to release. + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/HealthStatus.java b/eventlens-spi/src/main/java/io/eventlens/spi/HealthStatus.java new file mode 100644 index 0000000..9d850ec --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/HealthStatus.java @@ -0,0 +1,26 @@ +package io.eventlens.spi; + +import java.util.Map; + +public record HealthStatus( + State state, + String message, + Map details +) { + public HealthStatus { + details = Map.copyOf(details == null ? Map.of() : details); + } + + public enum State { + UP, + DOWN + } + + public static HealthStatus up() { + return new HealthStatus(State.UP, null, Map.of()); + } + + public static HealthStatus down(String message) { + return new HealthStatus(State.DOWN, message, Map.of()); + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/PluginLifecycle.java b/eventlens-spi/src/main/java/io/eventlens/spi/PluginLifecycle.java new file mode 100644 index 0000000..2f036fd --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/PluginLifecycle.java @@ -0,0 +1,10 @@ +package io.eventlens.spi; + +public enum PluginLifecycle { + DISCOVERED, + INITIALIZING, + READY, + DEGRADED, + FAILED, + STOPPED +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/ReducerPlugin.java b/eventlens-spi/src/main/java/io/eventlens/spi/ReducerPlugin.java new file mode 100644 index 0000000..aa167b1 --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/ReducerPlugin.java @@ -0,0 +1,19 @@ +package io.eventlens.spi; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.List; + +public interface ReducerPlugin { + String typeId(); + + String displayName(); + + default int spiVersion() { + return SpiVersions.CURRENT; + } + + String aggregateType(); + + JsonNode reduce(List events); +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/SpiVersions.java b/eventlens-spi/src/main/java/io/eventlens/spi/SpiVersions.java new file mode 100644 index 0000000..9da9cdb --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/SpiVersions.java @@ -0,0 +1,27 @@ +package io.eventlens.spi; + +import java.util.Optional; + +public final class SpiVersions { + public static final int CURRENT = 1; + public static final int MINIMUM_SUPPORTED = 1; + + private SpiVersions() { + } + + public static Optional checkCompatibility(String pluginId, int pluginSpiVersion) { + if (pluginSpiVersion < MINIMUM_SUPPORTED) { + return Optional.of( + "Plugin '%s' uses SPI version %d, minimum supported is %d. Please upgrade the plugin." + .formatted(pluginId, pluginSpiVersion, MINIMUM_SUPPORTED) + ); + } + if (pluginSpiVersion > CURRENT) { + return Optional.of( + "Plugin '%s' uses SPI version %d, but this EventLens supports up to %d. Please upgrade EventLens." + .formatted(pluginId, pluginSpiVersion, CURRENT) + ); + } + return Optional.empty(); + } +} diff --git a/eventlens-spi/src/main/java/io/eventlens/spi/StreamAdapterPlugin.java b/eventlens-spi/src/main/java/io/eventlens/spi/StreamAdapterPlugin.java new file mode 100644 index 0000000..3e03d5f --- /dev/null +++ b/eventlens-spi/src/main/java/io/eventlens/spi/StreamAdapterPlugin.java @@ -0,0 +1,34 @@ +package io.eventlens.spi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +import java.util.Map; +import java.util.function.Consumer; + +public interface StreamAdapterPlugin extends AutoCloseable { + String typeId(); + + String displayName(); + + default int spiVersion() { + return SpiVersions.CURRENT; + } + + void initialize(String instanceId, Map config); + + void subscribe(Consumer listener); + + void unsubscribe(); + + HealthStatus healthCheck(); + + default JsonNode configSchema() { + return NullNode.getInstance(); + } + + @Override + default void close() { + // Optional for plugins with no resources to release. + } +} diff --git a/eventlens-spi/src/test/java/io/eventlens/spi/EventQueryTest.java b/eventlens-spi/src/test/java/io/eventlens/spi/EventQueryTest.java new file mode 100644 index 0000000..3c986bb --- /dev/null +++ b/eventlens-spi/src/test/java/io/eventlens/spi/EventQueryTest.java @@ -0,0 +1,56 @@ +package io.eventlens.spi; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EventQueryTest { + + @Test + void builder_sets_expected_values() { + var query = EventQuery.builder(EventQuery.QueryType.TIMELINE) + .aggregateId("agg-1") + .limit(50) + .cursor("10") + .fields(EventQuery.Fields.METADATA) + .build(); + + assertThat(query.type()).isEqualTo(EventQuery.QueryType.TIMELINE); + assertThat(query.aggregateId()).isEqualTo("agg-1"); + assertThat(query.limit()).isEqualTo(50); + assertThat(query.cursor()).isEqualTo("10"); + assertThat(query.fields()).isEqualTo(EventQuery.Fields.METADATA); + } + + @Test + void defaults_fields_to_all_when_null() { + var query = new EventQuery( + EventQuery.QueryType.SEARCH, + null, null, null, null, null, + 25, null, null + ); + + assertThat(query.fields()).isEqualTo(EventQuery.Fields.ALL); + } + + @Test + void rejects_non_positive_limit() { + assertThatThrownBy(() -> new EventQuery( + EventQuery.QueryType.SEARCH, + null, null, null, null, null, + 0, null, EventQuery.Fields.ALL + )).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void rejects_invalid_time_range() { + assertThatThrownBy(() -> EventQuery.builder(EventQuery.QueryType.SEARCH) + .from(Instant.parse("2026-01-02T00:00:00Z")) + .to(Instant.parse("2026-01-01T00:00:00Z")) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/eventlens-spi/src/test/java/io/eventlens/spi/SpiTypesSerializationTest.java b/eventlens-spi/src/test/java/io/eventlens/spi/SpiTypesSerializationTest.java new file mode 100644 index 0000000..672fbb7 --- /dev/null +++ b/eventlens-spi/src/test/java/io/eventlens/spi/SpiTypesSerializationTest.java @@ -0,0 +1,76 @@ +package io.eventlens.spi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SpiTypesSerializationTest { + + private final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Test + void event_query_round_trip_json() throws Exception { + var query = EventQuery.builder(EventQuery.QueryType.SEARCH) + .aggregateType("ORDER") + .eventType("ORDER_PLACED") + .from(Instant.parse("2026-01-01T00:00:00Z")) + .to(Instant.parse("2026-01-02T00:00:00Z")) + .limit(100) + .cursor("abc") + .fields(EventQuery.Fields.METADATA) + .build(); + + var json = mapper.writeValueAsString(query); + var roundTrip = mapper.readValue(json, EventQuery.class); + + assertThat(roundTrip).isEqualTo(query); + } + + @Test + void health_status_factory_methods_work() { + assertThat(HealthStatus.up().state()).isEqualTo(HealthStatus.State.UP); + assertThat(HealthStatus.down("x").state()).isEqualTo(HealthStatus.State.DOWN); + } + + @Test + void capabilities_basic_factory_is_stable() { + var capabilities = EventSourceCapabilities.basic(); + assertThat(capabilities.supportsCursorPagination()).isTrue(); + assertThat(capabilities.filterableFields()).isNotEmpty(); + } + + @Test + void collections_are_defensively_copied() { + var details = new java.util.HashMap(); + details.put("k", "v"); + var health = new HealthStatus(HealthStatus.State.UP, null, details); + details.put("x", "y"); + assertThat(health.details()).containsOnlyKeys("k"); + + var fields = new java.util.HashSet(); + fields.add("aggregate_id"); + var capabilities = new EventSourceCapabilities(true, false, true, true, fields); + fields.add("event_type"); + assertThat(capabilities.filterableFields()).containsExactly("aggregate_id"); + + var result = new EventQueryResult(new java.util.ArrayList<>(), false, null); + assertThatThrownBy(() -> result.events().add(null)).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void spi_versions_compatibility_contract() { + Optional ok = SpiVersions.checkCompatibility("plugin-a", SpiVersions.CURRENT); + Optional tooOld = SpiVersions.checkCompatibility("plugin-a", SpiVersions.MINIMUM_SUPPORTED - 1); + Optional tooNew = SpiVersions.checkCompatibility("plugin-a", SpiVersions.CURRENT + 1); + + assertThat(ok).isEmpty(); + assertThat(tooOld).isPresent(); + assertThat(tooNew).isPresent(); + } +} diff --git a/eventlens-stream-kafka/build.gradle.kts b/eventlens-stream-kafka/build.gradle.kts new file mode 100644 index 0000000..07c753a --- /dev/null +++ b/eventlens-stream-kafka/build.gradle.kts @@ -0,0 +1,11 @@ +dependencies { + implementation(project(":eventlens-core")) + implementation(project(":eventlens-spi")) + implementation("org.apache.kafka:kafka-clients:4.2.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.21.1") + + testImplementation(project(":eventlens-plugin-test")) + testImplementation("org.testcontainers:junit-jupiter:1.21.4") + testImplementation("org.testcontainers:kafka:1.21.4") +} + diff --git a/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaConfig.java b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaConfig.java new file mode 100644 index 0000000..a1debc9 --- /dev/null +++ b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaConfig.java @@ -0,0 +1,12 @@ +package io.eventlens.kafka; + +/** + * Kafka connection configuration. + * + * @param bootstrapServers Kafka bootstrap servers (e.g. "localhost:9092") + * @param topic topic containing domain events + */ +public record KafkaConfig( + String bootstrapServers, + String topic) { +} diff --git a/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaEventMapper.java b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaEventMapper.java new file mode 100644 index 0000000..7c95778 --- /dev/null +++ b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaEventMapper.java @@ -0,0 +1,80 @@ +package io.eventlens.kafka; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.eventlens.core.model.StoredEvent; +import io.eventlens.spi.Event; +import org.apache.kafka.clients.consumer.ConsumerRecord; + +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class KafkaEventMapper { + + private static final ObjectMapper MAPPER = new ObjectMapper().findAndRegisterModules(); + + public static StoredEvent fromRecord(ConsumerRecord record) { + try { + Map value = MAPPER.readValue(record.value(), new TypeReference<>() {}); + + if (value.containsKey("eventId") && value.containsKey("aggregateId")) { + return new StoredEvent( + Objects.toString(value.get("eventId"), ""), + (String) value.get("aggregateId"), + (String) value.getOrDefault("aggregateType", "unknown"), + toLong(value.getOrDefault("sequenceNumber", 0L)), + (String) value.getOrDefault("eventType", "UnknownEvent"), + MAPPER.writeValueAsString(value.getOrDefault("payload", "{}")), + MAPPER.writeValueAsString(value.getOrDefault("metadata", "{}")), + Instant.parse((String) value.getOrDefault("timestamp", Instant.now().toString())), + record.offset()); + } + + String eventType = (String) value.getOrDefault("eventType", value.getOrDefault("type", "UnknownEvent")); + String aggregateId = record.key() != null ? record.key() : "unknown"; + + return new StoredEvent( + UUID.randomUUID().toString(), + aggregateId, + "unknown", + record.offset(), + eventType, + record.value(), + "{}", + Instant.ofEpochMilli(record.timestamp()), + record.offset()); + } catch (Exception e) { + throw new RuntimeException("Cannot parse Kafka record at offset " + record.offset(), e); + } + } + + public static Event toSpiEvent(StoredEvent event) { + try { + return new Event( + event.eventId(), + event.aggregateId(), + event.aggregateType(), + event.sequenceNumber(), + event.eventType(), + MAPPER.readTree(event.payload()), + MAPPER.readTree(event.metadata()), + event.timestamp(), + event.globalPosition()); + } catch (Exception e) { + throw new RuntimeException("Cannot convert Kafka event " + event.eventId(), e); + } + } + + private static long toLong(Object val) { + if (val instanceof Number n) { + return n.longValue(); + } + try { + return Long.parseLong(val.toString()); + } catch (Exception e) { + return 0L; + } + } +} diff --git a/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaLiveTail.java b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaLiveTail.java new file mode 100644 index 0000000..51e3afe --- /dev/null +++ b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaLiveTail.java @@ -0,0 +1,105 @@ +package io.eventlens.kafka; + +import io.eventlens.core.model.StoredEvent; +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +/** + * Consumes events from a Kafka topic on a virtual thread, + * notifying registered listeners for each event received. + * + *

+ * Uses a random group ID so it never interferes with your application's + * consumers. + * Fails gracefully — if Kafka is unreachable, a warning is logged and the live + * tail + * falls back to PostgreSQL polling. + */ +public class KafkaLiveTail implements AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(KafkaLiveTail.class); + + private final KafkaConsumer consumer; + private final String topic; + private final List> listeners = new CopyOnWriteArrayList<>(); + private volatile boolean running = false; + + public KafkaLiveTail(KafkaConfig config) { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, config.bootstrapServers()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "eventlens-livetail-" + UUID.randomUUID()); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "5000"); + props.put(ConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "5000"); + + this.consumer = new KafkaConsumer<>(props); + this.topic = config.topic(); + log.info("KafkaLiveTail initialized for topic '{}' on {}", topic, config.bootstrapServers()); + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void start() { + running = true; + consumer.subscribe(List.of(topic)); + Thread.ofVirtual().name("eventlens-kafka-tail").start(this::pollLoop); + log.info("Kafka live tail started on topic '{}'", topic); + } + + @Override + public void close() { + running = false; + consumer.wakeup(); + log.info("Kafka live tail stopped"); + } + + private void pollLoop() { + int backoffMs = 1_000; + final int maxBackoffMs = 30_000; + + while (running) { + try { + ConsumerRecords records = consumer.poll(Duration.ofMillis(500)); + backoffMs = 1_000; // reset on successful poll + for (ConsumerRecord record : records) { + try { + StoredEvent event = KafkaEventMapper.fromRecord(record); + listeners.forEach(l -> l.accept(event)); + } catch (Exception e) { + log.warn("Skipping malformed Kafka event at offset {}: {}", + record.offset(), e.getMessage()); + } + } + } catch (org.apache.kafka.common.errors.WakeupException e) { + break; + } catch (Exception e) { + log.error("Kafka poll loop failed, reconnecting in {}ms: {}", backoffMs, e.getMessage()); + try { + Thread.sleep(backoffMs); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + backoffMs = Math.min(backoffMs * 2, maxBackoffMs); + } + } + + try { + consumer.close(); + } catch (Exception e) { + log.debug("Consumer close: {}", e.getMessage()); + } + } +} diff --git a/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaStreamAdapterPlugin.java b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaStreamAdapterPlugin.java new file mode 100644 index 0000000..758266a --- /dev/null +++ b/eventlens-stream-kafka/src/main/java/io/eventlens/kafka/KafkaStreamAdapterPlugin.java @@ -0,0 +1,82 @@ +package io.eventlens.kafka; + +import io.eventlens.spi.Event; +import io.eventlens.spi.HealthStatus; +import io.eventlens.spi.StreamAdapterPlugin; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class KafkaStreamAdapterPlugin implements StreamAdapterPlugin { + + private volatile KafkaLiveTail liveTail; + private volatile KafkaConfig config; + private final AtomicBoolean subscribed = new AtomicBoolean(false); + + @Override + public String typeId() { + return "kafka"; + } + + @Override + public String displayName() { + return "Kafka Stream Adapter"; + } + + @Override + public void initialize(String instanceId, Map config) { + this.config = new KafkaConfig(requireString(config, "bootstrapServers"), requireString(config, "topic")); + this.liveTail = new KafkaLiveTail(this.config); + } + + @Override + public void subscribe(Consumer listener) { + KafkaLiveTail activeTail = requireLiveTail(); + activeTail.addListener(event -> listener.accept(KafkaEventMapper.toSpiEvent(event))); + if (subscribed.compareAndSet(false, true)) { + activeTail.start(); + } + } + + @Override + public void unsubscribe() { + subscribed.set(false); + KafkaLiveTail activeTail = liveTail; + if (activeTail != null) { + activeTail.close(); + liveTail = config != null ? new KafkaLiveTail(config) : null; + } + } + + @Override + public HealthStatus healthCheck() { + return config == null ? HealthStatus.down("Kafka plugin not initialized") : HealthStatus.up(); + } + + @Override + public void close() { + KafkaLiveTail activeTail = liveTail; + liveTail = null; + if (activeTail != null) { + activeTail.close(); + } + } + + private KafkaLiveTail requireLiveTail() { + KafkaLiveTail activeTail = liveTail; + if (activeTail == null) { + throw new IllegalStateException("Kafka plugin is not initialized"); + } + return activeTail; + } + + private static String requireString(Map config, String key) { + Object value = config.get(key); + if (value == null || Objects.toString(value, "").isBlank()) { + throw new IllegalArgumentException("Missing required kafka config: " + key); + } + return value.toString(); + } +} diff --git a/eventlens-stream-kafka/src/main/resources/META-INF/services/io.eventlens.spi.StreamAdapterPlugin b/eventlens-stream-kafka/src/main/resources/META-INF/services/io.eventlens.spi.StreamAdapterPlugin new file mode 100644 index 0000000..a8280cb --- /dev/null +++ b/eventlens-stream-kafka/src/main/resources/META-INF/services/io.eventlens.spi.StreamAdapterPlugin @@ -0,0 +1 @@ +io.eventlens.kafka.KafkaStreamAdapterPlugin diff --git a/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaLiveTailTest.java b/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaLiveTailTest.java new file mode 100644 index 0000000..91610fd --- /dev/null +++ b/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaLiveTailTest.java @@ -0,0 +1,74 @@ +package io.eventlens.kafka; + +import io.eventlens.core.model.StoredEvent; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.kafka.KafkaContainer; + +import java.time.Duration; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers(disabledWithoutDocker = true) +class KafkaLiveTailTest { + + @Container + static KafkaContainer kafka = new KafkaContainer("apache/kafka:3.8.0"); + + private static KafkaProducer producer; + + @BeforeAll + static void setUpProducer() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers()); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + producer = new KafkaProducer<>(props); + } + + @AfterAll + static void tearDownProducer() { + if (producer != null) { + producer.close(Duration.ofSeconds(5)); + } + } + + @Test + void liveTailConsumesEventsFromKafkaTopic() throws Exception { + String topic = "domain-events"; + KafkaConfig config = new KafkaConfig(kafka.getBootstrapServers(), topic); + + List received = new CopyOnWriteArrayList<>(); + try (KafkaLiveTail tail = new KafkaLiveTail(config)) { + tail.addListener(received::add); + tail.start(); + + // The LiveTail consumer uses auto.offset.reset="latest". + // We must wait until it has fully joined the group before our produced messages + // will be caught. To avoid fragile sleeps, produce repeatedly until we get them. + long start = System.currentTimeMillis(); + while (received.size() < 2 && System.currentTimeMillis() - start < 30_000) { + producer.send(new ProducerRecord<>(topic, "ACC-001", + "{\"eventType\":\"AccountCreated\",\"payload\":{\"balance\":0}}")).get(); + producer.send(new ProducerRecord<>(topic, "ACC-001", + "{\"eventType\":\"MoneyDeposited\",\"payload\":{\"amount\":50}}")).get(); + Thread.sleep(1000); + } + } + + assertThat(received).hasSizeGreaterThanOrEqualTo(2); + assertThat(received.get(0).aggregateId()).isEqualTo("ACC-001"); + } +} + + diff --git a/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaStreamAdapterPluginContractTest.java b/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaStreamAdapterPluginContractTest.java new file mode 100644 index 0000000..d712158 --- /dev/null +++ b/eventlens-stream-kafka/src/test/java/io/eventlens/kafka/KafkaStreamAdapterPluginContractTest.java @@ -0,0 +1,91 @@ +package io.eventlens.kafka; + +import io.eventlens.spi.StreamAdapterPlugin; +import io.eventlens.test.StreamAdapterPluginTestKit; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.kafka.KafkaContainer; + +import java.time.Duration; +import java.time.Instant; +import java.util.Properties; +import java.util.Map; + +@Testcontainers(disabledWithoutDocker = true) +class KafkaStreamAdapterPluginContractTest extends StreamAdapterPluginTestKit { + + @Container + @SuppressWarnings("resource") + static KafkaContainer kafka = new KafkaContainer("apache/kafka:3.8.0"); + + private static KafkaProducer producer; + private static final String TOPIC = "contract-events"; + + @BeforeAll + static void setUpProducer() { + Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers()); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + producer = new KafkaProducer<>(props); + } + + @AfterAll + static void tearDownProducer() { + if (producer != null) { + producer.close(Duration.ofSeconds(5)); + } + } + + @Override + protected StreamAdapterPlugin createPlugin() { + var plugin = new KafkaStreamAdapterPlugin(); + plugin.initialize("contract-kafka", Map.of( + "bootstrapServers", kafka.getBootstrapServers(), + "topic", TOPIC + )); + return plugin; + } + + @Override + protected void emitCanonicalEvents() throws Exception { + String first = """ + { + "eventId":"evt-1", + "aggregateId":"ACC-001", + "aggregateType":"BankAccount", + "sequenceNumber":1, + "eventType":"AccountCreated", + "payload":{"balance":0}, + "metadata":{"source":"contract"}, + "timestamp":"%s" + } + """.formatted(Instant.now().toString()); + String second = """ + { + "eventId":"evt-2", + "aggregateId":"ACC-001", + "aggregateType":"BankAccount", + "sequenceNumber":2, + "eventType":"MoneyDeposited", + "payload":{"amount":100}, + "metadata":{"source":"contract"}, + "timestamp":"%s" + } + """.formatted(Instant.now().plusSeconds(1).toString()); + + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < 10_000) { + producer.send(new ProducerRecord<>(TOPIC, "ACC-001", first)).get(); + producer.send(new ProducerRecord<>(TOPIC, "ACC-001", second)).get(); + producer.flush(); + Thread.sleep(750); + } + } +} diff --git a/eventlens-ui/src/App.tsx b/eventlens-ui/src/App.tsx index 7d670dd..7263f0a 100644 --- a/eventlens-ui/src/App.tsx +++ b/eventlens-ui/src/App.tsx @@ -1,13 +1,23 @@ -import { useState, useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { useQueries, useQuery } from '@tanstack/react-query'; import SearchBar from './components/SearchBar'; import Timeline from './components/Timeline'; import StateViewer, { type TabId } from './components/StateViewer'; import LiveStream from './components/LiveStream'; import AnomalyPanel from './components/AnomalyPanel'; import KeyboardHints from './components/KeyboardHints'; -import { useQuery } from '@tanstack/react-query'; -import { getHealth, getRecentEvents, getTransitions } from './api/client'; -import { DEMO_AGGREGATE_ID } from './demo/demoData'; +import { + getDatasourceHealth, + getDatasources, + getHealth, + getPlugins, + getRecentEvents, + getTimeline, + getTransitions, + type DatasourceHealth, + type DatasourceSummary, + type PluginSummary, +} from './api/client'; import { isDemoMode } from './demo/demoMode'; import { parseEventTimestamp } from './utils/time'; @@ -43,7 +53,24 @@ function MiniWaveform() { ); } -function ConnectionStats({ isUp }: { isUp: boolean }) { +function statusTone(status: string) { + const normalized = status.toLowerCase(); + if (normalized === 'ready' || normalized === 'up') return '#00ff88'; + if (normalized === 'degraded' || normalized === 'initializing') return '#ffd166'; + return '#ff6b6b'; +} + +function isHealthyStatus(status: string) { + const normalized = status.toLowerCase(); + return normalized === 'ready' || normalized === 'up'; +} + +function isSelectableDatasource(status: string) { + const normalized = status.toLowerCase(); + return normalized === 'ready'; +} + +function ConnectionStats({ isUp, source }: { isUp: boolean; source?: string | null }) { const [uptime, setUptime] = useState(0); const [eventCount, setEventCount] = useState(null); const intervalRef = useRef>(undefined); @@ -58,20 +85,22 @@ function ConnectionStats({ isUp }: { isUp: boolean }) { useEffect(() => { const fetchCount = () => { - getRecentEvents(500) + getRecentEvents(500, source) .then((data) => setEventCount(data.length)) .catch(() => {}); }; fetchCount(); const id = setInterval(fetchCount, 15000); return () => clearInterval(id); - }, []); + }, [source]); const fmtUptime = (s: number) => { const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; - return h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${sec}s` : `${sec}s`; + if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m`; + if (m > 0) return `${String(m).padStart(2, '0')}m ${String(sec).padStart(2, '0')}s`; + return `${String(sec).padStart(2, '0')}s`; }; return ( @@ -81,31 +110,32 @@ function ConnectionStats({ isUp }: { isUp: boolean }) { API {isUp ? 'Healthy' : 'Down'} -

+
Events {eventCount ?? '...'}
-
+
Uptime - {fmtUptime(uptime)} + {fmtUptime(uptime)}
); } -/** Sticky summary bar shown when an event is selected */ function EventSummaryBar({ aggregateId, sequence, totalEvents, + source, }: { aggregateId: string; sequence: number; totalEvents: number; + source?: string | null; }) { const { data: transitions } = useQuery({ - queryKey: ['transitions', aggregateId], - queryFn: () => getTransitions(aggregateId), + queryKey: ['transitions', aggregateId, source ?? 'default'], + queryFn: () => getTransitions(aggregateId, source), staleTime: 30_000, }); @@ -122,9 +152,10 @@ function EventSummaryBar({ {event.eventType} seq #{sequence} - {stepIndex !== null && ` · step ${stepIndex} of ${totalEvents}`} - {' · '} + {stepIndex !== null && ` step ${stepIndex} of ${totalEvents}`} + {' '} {parseEventTimestamp(event.timestamp).toLocaleTimeString()} + {source ? ` source ${source}` : ''}
{changeCount > 0 && ( @@ -136,12 +167,79 @@ function EventSummaryBar({ ); } +function PluginHealthPage({ + datasources, + datasourceHealth, + plugins, +}: { + datasources: DatasourceSummary[]; + datasourceHealth: Array; + plugins: PluginSummary[]; +}) { + return ( +
+
+
Datasources
+
+ {datasources.map((source, index) => { + const health = datasourceHealth[index]; + const tone = statusTone(source.status); + return ( +
+
+ {source.displayName} + + {source.status} + +
+
{source.id}
+ {health && ( +
+ {health.health.message} + {health.failureReason ? ` | ${health.failureReason}` : ''} +
+ )} +
+ ); + })} +
+
+ +
+
All Plugins
+
+ {plugins.map(plugin => ( +
+
+ {plugin.displayName} + + {plugin.lifecycle} + +
+
+ {plugin.pluginType} | {plugin.typeId} +
+
{plugin.instanceId}
+
+ {plugin.health.message} + {plugin.failureReason ? ` | ${plugin.failureReason}` : ''} +
+
+ ))} +
+
+
+ ); +} + export default function App() { const [selectedAggregate, setSelectedAggregate] = useState(null); const [selectedSequence, setSelectedSequence] = useState(null); const [activeTab, setActiveTab] = useState('changes'); + const [selectedSource, setSelectedSource] = useState(''); + const [currentHash, setCurrentHash] = useState(window.location.hash || ''); + const [workspaceDockOpen, setWorkspaceDockOpen] = useState(false); - // Listen for keyboard tab-switch (1-4 keys dispatched from Timeline) useEffect(() => { const handler = (e: Event) => { const tab = (e as CustomEvent).detail as TabId; @@ -151,12 +249,18 @@ export default function App() { return () => window.removeEventListener('eventlens:switchtab', handler); }, []); - // Hydrate state from URL on mount + useEffect(() => { + const syncHash = () => setCurrentHash(window.location.hash || ''); + window.addEventListener('hashchange', syncHash); + return () => window.removeEventListener('hashchange', syncHash); + }, []); + useEffect(() => { const params = new URLSearchParams(window.location.search); const aggregateId = params.get('aggregateId'); const seq = params.get('seq'); const tab = params.get('tab') as TabId | null; + const source = params.get('source'); if (aggregateId) setSelectedAggregate(aggregateId); if (seq !== null) { const n = Number(seq); @@ -165,9 +269,11 @@ export default function App() { if (tab && ['changes', 'before-after', 'raw'].includes(tab)) { setActiveTab(tab); } + if (source) { + setSelectedSource(source); + } }, []); - // Reflect selection + tab in URL useEffect(() => { const params = new URLSearchParams(window.location.search); if (selectedAggregate) { @@ -181,10 +287,15 @@ export default function App() { params.delete('seq'); } params.set('tab', activeTab); + if (selectedSource) { + params.set('source', selectedSource); + } else { + params.delete('source'); + } const qs = params.toString(); - const newUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname; + const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}${window.location.hash}`; window.history.replaceState(null, '', newUrl); - }, [selectedAggregate, selectedSequence, activeTab]); + }, [selectedAggregate, selectedSequence, activeTab, selectedSource]); const { data: health } = useQuery({ queryKey: ['health'], @@ -192,6 +303,27 @@ export default function App() { refetchInterval: 30_000, }); + const { data: datasources = [] } = useQuery({ + queryKey: ['datasources'], + queryFn: getDatasources, + staleTime: 10_000, + }); + + const { data: plugins = [] } = useQuery({ + queryKey: ['plugins'], + queryFn: getPlugins, + staleTime: 10_000, + }); + + const datasourceHealthQueries = useQueries({ + queries: datasources.map(source => ({ + queryKey: ['datasource-health', source.id], + queryFn: () => getDatasourceHealth(source.id), + staleTime: 10_000, + })), + }); + const datasourceHealth = datasourceHealthQueries.map(query => query.data); + const isUp = health?.status === 'UP'; const handleSelectAggregate = (id: string) => { @@ -199,14 +331,18 @@ export default function App() { setSelectedSequence(null); }; - // Get total event count for the summary bar - const { data: transitions } = useQuery({ - queryKey: ['transitions', selectedAggregate], - queryFn: () => getTransitions(selectedAggregate!), + const { data: timelineSummary } = useQuery({ + queryKey: ['timeline-summary', selectedAggregate, selectedSource || 'default'], + queryFn: () => getTimeline(selectedAggregate!, 500, 0, selectedSource || null, 'metadata'), enabled: !!selectedAggregate, staleTime: 30_000, }); - const totalEvents = transitions?.length ?? 0; + const totalEvents = timelineSummary?.totalEvents ?? 0; + + const pluginView = currentHash === '#/plugins'; + const healthySources = datasources.filter(source => isHealthyStatus(source.status)).length; + const healthyPlugins = plugins.filter(plugin => isHealthyStatus(plugin.lifecycle)).length; + const issueCount = (datasources.length - healthySources) + (plugins.length - healthyPlugins); return (
@@ -221,10 +357,17 @@ export default function App() {
-
EventLens
+
+ {isDemoMode() && ( +
+ Demo mode +
+ )} +
EventLens
+
-
- +
+
@@ -234,55 +377,138 @@ export default function App() {
-
- {isDemoMode() && ( -
- Demo mode (frontend only): API calls are stubbed with sample data. Search{' '} - {DEMO_AGGREGATE_ID} or demo to load the sample aggregate. + + + {workspaceDockOpen && ( + +
+ )}
)} -
- {selectedAggregate && ( - - )} + {pluginView ? ( + + ) : ( + <> + {selectedAggregate && ( + + )} - {selectedAggregate && selectedSequence !== null && ( - - )} + {selectedAggregate && selectedSequence !== null && ( + + )} - {selectedAggregate && selectedSequence !== null && ( - - )} + {selectedAggregate && selectedSequence !== null && ( + + )} -
- - +
+ + +
+ + )}
diff --git a/eventlens-ui/src/api/client.ts b/eventlens-ui/src/api/client.ts index 6813827..d3576c1 100644 --- a/eventlens-ui/src/api/client.ts +++ b/eventlens-ui/src/api/client.ts @@ -1,5 +1,14 @@ import axios from 'axios'; -import type { AnomalyReport, BisectResult, ReplayResult, StateTransition, StoredEvent } from './types'; +import type { + AnomalyReport, + BisectResult, + DatasourceHealth, + DatasourceSummary, + PluginSummary, + ReplayResult, + StateTransition, + StoredEvent, +} from './types'; import { demoAggregateTypes, demoAnomalies, @@ -22,74 +31,88 @@ function delay(ms: number) { }); } -// ── Types (re-exported for existing imports from `api/client`) ───────────── +function withOptionalSource(path: string, source?: string | null) { + if (!source) { + return path; + } + const separator = path.includes('?') ? '&' : '?'; + return `${path}${separator}source=${encodeURIComponent(source)}`; +} + export type { AnomalyReport, BisectResult, + DatasourceHealth, + DatasourceSummary, FieldChange, + LiveStreamUnavailableMessage, + PluginSummary, ReplayResult, StateTransition, StoredEvent, } from './types'; -// ── API calls ────────────────────────────────────────────────────────────── -export const searchAggregates = async (q: string, limit = 20) => { +export const searchAggregates = async (q: string, limit = 20, source?: string | null) => { + const path = withOptionalSource(`/aggregates/search?q=${encodeURIComponent(q)}&limit=${limit}`, source); if (isDemoMode()) { await delay(40); const demo = demoSearchAggregates(q); try { - const r = await api.get( - `/aggregates/search?q=${encodeURIComponent(q)}&limit=${limit}` - ); + const r = await api.get(path); return [...new Set([...demo, ...r.data])].slice(0, limit); } catch { return demo; } } - return api - .get(`/aggregates/search?q=${encodeURIComponent(q)}&limit=${limit}`) - .then(r => r.data); + return api.get(path).then(r => r.data); }; -export const getAggregateTypes = async () => { +export const getAggregateTypes = async (source?: string | null) => { + const path = withOptionalSource('/meta/types', source); if (isDemoMode()) { await delay(30); try { - const r = await api.get('/meta/types'); + const r = await api.get(path); return [...new Set([...demoAggregateTypes(), ...r.data])]; } catch { return demoAggregateTypes(); } } - return api.get('/meta/types').then(r => r.data); + return api.get(path).then(r => r.data); }; -export const getTimeline = async (id: string, limit = 500, offset = 0) => { +export const getTimeline = async (id: string, limit = 500, offset = 0, source?: string | null, fields: 'full' | 'metadata' = 'full') => { if (isDemoMode() && id === DEMO_AGGREGATE_ID) { await delay(50); return demoTimeline(id, limit, offset); } + const path = withOptionalSource( + `/aggregates/${id}/timeline?limit=${limit}&offset=${offset}&fields=${fields}`, + source + ); return api - .get<{ events: StoredEvent[]; totalEvents: number }>( - `/aggregates/${id}/timeline?limit=${limit}&offset=${offset}` - ) + .get<{ events: StoredEvent[]; totalEvents: number }>(path) .then(r => r.data); }; -export const getTransitions = async (id: string) => { +export const getTransitions = async (id: string, source?: string | null) => { if (isDemoMode() && id === DEMO_AGGREGATE_ID) { await delay(50); return demoTransitions(id); } - return api.get(`/aggregates/${id}/transitions`).then(r => r.data); + return api + .get(withOptionalSource(`/aggregates/${id}/transitions`, source)) + .then(r => r.data); }; -export const replayTo = async (id: string, seq: number) => { +export const replayTo = async (id: string, seq: number, source?: string | null) => { if (isDemoMode() && id === DEMO_AGGREGATE_ID) { await delay(40); return demoReplayTo(id, seq); } - return api.get(`/aggregates/${id}/replay/${seq}`).then(r => r.data); + return api + .get(withOptionalSource(`/aggregates/${id}/replay/${seq}`, source)) + .then(r => r.data); }; export const bisect = async (id: string, expression: string) => { @@ -104,20 +127,20 @@ export const bisect = async (id: string, expression: string) => { .then(r => r.data); }; -export const getAnomalies = async (limit = 100) => { +export const getAnomalies = async (limit = 100, source?: string | null) => { if (isDemoMode()) { await delay(45); return demoAnomalies(limit); } - return api.get(`/anomalies/recent?limit=${limit}`).then(r => r.data); + return api.get(withOptionalSource(`/anomalies/recent?limit=${limit}`, source)).then(r => r.data); }; -export const getRecentEvents = async (limit = 50) => { +export const getRecentEvents = async (limit = 50, source?: string | null) => { if (isDemoMode()) { await delay(35); return demoRecentEvents(limit); } - return api.get(`/events/recent?limit=${limit}`).then(r => r.data); + return api.get(withOptionalSource(`/events/recent?limit=${limit}`, source)).then(r => r.data); }; export const getHealth = async () => { @@ -128,4 +151,11 @@ export const getHealth = async () => { return api.get('/health').then(r => r.data); }; +export const getDatasources = async () => api.get('/v1/datasources').then(r => r.data); + +export const getDatasourceHealth = async (id: string) => + api.get(`/v1/datasources/${encodeURIComponent(id)}/health`).then(r => r.data); + +export const getPlugins = async () => api.get('/v1/plugins').then(r => r.data); + export { DEMO_AGGREGATE_ID } from '../demo/demoData'; diff --git a/eventlens-ui/src/api/types.ts b/eventlens-ui/src/api/types.ts index faf489b..16330bf 100644 --- a/eventlens-ui/src/api/types.ts +++ b/eventlens-ui/src/api/types.ts @@ -4,7 +4,7 @@ export interface StoredEvent { aggregateType: string; sequenceNumber: number; eventType: string; - payload: string; + payload: string | null; metadata: string; timestamp: string; globalPosition: number; @@ -47,3 +47,42 @@ export interface AnomalyReport { timestamp: string; stateAtAnomaly: Record; } + +export interface DatasourceSummary { + id: string; + displayName: string; + status: string; + healthMessage: string; + capabilities: string[]; +} + +export interface DatasourceHealth { + id: string; + displayName: string; + status: string; + health: { + state: string; + message: string; + }; + lastHealthCheck: string; + failureReason: string; +} + +export interface PluginSummary { + instanceId: string; + typeId: string; + displayName: string; + pluginType: string; + lifecycle: string; + health: { + state: string; + message: string; + }; + lastHealthCheck: string; + failureReason: string | null; +} + +export interface LiveStreamUnavailableMessage { + type: 'NO_LIVE_STREAM'; + source: string; +} diff --git a/eventlens-ui/src/components/AnomalyPanel.tsx b/eventlens-ui/src/components/AnomalyPanel.tsx index 83e7d24..fca68e1 100644 --- a/eventlens-ui/src/components/AnomalyPanel.tsx +++ b/eventlens-ui/src/components/AnomalyPanel.tsx @@ -2,6 +2,10 @@ import { useQuery } from '@tanstack/react-query'; import { getAnomalies, AnomalyReport } from '../api/client'; import { parseEventTimestamp } from '../utils/time'; +interface Props { + source?: string | null; +} + function severityBadgeClass(sev: string): string { switch (sev) { case 'CRITICAL': @@ -80,10 +84,10 @@ function GaugeWave({ color }: { color: 'green' | 'cyan' }) { ); } -export default function AnomalyPanel() { +export default function AnomalyPanel({ source }: Props) { const { data: anomalies, isLoading } = useQuery({ - queryKey: ['anomalies'], - queryFn: () => getAnomalies(), + queryKey: ['anomalies', source ?? 'default'], + queryFn: () => getAnomalies(100, source), refetchInterval: 30_000, }); diff --git a/eventlens-ui/src/components/LiveStream.tsx b/eventlens-ui/src/components/LiveStream.tsx index 6301242..ee7c4ee 100644 --- a/eventlens-ui/src/components/LiveStream.tsx +++ b/eventlens-ui/src/components/LiveStream.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react'; -import { StoredEvent } from '../api/client'; +import { LiveStreamUnavailableMessage, StoredEvent } from '../api/client'; import { demoLiveStreamSeed } from '../demo/demoData'; import { isDemoMode } from '../demo/demoMode'; import { useWebSocket } from '../hooks/useWebSocket'; @@ -38,20 +38,49 @@ function eventIcon(t: string): string { return '\u25C6'; } -export default function LiveStream() { +export default function LiveStream({ source }: { source?: string | null }) { + return ; +} + +type LiveStreamMessage = StoredEvent | LiveStreamUnavailableMessage; + +function isUnavailableMessage(message: LiveStreamMessage): message is LiveStreamUnavailableMessage { + return 'type' in message && message.type === 'NO_LIVE_STREAM'; +} + +function buildSocketPath(source?: string | null) { + if (!source) { + return '/ws/live'; + } + return `/ws/live?source=${encodeURIComponent(source)}`; +} + +function SourceAwareLiveStream({ source }: { source?: string | null }) { const demo = isDemoMode(); const [events, setEvents] = useState(() => (demo ? demoLiveStreamSeed() : [])); const [paused, setPaused] = useState(false); + const [unavailableSource, setUnavailableSource] = useState(null); const scrollRef = useRef(null); const pausedRef = useRef(paused); pausedRef.current = paused; const { notify } = useToast(); - const wsStatus = useWebSocket( - '/ws/live', - event => { + useEffect(() => { + setUnavailableSource(null); + setEvents(demo ? demoLiveStreamSeed() : []); + }, [source, demo]); + + const wsStatus = useWebSocket( + buildSocketPath(source), + message => { + if (isUnavailableMessage(message)) { + setUnavailableSource(message.source); + setEvents([]); + return; + } + setUnavailableSource(null); if (pausedRef.current) return; - setEvents(prev => [...prev.slice(-(BACKFILL_CAP - 1)), event]); + setEvents(prev => [...prev.slice(-(BACKFILL_CAP - 1)), message]); }, { enabled: !demo } ); @@ -105,9 +134,19 @@ export default function LiveStream() {
+ {unavailableSource && ( +
+ Live stream not available for this source + {source ? ` (${source})` : unavailableSource ? ` (${unavailableSource})` : ''}. +
+ )} {events.length === 0 && (
- {demo ? 'Demo stream (static sample events)' : 'Waiting for events\u2026'} + {unavailableSource + ? null + : demo + ? 'Demo stream (static sample events)' + : 'Waiting for events\u2026'}
)} {events.map((e) => ( diff --git a/eventlens-ui/src/components/SearchBar.tsx b/eventlens-ui/src/components/SearchBar.tsx index 686bffb..0668994 100644 --- a/eventlens-ui/src/components/SearchBar.tsx +++ b/eventlens-ui/src/components/SearchBar.tsx @@ -4,6 +4,7 @@ import { searchAggregates } from '../api/client'; interface Props { onSelect: (id: string) => void; + source?: string | null; } function useDebounce(value: T, delay: number): T { @@ -15,20 +16,19 @@ function useDebounce(value: T, delay: number): T { return debounced; } -export default function SearchBar({ onSelect }: Props) { +export default function SearchBar({ onSelect, source }: Props) { const [query, setQuery] = useState(''); const [open, setOpen] = useState(false); const wrapperRef = useRef(null); const debouncedQuery = useDebounce(query, 300); const { data: results = [] } = useQuery({ - queryKey: ['search', debouncedQuery], - queryFn: () => searchAggregates(debouncedQuery), + queryKey: ['search', debouncedQuery, source ?? 'default'], + queryFn: () => searchAggregates(debouncedQuery, 20, source), enabled: debouncedQuery.length >= 2, staleTime: 5_000, }); - // Close dropdown on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { @@ -39,7 +39,6 @@ export default function SearchBar({ onSelect }: Props) { return () => document.removeEventListener('mousedown', handler); }, []); - // Cmd/Ctrl+K — focus via custom event from Timeline keyboard handler const inputRef = useRef(null); const focus = useCallback(() => { inputRef.current?.focus(); @@ -57,7 +56,7 @@ export default function SearchBar({ onSelect }: Props) { return (
- 🔎 + ?? handleSelect(id)} role="option" > - + ? ID : diff --git a/eventlens-ui/src/components/StateViewer.tsx b/eventlens-ui/src/components/StateViewer.tsx index 2534ef1..ce88c10 100644 --- a/eventlens-ui/src/components/StateViewer.tsx +++ b/eventlens-ui/src/components/StateViewer.tsx @@ -8,6 +8,7 @@ interface Props { sequence: number; activeTab?: TabId; onTabChange?: (tab: TabId) => void; + source?: string | null; } export type TabId = 'changes' | 'before-after' | 'raw'; @@ -18,8 +19,8 @@ const TABS: { id: TabId; label: string; emoji: string }[] = [ { id: 'raw', label: 'Raw JSON', emoji: '{ }' }, ]; -export default function StateViewer({ aggregateId, sequence, activeTab: externalTab, onTabChange }: Props) { - const { data: transitions, isLoading } = useReplay(aggregateId); +export default function StateViewer({ aggregateId, sequence, activeTab: externalTab, onTabChange, source }: Props) { + const { data: transitions, isLoading } = useReplay(aggregateId, source); const [localTab, setLocalTab] = useState('changes'); const activeTab = externalTab ?? localTab; @@ -132,3 +133,5 @@ export default function StateViewer({ aggregateId, sequence, activeTab: external
); } + + diff --git a/eventlens-ui/src/components/Timeline.tsx b/eventlens-ui/src/components/Timeline.tsx index d634204..93848e9 100644 --- a/eventlens-ui/src/components/Timeline.tsx +++ b/eventlens-ui/src/components/Timeline.tsx @@ -1,5 +1,5 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { StateTransition } from '../api/client'; +import type { StoredEvent } from '../api/client'; import { useTimeline } from '../hooks/useTimeline'; import { parseEventTimestamp } from '../utils/time'; @@ -7,10 +7,15 @@ interface Props { aggregateId: string; selectedSequence: number | null; onSelectEvent: (seq: number) => void; + source?: string | null; } const MIN_SAME_TYPE_RUN = 4; +type Segment = + | { kind: 'single'; event: StoredEvent; index: number } + | { kind: 'group'; eventType: string; items: StoredEvent[]; startIndex: number }; + function dotClass(eventType: string): string { const t = eventType.toLowerCase(); if (t.includes('created') || t.includes('opened') || t.includes('placed') || t.includes('submitted')) return 'created'; @@ -23,17 +28,13 @@ function dotClass(eventType: string): string { return 'default'; } -type Segment = - | { kind: 'single'; transition: StateTransition; index: number } - | { kind: 'group'; eventType: string; items: StateTransition[]; startIndex: number }; - -function buildSegments(transitions: StateTransition[]): Segment[] { +function buildSegments(events: StoredEvent[]): Segment[] { const out: Segment[] = []; let i = 0; - while (i < transitions.length) { - const type = transitions[i].event.eventType; + while (i < events.length) { + const type = events[i].eventType; let j = i + 1; - while (j < transitions.length && transitions[j].event.eventType === type) { + while (j < events.length && events[j].eventType === type) { j++; } const runLen = j - i; @@ -41,13 +42,13 @@ function buildSegments(transitions: StateTransition[]): Segment[] { out.push({ kind: 'group', eventType: type, - items: transitions.slice(i, j), + items: events.slice(i, j), startIndex: i, }); i = j; } else { for (let k = i; k < j; k++) { - out.push({ kind: 'single', transition: transitions[k], index: k }); + out.push({ kind: 'single', event: events[k], index: k }); } i = j; } @@ -59,80 +60,64 @@ function groupKey(startIndex: number, len: number): string { return `${startIndex}-${len}`; } -interface StepButtonProps { - transition: StateTransition; +function StepButton({ + event, + stepNumber, + selectedSequence, + onSelectEvent, + compact, +}: { + event: StoredEvent; stepNumber: number; selectedSequence: number | null; onSelectEvent: (seq: number) => void; compact?: boolean; - hasDiff?: boolean; -} - -function StepButton({ transition, stepNumber, selectedSequence, onSelectEvent, compact, hasDiff }: StepButtonProps) { - const e = transition.event; - const dc = dotClass(e.eventType); - const active = selectedSequence === e.sequenceNumber; +}) { + const dc = dotClass(event.eventType); + const active = selectedSequence === event.sequenceNumber; return ( ); } -export default function Timeline({ aggregateId, selectedSequence, onSelectEvent }: Props) { - const { data: transitions, isLoading } = useTimeline(aggregateId); +export default function Timeline({ aggregateId, selectedSequence, onSelectEvent, source }: Props) { + const { data: timeline, isLoading } = useTimeline(aggregateId, source); + const events = timeline?.events ?? []; + const totalEvents = timeline?.totalEvents ?? 0; const [expandedGroupKey, setExpandedGroupKey] = useState(null); const [jumpInput, setJumpInput] = useState(''); - const [filterType, setFilterType] = useState(''); - - const segments = useMemo(() => (transitions?.length ? buildSegments(transitions) : []), [transitions]); - - // Derive unique event types for filter chips - const eventTypes = useMemo(() => { - if (!transitions?.length) return []; - const types = [...new Set(transitions.map(t => t.event.eventType))]; - return types.sort(); - }, [transitions]); - - // Apply filter: when a type is active, only show matching steps - const filteredTransitions = useMemo(() => { - if (!filterType || !transitions?.length) return transitions; - return transitions.filter(t => t.event.eventType === filterType); - }, [transitions, filterType]); - - const filteredSegments = useMemo( - () => (filteredTransitions?.length ? buildSegments(filteredTransitions) : []), - [filteredTransitions] - ); + const [filterType, setFilterType] = useState(''); + const segments = useMemo(() => (events.length ? buildSegments(events) : []), [events]); + const eventTypes = useMemo(() => (events.length ? [...new Set(events.map(event => event.eventType))].sort() : []), [events]); + const filteredEvents = useMemo(() => (!filterType ? events : events.filter(event => event.eventType === filterType)), [events, filterType]); + const filteredSegments = useMemo(() => (filteredEvents.length ? buildSegments(filteredEvents) : []), [filteredEvents]); const activeSegments = filterType ? filteredSegments : segments; - const activeTransitions = filterType ? filteredTransitions : transitions; + const activeEvents = filterType ? filteredEvents : events; - const selectedIndex = - selectedSequence != null && activeTransitions?.length - ? activeTransitions.findIndex(t => t.event.sequenceNumber === selectedSequence) - : -1; + const selectedIndex = selectedSequence != null ? activeEvents.findIndex(event => event.sequenceNumber === selectedSequence) : -1; const stepDisplay = selectedIndex >= 0 ? selectedIndex + 1 : null; - - const minSeq = activeTransitions?.[0]?.event.sequenceNumber ?? 0; - const maxSeq = activeTransitions?.[activeTransitions.length - 1]?.event.sequenceNumber ?? 0; + const minSeq = activeEvents[0]?.sequenceNumber ?? 0; + const maxSeq = activeEvents[activeEvents.length - 1]?.sequenceNumber ?? 0; const expandedSeg = useMemo(() => { if (!expandedGroupKey) return null; for (const seg of activeSegments) { - if (seg.kind !== 'group') continue; - if (groupKey(seg.startIndex, seg.items.length) === expandedGroupKey) return seg; + if (seg.kind === 'group' && groupKey(seg.startIndex, seg.items.length) === expandedGroupKey) { + return seg; + } } return null; }, [expandedGroupKey, activeSegments]); @@ -165,9 +150,8 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent setExpandedGroupKey(prev => (prev === key ? null : key)); }; - // ── Keyboard navigation ────────────────────────────────────────────── const handleKeyNav = useCallback((e: KeyboardEvent) => { - if (!activeTransitions?.length) return; + if (!activeEvents.length) return; const target = e.target as HTMLElement; if (target.tagName === 'INPUT') return; @@ -176,32 +160,25 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent const dir = e.key === 'ArrowLeft' ? -1 : 1; if (e.shiftKey) { - // Jump to prev/next group boundary const currentSeg = selectedIndex >= 0 - ? activeSegments.find(seg => - seg.kind === 'group' - ? selectedIndex >= seg.startIndex && selectedIndex < seg.startIndex + seg.items.length - : seg.index === selectedIndex - ) + ? activeSegments.find(seg => seg.kind === 'group' + ? selectedIndex >= seg.startIndex && selectedIndex < seg.startIndex + seg.items.length + : seg.index === selectedIndex) : null; const segIdx = currentSeg ? activeSegments.indexOf(currentSeg) : -1; const targetSeg = activeSegments[segIdx + dir]; if (targetSeg) { - const firstT = targetSeg.kind === 'single' - ? targetSeg.transition - : targetSeg.items[0]; - onSelectEvent(firstT.event.sequenceNumber); + const firstEvent = targetSeg.kind === 'single' ? targetSeg.event : targetSeg.items[0]; + onSelectEvent(firstEvent.sequenceNumber); } } else { - // Step one event at a time const nextIndex = selectedIndex + dir; - if (nextIndex >= 0 && nextIndex < activeTransitions.length) { - onSelectEvent(activeTransitions[nextIndex].event.sequenceNumber); + if (nextIndex >= 0 && nextIndex < activeEvents.length) { + onSelectEvent(activeEvents[nextIndex].sequenceNumber); } } } - // Number keys 1-4 switch StateViewer tabs — dispatched as custom event if (['1', '2', '3', '4'].includes(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) { if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') { const tabMap: Record = { '1': 'changes', '2': 'before-after', '3': 'raw' }; @@ -209,18 +186,16 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent } } - // Space = pause/resume live stream if (e.key === ' ' && target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA' && target.tagName !== 'BUTTON') { e.preventDefault(); window.dispatchEvent(new CustomEvent('eventlens:togglestream')); } - // Cmd/Ctrl+K focuses search if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); document.getElementById('aggregate-search')?.focus(); } - }, [activeTransitions, activeSegments, selectedIndex, onSelectEvent]); + }, [activeEvents, activeSegments, selectedIndex, onSelectEvent]); const handleKeyNavRef = useRef(handleKeyNav); handleKeyNavRef.current = handleKeyNav; @@ -233,7 +208,7 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent const handleJump = () => { const seq = parseInt(jumpInput, 10); - if (!isNaN(seq) && activeTransitions?.some(t => t.event.sequenceNumber === seq)) { + if (!Number.isNaN(seq) && activeEvents.some(event => event.sequenceNumber === seq)) { onSelectEvent(seq); setJumpInput(''); } @@ -242,16 +217,16 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent if (isLoading) { return (
-
⏱ Event sequence
+
Event sequence
); } - if (!transitions?.length) { + if (!events.length) { return (
-
⏱ Event sequence
+
Event sequence

No events found for this aggregate.

); @@ -259,16 +234,14 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent return (
- {/* Header row */}
- ⏱ Event sequence + Event sequence - {filterType ? `${activeTransitions?.length} / ${transitions.length}` : transitions.length} events + {filterType ? `${activeEvents.length} / ${totalEvents}` : totalEvents} events
- {/* Jump to seq */}
e.key === 'Enter' && handleJump()} aria-label="Jump to sequence number" /> - +
- {/* Event type filter chips */} {eventTypes.length > 1 && (
@@ -310,15 +282,14 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent
{activeSegments.map((seg, si) => ( - - {si > 0 && } + + {si > 0 && {'>'}} {seg.kind === 'single' ? ( 0} /> ) : ( {expandedSeg.eventType} - {expandedSeg.items.length} events · steps {expandedSeg.startIndex + 1}– + {expandedSeg.items.length} events steps {expandedSeg.startIndex + 1}- {expandedSeg.startIndex + expandedSeg.items.length}
- {expandedSeg.items.map((t, i) => ( - - {i > 0 && } + {expandedSeg.items.map((event, i) => ( + + {i > 0 && {'>'}} 0} /> ))} @@ -375,22 +345,22 @@ export default function Timeline({ aggregateId, selectedSequence, onSelectEvent />
- First · seq #{minSeq} + First seq #{minSeq} {stepDisplay != null ? ( <> - Step {stepDisplay} of {activeTransitions?.length} - · sequence #{selectedSequence} + Step {stepDisplay} of {activeEvents.length} + sequence #{selectedSequence}
- {activeTransitions?.find(t => t.event.sequenceNumber === selectedSequence)?.event.eventType ?? ''} + {activeEvents.find(event => event.sequenceNumber === selectedSequence)?.eventType ?? ''} ) : ( 'Select an event above or drag the scrubber' )}
- Last · seq #{maxSeq} + Last seq #{maxSeq}
); @@ -408,11 +378,10 @@ function GroupSummaryChip({ onToggle: () => void; }) { const { items, startIndex, eventType } = segment; - const first = items[0].event; - const last = items[items.length - 1].event; + const first = items[0]; + const last = items[items.length - 1]; const dc = dotClass(eventType); - const containsSelection = - selectedSequence != null && items.some(t => t.event.sequenceNumber === selectedSequence); + const containsSelection = selectedSequence != null && items.some(event => event.sequenceNumber === selectedSequence); const showAnchor = containsSelection && !expanded; return ( @@ -422,17 +391,17 @@ function GroupSummaryChip({ onClick={onToggle} aria-expanded={expanded} data-timeline-group-anchor={showAnchor ? '1' : undefined} - title={`${items.length} × ${eventType}. Click to ${expanded ? 'collapse' : 'show every step'}.`} + title={`${items.length} x ${eventType}. Click to ${expanded ? 'collapse' : 'show every step'}.`} > - ×{items.length} + x{items.length} - {expanded ? '▲' : '▼'} + {expanded ? 'v' : '>'} {eventType} - steps {startIndex + 1}–{startIndex + items.length} · seq #{first.sequenceNumber}–#{last.sequenceNumber} + steps {startIndex + 1}-{startIndex + items.length} seq #{first.sequenceNumber}-#{last.sequenceNumber} ); diff --git a/eventlens-ui/src/hooks/useReplay.ts b/eventlens-ui/src/hooks/useReplay.ts index bfd4c33..57b40fc 100644 --- a/eventlens-ui/src/hooks/useReplay.ts +++ b/eventlens-ui/src/hooks/useReplay.ts @@ -1,12 +1,9 @@ import { useQuery } from '@tanstack/react-query'; import { getTransitions } from '../api/client'; -export function useReplay(aggregateId: string) { - const query = useQuery({ - queryKey: ['transitions', aggregateId], - queryFn: () => getTransitions(aggregateId), +export function useReplay(aggregateId: string, source?: string | null) { + return useQuery({ + queryKey: ['transitions', aggregateId, source ?? 'default'], + queryFn: () => getTransitions(aggregateId, source), }); - - return query; } - diff --git a/eventlens-ui/src/hooks/useTimeline.ts b/eventlens-ui/src/hooks/useTimeline.ts index d36ceb0..dc1a352 100644 --- a/eventlens-ui/src/hooks/useTimeline.ts +++ b/eventlens-ui/src/hooks/useTimeline.ts @@ -1,12 +1,9 @@ import { useQuery } from '@tanstack/react-query'; -import { getTransitions } from '../api/client'; +import { getTimeline } from '../api/client'; -export function useTimeline(aggregateId: string) { - const query = useQuery({ - queryKey: ['transitions', aggregateId], - queryFn: () => getTransitions(aggregateId), +export function useTimeline(aggregateId: string, source?: string | null) { + return useQuery({ + queryKey: ['timeline', aggregateId, source ?? 'default', 'metadata'], + queryFn: () => getTimeline(aggregateId, 500, 0, source, 'metadata'), }); - - return query; } - diff --git a/eventlens-ui/src/index.css b/eventlens-ui/src/index.css index 95052a6..36a3d95 100644 --- a/eventlens-ui/src/index.css +++ b/eventlens-ui/src/index.css @@ -74,10 +74,14 @@ body::after { .app { display: flex; flex-direction: column; min-height: 100vh; } .app-header { - display: flex; + --header-pad-x: 24px; + --header-pad-right: 48px; /* room for workspace chevron; mirrors visual weight of left brand */ + --header-control-h: 34px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; - justify-content: space-between; - padding: 0 28px; + column-gap: clamp(12px, 2vw, 20px); + padding: 0 var(--header-pad-right) 0 var(--header-pad-x); height: 64px; background: linear-gradient(180deg, #0d1020 0%, #080b14 100%); border-bottom: 1px solid var(--border); @@ -87,7 +91,13 @@ body::after { box-shadow: 0 2px 20px rgba(0, 0, 0, 0.5), inset 0 -1px 0 rgba(0, 240, 255, 0.06); } -.brand { display: flex; align-items: center; gap: 12px; } +.brand { + justify-self: start; + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} .brand-logo { width: 36px; @@ -119,12 +129,14 @@ body::after { letter-spacing: 0.5px; } -/* center title */ +/* center cluster: demo + title + datasource share one baseline-aligned row */ .header-title { font-family: var(--font-display); - font-size: 22px; + font-size: 20px; font-weight: 800; - letter-spacing: 4px; + letter-spacing: 3px; + line-height: 1; + margin: 0; text-transform: uppercase; background: linear-gradient(135deg, var(--neon-cyan), #4facfe, var(--neon-magenta)); -webkit-background-clip: text; @@ -132,6 +144,100 @@ body::after { background-clip: text; filter: drop-shadow(0 0 8px rgba(0, 240, 255, 0.4)); text-align: center; + flex-shrink: 0; +} + +.header-center { + justify-self: center; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + gap: clamp(10px, 1.5vw, 16px); + min-width: 0; +} + +.header-demo-pill { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--header-control-h); + padding: 0 12px; + border-radius: 999px; + border: 1px solid rgba(255, 170, 0, 0.35); + background: var(--neon-amber-dim); + color: var(--neon-amber); + font-family: var(--font-mono); + font-size: 11px; + line-height: 1; + white-space: nowrap; + flex-shrink: 0; +} + +.header-actions { + justify-self: end; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + gap: clamp(10px, 1.2vw, 16px); + min-width: 0; +} + +.header-actions .conn-stats, +.header-actions .header-status { + flex-shrink: 0; +} + +/* Datasource lives in workspace dock panel only */ +.workspace-datasource { + display: flex; + flex-direction: column; + gap: 6px; + margin: 0; + min-width: 0; +} + +.workspace-datasource-label { + color: var(--text-muted); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + line-height: 1; +} + +.workspace-datasource-select { + box-sizing: border-box; + width: 100%; + height: 34px; + appearance: none; + -webkit-appearance: none; + color: var(--text-primary); + background-color: rgba(12, 16, 32, 0.95); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3c0' d='M3 4.5 6 8l3-3.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 8px; + padding: 0 32px 0 12px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1; + cursor: pointer; + box-shadow: inset 0 1px 0 rgba(0, 240, 255, 0.06); + transition: border-color var(--transition), box-shadow var(--transition); +} + +.workspace-datasource-select:hover { + border-color: rgba(0, 240, 255, 0.25); +} + +.workspace-datasource-select:focus { + outline: none; + border-color: var(--neon-cyan-mid); + box-shadow: 0 0 0 1px rgba(0, 240, 255, 0.2); } .header-status { @@ -236,6 +342,344 @@ body::after { text-shadow: 0 0 6px rgba(0, 240, 255, 0.2); } +.control-ribbon { + padding: 14px 18px; +} + +.control-ribbon-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + flex-wrap: wrap; +} + +.control-ribbon-title { + margin-bottom: 4px; +} + +.control-ribbon-subtitle { + color: var(--text-muted); + font-size: 12px; +} + +.control-ribbon-nav { + display: inline-flex; + align-items: center; + gap: 12px; + font-size: 13px; +} + +.control-ribbon-nav a { + text-decoration: none; +} + +.control-ribbon-nav a[aria-current="page"] { + text-decoration: underline; + text-underline-offset: 2px; +} + +.control-panel { + display: grid; + gap: 12px; + padding: 16px 18px; +} + +.control-panel-grid { + display: grid; + grid-template-columns: minmax(220px, 360px) minmax(0, 1fr); + gap: 12px; + align-items: end; +} + +.control-field { + min-width: 0; +} + +.control-field--search { + min-width: 0; +} + +.control-field-label { + display: block; + margin-bottom: 8px; + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.control-select { + width: 100%; + background: rgba(13, 17, 35, 0.92); + color: var(--text-primary); + border: 1px solid rgba(255,255,255,0.14); + border-radius: 10px; + padding: 10px 12px; + font-family: var(--font-mono); + font-size: 12px; + outline: none; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.control-select:focus { + border-color: var(--neon-cyan-mid); + box-shadow: 0 0 14px rgba(0, 240, 255, 0.15); +} + +.datasource-pills { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.datasource-pill { + border: 1px solid var(--border); + padding: 4px 8px; + border-radius: 999px; + font-size: 11px; + font-family: var(--font-mono); +} + +.selection-summary { + margin-top: 2px; + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-mono); +} + +.selection-clear-btn { + margin-left: 12px; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-family: var(--font-mono); +} + +.selection-clear-btn:hover { + color: var(--neon-cyan); +} + +/* Right workspace: chevron-only tab; open = same compact footprint, content-height panel */ +.workspace-dock { + position: fixed; + z-index: 110; + right: 0; + left: auto; + top: 50%; + bottom: auto; + transform: translateY(-50%); + display: flex; + flex-direction: row; + align-items: center; + width: auto; + height: auto; + max-height: min(72vh, calc(100vh - 64px - 48px)); + box-shadow: -6px 4px 22px rgba(0, 0, 0, 0.42); + border-radius: 10px 0 0 10px; + border: 1px solid var(--border-muted); + border-right: none; + overflow: hidden; + pointer-events: auto; + transition: box-shadow 0.2s ease; +} + +.workspace-dock--open { + box-shadow: -8px 6px 28px rgba(0, 0, 0, 0.48), 0 0 0 1px rgba(0, 240, 255, 0.08); +} + +.workspace-dock-handle { + flex-shrink: 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + border: none; + border-left: 1px solid var(--border-muted); + background: linear-gradient(180deg, #0f1324 0%, #0a0e18 100%); + color: var(--neon-cyan); + font-family: var(--font-mono); + cursor: pointer; + transition: background var(--transition), color var(--transition); +} + +.workspace-dock-handle:hover { + background: linear-gradient(180deg, #141a30 0%, #0d1220 100%); + color: var(--text-primary); +} + +.workspace-dock-handle:focus-visible { + outline: 2px solid var(--neon-cyan); + outline-offset: -2px; +} + +.workspace-dock-chevron { + font-size: 15px; + line-height: 1; + font-weight: 700; +} + +.workspace-dock-panel { + flex: 0 1 auto; + min-width: 0; + width: min(252px, calc(100vw - 48px)); + max-height: min(72vh, calc(100vh - 64px - 48px)); + overflow-y: auto; + overflow-x: hidden; + padding: 10px 12px 12px 14px; + background: linear-gradient(145deg, #0c101c 0%, #080c14 100%); + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 8px; +} + +/* [hidden] must win: our display:flex was overriding native hidden and leaked panel text when collapsed */ +.workspace-dock-panel[hidden] { + display: none !important; +} + +.workspace-dock-title { + font-family: var(--font-sans); + font-size: 12px; + font-weight: 600; + color: var(--neon-cyan); + letter-spacing: 0.2px; +} + +.workspace-dock-scrim { + position: fixed; + top: 64px; + left: 0; + right: 0; + bottom: 36px; + z-index: 109; + margin: 0; + padding: 0; + border: none; + background: rgba(3, 5, 12, 0.45); + cursor: pointer; +} + +.workspace-sidebar-kpis { + border: 1px solid var(--border-muted); + border-radius: var(--radius); + background: rgba(8, 11, 20, 0.55); + padding: 8px 10px; + display: grid; + gap: 6px; +} + +.workspace-kpi-row { + display: flex; + justify-content: space-between; + gap: 8px; + color: var(--text-muted); + font-size: 11px; + font-family: var(--font-mono); +} + +.workspace-kpi-row strong { + color: var(--text-primary); +} + +.workspace-sidebar-links { + display: grid; + gap: 6px; + color: var(--text-secondary); + font-size: 12px; +} + +.workspace-content { + display: grid; + gap: 16px; +} + +.search-panel { + width: 100%; + min-width: 0; +} + +.selection-clear-btn:focus-visible, +.control-ribbon-nav a:focus-visible, +.control-select:focus-visible, +.workspace-datasource-select:focus-visible { + outline: 2px solid var(--neon-cyan); + outline-offset: 2px; +} + +.plugin-dashboard { + display: grid; + gap: 14px; +} + +.plugin-cards-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); +} + +.plugin-cards-grid--dense { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +} + +.plugin-card { + border: 1px solid var(--border-muted); + border-radius: var(--radius-lg); + padding: 12px 14px; + background: linear-gradient(140deg, rgba(16, 21, 39, 0.95), rgba(10, 14, 28, 0.95)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); +} + +.plugin-card--interactive { + transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition); +} + +.plugin-card--interactive:hover { + border-color: var(--neon-cyan-mid); + box-shadow: 0 0 16px rgba(0, 240, 255, 0.1); + transform: translateY(-1px); +} + +.plugin-card--interactive:focus-within { + border-color: var(--neon-cyan-mid); + box-shadow: 0 0 0 2px rgba(0, 240, 255, 0.15); +} + +.plugin-card-head { + display: flex; + justify-content: space-between; + gap: 10px; + align-items: center; +} + +.plugin-pill { + border: 1px solid var(--border); + border-radius: 999px; + padding: 2px 8px; + font-size: 11px; + font-family: var(--font-mono); +} + +.plugin-card-meta { + color: var(--text-muted); + font-size: 12px; + margin-top: 8px; + font-family: var(--font-mono); + overflow-wrap: anywhere; +} + +.plugin-card-detail { + color: var(--text-secondary); + font-size: 12px; + margin-top: 8px; + line-height: 1.55; +} + /* ── SearchBar ────────────────────────────────────────────────────────────── */ .search-wrapper { position: relative; } @@ -375,6 +819,14 @@ body::after { .conn-stat-value.green { color: var(--neon-green); text-shadow: 0 0 6px rgba(0, 255, 136, 0.4); } .conn-stat-value.amber { color: var(--neon-amber); text-shadow: 0 0 6px rgba(255, 170, 0, 0.4); } +.conn-stat--metric .conn-stat-value, +.conn-stat-value--uptime { + display: inline-block; + min-width: 6.5ch; + text-align: right; + font-variant-numeric: tabular-nums; +} + /* mini waveform bar */ .mini-wave { display: flex; @@ -1661,8 +2113,12 @@ body::after { @media (max-width: 900px) { .state-grid { grid-template-columns: 1fr; } .bottom-grid { grid-template-columns: 1fr; } - .header-title { display: none; } + .header-center .header-demo-pill { display: none; } + .workspace-dock-panel { + width: min(240px, 88vw); + } .gauge-row { grid-template-columns: 1fr; } + .control-panel-grid { grid-template-columns: 1fr; } .timeline-info { grid-template-columns: 1fr; text-align: center; diff --git a/scripts/v3-release-smoke.ps1 b/scripts/v3-release-smoke.ps1 new file mode 100644 index 0000000..dcc66ef --- /dev/null +++ b/scripts/v3-release-smoke.ps1 @@ -0,0 +1,84 @@ +param( + [switch]$SkipGradle +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +Set-Location $repoRoot + +function Assert-PathExists { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Label + ) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "Missing $Label at $Path" + } + + Write-Host "[ok] $Label" -ForegroundColor Green +} + +function Run-Step { + param( + [Parameter(Mandatory = $true)][string]$Label, + [Parameter(Mandatory = $true)][scriptblock]$Action + ) + + Write-Host "==> $Label" -ForegroundColor Cyan + & $Action +} + +Write-Host "EventLens v3 release smoke" -ForegroundColor Yellow +Write-Host "Repo: $repoRoot" + +Run-Step 'Verify phase evidence files exist' { + $checks = @( + @{ Path = (Join-Path $repoRoot 'eventlens-spi\src\main\java\io\eventlens\spi\EventSourcePlugin.java'); Label = 'Phase 1 SPI contract' }, + @{ Path = (Join-Path $repoRoot 'eventlens-core\src\test\java\io\eventlens\core\plugin\PluginManagerTest.java'); Label = 'Phase 2 plugin manager tests' }, + @{ Path = (Join-Path $repoRoot 'eventlens-source-postgres\src\test\java\io\eventlens\pg\PostgresEventSourcePluginContractTest.java'); Label = 'Phase 3 postgres contract test' }, + @{ Path = (Join-Path $repoRoot 'eventlens-stream-kafka\src\test\java\io\eventlens\kafka\KafkaStreamAdapterPluginContractTest.java'); Label = 'Phase 3 kafka contract test' }, + @{ Path = (Join-Path $repoRoot 'eventlens-source-mysql\src\test\java\io\eventlens\mysql\MySqlEventSourcePluginContractTest.java'); Label = 'Phase 4 mysql contract test' }, + @{ Path = (Join-Path $repoRoot 'eventlens-core\src\test\java\io\eventlens\core\ConfigLoaderTest.java'); Label = 'Phase 4 config migration tests' }, + @{ Path = (Join-Path $repoRoot 'eventlens-api\src\test\java\io\eventlens\api\cache\QueryResultCacheBenchmarkTest.java'); Label = 'Phase 5 cache benchmark test' }, + @{ Path = (Join-Path $repoRoot 'eventlens-api\src\test\java\io\eventlens\api\routes\TimelineMetadataPayloadBenchmarkTest.java'); Label = 'Phase 5 metadata benchmark test' }, + @{ Path = (Join-Path $repoRoot 'eventlens-core\src\test\java\io\eventlens\core\plugin\PluginDiscoveryExternalJarTest.java'); Label = 'Phase 6 external plugin loading test' }, + @{ Path = (Join-Path $repoRoot 'docs\plugin-authoring.md'); Label = 'Phase 6 plugin authoring docs' }, + @{ Path = (Join-Path $repoRoot 'docs\v3-ga-checklist.md'); Label = 'Phase 6 GA checklist docs' }, + @{ Path = (Join-Path $repoRoot 'plans\learned\v3_reusable_notes.md'); Label = 'Reusable notes' } + ) + + foreach ($check in $checks) { + Assert-PathExists -Path $check.Path -Label $check.Label + } +} + +if (-not $SkipGradle) { + Run-Step 'Run Gradle test gate' { + & .\gradlew.bat test + if ($LASTEXITCODE -ne 0) { + throw 'Gradle test failed' + } + } + + Run-Step 'Run Gradle check gate' { + & .\gradlew.bat check + if ($LASTEXITCODE -ne 0) { + throw 'Gradle check failed' + } + } +} else { + Write-Host 'Skipping Gradle execution because -SkipGradle was provided.' -ForegroundColor Yellow +} + +Write-Host '' +Write-Host 'v3 release smoke passed.' -ForegroundColor Green +Write-Host 'Coverage summary:' -ForegroundColor Green +Write-Host '- Phase 1: SPI contract presence verified' +Write-Host '- Phase 2: plugin manager evidence verified' +Write-Host '- Phase 3: extracted postgres and kafka contract tests verified' +Write-Host '- Phase 4: mysql and config migration evidence verified' +Write-Host '- Phase 5: cache and metadata benchmark evidence verified' +Write-Host '- Phase 6: external plugin loading and docs evidence verified' diff --git a/settings.gradle.kts b/settings.gradle.kts index 34629bc..1afdf52 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,11 +1,15 @@ rootProject.name = "eventlens" include( - "eventlens-core", // Domain model, engines, SPI - "eventlens-pg", // PostgreSQL reader - "eventlens-kafka", // Kafka consumer (optional) - "eventlens-api", // REST + WebSocket - "eventlens-cli", // CLI commands - "eventlens-ui", // React frontend (builds into resources) - "eventlens-app" // Fat JAR assembly + "eventlens-spi", + "eventlens-core", + "eventlens-source-postgres", + "eventlens-source-mysql", + "eventlens-stream-kafka", + "eventlens-plugin-test", + "eventlens-api", + "eventlens-cli", + "eventlens-ui", + "eventlens-app" ) + diff --git a/tests/README.md b/tests/README.md index 3b970d2..28befad 100644 --- a/tests/README.md +++ b/tests/README.md @@ -404,3 +404,8 @@ Assumptions: - Build succeeds for both amd64 and arm64. - Container health status reports `"Status": "healthy"` based on `/api/v1/health/live`. + +## v3 Smoke Script + +Use [scripts/v3-release-smoke.ps1](C:/Java%20Developer/EventDebug/scripts/v3-release-smoke.ps1) for a compact cross-phase release smoke run. +It is intentionally smaller than a script-per-bullet approach and relies on the automated test suite for the heavy lifting.