From eace7562c4c61c315ed1ed924858d28ecd88e247 Mon Sep 17 00:00:00 2001 From: Guanying Chen Date: Wed, 4 Nov 2026 19:02:47 +0000 Subject: [PATCH 1/9] Rebuild portable durable ingestion and three-page investigation workspace --- .clang-format | 3 + .gitignore | 7 +- .prettierrc.json | 1 + CMakeLists.txt | 40 + agent/src/main.cpp | 322 +- anomaly/src/db/database.py | 10 +- anomaly/src/db/log_repository.py | 36 +- anomaly/src/main.py | 11 - anomaly/src/workers/polling_worker.py | 5 +- api/src/app.ts | 75 + api/src/db/alertRepository.ts | 121 +- api/src/db/database.ts | 11 + api/src/db/logPageReader.ts | 103 + api/src/db/logRepository.ts | 143 +- api/src/index.ts | 137 +- api/src/routes/alerts.ts | 44 +- api/src/routes/logs.ts | 61 +- api/src/services/alertService.ts | 20 +- api/src/services/ingestionController.ts | 37 + api/src/services/ingestionWorker.ts | 116 + api/src/services/queueConsumer.ts | 23 +- api/src/services/queueService.ts | 46 +- api/src/types/log.ts | 3 + api/src/utils/logNormalizer.ts | 4 +- api/src/utils/queryHelpers.ts | 11 +- api/tests/unit/db/logPage.test.ts | 11 + api/tsconfig.json | 12 +- frontend/.eslintrc.json | 3 - frontend/eslint.config.mjs | 8 + frontend/next.config.ts | 16 +- frontend/package-lock.json | 3439 ++++++++++++----- frontend/package.json | 11 +- frontend/src/app/alerts/page.tsx | 2 + frontend/src/app/globals.css | 2241 +++++++++-- frontend/src/app/layout.tsx | 25 +- frontend/src/app/overview/page.tsx | 2 + frontend/src/app/page.tsx | 12 +- frontend/src/components/AlertDetail.tsx | 172 + frontend/src/components/AlertRelatedLogs.tsx | 131 + frontend/src/components/AlertsPage.tsx | 302 ++ frontend/src/components/DashboardProvider.tsx | 51 + frontend/src/components/ExplorePage.tsx | 371 ++ frontend/src/components/Icon.tsx | 37 + frontend/src/components/JsonView.tsx | 31 + frontend/src/components/LogChart.tsx | 77 - frontend/src/components/LogDashboard.tsx | 506 --- frontend/src/components/LogInspector.tsx | 143 + frontend/src/components/LogSearch.tsx | 58 - frontend/src/components/LogTable.tsx | 60 - frontend/src/components/OverviewPage.tsx | 239 ++ frontend/src/components/PipelineFlow.tsx | 174 + frontend/src/components/Shell.tsx | 135 + frontend/src/components/TelemetryChart.tsx | 353 ++ frontend/src/hooks/usePolling.ts | 73 + frontend/src/lib/api.ts | 15 + frontend/src/lib/chart.ts | 64 + frontend/src/lib/events.ts | 116 + frontend/src/lib/model.ts | 69 + native/include/common.hpp | 57 + package.json | 18 + queue/include/sqlite_queue.hpp | 46 +- queue/src/main.cpp | 269 +- queue/src/sqlite_queue.cpp | 375 +- scripts/build-native.mjs | 5 + scripts/build.mjs | 3 + scripts/dev.mjs | 22 + scripts/runtime.mjs | 40 + scripts/setup.mjs | 7 + scripts/simulator.mjs | 5 + scripts/stack.mjs | 19 + scripts/test.mjs | 2 + 71 files changed, 7927 insertions(+), 3290 deletions(-) create mode 100644 .clang-format create mode 100644 .prettierrc.json create mode 100644 CMakeLists.txt create mode 100644 api/src/app.ts create mode 100644 api/src/db/database.ts create mode 100644 api/src/db/logPageReader.ts create mode 100644 api/src/services/ingestionController.ts create mode 100644 api/src/services/ingestionWorker.ts create mode 100644 api/tests/unit/db/logPage.test.ts delete mode 100644 frontend/.eslintrc.json create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/src/app/alerts/page.tsx create mode 100644 frontend/src/app/overview/page.tsx create mode 100644 frontend/src/components/AlertDetail.tsx create mode 100644 frontend/src/components/AlertRelatedLogs.tsx create mode 100644 frontend/src/components/AlertsPage.tsx create mode 100644 frontend/src/components/DashboardProvider.tsx create mode 100644 frontend/src/components/ExplorePage.tsx create mode 100644 frontend/src/components/Icon.tsx create mode 100644 frontend/src/components/JsonView.tsx delete mode 100644 frontend/src/components/LogChart.tsx delete mode 100644 frontend/src/components/LogDashboard.tsx create mode 100644 frontend/src/components/LogInspector.tsx delete mode 100644 frontend/src/components/LogSearch.tsx delete mode 100644 frontend/src/components/LogTable.tsx create mode 100644 frontend/src/components/OverviewPage.tsx create mode 100644 frontend/src/components/PipelineFlow.tsx create mode 100644 frontend/src/components/Shell.tsx create mode 100644 frontend/src/components/TelemetryChart.tsx create mode 100644 frontend/src/hooks/usePolling.ts create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/chart.ts create mode 100644 frontend/src/lib/events.ts create mode 100644 frontend/src/lib/model.ts create mode 100644 native/include/common.hpp create mode 100644 package.json create mode 100644 scripts/build-native.mjs create mode 100644 scripts/build.mjs create mode 100644 scripts/dev.mjs create mode 100644 scripts/runtime.mjs create mode 100644 scripts/setup.mjs create mode 100644 scripts/simulator.mjs create mode 100644 scripts/stack.mjs create mode 100644 scripts/test.mjs diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..75a203b --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 100 diff --git a/.gitignore b/.gitignore index 85b83b8..af81109 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,9 @@ AGENTS.md start-local.bat stop-local.bat -scripts/ + +# Runtime data and local verification artifacts +.runtime/ +.tools/ +*.db-wal +*.db-shm diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4926aa8 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{ "singleQuote": true, "trailingComma": "all", "printWidth": 100 } diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..74ecb09 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.18) +project(log_pipeline VERSION 2.0 LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +foreach(config DEBUG RELEASE RELWITHDEBINFO MINSIZEREL) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${config} ${CMAKE_BINARY_DIR}/bin) +endforeach() +include(FetchContent) +find_package(Threads REQUIRED) +FetchContent_Declare(json URL https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.tar.gz) +FetchContent_Declare(httplib URL https://github.com/yhirose/cpp-httplib/archive/refs/tags/v0.18.7.tar.gz SOURCE_SUBDIR unused) +FetchContent_Declare(sha2 URL https://github.com/okdshin/PicoSHA2/archive/161cb3fc4170fa7a3eca9e582cebd27cc4d1fe29.tar.gz SOURCE_SUBDIR unused) +FetchContent_MakeAvailable(json httplib sha2) +if(NOT LOG_SQLITE_DIR) + FetchContent_Declare(sqlite_source URL https://github.com/SRombauts/SQLiteCpp/archive/refs/tags/3.3.3.tar.gz SOURCE_SUBDIR unused) + FetchContent_MakeAvailable(sqlite_source) + set(LOG_SQLITE_DIR ${sqlite_source_SOURCE_DIR}/sqlite3) +endif() +add_library(log_sqlite STATIC ${LOG_SQLITE_DIR}/sqlite3.c) +target_include_directories(log_sqlite PUBLIC ${LOG_SQLITE_DIR}) +target_compile_definitions(log_sqlite PRIVATE SQLITE_THREADSAFE=1 SQLITE_DEFAULT_WAL_SYNCHRONOUS=2) +target_link_libraries(log_sqlite PUBLIC Threads::Threads ${CMAKE_DL_LIBS}) +add_executable(log_queue queue/src/main.cpp queue/src/sqlite_queue.cpp) +add_executable(log_agent agent/src/main.cpp agent/src/log_parser.cpp) +target_include_directories(log_agent PRIVATE agent/include) +foreach(target log_queue log_agent) + target_include_directories(${target} PRIVATE native/include ${httplib_SOURCE_DIR} ${sha2_SOURCE_DIR}) + target_link_libraries(${target} PRIVATE nlohmann_json::nlohmann_json Threads::Threads) + if(WIN32) + target_link_libraries(${target} PRIVATE ws2_32) + target_compile_definitions(${target} PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) + endif() + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /EHsc) + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic) + endif() +endforeach() +target_link_libraries(log_queue PRIVATE log_sqlite) diff --git a/agent/src/main.cpp b/agent/src/main.cpp index d3ee74f..1625606 100644 --- a/agent/src/main.cpp +++ b/agent/src/main.cpp @@ -1,252 +1,78 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "batcher.hpp" -#include "file_tailer.hpp" +#include "../../native/include/common.hpp" +#include +#include #include "log_parser.hpp" -#include "queue_client.hpp" - -namespace { - // Signal-safe flag for shutdown - volatile sig_atomic_t g_shutdown_requested = 0; - - constexpr auto kPollInterval = std::chrono::milliseconds(100); - - void signalHandler(int) { - g_shutdown_requested = 1; - } - - void printUsage(const char* programName) { - std::cout << "Usage: " << programName << " [options]\n" - << "Options:\n" - << " --file Log file to tail (required)\n" - << " --queue-host Queue server host (default: localhost)\n" - << " --queue-port Queue server port (default: 8080)\n" - << " --batch-size Batch size (default: 100)\n" - << " --flush-interval Flush interval in ms (default: 1000)\n" - << " -h, --help Show this help message\n"; - } - - struct Config { - std::string log_file; - std::string queue_host = "localhost"; - int queue_port = 8080; - size_t batch_size = 100; - std::chrono::milliseconds flush_interval{1000}; - }; - - Config parseArgs(int argc, char* argv[]) { - Config config; - - for (int i = 1; i < argc; ++i) { - std::string arg = argv[i]; - - if (arg == "-h" || arg == "--help") { - printUsage(argv[0]); - exit(0); - } else if (arg == "--file") { - if (i + 1 >= argc || argv[i + 1][0] == '-') { - std::cerr << "Error: --file requires a value" << std::endl; - exit(1); - } - config.log_file = argv[++i]; - } else if (arg == "--queue-host") { - if (i + 1 >= argc || argv[i + 1][0] == '-') { - std::cerr << "Error: --queue-host requires a value" << std::endl; - exit(1); - } - config.queue_host = argv[++i]; - } else if (arg == "--queue-port") { - if (i + 1 >= argc || argv[i + 1][0] == '-') { - std::cerr << "Error: --queue-port requires a value" << std::endl; - exit(1); - } - try { - config.queue_port = std::stoi(argv[++i]); - if (config.queue_port < 1 || config.queue_port > 65535) { - std::cerr << "Error: Port must be between 1 and 65535" << std::endl; - exit(1); - } - } catch (const std::exception&) { - std::cerr << "Error: Invalid port number: " << argv[i] << std::endl; - exit(1); - } - } else if (arg == "--batch-size") { - if (i + 1 >= argc || argv[i + 1][0] == '-') { - std::cerr << "Error: --batch-size requires a value" << std::endl; - exit(1); - } - try { - config.batch_size = std::stoul(argv[++i]); - } catch (const std::exception&) { - std::cerr << "Error: Invalid batch size: " << argv[i] << std::endl; - exit(1); - } - } else if (arg == "--flush-interval") { - if (i + 1 >= argc || argv[i + 1][0] == '-') { - std::cerr << "Error: --flush-interval requires a value" << std::endl; - exit(1); - } - try { - config.flush_interval = std::chrono::milliseconds(std::stoul(argv[++i])); - } catch (const std::exception&) { - std::cerr << "Error: Invalid flush interval: " << argv[i] << std::endl; - exit(1); - } - } else { - std::cerr << "Unknown option: " << arg << std::endl; - printUsage(argv[0]); - exit(1); - } - } - - return config; - } - - // Thread-safe time formatting - std::string formatTimestamp(std::chrono::system_clock::time_point tp) { - auto time_t = std::chrono::system_clock::to_time_t(tp); - char time_str[100]; - - #ifdef _WIN32 - std::tm time_info; - localtime_s(&time_info, &time_t); - std::strftime(time_str, sizeof(time_str), "%Y-%m-%dT%H:%M:%S", &time_info); - #else - std::tm time_info; - localtime_r(&time_t, &time_info); - std::strftime(time_str, sizeof(time_str), "%Y-%m-%dT%H:%M:%S", &time_info); - #endif - - return std::string(time_str); - } +using namespace pipeline; +std::string source_key(const std::filesystem::path& file) { +#ifdef _WIN32 + HANDLE handle=CreateFileW(file.c_str(),GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,nullptr,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,nullptr); + if(handle==INVALID_HANDLE_VALUE)throw std::runtime_error("Cannot open source"); + BY_HANDLE_FILE_INFORMATION info{};bool ok=GetFileInformationByHandle(handle,&info);CloseHandle(handle); + if(!ok)throw std::runtime_error("Cannot identify source"); + return fingerprint(std::filesystem::absolute(file).generic_string()+":"+std::to_string(info.dwVolumeSerialNumber)+":"+std::to_string(info.nFileIndexHigh)+":"+std::to_string(info.nFileIndexLow)); +#else + struct stat info{}; if(::stat(file.c_str(),&info)!=0) throw std::runtime_error("Cannot stat source"); +#endif +#ifndef _WIN32 + return fingerprint(std::filesystem::absolute(file).generic_string()+":"+std::to_string(info.st_dev)+":"+std::to_string(info.st_ino)); +#endif } - -int main(int argc, char* argv[]) { - try { - // Parse command line arguments - Config config = parseArgs(argc, argv); - - if (config.log_file.empty()) { - std::cerr << "Error: --file is required" << std::endl; - printUsage(argv[0]); - return 1; - } - - std::cout << "Log Agent Starting..." << std::endl; - std::cout << " Log file: " << config.log_file << std::endl; - std::cout << " Queue: " << config.queue_host << ":" << config.queue_port << std::endl; - std::cout << " Batch size: " << config.batch_size << std::endl; - std::cout << " Flush interval: " << config.flush_interval.count() << "ms" << std::endl; - - // Set up signal handlers - if (std::signal(SIGINT, signalHandler) == SIG_ERR) { - std::cerr << "Failed to install SIGINT handler" << std::endl; - return 1; - } - if (std::signal(SIGTERM, signalHandler) == SIG_ERR) { - std::cerr << "Failed to install SIGTERM handler" << std::endl; - return 1; - } - - // Create queue client - log_agent::QueueClient queue_client(config.queue_host, config.queue_port); - - // Create batcher with callback to send to queue - // Note: Lambda captures by reference are safe because Batcher doesn't store - // the callback for async execution - it only calls it synchronously from its - // own internal thread or from flush() - std::unique_ptr batcher = std::make_unique( - config.batch_size, - config.flush_interval, - [&queue_client](const std::vector& batch) { - if (!batch.empty()) { - std::cout << "Sending batch of " << batch.size() << " logs" << std::endl; - if (!queue_client.send(batch)) { - std::cerr << "Failed to send batch to queue" << std::endl; - } - } - } - ); - - // Create log parser - log_agent::LogParser parser; - - // Create file tailer - // Note: Lambda captures by reference are safe because FileTailer only calls - // the callback synchronously from its internal thread - std::unique_ptr tailer = std::make_unique( - config.log_file, - [&parser, &batcher, &config](const std::string& line) { - try { - // Parse the log line - auto entry = parser.parse(line); - if (entry) { - // Convert to JSON and add to batcher - nlohmann::json json_entry; - json_entry["message"] = entry->message; - json_entry["level"] = entry->level; - json_entry["source"] = entry->source; - json_entry["timestamp"] = formatTimestamp(entry->timestamp); - - if (entry->raw) { - json_entry["raw"] = *entry->raw; - } - - batcher->add(json_entry.dump()); - } else { - // If parsing failed, send raw line - nlohmann::json json_entry; - json_entry["message"] = line; - json_entry["level"] = "unknown"; - json_entry["source"] = config.log_file; - json_entry["timestamp"] = formatTimestamp(std::chrono::system_clock::now()); - - batcher->add(json_entry.dump()); - } - } catch (const std::exception& e) { - std::cerr << "Error processing log line: " << e.what() << std::endl; - } - } - ); - - // Start the tailer - tailer->start(); - - std::cout << "Agent is running. Press Ctrl+C to stop." << std::endl; - - // Main loop - wait for shutdown signal - while (!g_shutdown_requested) { - std::this_thread::sleep_for(kPollInterval); - } - - // Shutdown gracefully - std::cout << "\nShutting down..." << std::endl; - - // Stop the tailer (this will flush incomplete line) - tailer->stop(); - tailer.reset(); - - // Flush any remaining batch - if (batcher) { - batcher->flush(); - batcher.reset(); - } - - std::cout << "Agent stopped." << std::endl; - return 0; - - } catch (const std::exception& e) { - std::cerr << "Fatal error: " << e.what() << std::endl; - return 1; +int main(int argc,char** argv) { + try { + signals(); + std::filesystem::path file=option(argc,argv,"--file","data/events.ndjson"); + std::filesystem::path checkpoint=option(argc,argv,"--checkpoint",file.string()+".checkpoint.json"); + auto port=number(option(argc,argv,"--queue-port","8081"),1,65535); + auto batch_size=number(option(argc,argv,"--batch-size","2048"),1,4096); + httplib::Client client(option(argc,argv,"--queue-host","127.0.0.1"),port); + client.set_connection_timeout(1,0); client.set_read_timeout(5,0); client.set_write_timeout(5,0); + uint64_t offset=0,generation=0; std::string key; + if(std::filesystem::exists(checkpoint)) { + std::ifstream in(checkpoint); auto state=json::parse(in); offset=state.at("offset"); key=state.at("source"); generation=state.value("generation",0ULL); + } + while(!stopped) { + if(!std::filesystem::exists(file)) {std::this_thread::sleep_for(std::chrono::milliseconds(50));continue;} + auto current_key=source_key(file); auto size=std::filesystem::file_size(file); + if(current_key!=key) {key=current_key;offset=0;generation=0;} + if(size(offset)); + std::string buffer(static_cast(std::min(4*1024*1024,size-offset)),'\0'); in.read(buffer.data(),static_cast(buffer.size())); buffer.resize(static_cast(in.gcount())); + if(!in && !in.eof()) throw std::runtime_error("Source read failed"); + json logs=json::array(); size_t cursor=0; uint64_t end=offset; + while(logs.size()1024*1024) throw std::runtime_error("Source line exceeds 1 MiB; checkpoint unchanged"); + if(!line.empty() && line.back()=='\r') line.pop_back(); + if(!line.empty()) { + auto entry=json::parse(line,nullptr,false); + if(!entry.is_object()) {auto parsed=log_agent::LogParser{}.parse(line);entry={{"message",parsed?parsed->message:line},{"level",parsed?parsed->level:"info"},{"source",file.filename().string()}};if(parsed)entry["timestamp"]=std::chrono::duration_cast(parsed->timestamp.time_since_epoch()).count();} + if(!entry.contains("message") || !entry["message"].is_string()) entry["message"]=line; + if(!entry.contains("source")) entry["source"]=file.filename().string(); + if(!entry.contains("id") || !entry["id"].is_string() || entry["id"].get_ref().empty()) entry["id"]=fingerprint(key+":"+std::to_string(generation)+":"+std::to_string(offset+cursor)+":"+line); + if(!entry.contains("raw")) entry["raw"]=line; + logs.push_back(std::move(entry)); + } + cursor=newline+1; end=offset+cursor; + } + if(end==offset) { + if(buffer.size()>1024*1024) throw std::runtime_error("Incomplete source line exceeds 1 MiB; checkpoint unchanged"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); continue; + } + if(!logs.empty()) { + auto payload=json({{"id",fingerprint(key+":"+std::to_string(offset)+":"+std::to_string(end)+":"+logs.dump())},{"logs",std::move(logs)}}).dump(); + bool accepted=false; + while(!stopped) { + auto response=client.Post("/api/logs/batch",payload,"application/json"); + if(response && response->status>=200 && response->status<300) {accepted=true;break;} + if(response && (response->status==400 || response->status==409 || response->status==413)) throw std::runtime_error("Queue rejected source batch; checkpoint unchanged: "+response->body); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + if(!accepted) break; + } + atomic_write(checkpoint,json({{"offset",end},{"source",key},{"generation",generation}}).dump()); offset=end; + } + return 0; + } catch(const std::exception& error) {std::cerr< None: def get_cursor(self) -> Generator[sqlite3.Cursor, None, None]: with self._lock: cursor = self.connection.cursor() - try: - yield cursor - finally: - cursor.close() + try: + yield cursor + finally: + cursor.close() def execute(self, query: str, params: tuple = ()) -> sqlite3.Cursor: with self._lock: - return self._connection.execute(query, params) + return self.connection.execute(query, params) def commit(self) -> None: with self._lock: diff --git a/anomaly/src/db/log_repository.py b/anomaly/src/db/log_repository.py index bb0d09d..d818123 100644 --- a/anomaly/src/db/log_repository.py +++ b/anomaly/src/db/log_repository.py @@ -65,27 +65,18 @@ def fetch_logs_since( since_id: str | None, limit: int = 1000, ) -> list[LogEntry]: + position = 0 if since_id: - rows = self.db.execute( - """ - SELECT id, timestamp, level, source, message, raw - FROM logs - WHERE id > ? - ORDER BY id ASC - LIMIT ? - """, - (since_id, limit), - ).fetchall() - else: - rows = self.db.execute( - """ - SELECT id, timestamp, level, source, message, raw - FROM logs - ORDER BY id ASC - LIMIT ? - """, - (limit,), - ).fetchall() + row = self.db.execute("SELECT rowid AS position FROM logs WHERE id = ?", (since_id,)).fetchone() + if row: + position = row["position"] + else: + saved = self.db.execute("SELECT value FROM anomaly_state WHERE key = 'last_processed_rowid'").fetchone() + position = int(saved["value"]) if saved else 0 + rows = self.db.execute( + "SELECT id, timestamp, level, source, message, raw FROM logs WHERE rowid > ? ORDER BY rowid ASC LIMIT ?", + (position, limit), + ).fetchall() return [LogEntry(**dict(row)) for row in rows] @@ -157,7 +148,7 @@ def get_last_processed_id(self) -> str | None: def set_last_processed_id(self, log_id: str | None) -> None: with self.db.get_cursor() as cursor: if log_id is None: - cursor.execute("DELETE FROM anomaly_state WHERE key = 'last_processed_id'") + cursor.execute("DELETE FROM anomaly_state WHERE key IN ('last_processed_id', 'last_processed_rowid')") else: cursor.execute( """ @@ -166,6 +157,9 @@ def set_last_processed_id(self, log_id: str | None) -> None: """, (log_id, datetime.utcnow().isoformat()), ) + row = cursor.execute("SELECT rowid AS position FROM logs WHERE id = ?", (log_id,)).fetchone() + if row: + cursor.execute("INSERT OR REPLACE INTO anomaly_state VALUES ('last_processed_rowid', ?, ?)", (str(row["position"]), datetime.utcnow().isoformat())) self.db.commit() def get_volume_stats(self, hours: float = 24.0) -> dict[str, Any]: diff --git a/anomaly/src/main.py b/anomaly/src/main.py index 0df2174..14f2115 100644 --- a/anomaly/src/main.py +++ b/anomaly/src/main.py @@ -110,17 +110,6 @@ async def lifespan(app: FastAPI): detectors=detectors, ) - def signal_handler(signum, frame): - logger.info("shutdown_signal_received", signal=signum) - if worker: - worker.stop() - if db: - db.close() - sys.exit(0) - - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - worker.start() logger.info("service_started") diff --git a/anomaly/src/workers/polling_worker.py b/anomaly/src/workers/polling_worker.py index d998506..9059c7a 100644 --- a/anomaly/src/workers/polling_worker.py +++ b/anomaly/src/workers/polling_worker.py @@ -45,6 +45,7 @@ def start(self) -> None: logger.warning("worker_already_running") return + self._shutdown_event.clear() self._running_event.set() with self._state_lock: self.start_time = datetime.now(UTC) @@ -138,10 +139,10 @@ def _create_alert(self, anomaly: AnomalyResult) -> None: try: self.alert_repo.create( - log_id=anomaly.log_id, + log_id=(self.log_repo.fetch_recent_logs(limit=1)[0].id if anomaly.log_id == "system" else anomaly.log_id), severity=anomaly.severity, message=anomaly.message, - details=anomaly.details, + details={**(anomaly.details or {}), "anomaly_type": anomaly.anomaly_type}, ) with self._state_lock: self.alerts_created += 1 diff --git a/api/src/app.ts b/api/src/app.ts new file mode 100644 index 0000000..ff5aaf3 --- /dev/null +++ b/api/src/app.ts @@ -0,0 +1,75 @@ +import express from 'express'; +import { performance } from 'node:perf_hooks'; +import type Database from 'better-sqlite3'; +import { LogRepository } from './db/logRepository'; +import { AlertRepository } from './db/alertRepository'; +import { AlertService } from './services/alertService'; +import { createLogsRouter } from './routes/logs'; +import { createAlertsRouter } from './routes/alerts'; +import type { IngestionController } from './services/ingestionController'; +export function createApp(db: Database.Database, queueUrl: string, ingestion: IngestionController) { + const app = express(), + logs = new LogRepository(db), + alerts = new AlertRepository(db), + monitor = new AlertService(alerts, logs), + samples: number[] = []; + const start = performance.now(); + app.disable('x-powered-by'); + app.use(express.json({ limit: '8mb' })); + app.get('/health', (_req, res) => + res + .status(ingestion.state.isRunning ? 200 : 503) + .json({ status: ingestion.state.isRunning ? 'ok' : 'degraded', ingestion: ingestion.state }), + ); + app.get('/metrics', async (_req, res) => { + let queue: Record = { reachable: false }; + try { + const response = await fetch(queueUrl + '/api/queue/stats', { + signal: AbortSignal.timeout(1200), + }); + if (response.ok) queue = { ...((await response.json()) as object), reachable: true }; + } catch {} + const sorted = [...samples].sort((a, b) => a - b); + const percentile = (p: number) => + sorted.length ? sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * p) - 1)] : null; + res.json({ + indexedLogs: logs.count(), + uptimeSeconds: (performance.now() - start) / 1000, + ingestion: ingestion.state, + queue, + search: { samples: samples.length, p50Ms: percentile(0.5), p95Ms: percentile(0.95) }, + }); + }); + app.use( + '/api/logs', + (req, res, next) => { + const begin = performance.now(); + res.on('finish', () => { + if ( + req.method === 'GET' && + (req.path === '/' || req.path === '/page') && + res.statusCode === 200 + ) { + samples.push(performance.now() - begin); + if (samples.length > 256) samples.shift(); + } + }); + next(); + }, + createLogsRouter(logs), + ); + app.use('/api/alerts', createAlertsRouter(alerts, monitor)); + app.use( + ( + error: Error & { status?: number }, + _req: express.Request, + res: express.Response, + _next: express.NextFunction, + ) => { + res + .status(error.status === 400 ? 400 : error.status === 413 ? 413 : 500) + .json({ error: 'Request could not be processed' }); + }, + ); + return { app, monitor, logs, alerts }; +} diff --git a/api/src/db/alertRepository.ts b/api/src/db/alertRepository.ts index a3f055c..95c8168 100644 --- a/api/src/db/alertRepository.ts +++ b/api/src/db/alertRepository.ts @@ -40,7 +40,7 @@ export class AlertRepository { alert.message, alert.details ? this.safeStringify(alert.details) : null, alert.createdAt, - alert.acknowledged ? 1 : 0 + alert.acknowledged ? 1 : 0, ); } @@ -49,6 +49,18 @@ export class AlertRepository { const values: (string | number)[] = []; const countValues: (string | number)[] = []; + if (filter.q) { + conditions.push('instr(lower(message),lower(?))>0'); + values.push(filter.q); + countValues.push(filter.q); + } + if (filter.source) { + conditions.push( + "CASE WHEN json_valid(details) THEN COALESCE(json_extract(details,'$.source'),json_extract(details,'$.log_source')) END = ?", + ); + values.push(filter.source); + countValues.push(filter.source); + } if (filter.severity) { conditions.push('severity = ?'); values.push(filter.severity); @@ -62,14 +74,14 @@ export class AlertRepository { } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; - + const countQuery = `SELECT COUNT(*) as total FROM alerts ${whereClause}`; const countResult = this.db.prepare(countQuery).get(...countValues) as { total: number }; const total = countResult?.total || 0; const limit = Math.min(Math.max(filter.limit || 50, 1), 1000); const offset = Math.max(filter.offset || 0, 0); - + const query = ` SELECT id, log_id, severity, message, details, created_at, acknowledged FROM alerts @@ -77,9 +89,9 @@ export class AlertRepository { ORDER BY created_at DESC LIMIT ? OFFSET ? `; - + values.push(limit, offset); - + const rows = this.db.prepare(query).all(...values) as Array<{ id: string; log_id: string; @@ -90,31 +102,35 @@ export class AlertRepository { acknowledged: number; }>; - const alerts: Alert[] = rows.map(row => ({ + const alerts: Alert[] = rows.map((row) => ({ id: row.id, logId: row.log_id, severity: row.severity as Alert['severity'], message: row.message, details: this.safeParseJson(row.details), createdAt: row.created_at, - acknowledged: Boolean(row.acknowledged) + acknowledged: Boolean(row.acknowledged), })); return { alerts, total }; } findById(id: string): Alert | null { - const row = this.db.prepare( - 'SELECT id, log_id, severity, message, details, created_at, acknowledged FROM alerts WHERE id = ?' - ).get(id) as { - id: string; - log_id: string; - severity: string; - message: string; - details: string | null; - created_at: string; - acknowledged: number; - } | undefined; + const row = this.db + .prepare( + 'SELECT id, log_id, severity, message, details, created_at, acknowledged FROM alerts WHERE id = ?', + ) + .get(id) as + | { + id: string; + log_id: string; + severity: string; + message: string; + details: string | null; + created_at: string; + acknowledged: number; + } + | undefined; if (!row) return null; @@ -125,29 +141,27 @@ export class AlertRepository { message: row.message, details: this.safeParseJson(row.details), createdAt: row.created_at, - acknowledged: Boolean(row.acknowledged) + acknowledged: Boolean(row.acknowledged), }; } acknowledge(id: string): boolean { - const result = this.db.prepare( - 'UPDATE alerts SET acknowledged = 1 WHERE id = ?' - ).run(id); + const result = this.db.prepare('UPDATE alerts SET acknowledged = 1 WHERE id = ?').run(id); return result.changes > 0; } getStats(): AlertStats { - const totalResult = this.db.prepare( - 'SELECT COUNT(*) as count FROM alerts' - ).get() as { count: number }; - - const severityStats = this.db.prepare( - 'SELECT severity, COUNT(*) as count FROM alerts GROUP BY severity' - ).all() as Array<{ severity: string; count: number }>; - - const unacknowledgedResult = this.db.prepare( - 'SELECT COUNT(*) as count FROM alerts WHERE acknowledged = 0' - ).get() as { count: number }; + const totalResult = this.db.prepare('SELECT COUNT(*) as count FROM alerts').get() as { + count: number; + }; + + const severityStats = this.db + .prepare('SELECT severity, COUNT(*) as count FROM alerts GROUP BY severity') + .all() as Array<{ severity: string; count: number }>; + + const unacknowledgedResult = this.db + .prepare('SELECT COUNT(*) as count FROM alerts WHERE acknowledged = 0') + .get() as { count: number }; const bySeverity: Record = {}; for (const row of severityStats) { @@ -157,25 +171,32 @@ export class AlertRepository { return { total: totalResult.count, bySeverity, - unacknowledged: unacknowledgedResult.count + openBySeverity: Object.fromEntries( + ( + this.db + .prepare( + 'SELECT severity,COUNT(*) count FROM alerts WHERE acknowledged=0 GROUP BY severity', + ) + .all() as { severity: string; count: number }[] + ).map((row) => [row.severity, row.count]), + ), + unacknowledged: unacknowledgedResult.count, }; } deleteOldAlerts(days: number): number { const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); - - const result = this.db.prepare( - 'DELETE FROM alerts WHERE created_at < ? AND acknowledged = 1' - ).run(cutoff); - + + const result = this.db + .prepare('DELETE FROM alerts WHERE created_at < ? AND acknowledged = 1') + .run(cutoff); + return result.changes; } deleteAcknowledged(): number { - const result = this.db.prepare( - 'DELETE FROM alerts WHERE acknowledged = 1' - ).run(); - + const result = this.db.prepare('DELETE FROM alerts WHERE acknowledged = 1').run(); + return result.changes; } @@ -183,13 +204,15 @@ export class AlertRepository { // Escape LIKE wildcards and build pattern const escapedPattern = pattern.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_'); const likePattern = `%${escapedPattern}%`; - const row = this.db.prepare( - `SELECT id FROM alerts + const row = this.db + .prepare( + `SELECT id FROM alerts WHERE severity = ? AND acknowledged = 0 AND created_at >= ? AND message LIKE ? ESCAPE '\\' - LIMIT 1` - ).get(severity, since, likePattern) as { id: string } | undefined; - + LIMIT 1`, + ) + .get(severity, since, likePattern) as { id: string } | undefined; + return row ? this.findById(row.id) : null; } @@ -210,4 +233,4 @@ export class AlertRepository { return null; } } -} \ No newline at end of file +} diff --git a/api/src/db/database.ts b/api/src/db/database.ts new file mode 100644 index 0000000..6e2a6ae --- /dev/null +++ b/api/src/db/database.ts @@ -0,0 +1,11 @@ +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +export function openDatabase(file: string): Database.Database { + mkdirSync(path.dirname(file), { recursive: true }); + const db = new Database(file); + db.pragma('journal_mode=WAL'); + db.pragma('synchronous=FULL'); + db.pragma('busy_timeout=10000'); + return db; +} diff --git a/api/src/db/logPageReader.ts b/api/src/db/logPageReader.ts new file mode 100644 index 0000000..21bbff6 --- /dev/null +++ b/api/src/db/logPageReader.ts @@ -0,0 +1,103 @@ +import type Database from 'better-sqlite3'; +import { createHash } from 'node:crypto'; +import type { LogEntry, SearchParams } from '../types/log'; +export class InvalidCursor extends Error {} +type Position = { time: string; row: number; filter: string }; +export function readLogPage(db: Database.Database, params: SearchParams & { cursor?: string }) { + const filter = createHash('sha256') + .update( + JSON.stringify([ + params.q ?? '', + params.level ?? '', + params.source ?? '', + params.startDate ?? '', + params.endDate ?? '', + ]), + ) + .digest('hex') + .slice(0, 24); + const conditions: string[] = [], + values: (string | number)[] = []; + for (const [key, column] of [ + ['level', 'level'], + ['source', 'source'], + ['startDate', 'timestamp >='], + ['endDate', 'timestamp <='], + ] as const) { + const value = params[key]; + if (value) { + conditions.push(column.includes(' ') ? column + ' ?' : column + ' = ?'); + values.push(value); + } + } + if (params.cursor) { + let p: Position; + try { + if (params.cursor.length > 1024) throw new Error(); + p = JSON.parse(Buffer.from(params.cursor, 'base64url').toString()); + if ( + typeof p.time !== 'string' || + !Number.isFinite(Date.parse(p.time)) || + !Number.isSafeInteger(p.row) || + p.row < 1 || + p.filter !== filter + ) + throw new Error(); + } catch { + throw new InvalidCursor('Cursor does not match this search.'); + } + conditions.push('(timestamp < ? OR (timestamp = ? AND rowid < ?))'); + values.push(p.time, p.time, p.row); + } + const where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : '', + limit = Math.min(Math.max(params.limit || 50, 1), 1000); + const fields = 'rowid AS cursor_row,id,timestamp,level,source,message,raw'; + let rows: Record[]; + if (params.q) { + const phrase = '"' + params.q.replace(/"/g, '""') + '"'; + // Correlated rowid+MATCH checks stop after enough recent hits; rare phrases + // fall back to the global FTS index. Both paths preserve the same ordering. + rows = db + .prepare( + 'WITH recent AS MATERIALIZED (SELECT ' + + fields + + ' FROM logs ' + + where + + ' ORDER BY timestamp DESC,rowid DESC LIMIT 4096) SELECT * FROM recent WHERE EXISTS(SELECT 1 FROM logs_fts WHERE rowid=recent.cursor_row AND logs_fts MATCH ?) ORDER BY timestamp DESC,cursor_row DESC LIMIT ?', + ) + .all(...values, phrase, limit + 1) as Record[]; + if (rows.length <= limit) + rows = db + .prepare( + 'SELECT ' + + fields + + ' FROM logs ' + + where + + (where ? ' AND ' : ' WHERE ') + + 'rowid IN (SELECT rowid FROM logs_fts WHERE logs_fts MATCH ?) ORDER BY timestamp DESC,rowid DESC LIMIT ?', + ) + .all(...values, phrase, limit + 1) as Record[]; + } else + rows = db + .prepare( + 'SELECT ' + fields + ' FROM logs ' + where + ' ORDER BY timestamp DESC,rowid DESC LIMIT ?', + ) + .all(...values, limit + 1) as Record[]; + const hasMore = rows.length > limit, + page = rows.slice(0, limit), + last = page.at(-1); + const logs = page.map(({ cursor_row: _, ...entry }) => ({ + ...entry, + raw: entry.raw ?? undefined, + })) as unknown as LogEntry[]; + return { + logs, + hasMore, + nextCursor: + hasMore && last + ? Buffer.from( + JSON.stringify({ time: last.timestamp, row: last.cursor_row, filter }), + ).toString('base64url') + : null, + }; +} diff --git a/api/src/db/logRepository.ts b/api/src/db/logRepository.ts index f32924d..eac0a53 100644 --- a/api/src/db/logRepository.ts +++ b/api/src/db/logRepository.ts @@ -1,8 +1,10 @@ import Database from 'better-sqlite3'; import { LogEntry, SearchParams, LogStats } from '../types/log'; +import { readLogPage } from './logPageReader'; export class LogRepository { private db: Database.Database; + private delivered = new Map(); constructor(db: Database.Database) { this.db = db; @@ -45,11 +47,48 @@ export class LogRepository { END; CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp); + CREATE INDEX IF NOT EXISTS idx_logs_time_row ON logs(timestamp DESC); + CREATE INDEX IF NOT EXISTS idx_logs_source_time ON logs(source,timestamp DESC); + CREATE INDEX IF NOT EXISTS idx_logs_level_time ON logs(level,timestamp DESC); CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level); CREATE INDEX IF NOT EXISTS idx_logs_source ON logs(source); `); } + searchPage(params: SearchParams & { cursor?: string }) { + return readLogPage(this.db, params); + } + + count(): number { + return (this.db.prepare('SELECT COUNT(*) n FROM logs').get() as { n: number }).n; + } + + insertDeliveredBatch(entries: LogEntry[]): number { + let added = 0; + for (let offset = 0; offset < entries.length; offset += 128) { + const chunk = entries.slice(offset, offset + 128); + let stmt = this.delivered.get(chunk.length); + if (!stmt) { + stmt = this.db.prepare( + 'INSERT OR IGNORE INTO logs(id,timestamp,level,source,message,raw) VALUES ' + + chunk.map(() => '(?,?,?,?,?,?)').join(','), + ); + this.delivered.set(chunk.length, stmt); + } + added += stmt.run( + ...chunk.flatMap((entry) => [ + entry.id, + entry.timestamp, + entry.level, + entry.source, + entry.message, + entry.raw ?? null, + ]), + ).changes; + } + return added; + } + insert(entry: LogEntry): void { const stmt = this.db.prepare(` INSERT INTO logs (id, timestamp, level, source, message, raw) @@ -61,7 +100,7 @@ export class LogRepository { entry.level, entry.source, entry.message, - entry.raw || null + entry.raw || null, ); } @@ -72,7 +111,7 @@ export class LogRepository { INSERT INTO logs (id, timestamp, level, source, message, raw) VALUES (?, ?, ?, ?, ?, ?) `); - + const transaction = this.db.transaction((items: LogEntry[]) => { for (const entry of items) { insertStmt.run( @@ -81,12 +120,12 @@ export class LogRepository { entry.level, entry.source, entry.message, - entry.raw || null + entry.raw || null, ); } return items.length; }); - + return transaction(entries); } @@ -129,20 +168,20 @@ export class LogRepository { } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; - + // Build count query using same conditions but without limit/offset const countQuery = ` SELECT COUNT(*) as total FROM logs ${whereClause} `; - + const countResult = this.db.prepare(countQuery).get(...countValues) as { total: number }; const total = countResult?.total || 0; const limit = Math.min(Math.max(params.limit || 50, 1), 1000); const offset = Math.max(params.offset || 0, 0); - + const query = ` SELECT id, timestamp, level, source, message, raw FROM logs @@ -150,9 +189,9 @@ export class LogRepository { ORDER BY timestamp DESC LIMIT ? OFFSET ? `; - + values.push(limit, offset); - + const stmt = this.db.prepare(query); const rows = stmt.all(...values) as Array<{ id: string; @@ -163,13 +202,13 @@ export class LogRepository { raw: string | null; }>; - const logs: LogEntry[] = rows.map(row => ({ + const logs: LogEntry[] = rows.map((row) => ({ id: row.id, timestamp: row.timestamp, level: row.level as LogEntry['level'], source: row.source, message: row.message, - raw: row.raw || undefined + raw: row.raw || undefined, })); return { logs, total }; @@ -179,18 +218,20 @@ export class LogRepository { // Clamp hours to a reasonable range (1 hour to 1 year) const clampedHours = Math.min(Math.max(hours, 1), 8760); const since = new Date(Date.now() - clampedHours * 60 * 60 * 1000).toISOString(); - - const totalResult = this.db.prepare( - 'SELECT COUNT(*) as count FROM logs WHERE timestamp >= ?' - ).get(since) as { count: number }; - - const levelStats = this.db.prepare( - 'SELECT level, COUNT(*) as count FROM logs WHERE timestamp >= ? GROUP BY level' - ).all(since) as Array<{ level: string; count: number }>; - - const sourceStats = this.db.prepare( - 'SELECT source, COUNT(*) as count FROM logs WHERE timestamp >= ? GROUP BY source ORDER BY count DESC LIMIT 100' - ).all(since) as Array<{ source: string; count: number }>; + + const totalResult = this.db + .prepare('SELECT COUNT(*) as count FROM logs WHERE timestamp >= ?') + .get(since) as { count: number }; + + const levelStats = this.db + .prepare('SELECT level, COUNT(*) as count FROM logs WHERE timestamp >= ? GROUP BY level') + .all(since) as Array<{ level: string; count: number }>; + + const sourceStats = this.db + .prepare( + 'SELECT source, COUNT(*) as count FROM logs WHERE timestamp >= ? GROUP BY source ORDER BY count DESC LIMIT 100', + ) + .all(since) as Array<{ source: string; count: number }>; const byLevel: Record = {}; const bySource: Record = {}; @@ -206,31 +247,31 @@ export class LogRepository { return { total: totalResult.count, byLevel, - bySource + bySource, }; } deleteOldLogs(days: number): number { const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); - - const result = this.db.prepare( - 'DELETE FROM logs WHERE timestamp < ?' - ).run(cutoff); - + + const result = this.db.prepare('DELETE FROM logs WHERE timestamp < ?').run(cutoff); + return result.changes; } getById(id: string): LogEntry | null { - const row = this.db.prepare( - 'SELECT id, timestamp, level, source, message, raw FROM logs WHERE id = ?' - ).get(id) as { - id: string; - timestamp: string; - level: string; - source: string; - message: string; - raw: string | null; - } | undefined; + const row = this.db + .prepare('SELECT id, timestamp, level, source, message, raw FROM logs WHERE id = ?') + .get(id) as + | { + id: string; + timestamp: string; + level: string; + source: string; + message: string; + raw: string | null; + } + | undefined; if (!row) return null; @@ -240,14 +281,16 @@ export class LogRepository { level: row.level as LogEntry['level'], source: row.source, message: row.message, - raw: row.raw || undefined + raw: row.raw || undefined, }; } getRecent(limit: number): LogEntry[] { - const rows = this.db.prepare( - 'SELECT id, timestamp, level, source, message, raw FROM logs ORDER BY timestamp DESC LIMIT ?' - ).all(limit) as Array<{ + const rows = this.db + .prepare( + 'SELECT id, timestamp, level, source, message, raw FROM logs ORDER BY timestamp DESC LIMIT ?', + ) + .all(limit) as Array<{ id: string; timestamp: string; level: string; @@ -256,21 +299,21 @@ export class LogRepository { raw: string | null; }>; - return rows.map(row => ({ + return rows.map((row) => ({ id: row.id, timestamp: row.timestamp, level: row.level as LogEntry['level'], source: row.source, message: row.message, - raw: row.raw || undefined + raw: row.raw || undefined, })); } getErrorCountSince(since: string, level: string = 'error'): number { - const result = this.db.prepare( - 'SELECT COUNT(*) as count FROM logs WHERE level = ? AND timestamp >= ?' - ).get(level, since) as { count: number }; - + const result = this.db + .prepare('SELECT COUNT(*) as count FROM logs WHERE level = ? AND timestamp >= ?') + .get(level, since) as { count: number }; + return result.count; } -} \ No newline at end of file +} diff --git a/api/src/index.ts b/api/src/index.ts index 7cc3a2d..61c9b82 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,117 +1,26 @@ -import express, { Request, Response, NextFunction } from 'express'; -import cors from 'cors'; -import fs from 'fs'; -import path from 'path'; -import Database from 'better-sqlite3'; +import { openDatabase } from './db/database'; +import { IngestionController } from './services/ingestionController'; +import { createApp } from './app'; +const dbPath = process.env.DB_PATH || './data/logs.db', + queueUrl = process.env.QUEUE_URL || 'http://127.0.0.1:8081'; +const db = openDatabase(dbPath); +// Initialize the schema before starting a second SQLite connection. import { LogRepository } from './db/logRepository'; -import { AlertRepository } from './db/alertRepository'; -import { QueueService } from './services/queueService'; -import { QueueConsumer } from './services/queueConsumer'; -import { AlertService } from './services/alertService'; -import { createLogsRouter } from './routes/logs'; -import { createAlertsRouter } from './routes/alerts'; - -const app = express(); -const PORT = process.env.PORT || 3000; -const DB_PATH = process.env.DB_PATH || './data/logs.db'; -const QUEUE_URL = process.env.QUEUE_URL || 'http://localhost:8081'; -const QUEUE_POLL_INTERVAL = parseInt(process.env.QUEUE_POLL_INTERVAL_MS || '5000', 10) || 5000; - -// Ensure data directory exists -const dataDir = path.dirname(DB_PATH); -if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir, { recursive: true }); -} - -// Initialize database and repositories (single shared connection) -const db = new Database(DB_PATH); -const logRepo = new LogRepository(db); -const alertRepo = new AlertRepository(db); - -// Initialize services -const queueService = new QueueService(QUEUE_URL); -const queueConsumer = new QueueConsumer(logRepo, queueService, QUEUE_POLL_INTERVAL); -const alertService = new AlertService(alertRepo, logRepo); - -// Middleware -app.use(cors()); -app.use(express.json({ limit: '100kb' })); - -// Health check -app.get('/health', (_req: Request, res: Response) => { - const consumerStatus = queueConsumer.getStatus(); - const alertStatus = alertService.getStatus(); - - res.json({ - status: 'ok', - services: { - queueConsumer: consumerStatus.isRunning ? 'running' : 'stopped', - alertService: alertStatus.isRunning ? 'running' : 'stopped' - } - }); -}); - -// Mount routes -app.use('/api/logs', createLogsRouter(logRepo)); -app.use('/api/alerts', createAlertsRouter(alertRepo, alertService)); - -// Error handling middleware (must have 4 parameters) -app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { - console.error('Unhandled error:', err); - res.status(500).json({ error: 'Internal server error' }); +new LogRepository(db); +const ingestion = new IngestionController(dbPath, queueUrl); +const { app, monitor } = createApp(db, queueUrl, ingestion); +const server = app.listen(Number(process.env.PORT || 3000), process.env.HOST || '127.0.0.1', () => { + monitor.start(); + console.log('Search API ready'); }); - -// Start server -const server = app.listen(PORT, () => { - console.log(`API server running on port ${PORT}`); - console.log(`Database: ${DB_PATH}`); - console.log(`Queue URL: ${QUEUE_URL}`); - - // Start background services - queueConsumer.start(); - alertService.start(); -}); - -// Graceful shutdown -let shuttingDown = false; - -function gracefulShutdown(signal: string): void { - if (shuttingDown) { - console.log(`Already shutting down, ignoring ${signal}`); - return; - } - shuttingDown = true; - - console.log(`\nReceived ${signal}. Starting graceful shutdown...`); - - // Stop background services - queueConsumer.stop(); - alertService.stop(); - - // Close server first, then close database after in-flight requests finish - server.close(() => { - db.close(); - console.log('Server closed. Exiting.'); - process.exit(0); - }); - - // Force shutdown after 10 seconds - setTimeout(() => { - console.error('Forced shutdown after timeout'); - process.exit(1); - }, 10000); +let closing = false; +async function stop() { + if (closing) return; + closing = true; + monitor.stop(); + const closed=new Promise(resolve=>server.close(()=>resolve())); + await Promise.all([closed,ingestion.stop()]); + db.close(); } - -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); -process.on('SIGINT', () => gracefulShutdown('SIGINT')); - -// Handle uncaught exceptions -process.on('uncaughtException', (error) => { - console.error('Uncaught exception:', error); - gracefulShutdown('uncaughtException'); -}); - -process.on('unhandledRejection', (reason) => { - console.error('Unhandled rejection:', reason); - gracefulShutdown('unhandledRejection'); -}); \ No newline at end of file +process.on('SIGINT', () => void stop()); +process.on('SIGTERM', () => void stop()); diff --git a/api/src/routes/alerts.ts b/api/src/routes/alerts.ts index c7f6bb8..0aede40 100644 --- a/api/src/routes/alerts.ts +++ b/api/src/routes/alerts.ts @@ -4,25 +4,37 @@ import { AlertService } from '../services/alertService'; import { AlertFilter } from '../types/log'; import { parseQueryInt } from '../utils/queryHelpers'; -export function createAlertsRouter( - alertRepo: AlertRepository, - alertService: AlertService -): Router { +export function createAlertsRouter(alertRepo: AlertRepository, alertService: AlertService): Router { const router = Router(); // GET /api/alerts - List alerts router.get('/', (req: Request, res: Response) => { try { + for (const key of ['q', 'source', 'severity', 'acknowledged']) + if (req.query[key] !== undefined && typeof req.query[key] !== 'string') { + res.status(400).json({ error: 'Invalid alert filter' }); + return; + } + if ( + (typeof req.query.q === 'string' && req.query.q.length > 10000) || + (typeof req.query.source === 'string' && req.query.source.length > 500) || + (req.query.acknowledged !== undefined && + !['true', 'false'].includes(String(req.query.acknowledged))) + ) { + res.status(400).json({ error: 'Invalid alert filter' }); + return; + } const limit = parseQueryInt(req.query.limit as string, 50, 1, 1000); const offset = parseQueryInt(req.query.offset as string, 0, 0); const filter: AlertFilter = { + q: req.query.q as string | undefined, + source: req.query.source as string | undefined, severity: req.query.severity as string | undefined, - acknowledged: req.query.acknowledged !== undefined - ? req.query.acknowledged === 'true' - : undefined, + acknowledged: + req.query.acknowledged !== undefined ? req.query.acknowledged === 'true' : undefined, limit, - offset + offset, }; const result = alertRepo.findAll(filter); @@ -30,7 +42,7 @@ export function createAlertsRouter( alerts: result.alerts, total: result.total, limit, - offset + offset, }); } catch (error) { console.error('Error fetching alerts:', error); @@ -43,14 +55,14 @@ export function createAlertsRouter( try { const stats = alertRepo.getStats(); const serviceStatus = alertService.getStatus(); - + res.json({ ...stats, monitoring: { isRunning: serviceStatus.isRunning, lastCheck: serviceStatus.lastCheck, - config: serviceStatus.config - } + config: serviceStatus.config, + }, }); } catch (error) { console.error('Error fetching alert stats:', error); @@ -62,7 +74,7 @@ export function createAlertsRouter( router.get('/:id', (req: Request, res: Response) => { try { const alert = alertRepo.findById(req.params.id); - + if (!alert) { res.status(404).json({ error: 'Alert not found' }); return; @@ -79,7 +91,7 @@ export function createAlertsRouter( router.post('/:id/acknowledge', (req: Request, res: Response) => { try { const success = alertRepo.acknowledge(req.params.id); - + if (!success) { res.status(404).json({ error: 'Alert not found' }); return; @@ -96,12 +108,12 @@ export function createAlertsRouter( router.delete('/', (req: Request, res: Response) => { try { const days = parseInt(req.query.olderThanDays as string) || 30; - + if (days <= 0) { res.status(400).json({ error: 'olderThanDays must be a positive number' }); return; } - + const deleted = alertRepo.deleteOldAlerts(days); res.json({ deleted, olderThanDays: days }); } catch (error) { diff --git a/api/src/routes/logs.ts b/api/src/routes/logs.ts index 96bd11c..713ce26 100644 --- a/api/src/routes/logs.ts +++ b/api/src/routes/logs.ts @@ -4,6 +4,7 @@ import { LogRepository } from '../db/logRepository'; import { LogEntry, SearchParams } from '../types/log'; import { parseQueryInt } from '../utils/queryHelpers'; import { normalizeLevel } from '../utils/logNormalizer'; +import { InvalidCursor } from '../db/logPageReader'; export function createLogsRouter(logRepo: LogRepository): Router { const router = Router(); @@ -32,7 +33,7 @@ export function createLogsRouter(logRepo: LogRepository): Router { level: normalizeLevel(level), source: validatedSource || 'api', message, - raw: typeof raw === 'string' ? raw : undefined + raw: typeof raw === 'string' ? raw : undefined, }; logRepo.insert(entry); @@ -45,6 +46,11 @@ export function createLogsRouter(logRepo: LogRepository): Router { // GET /api/logs - Search logs router.get('/', (req: Request, res: Response) => { + for (const key of ['q', 'level', 'source', 'startDate', 'endDate']) + if (req.query[key] !== undefined && typeof req.query[key] !== 'string') { + res.status(400).json({ error: 'Invalid filter' }); + return; + } try { const limit = parseQueryInt(req.query.limit as string, 50, 1, 1000); const offset = parseQueryInt(req.query.offset as string, 0, 0); @@ -56,7 +62,7 @@ export function createLogsRouter(logRepo: LogRepository): Router { startDate: req.query.startDate as string | undefined, endDate: req.query.endDate as string | undefined, limit, - offset + offset, }; const result = logRepo.search(params); @@ -64,7 +70,7 @@ export function createLogsRouter(logRepo: LogRepository): Router { logs: result.logs, total: result.total, limit, - offset + offset, }); } catch (error) { console.error('Error searching logs:', error); @@ -72,6 +78,47 @@ export function createLogsRouter(logRepo: LogRepository): Router { } }); + router.get('/page', (req: Request, res: Response) => { + for (const key of ['q', 'level', 'source', 'startDate', 'endDate', 'cursor']) + if (req.query[key] !== undefined && typeof req.query[key] !== 'string') { + res.status(400).json({ error: 'Invalid filter' }); + return; + } + const q = req.query.q as string | undefined, + source = req.query.source as string | undefined; + if ((q && q.length > 10000) || (source && source.length > 500)) { + res.status(400).json({ error: 'Filter too long' }); + return; + } + const startDate = req.query.startDate as string | undefined, + endDate = req.query.endDate as string | undefined; + if ( + (startDate && !Number.isFinite(Date.parse(startDate))) || + (endDate && !Number.isFinite(Date.parse(endDate))) || + (startDate && endDate && Date.parse(startDate) > Date.parse(endDate)) + ) { + res.status(400).json({ error: 'Invalid time range' }); + return; + } + try { + res.json( + logRepo.searchPage({ + q, + source, + level: req.query.level ? String(req.query.level).toLowerCase() : undefined, + startDate, + endDate, + limit: parseQueryInt(req.query.limit as string, 50, 1, 1000), + cursor: req.query.cursor as string | undefined, + }), + ); + } catch (error) { + res + .status(error instanceof InvalidCursor ? 400 : 500) + .json({ error: error instanceof InvalidCursor ? error.message : 'Search failed' }); + } + }); + // GET /api/logs/stats - Get statistics (must be before /:id) router.get('/stats', (req: Request, res: Response) => { try { @@ -88,7 +135,7 @@ export function createLogsRouter(logRepo: LogRepository): Router { router.get('/:id', (req: Request, res: Response) => { try { const log = logRepo.getById(req.params.id); - + if (!log) { res.status(404).json({ error: 'Log not found' }); return; @@ -124,9 +171,9 @@ export function createLogsRouter(logRepo: LogRepository): Router { function validateTimestamp(timestamp: string | undefined): string | null { if (!timestamp) return null; - + const date = new Date(timestamp); if (isNaN(date.getTime())) return null; - + return date.toISOString(); -} \ No newline at end of file +} diff --git a/api/src/services/alertService.ts b/api/src/services/alertService.ts index 4da404a..e46cf9b 100644 --- a/api/src/services/alertService.ts +++ b/api/src/services/alertService.ts @@ -26,17 +26,17 @@ export class AlertService { constructor( alertRepo: AlertRepository, logRepo: LogRepository, - config: Partial = {} + config: Partial = {}, ) { this.alertRepo = alertRepo; this.logRepo = logRepo; this.config = { errorRate: { errorsPerMinute: 10, - windowMinutes: 5 + windowMinutes: 5, }, enabled: true, - ...config + ...config, }; this.lastCheck = new Date(); this.checkInterval = 60000; // Check every minute @@ -93,7 +93,7 @@ export class AlertService { } const windowStart = new Date( - Date.now() - this.config.errorRate.windowMinutes * 60 * 1000 + Date.now() - this.config.errorRate.windowMinutes * 60 * 1000, ).toISOString(); const errorCount = this.logRepo.getErrorCountSince(windowStart); @@ -102,7 +102,7 @@ export class AlertService { if (errorsPerMinute > this.config.errorRate.errorsPerMinute) { const since = new Date(Date.now() - 60 * 60 * 1000).toISOString(); const existingAlert = this.alertRepo.findRecentUnacknowledged('high', 'error_rate', since); - + if (!existingAlert) { this.createErrorRateAlert(errorCount, errorsPerMinute); } @@ -120,10 +120,10 @@ export class AlertService { errorsPerMinute, windowMinutes: this.config.errorRate.windowMinutes, threshold: this.config.errorRate.errorsPerMinute, - type: 'error_rate' + type: 'error_rate', }, createdAt: new Date().toISOString(), - acknowledged: false + acknowledged: false, }; this.alertRepo.create(alert); @@ -134,7 +134,7 @@ export class AlertService { const fullAlert: Alert = { ...alert, id: uuidv4(), - createdAt: new Date().toISOString() + createdAt: new Date().toISOString(), }; this.alertRepo.create(fullAlert); @@ -158,7 +158,7 @@ export class AlertService { return { isRunning: this.isRunning, lastCheck: this.lastCheck, - config: this.getConfig() + config: this.getConfig(), }; } -} \ No newline at end of file +} diff --git a/api/src/services/ingestionController.ts b/api/src/services/ingestionController.ts new file mode 100644 index 0000000..d2a1dd8 --- /dev/null +++ b/api/src/services/ingestionController.ts @@ -0,0 +1,37 @@ +import { Worker } from 'node:worker_threads'; +import path from 'node:path'; +export class IngestionController { + state: { isRunning: boolean; lastError: string | null; duplicates: number; rejected: number } = { + isRunning: true, + lastError: null, + duplicates: 0, + rejected: 0, + }; + private worker: Worker; + private exited = false; + constructor(dbPath: string, queueUrl: string) { + this.worker = new Worker(path.join(__dirname, 'ingestionWorker.js'), { + workerData: { dbPath, queueUrl }, + }); + this.worker.on('message', (state) => { + this.state = state; + }); + this.worker.on('error', (error) => { + this.state = { ...this.state, isRunning: false, lastError: error.message }; + }); + this.worker.on('exit', () => { + this.exited=true; + this.state = { ...this.state, isRunning: false }; + }); + } + async stop() { + if(this.exited)return; + this.worker.postMessage('stop'); + const timer = setTimeout(() => void this.worker.terminate(), 12000); + await new Promise((resolve) => { + this.worker.once('exit', () => resolve()); + if (!this.state.isRunning) void this.worker.terminate(); + }); + clearTimeout(timer); + } +} diff --git a/api/src/services/ingestionWorker.ts b/api/src/services/ingestionWorker.ts new file mode 100644 index 0000000..988490d --- /dev/null +++ b/api/src/services/ingestionWorker.ts @@ -0,0 +1,116 @@ +import { parentPort, workerData } from 'node:worker_threads'; +import { createHash } from 'node:crypto'; +import { setTimeout as delay } from 'node:timers/promises'; +import { openDatabase } from '../db/database'; +import { LogRepository } from '../db/logRepository'; +import { LogEntry } from '../types/log'; +import { normalizeLevel } from '../utils/logNormalizer'; +const db = openDatabase(workerData.dbPath), + repo = new LogRepository(db); +db.exec( + 'CREATE TABLE IF NOT EXISTS rejected_logs(id TEXT PRIMARY KEY,payload TEXT NOT NULL,reason TEXT NOT NULL,created_at TEXT NOT NULL)', +); +const rejected = db.prepare('INSERT OR IGNORE INTO rejected_logs VALUES(?,?,?,?)'); +let stopping = false, + duplicates = 0, + lastError: string | null = null; +parentPort!.on('message', (message) => { + if (message === 'stop') stopping = true; +}); +const status = () => + parentPort!.postMessage({ + isRunning: !stopping, + duplicates, + rejected: (db.prepare('SELECT COUNT(*) n FROM rejected_logs').get() as { n: number }).n, + lastError, + }); +async function request(route: string, body?: unknown) { + const res = await fetch(workerData.queueUrl + route, { + method: body ? 'POST' : 'GET', + headers: body ? { 'content-type': 'application/json' } : undefined, + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(10000), + }); + if (!res.ok) throw new Error('Queue HTTP ' + res.status); + return res.json(); +} +function normalize(value: unknown, fallback: string): LogEntry { + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw new Error('Expected a log object'); + const raw = value as Record; + if (typeof raw.message !== 'string' || raw.message.length > 10000) + throw new Error('Invalid message'); + if (raw.source != null && (typeof raw.source !== 'string' || raw.source.length > 500)) + throw new Error('Invalid source'); + if (raw.id != null && (typeof raw.id !== 'string' || !raw.id || raw.id.length > 256)) + throw new Error('Invalid event id'); + const date = + raw.timestamp == null + ? new Date() + : new Date(typeof raw.timestamp === 'number' ? raw.timestamp : String(raw.timestamp)); + if (!Number.isFinite(date.getTime())) throw new Error('Invalid timestamp'); + return { + id: typeof raw.id === 'string' ? raw.id : createHash('sha256').update(fallback).digest('hex'), + timestamp: date.toISOString(), + message: raw.message, + source: typeof raw.source === 'string' && raw.source ? raw.source : 'unknown', + level: normalizeLevel(raw.level as string), + raw: typeof raw.raw === 'string' ? raw.raw : JSON.stringify(raw), + }; +} +async function main() { + status(); + while (!stopping) { + try { + const page = (await request('/api/logs/pending')) as { + messages?: { id: string; lease: string; logs: unknown[] }[]; + }; + if (!Array.isArray(page.messages)) throw new Error('Invalid queue response'); + for (const batch of page.messages) { + if ( + typeof batch.id !== 'string' || + typeof batch.lease !== 'string' || + !Array.isArray(batch.logs) || + batch.logs.length > 4096 + ) + throw new Error('Invalid leased batch'); + const entries: LogEntry[] = []; + const poison: { id: string; payload: string; reason: string }[] = []; + batch.logs.forEach((value, index) => { + try { + entries.push(normalize(value, batch.id + ':' + index)); + } catch (error) { + poison.push({ + id: batch.id + ':' + index, + payload: JSON.stringify(value), + reason: String(error), + }); + } + }); + db.transaction(() => { + const added = repo.insertDeliveredBatch(entries); + duplicates += entries.length - added; + for (const row of poison) + rejected.run(row.id, row.payload, row.reason, new Date().toISOString()); + })(); + // Acknowledgment is fenced by a lease and follows the durable index commit. + await request('/api/logs/ack', { leases: [{ id: batch.id, lease: batch.lease }] }); + } + lastError = null; + status(); + if (page.messages.length === 0) await delay(20); + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + status(); + await delay(250); + } + } + db.close(); + parentPort!.close(); +} +void main().catch((error) => { + console.error(error); + process.exitCode = 1; + db.close(); + parentPort!.close(); +}); diff --git a/api/src/services/queueConsumer.ts b/api/src/services/queueConsumer.ts index cfdcd92..b6722aa 100644 --- a/api/src/services/queueConsumer.ts +++ b/api/src/services/queueConsumer.ts @@ -24,11 +24,7 @@ export class QueueConsumer { private failedMessageIds: Map = new Map(); // id -> timestamp private readonly MAX_FAILED_IDS = 10000; - constructor( - logRepo: LogRepository, - queueService: QueueService, - pollInterval: number = 5000 - ) { + constructor(logRepo: LogRepository, queueService: QueueService, pollInterval: number = 5000) { this.logRepo = logRepo; this.queueService = queueService; this.pollInterval = pollInterval; @@ -72,7 +68,7 @@ export class QueueConsumer { if (messages.length > 0) { console.log(`[QueueConsumer] Fetched ${messages.length} messages from queue`); const entries = this.parseMessages(messages); - + if (entries.length > 0) { try { const inserted = this.logRepo.insertBatch(entries); @@ -82,13 +78,16 @@ export class QueueConsumer { throw insertError; // Re-throw to trigger retry logic } } - + this.consecutiveErrors = 0; } } catch (error) { this.consecutiveErrors++; - console.error(`[QueueConsumer] Error processing batch (attempt ${this.consecutiveErrors}):`, error); - + console.error( + `[QueueConsumer] Error processing batch (attempt ${this.consecutiveErrors}):`, + error, + ); + if (this.consecutiveErrors >= this.maxConsecutiveErrors) { console.error('[QueueConsumer] Too many consecutive errors, stopping consumer'); this.pollLock = false; @@ -153,7 +152,7 @@ export class QueueConsumer { level: normalizeLevel(raw.level), source: raw.source || 'unknown', message: raw.message || JSON.stringify(raw), - raw: JSON.stringify(raw) + raw: JSON.stringify(raw), }; } @@ -178,7 +177,7 @@ export class QueueConsumer { getStatus(): { isRunning: boolean; consecutiveErrors: number } { return { isRunning: this.isRunning, - consecutiveErrors: this.consecutiveErrors + consecutiveErrors: this.consecutiveErrors, }; } -} \ No newline at end of file +} diff --git a/api/src/services/queueService.ts b/api/src/services/queueService.ts index f5c4e77..eb439f6 100644 --- a/api/src/services/queueService.ts +++ b/api/src/services/queueService.ts @@ -11,7 +11,7 @@ export class QueueService { baseUrl: string, timeout: number = 5000, maxRetries: number = 3, - maxResponseSize: number = 10 * 1024 * 1024 // 10MB default + maxResponseSize: number = 10 * 1024 * 1024, // 10MB default ) { this.baseUrl = baseUrl.replace(/\/$/, ''); this.timeout = timeout; @@ -21,19 +21,19 @@ export class QueueService { async fetchBatch(): Promise { const url = `${this.baseUrl}/api/logs/pending`; - + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { const response = await this.httpGet(url); const data = JSON.parse(response) as { messages?: Array<{ id: number; content: string }> }; - + if (!data.messages || !Array.isArray(data.messages)) { return []; } - return data.messages.map(msg => ({ + return data.messages.map((msg) => ({ id: msg.id, - content: msg.content + content: msg.content, })); } catch (error) { if (attempt === this.maxRetries) { @@ -45,20 +45,20 @@ export class QueueService { await this.delay(baseDelay + jitter); } } - + throw new Error('Max retries exceeded'); } async getStats(): Promise { const url = `${this.baseUrl}/api/queue/stats`; - + try { const response = await this.httpGet(url); const data = JSON.parse(response) as { pending?: number; total?: number }; - + return { pending: data.pending || 0, - total: data.total || 0 + total: data.total || 0, }; } catch (error) { return { pending: 0, total: 0 }; @@ -69,30 +69,38 @@ export class QueueService { return new Promise((resolve, reject) => { let settled = false; const safeResolve = (value: string): void => { - if (!settled) { settled = true; resolve(value); } + if (!settled) { + settled = true; + resolve(value); + } }; const safeReject = (error: Error): void => { - if (!settled) { settled = true; reject(error); } + if (!settled) { + settled = true; + reject(error); + } }; const request = http.get(url, { timeout: this.timeout }, (response) => { let data = ''; let receivedSize = 0; - + response.on('data', (chunk: Buffer) => { receivedSize += chunk.length; if (receivedSize > this.maxResponseSize) { request.destroy(); - safeReject(new Error(`Response exceeded maximum size of ${this.maxResponseSize} bytes`)); + safeReject( + new Error(`Response exceeded maximum size of ${this.maxResponseSize} bytes`), + ); return; } data += chunk; }); - + response.on('error', (error) => { safeReject(error); }); - + response.on('end', () => { if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) { safeResolve(data); @@ -101,11 +109,11 @@ export class QueueService { } }); }); - + request.on('error', (error) => { safeReject(error); }); - + request.on('timeout', () => { request.destroy(); safeReject(new Error('Request timeout')); @@ -114,6 +122,6 @@ export class QueueService { } private delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } -} \ No newline at end of file +} diff --git a/api/src/types/log.ts b/api/src/types/log.ts index c51b796..edf6cac 100644 --- a/api/src/types/log.ts +++ b/api/src/types/log.ts @@ -34,6 +34,8 @@ export interface Alert { } export interface AlertFilter { + q?: string; + source?: string; severity?: string; acknowledged?: boolean; limit?: number; @@ -44,6 +46,7 @@ export interface AlertStats { total: number; bySeverity: Record; unacknowledged: number; + openBySeverity: Record; } export interface QueueMessage { diff --git a/api/src/utils/logNormalizer.ts b/api/src/utils/logNormalizer.ts index fd5bab3..269332d 100644 --- a/api/src/utils/logNormalizer.ts +++ b/api/src/utils/logNormalizer.ts @@ -4,11 +4,11 @@ export function normalizeLevel(level: string | undefined): LogEntry['level'] { if (!level) return 'info'; const normalized = level.toLowerCase(); - + if (['debug', 'dbg'].includes(normalized)) return 'debug'; if (['info', 'information', 'log'].includes(normalized)) return 'info'; if (['warn', 'warning'].includes(normalized)) return 'warn'; if (['error', 'err', 'fatal', 'critical', 'severe'].includes(normalized)) return 'error'; - + return 'info'; } diff --git a/api/src/utils/queryHelpers.ts b/api/src/utils/queryHelpers.ts index 78b08d7..32861d9 100644 --- a/api/src/utils/queryHelpers.ts +++ b/api/src/utils/queryHelpers.ts @@ -1,9 +1,14 @@ -export function parseQueryInt(value: string | undefined, defaultValue: number, min: number, max?: number): number { +export function parseQueryInt( + value: string | undefined, + defaultValue: number, + min: number, + max?: number, +): number { if (!value) return defaultValue; - + const parsed = parseInt(value, 10); if (isNaN(parsed)) return defaultValue; - + if (max !== undefined) { return Math.max(min, Math.min(parsed, max)); } diff --git a/api/tests/unit/db/logPage.test.ts b/api/tests/unit/db/logPage.test.ts new file mode 100644 index 0000000..d8270ab --- /dev/null +++ b/api/tests/unit/db/logPage.test.ts @@ -0,0 +1,11 @@ +import {createTestDb,createSampleLog} from '../../helpers/testDb'; +import {LogRepository} from '../../../src/db/logRepository'; +import {InvalidCursor} from '../../../src/db/logPageReader'; +describe('cursor pages and idempotent delivery',()=>{ + let resource:ReturnType,repo:LogRepository; + beforeEach(()=>{resource=createTestDb();repo=new LogRepository(resource.db);});afterEach(()=>resource.cleanup()); + it('does not lose timestamp ties or repeat events when new logs arrive between pages',()=>{const timestamp='2026-11-04T10:00:00.000Z';repo.insertBatch(Array.from({length:60},(_,i)=>createSampleLog({id:String(i),timestamp})));const first=repo.searchPage({limit:25});repo.insert(createSampleLog({id:'new',timestamp:'2026-11-04T11:00:00Z'}));const second=repo.searchPage({limit:25,cursor:first.nextCursor!}),third=repo.searchPage({limit:25,cursor:second.nextCursor!});const ids=[...first.logs,...second.logs,...third.logs].map(log=>log.id);expect(ids).toHaveLength(60);expect(new Set(ids).size).toBe(60);expect(ids).not.toContain('new');expect(third.hasMore).toBe(false);}); + it('binds cursors to the actual filters and rejects malformed positions',()=>{repo.insertBatch(Array.from({length:3},()=>createSampleLog({source:'auth'})));const page=repo.searchPage({source:'auth',limit:1});expect(()=>repo.searchPage({source:'payments',cursor:page.nextCursor!})).toThrow(InvalidCursor);expect(()=>repo.searchPage({cursor:'not-json'})).toThrow(InvalidCursor);}); + it('uses the global FTS fallback for rare, older phrases and retains literal phrase semantics',()=>{repo.insert(createSampleLog({id:'rare',message:'connection timeout',timestamp:'2020-01-01T00:00:00Z'}));repo.insertBatch(Array.from({length:4200},(_,i)=>createSampleLog({id:'normal-'+i,message:'request completed'})));expect(repo.searchPage({q:'connection timeout'}).logs.map(log=>log.id)).toEqual(['rare']);expect(repo.searchPage({q:'" OR "'}).logs).toEqual([]);}); + it('commits a replayed delivery only once without updating the original row or FTS content',()=>{const log=createSampleLog({id:'same',message:'original timeout'});resource.db.transaction(()=>repo.insertDeliveredBatch([log,log]))();resource.db.transaction(()=>repo.insertDeliveredBatch([{...log,message:'modified replay'}]))();expect(repo.count()).toBe(1);expect(repo.getById('same')?.message).toBe('original timeout');expect(repo.searchPage({q:'original timeout'}).logs).toHaveLength(1);expect(repo.searchPage({q:'modified replay'}).logs).toHaveLength(0);}); +}); diff --git a/api/tsconfig.json b/api/tsconfig.json index f44baa9..d3a1624 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -12,8 +12,14 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "isolatedModules": true }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] } diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..13f6fe9 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,8 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +const compat = new FlatCompat({ baseDirectory: path.dirname(fileURLToPath(import.meta.url)) }); +export default [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + { ignores: ['.next/**', 'node_modules/**'] }, +]; diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 94647ad..55699cc 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,15 @@ import type { NextConfig } from 'next'; - -const nextConfig: NextConfig = { +const api = process.env.API_INTERNAL_URL || 'http://127.0.0.1:3000', + detector = process.env.ANOMALY_INTERNAL_URL || 'http://127.0.0.1:5000'; +const config: NextConfig = { output: 'standalone', + devIndicators: false, + async rewrites() { + return [ + { source: '/api/:path*', destination: api + '/api/:path*' }, + { source: '/pipeline-metrics', destination: api + '/metrics' }, + { source: '/detector-status', destination: detector + '/stats' }, + ]; + }, }; - -export default nextConfig; +export default config; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 99c782f..8b72e29 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,19 +8,187 @@ "name": "log-aggregation-frontend", "version": "1.0.0", "dependencies": { - "d3": "^7.9.0", "next": "^15.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@types/d3": "^7.4.3", + "@testing-library/react": "^16.3.3", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "eslint": "^9.0.0", "eslint-config-next": "^15.0.0", - "typescript": "^5.5.0" + "jsdom": "^26.1.0", + "prettier": "^3.9.10", + "typescript": "^5.5.0", + "vitest": "^3.2.7" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz", + "integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@emnapi/core": { @@ -30,30 +198,472 @@ "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -718,6 +1328,13 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -923,335 +1540,494 @@ "node": ">=12.4.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.5.tgz", + "integrity": "sha512-FdWPeulkH+FH+rBzyKBD59tuVN4tRKvmVilSJm0OcnoYsQTpd9UhD78OaPcsYqo2fX6hCwAG0YIjrDLoQ3pR3g==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", - "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.5.tgz", + "integrity": "sha512-bmkDuBGnF+Ji8atWlXcNjxIC52ShCbdXPbW7Nz0lRoCFi1lrvsO0XobJY36kTj7duc1vOmGJeQ9vtVuGjnzihA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } + "node_modules/@rollup/rollup-android-x64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.5.tgz", + "integrity": "sha512-6KvVfnqPTqmbQ5YobMl9HYiSymAsCt8HlNeriDshwJQcveXwT+vIMW6wGL1RcJbXH9+r3BAJvjab5+rIgtSEqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.5.tgz", + "integrity": "sha512-SNamgdrY69oPdBrQV9fKGo6PSmKcVhyn0D0jnDmcqgRJvrxQGGBhtekOHTUJ7TCgkW0nE8XMH9v2CwXmjNIzVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.5.tgz", + "integrity": "sha512-HNdl34thiJtHDSglogjmAlQ296L8cc8zA7ODH65nnmY7E+jPP55WEbh8J5583rWFLMjxyYywxZuwPOwRvEi5tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.5.tgz", + "integrity": "sha512-WvZYtANNswfHzSNHaJAoG6Ipfc4SyZIDYuIjFdHv07xXxhHl+Xx8o+bNDWsxpOADFT2mKXcXVrfOG4OlWTLo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.5.tgz", + "integrity": "sha512-EUU+Wgf9NyHekceC+DxAdm42f6bq2ezH/7DEb/JnCb8SAUhG2xph5BiVukxplWal2m5pP8CzJZE+FLwRopywjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.5.tgz", + "integrity": "sha512-dgU1CvUuE2IkaJBAeuAbxpDdMHAdZBT5oth5u22DtuoMg4gXXYr2+Sm30RigPJG1F2VEoiGJUziGLuIh9+jPxA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.5.tgz", + "integrity": "sha512-rdgJcJig2VngYlcTCRQw2BA8kLWM6QwDFe9uLFo81lya0jPC1r8wZu92sAvTUIv48GWlkRVi6/fpsa5CoFSKoQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.5.tgz", + "integrity": "sha512-+jtCiQnrEFHWAUAcUpP7wppb1p6HKOgBP3mXx75cEYpDF2Y5MEqBky2HNz+osFuVx5uX44gnMTmbe5RJauzn6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.5.tgz", + "integrity": "sha512-heflX7ENhL8eHmLcUxDEHCSK8CKLyPprysb2JGoSTh5Ip7nHrzIRc+2USyxul3nxi98H8pGxJIEDEpURlS1X5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.5.tgz", + "integrity": "sha512-rRtNU0zBfRHkJ8pscHN9qRtUYLq8Rao4EX7aVWvKcFmJUANkCCXFRU7ovNBEMhpEONyUEB/3DRVDXwDDguSfHA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.5.tgz", + "integrity": "sha512-/6auyi9yj8MD/Q+8P+f2ay57jQ7a0j7Yp1JK86pecH6ZnRViQZ5GOjoRSjGArFeTSHFznRbM2QPQcBgDPyNMfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.5.tgz", + "integrity": "sha512-Hsc0vlt+vPvqzUkHKffWKf3YE/yexrY+yeFf7IsUvDC6PV7YGCsA/gJvPa+WJdgxGJnwuoO1HmLkbahaLKabYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.5.tgz", + "integrity": "sha512-Jx5TMMSPJwHY7F/JwPdS6nuBwny2ECRK+EsAJV5HZrfiddeSS6ZrohkLkFZm3rjqBUuQ4FPX5gGMtakUuQO6fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.5.tgz", + "integrity": "sha512-fGWcq2lo/XCqAkBLOokg66D/z+MUouryB8i+QdU9RR6O5knG7rucc0WFPqWYL1Wk/wuiJouDfmyBxmZKRM9bag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.5.tgz", + "integrity": "sha512-JwO/Ys3jybc0UW3V5XO2CMnp0RhvoczDDQTgAEE1IctT+HRY3W5parIIWLUpmAK4gHSeeYJofm2iQPFA62K6ag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.5.tgz", + "integrity": "sha512-vVD4iqel6J6A1xAJlb4mxEO9i1wOJ4/SkcKoDKAuK/wqEBPOA7/P5dDyZvNx46ocHwOUwzo3WjhTdKqET9kEAg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.5.tgz", + "integrity": "sha512-HmEfNh5Cdd/NHxgqwMzKnjlvicQQA+D+w+vQXDJSV0x/aHiQSjVwh1Th5RoA7QsMieIaxibt/5iJq68BraTSUg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "dev": true, - "license": "MIT" + "os": [ + "linux" + ] }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.5.tgz", + "integrity": "sha512-NfmALpbe009nYzZ2QqlSW6nzbGddXpqk3nwualrIr+4QflQCTGMzt3QKK20yARiAnSV9fJdM5WNwIYUf31x3uA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.5.tgz", + "integrity": "sha512-ngcE7Udpy/iJ3lexVhYcbpUV2OJIakRGus3snc6K52BnAAJetDBG1Ku5Eh3CECtnW3mpBYWg5VCD4tTaOXKOVQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.5.tgz", + "integrity": "sha512-DZ01A2EdTYX8xVeGoGiWkJBRWDh+/BOflwGPVsBay43PDxcq8ez0xuovZnPnk1xjE3CdI0pdZXxsJp/Xjmf7uQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.5.tgz", + "integrity": "sha512-nJKiMKDO140DUCEcQZl+f4HiGPMBxrW2webjuWJ2E3qPeoruHf2zByml4TP52lA1zDi6L4I92D/5stxeA/6pig==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.5.tgz", + "integrity": "sha512-ykLkBCf+Kls24QmqM3IRf1T4sGMh0rXPyymtj+hXlWKPJnXbXraHqUUsKtOIxqLasjT1i7DhfQGAaZYeiHbSOw==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.5.tgz", + "integrity": "sha512-NiFPUFw8cHhkmBlL1k1eaz+5AXXm89rztj+DQdDuQ7VGPM0+ADFhezQ2KcBMF50UEFzGUr0+DVjVND743XwKWQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.5.tgz", + "integrity": "sha512-Khj/vrKCpdbiAWYbJAeSy4D5iep+qwW/86lNoKvdleJYGxgbRE7h87+5d5T9D7gt8NcqhNFpreRgIg8T4bLzKQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "dev": true, "license": "MIT" }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "dev": true, - "license": "MIT", + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", "dependencies": { - "@types/d3-dsv": "*" + "tslib": "^2.8.0" } }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/geojson": "*" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@types/d3-color": "*" + "dequal": "^2.0.3" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", "dev": true, "license": "MIT", "dependencies": { - "@types/d3-time": "*" + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@types/d3-path": "*" + "tslib": "^2.4.0" } }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } + "peer": true }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "node_modules/@types/estree": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "dev": true, "license": "MIT" }, @@ -1791,64 +2567,179 @@ "linux" ] }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, - "engines": { - "node": ">=14.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/acorn": { "version": "8.16.0", @@ -1873,6 +2764,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1890,6 +2791,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1991,193 +2903,13 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2186,29 +2918,36 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2217,518 +2956,342 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001776", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", - "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/color-convert": { + "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "engines": { - "node": ">= 8" + "node": ">= 0.4" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "license": "MIT" - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", + "license": "MIT", "dependencies": { - "d3-array": "^3.2.0" + "possible-typed-array-names": "^1.0.0" }, "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" + "node": ">= 0.4" }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-array": "2.5.0 - 3" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-color": "1 - 3" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/d3-path": { + "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } + "node_modules/caniuse-lite": { + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 16" } }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-array": "2 - 3" + "color-name": "~1.1.4" }, "engines": { - "node": ">=12" + "node": ">=7.0.0" } }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" + "node": ">= 8" } }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2736,6 +3299,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2808,6 +3385,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2851,13 +3445,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" } }, "node_modules/detect-libc": { @@ -2883,6 +3479,14 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2903,7 +3507,20 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/es-abstract": { "version": "1.24.1", @@ -3022,6 +3639,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3082,6 +3706,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3516,6 +4182,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3526,6 +4202,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3667,6 +4353,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3938,10 +4639,52 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -4002,15 +4745,6 @@ "node": ">= 0.4" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4268,6 +5002,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4465,6 +5206,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4595,6 +5376,41 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4660,9 +5476,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -4781,6 +5597,13 @@ "semver": "bin/semver.js" } }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4985,6 +5808,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5012,6 +5848,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5079,6 +5932,60 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5246,11 +6153,58 @@ "node": ">=0.10.0" } }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" + "node_modules/rollup": { + "version": "4.67.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.67.5.tgz", + "integrity": "sha512-3Ve5VH1hacCVutdpbmu/htg4kz+JCOIWgy4Q3hjHPYGHeHeIElNHJ0LcJ3rCfIsd5G7qOl5k5mwpzGMj9kdHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.10" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.67.5", + "@rollup/rollup-android-arm64": "4.67.5", + "@rollup/rollup-android-x64": "4.67.5", + "@rollup/rollup-darwin-arm64": "4.67.5", + "@rollup/rollup-darwin-x64": "4.67.5", + "@rollup/rollup-freebsd-arm64": "4.67.5", + "@rollup/rollup-freebsd-x64": "4.67.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.67.5", + "@rollup/rollup-linux-arm-musleabihf": "4.67.5", + "@rollup/rollup-linux-arm64-gnu": "4.67.5", + "@rollup/rollup-linux-arm64-musl": "4.67.5", + "@rollup/rollup-linux-loong64-gnu": "4.67.5", + "@rollup/rollup-linux-loong64-musl": "4.67.5", + "@rollup/rollup-linux-ppc64-gnu": "4.67.5", + "@rollup/rollup-linux-ppc64-musl": "4.67.5", + "@rollup/rollup-linux-riscv64-gnu": "4.67.5", + "@rollup/rollup-linux-riscv64-musl": "4.67.5", + "@rollup/rollup-linux-s390x-gnu": "4.67.5", + "@rollup/rollup-linux-x64-gnu": "4.67.5", + "@rollup/rollup-linux-x64-musl": "4.67.5", + "@rollup/rollup-openbsd-x64": "4.67.5", + "@rollup/rollup-openharmony-arm64": "4.67.5", + "@rollup/rollup-win32-arm64-msvc": "4.67.5", + "@rollup/rollup-win32-ia32-msvc": "4.67.5", + "@rollup/rollup-win32-x64-gnu": "4.67.5", + "@rollup/rollup-win32-x64-msvc": "4.67.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", @@ -5276,12 +6230,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -5341,8 +6289,22 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5555,10 +6517,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5571,6 +6540,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5721,6 +6704,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -5770,6 +6773,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5787,37 +6811,87 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=14.0.0" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "tldts-core": "^6.1.86" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "bin": { + "tldts": "bin/cli.js" } }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5831,6 +6905,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -6039,6 +7139,311 @@ "punycode": "^2.1.0" } }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6144,6 +7549,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6154,6 +7576,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.22.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.1.tgz", + "integrity": "sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d46b767..bfda951 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,21 +6,24 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "eslint src", + "test": "vitest run" }, "dependencies": { - "d3": "^7.9.0", "next": "^15.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@types/d3": "^7.4.3", + "@testing-library/react": "^16.3.3", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "eslint": "^9.0.0", "eslint-config-next": "^15.0.0", - "typescript": "^5.5.0" + "jsdom": "^26.1.0", + "prettier": "^3.9.10", + "typescript": "^5.5.0", + "vitest": "^3.2.7" } } diff --git a/frontend/src/app/alerts/page.tsx b/frontend/src/app/alerts/page.tsx new file mode 100644 index 0000000..de17521 --- /dev/null +++ b/frontend/src/app/alerts/page.tsx @@ -0,0 +1,2 @@ +import AlertsPage from '../../components/AlertsPage'; +export default AlertsPage; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index de11f62..a6947fc 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,527 +1,1920 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - :root { - --dd-bg: #0a0f1a; - --dd-bg-2: #101726; - --dd-panel: rgba(18, 25, 41, 0.92); - --dd-panel-soft: rgba(25, 34, 56, 0.85); - --dd-border: rgba(148, 163, 184, 0.14); - --dd-text: #ebf2ff; - --dd-muted: #94a8ca; - --dd-cyan: #66e3ff; - --dd-indigo: #7895ff; - --dd-amber: #f7bf5f; - --dd-rose: #ff6e86; - --dd-shadow: 0 30px 80px rgba(0, 0, 0, 0.34); -} - + color-scheme: dark; + --bg: #0b100f; + --panel: #101716; + --line: #293630; + --line-soft: #1d2924; + --text: #dce4e0; + --muted: #8b9d93; + --lime: #b9ed7c; + --amber: #e8bd66; + --red: #ff796b; + --blue: #8cb5ec; + --mono: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; +} * { box-sizing: border-box; } - html, body { + margin: 0; min-height: 100%; + background: var(--bg); } - body { - margin: 0; - color: var(--dd-text); - background: - radial-gradient(circle at 15% 10%, rgba(120, 149, 255, 0.18), transparent 18%), - radial-gradient(circle at 85% 12%, rgba(102, 227, 255, 0.1), transparent 20%), - linear-gradient(180deg, #080d16 0%, #0a101a 40%, #0b111d 100%); - font-family: "Segoe UI", "Inter", sans-serif; -} - + color: var(--text); + font: + 13px/1.5 'Segoe UI', + Inter, + Arial, + sans-serif; + background: radial-gradient(ellipse at 12% 0%, #12201940, transparent 50%), var(--bg); +} +button, +input, +select { + font: inherit; + color: inherit; +} +button, +a { + touch-action: manipulation; +} +button { + cursor: pointer; + border: 0; + background: transparent; +} +button:disabled { + cursor: default; + opacity: 0.4; +} a { color: inherit; text-decoration: none; } - -.app-shell { - min-height: 100vh; - padding: 24px; +button:focus-visible, +a:focus-visible, +select:focus-visible, +input:focus-visible, +summary:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid var(--lime); + outline-offset: 3px; +} +h1, +h2, +h3, +p { + margin: 0; } - -.dd-shell { - max-width: 1480px; - margin: 0 auto; - display: grid; - grid-template-columns: 250px 1fr; - gap: 22px; -} - -.dd-nav, -.dd-topbar, -.dd-stat-card, -.dd-panel { - border: 1px solid var(--dd-border); - background: linear-gradient(180deg, rgba(17, 23, 37, 0.96), rgba(12, 17, 28, 0.94)); - box-shadow: var(--dd-shadow); -} - -.dd-nav { - border-radius: 28px; - padding: 22px 18px; +h2 { + font-size: 14px; + font-weight: 600; +} +h3 { + font-size: 13px; + font-weight: 500; +} +svg { + flex-shrink: 0; +} +strong { + font-weight: 600; +} +select { + background: #111917; + border: 1px solid var(--line); + border-radius: 5px; + padding: 8px 28px 8px 12px; + min-height: 36px; + max-width: 230px; +} +input { + background: transparent; + border: 1px solid var(--line); + border-radius: 5px; + padding: 8px 10px; + min-width: 0; +} +input::placeholder { + color: #82968b; +} +table { + width: 100%; + border-collapse: collapse; + text-align: left; +} +th { + color: #9facaa; + font: 10px var(--mono); + text-transform: uppercase; + letter-spacing: 0.6px; + white-space: nowrap; +} +td, +th { + padding: 9px 16px; + border-bottom: 1px solid var(--line-soft); +} +td { + font-size: 12px; +} +thead { + background: #111a17; +} +tbody tr:last-child td { + border-bottom: 0; +} +.muted { + color: var(--muted); +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} +.sr-only:focus { + position: static; + width: auto; + height: auto; + clip-path: none; +} +.skip-link { + position: fixed; + top: -80px; + left: 16px; + background: var(--lime); + color: #14200e; + padding: 12px; + z-index: 100; +} +.skip-link:focus { + top: 10px; +} +.workspace { display: grid; - grid-template-rows: auto auto 1fr; - gap: 24px; + grid-template-columns: 208px minmax(0, 1fr); + min-height: 100svh; +} +.sidebar { position: sticky; - top: 24px; - height: fit-content; + top: 0; + height: 100svh; + display: flex; + flex-direction: column; + border-right: 1px solid var(--line); + padding: 58px 12px 16px; + background: #0c1210bb; } - -.dd-brand { +.primary-nav { + display: grid; + gap: 4px; +} +.nav-item { + height: 43px; display: flex; align-items: center; - gap: 12px; + gap: 13px; + border-radius: 5px; + padding: 0 15px; + color: #b9c7bf; + position: relative; +} +.nav-item:hover, +.source-link:hover { + background: #14201b; +} +.nav-item.active { + background: #18231f; + color: var(--lime); +} +.nav-item.active:before { + content: ''; + position: absolute; + left: 0; + top: 5px; + bottom: 5px; + width: 3px; + background: var(--lime); + border-radius: 4px; +} +.nav-count { + margin-left: auto; + background: var(--amber); + color: #211a0d; + border-radius: 20px; + padding: 0 6px; + min-width: 20px; + text-align: center; + font: 11px/20px var(--mono); +} +.sources-heading { + margin: 28px 6px 12px; + padding-top: 20px; + border-top: 1px solid var(--line); + text-transform: uppercase; + letter-spacing: 1.3px; + color: #a7b9ad; + font-size: 10px; + display: flex; + justify-content: space-between; +} +.sources-heading span { + color: var(--muted); + letter-spacing: 0; } - -.dd-brand-mark { - width: 44px; - height: 44px; - border-radius: 14px; +.source-nav { + display: flex; + flex-direction: column; + gap: 3px; + overflow: auto; + min-height: 0; +} +.source-link { display: flex; align-items: center; - justify-content: center; - background: linear-gradient(135deg, var(--dd-indigo), var(--dd-cyan)); - color: #07101d; - font-weight: 800; -} - -.dd-brand strong, -.dd-nav-card strong, -.dd-stat-card strong, -.dd-node strong, -.dd-chart-meta strong, -.dd-hotspot-row strong, -.dd-topbar h1 { - display: block; + gap: 12px; + min-height: 37px; + padding: 7px 14px; + border-radius: 4px; } - -.dd-brand span, -.dd-topbar p, -.dd-nav-card small, -.dd-panel-header p, -.dd-stat-card small, -.dd-node small, -.dd-chart-meta span, -.dd-hotspot-row small, -.dd-alert-topline small, -.dd-empty, -.dd-stream-time, -.dd-kicker { - color: var(--dd-muted); -} - -.dd-nav-list { - display: grid; - gap: 8px; +.source-link span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - -.dd-nav-list a { - padding: 12px 14px; - border-radius: 14px; - color: #d7e4ff; - background: transparent; - transition: background-color 0.2s ease, transform 0.2s ease; -} - -.dd-nav-list a:hover, -.dd-nav-list .is-active { - background: rgba(120, 149, 255, 0.14); - transform: translateX(2px); -} - -.dd-nav-card { - align-self: flex-end; - border: 1px solid rgba(120, 149, 255, 0.18); - border-radius: 18px; - padding: 16px; - background: linear-gradient(180deg, rgba(23, 33, 56, 0.95), rgba(14, 19, 33, 0.9)); -} - -.dd-nav-card span, -.dd-stat-card span, -.dd-badge, -.dd-pill, -.dd-stream-header-row, -.dd-stream-level, -.dd-severity { - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 11px; - font-weight: 700; +.source-link svg { + color: #a3b5aa; } - -.dd-main { - display: grid; - gap: 18px; +.environment { + margin-top: auto; + display: flex; + align-items: center; + gap: 9px; + padding: 12px 10px 2px; + color: #9aafa1; + font-size: 10px; +} +.environment > .status-dot { + margin-left: auto; } - -.dd-topbar { - border-radius: 28px; - padding: 26px 28px; +.main-workspace { + min-width: 0; +} +.topbar { + position: sticky; + top: 0; + z-index: 20; + min-height: 66px; + padding: 10px 22px; + border-bottom: 1px solid var(--line); + background: #0c1411ed; + backdrop-filter: blur(12px); display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; gap: 20px; } - -.dd-kicker { - display: inline-block; - margin-bottom: 10px; - text-transform: uppercase; - letter-spacing: 0.14em; - font-size: 12px; -} - -.dd-topbar h1 { - margin: 0 0 8px; - font-size: clamp(2rem, 4vw, 3.1rem); - line-height: 1; +.topbar-metrics { + display: flex; + align-items: center; + gap: 28px; } - -.dd-topbar-actions { +.inline-metric { display: flex; + align-items: center; gap: 12px; - flex-wrap: wrap; } - -.dd-pill { - border-radius: 999px; - padding: 10px 14px; - border: 1px solid rgba(148, 163, 184, 0.16); - background: rgba(15, 23, 42, 0.75); -} - -.dd-pill.is-accent, -.dd-badge { - background: linear-gradient(135deg, rgba(120, 149, 255, 0.22), rgba(102, 227, 255, 0.18)); - color: #dff7ff; -} - -.dd-summary-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; +.inline-metric + .inline-metric { + padding-left: 28px; + border-left: 1px solid var(--line); } - -.dd-stat-card { - border-radius: 20px; - padding: 18px 20px; - display: grid; +.inline-metric strong { + font-size: 19px; + display: block; + line-height: 1.25; +} +.inline-metric span { + font-size: 10px; + color: var(--muted); + display: block; + margin-top: 3px; +} +.topbar-controls { + display: flex; + align-items: center; gap: 10px; } - -.dd-stat-card strong { - font-size: 1.8rem; +.mode-pill { + display: flex; + gap: 7px; + align-items: center; + color: var(--lime); + font-size: 11px; + padding: 6px 9px; + border: 1px solid #344a2c; + border-radius: 4px; + white-space: nowrap; +} +.page-content { + padding: 18px 22px 22px; } - -.dd-grid { - display: grid; - grid-template-columns: 1.35fr 0.95fr 0.95fr; - gap: 18px; +.status-dot { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + background: #6c7f70; + flex: none; +} +.status-dot.positive { + background: var(--lime); +} +.status-dot.warning { + background: var(--amber); +} +.status-dot.danger { + background: var(--red); +} +.button { + display: inline-flex; + justify-content: center; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 8px 12px; + border: 1px solid var(--line); + border-radius: 5px; + background: #111917; + font-size: 12px; + white-space: nowrap; +} +.button:hover:not(:disabled) { + background: #1a281d; + border-color: #4c644e; +} +.button.primary { + border-color: #718e45; + background: #24341c; + color: var(--lime); +} +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 26px; + min-height: 28px; + color: #a2b3aa; + border-radius: 4px; +} +.icon-button:hover { + background: #233027; +} +.previous svg { + transform: rotate(180deg); } - -.dd-grid-bottom { - grid-template-columns: 0.9fr 1.4fr; +.icon-actions { + display: flex; + gap: 2px; } - -.dd-panel { - border-radius: 24px; - padding: 20px; +.text-button { + color: #afca99; + font-size: 11px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0; } - -.dd-panel-large { - grid-column: span 2; +.text-button:hover { + color: var(--lime); } - -.dd-panel-wide { +.panel { + border: 1px solid var(--line); + background: linear-gradient(130deg, #121b18b3, #0e1513b3); + border-radius: 5px; min-width: 0; } - -.dd-panel-header { +.panel-header { display: flex; - align-items: flex-start; justify-content: space-between; - gap: 16px; - margin-bottom: 18px; + align-items: center; + gap: 12px; + padding: 12px 15px; + border-bottom: 1px solid var(--line-soft); } - -.dd-panel-header-stack { - flex-direction: column; +.panel-header h2, +.panel-header h3 { + font-size: 13px; } - -.dd-panel-header h2 { - margin: 0 0 6px; - font-size: 1.1rem; +.panel-header > .muted { + font-size: 10px; } - -.dd-topology { - display: grid; - grid-template-columns: repeat(7, minmax(0, 1fr)); - gap: 10px; +.table-scroll { + overflow: auto; + min-width: 0; +} +.quiet-state { + padding: 15px; + color: var(--muted); + font-size: 12px; +} +.empty-state { + display: flex; + flex-direction: column; align-items: center; - margin-bottom: 20px; + justify-content: center; + gap: 10px; + padding: 48px 20px; + text-align: center; + color: var(--muted); + min-height: 200px; } - -.dd-node { - border: 1px solid rgba(148, 163, 184, 0.14); - border-radius: 18px; - padding: 14px; - background: rgba(16, 23, 40, 0.75); - min-height: 96px; +.empty-state strong { + color: var(--text); + font-size: 14px; } - -.dd-node.is-primary { - background: linear-gradient(180deg, rgba(41, 65, 118, 0.45), rgba(17, 26, 44, 0.85)); - border-color: rgba(120, 149, 255, 0.32); +.empty-state span { + max-width: 340px; + font-size: 12px; } - -.dd-link { - height: 2px; - background: linear-gradient(90deg, rgba(120, 149, 255, 0.15), rgba(102, 227, 255, 0.85)); +.empty-state svg { + color: #8ca477; } - -.dd-mini-charts { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); +.inline-error, +.connection-banner { + display: flex; + align-items: center; + justify-content: space-between; gap: 14px; + border: 1px solid #674537; + background: #2c211c; + color: #f1bba6; + border-radius: 4px; + padding: 10px 13px; + font-size: 12px; + margin: 10px 0; } - -.dd-chart-card, -.dd-query-bar, -.dd-alert-card, -.dd-stream-row, -.dd-hotspot-row { - border: 1px solid rgba(148, 163, 184, 0.12); - background: rgba(15, 23, 42, 0.68); -} - -.dd-chart-card { - border-radius: 18px; - padding: 14px; +.connection-banner { + margin: 0; + border-radius: 0; } - -.dd-chart-meta { +.results-footer { display: flex; - align-items: flex-end; + align-items: center; justify-content: space-between; - margin-bottom: 10px; + gap: 14px; + margin-top: 12px; + color: #a2b5a8; + font-size: 11px; } - -.dd-chart-card svg { - width: 100%; - height: 64px; +.results-footer small { + display: flex; + align-items: center; + gap: 7px; + font-size: 10px; + margin-top: 4px; } - -.dd-line { - fill: none; - stroke-width: 3; - stroke-linecap: round; - stroke-linejoin: round; +.footer-actions { + display: flex; + align-items: center; + gap: 10px; } - -.dd-line-indigo { - stroke: var(--dd-indigo); +.footer-actions .button, +.footer-actions select { + font-size: 11px; + min-height: 32px; + padding-top: 6px; + padding-bottom: 6px; } - -.dd-line-cyan { - stroke: var(--dd-cyan); +.load-older { + min-width: 180px; } - -.dd-bars { - height: 260px; - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 14px; - align-items: flex-end; +.segmented { + display: flex; + align-items: center; + border: 1px solid var(--line); + border-radius: 5px; + background: #101713; + padding: 2px; +} +.segmented button { + padding: 7px 12px; + color: #a5b5a8; + font-size: 11px; + white-space: nowrap; + border: 1px solid transparent; + border-radius: 3px; + min-height: 30px; } - -.dd-bar-column { - display: grid; - gap: 8px; - justify-items: center; +.segmented button.selected { + border-color: #718e45; + background: #24341c; + color: var(--lime); +} +.segmented button:hover:not(.selected) { + color: var(--text); + background: #19261d; } - -.dd-bar-track { +.search-input { + display: flex; + align-items: center; + gap: 10px; + background: #121c18; + border: 1px solid #3a4b40; + border-radius: 5px; + min-width: 120px; + flex: 1; + padding: 0 12px; +} +.search-input > svg { + color: #a6b8aa; +} +.search-input input { + border: 0; width: 100%; - height: 190px; - border-radius: 18px; - background: linear-gradient(180deg, rgba(148, 163, 184, 0.06), rgba(148, 163, 184, 0.14)); + padding: 9px 0; + outline: none; + background: transparent; + font-size: 13px; + min-height: 36px; +} +.search-input:focus-within { + border-color: var(--lime); + box-shadow: 0 0 0 1px #b9ed7c33; +} +.section-tools { display: flex; - align-items: flex-end; - padding: 8px; + justify-content: space-between; + margin: -4px 0 5px; +} +.pipeline-section { + position: relative; + padding-top: 9px; + margin-bottom: 8px; +} +.pipeline-legend { + position: absolute; + right: 2px; + top: 0; + font: 10px var(--mono); + color: #a7b7a9; } - -.dd-bar-fill { +.pipeline-scroll { + overflow-x: auto; +} +.pipeline { width: 100%; - border-radius: 12px; + min-width: 850px; + max-height: 190px; + display: block; } - -.dd-hotspots, -.dd-alert-list { - display: grid; - gap: 12px; +.pipeline rect { + fill: #111b16; + stroke: #4c5c50; +} +.pipeline .inner-frame { + fill: none; + stroke: #2b3d30; +} +.flow-route { + fill: none; + stroke: #56665b; + stroke-width: 1.5; +} +.pipeline text { + font-family: var(--mono); + fill: var(--text); +} +.pipeline .flow-label { + font-size: 12px; + letter-spacing: 1px; + fill: #adbdaf; +} +.pipeline .flow-value { + font-size: 21px; + font-family: 'Segoe UI', sans-serif; +} +.pipeline .flow-caption { + font-size: 11px; + fill: #99ad9f; +} +.chart { + padding: 15px 16px 11px; + min-width: 0; +} +.chart-heading { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + margin-bottom: 6px; +} +.chart h2 { + font-size: 12px; + font-weight: 500; +} +.chart h2 > span { + font-size: 10px; + color: var(--muted); + margin-left: 6px; } - -.dd-hotspot-row { - border-radius: 16px; - padding: 14px 16px; +.series-controls { display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 8px; +} +.series-controls button { + display: inline-flex; align-items: center; - gap: 14px; + gap: 5px; + font: 9px var(--mono); + padding: 0; + color: #adbfb0; +} +.series-controls button[aria-pressed='false'] { + opacity: 0.4; +} +.series-controls i, +.chart-tooltip i { + width: 14px; + height: 2px; + display: inline-block; +} +.chart-readout { + text-align: right; + flex: none; +} +.chart-readout strong { + display: block; + font-size: 23px; + line-height: 1.1; + font-variant-numeric: tabular-nums; } - -.dd-hotspot-rank { - width: 34px; - height: 34px; - border-radius: 12px; - background: rgba(120, 149, 255, 0.14); - color: #dce7ff; +.chart-readout > span { + font-size: 9px; + color: var(--muted); +} +.chart-plot { + position: relative; + min-width: 0; + touch-action: pan-y; + overflow: hidden; +} +.chart-plot svg { + display: block; + max-width: 100%; +} +.chart-plot text { + font: 9px var(--mono); + fill: #80988a; +} +.grid-line { + stroke: #283c30; + stroke-dasharray: 2 5; +} +.chart-line { + stroke-linejoin: round; + stroke-linecap: round; +} +.chart-tooltip { + position: absolute; + top: 10px; + right: 12px; + min-width: 170px; + padding: 10px 12px; + background: #0c1512f5; + border: 1px solid #4f6451; + border-radius: 4px; + pointer-events: none; + box-shadow: 0 6px 20px #0006; +} +.chart-tooltip strong { + display: block; + font: 10px var(--mono); + margin-bottom: 8px; +} +.chart-tooltip > span { display: flex; align-items: center; - justify-content: center; - font-weight: 700; + gap: 6px; + font-size: 10px; + margin: 4px 0; } - -.dd-alert-card { - border-radius: 16px; - padding: 14px; +.chart-tooltip b { + margin-left: auto; + font: 11px var(--mono); } - -.dd-alert-topline { +.chart-footer { display: flex; justify-content: space-between; gap: 12px; - margin-bottom: 10px; -} - -.dd-alert-card p { - margin: 0; - line-height: 1.45; + font-size: 9px; + color: #8ea497; + padding: 5px 0 0; } - -.dd-severity { - display: inline-flex; +.chart-wait { + display: flex; align-items: center; justify-content: center; - border-radius: 999px; - padding: 6px 10px; + color: var(--muted); + font-size: 11px; } - -.dd-query-bar { - width: 100%; - border-radius: 18px; - padding: 12px; - display: grid; - grid-template-columns: 1.8fr 0.8fr auto; +.compact-chart .chart-heading { + margin-bottom: 0; +} +.compact-chart .chart-readout strong { + font-size: 20px; +} +.search-toolbar { + display: flex; gap: 12px; + align-items: stretch; + margin: 14px 0 12px; } - -.dd-query-bar input, -.dd-query-bar select, -.dd-query-bar button { - border-radius: 14px; - border: 1px solid rgba(148, 163, 184, 0.15); - padding: 12px 14px; - background: rgba(7, 12, 22, 0.9); - color: var(--dd-text); - font: inherit; +.search-toolbar > select { + min-width: 130px; } - -.dd-query-bar input::placeholder { - color: #7f93b5; +.time-filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 12px; + margin-bottom: 10px; + align-items: center; + font-size: 11px; } - -.dd-query-bar button { - background: linear-gradient(135deg, var(--dd-indigo), var(--dd-cyan)); - color: #05101d; - font-weight: 800; - cursor: pointer; +.time-filters label { + display: flex; + align-items: center; + gap: 8px; } - -.dd-stream-table { - display: grid; - gap: 10px; +.search-context { + font: 10px var(--mono); + color: #b6c9aa; + margin-bottom: 12px; } - -.dd-stream-header-row, -.dd-stream-row { +.explorer-layout { display: grid; - grid-template-columns: 148px 88px 108px 1fr; + grid-template-columns: minmax(0, 1fr); gap: 14px; - align-items: center; + align-items: start; } - -.dd-stream-header-row { - padding: 0 6px 4px; - color: var(--dd-muted); +.explorer-layout.with-inspector { + grid-template-columns: minmax(0, 1fr) 370px; } - -.dd-stream-row { - border-radius: 16px; - padding: 14px 16px; +.log-table { + table-layout: fixed; + min-width: 600px; } - -.dd-stream-level, -.dd-stream-source, -.dd-stream-message { - min-width: 0; +.log-table th:first-child { + width: 138px; } - -.dd-stream-message { - font-family: "Consolas", "Courier New", monospace; - color: #dfe8fc; -} - -.dd-empty { - padding: 18px 0; -} - -@media (max-width: 1200px) { - .dd-shell, - .dd-grid, - .dd-grid-bottom, - .dd-summary-grid, - .dd-topology, - .dd-mini-charts { - grid-template-columns: 1fr; - } - - .dd-nav { - position: static; - } - - .dd-panel-large { - grid-column: span 1; - } - - .dd-link { - height: 28px; - width: 2px; - justify-self: center; - background: linear-gradient(180deg, rgba(120, 149, 255, 0.15), rgba(102, 227, 255, 0.85)); - } +.log-table th:nth-child(2) { + width: 80px; } - -@media (max-width: 760px) { - .app-shell { - padding: 14px; +.log-table th:nth-child(3) { + width: 110px; +} +.log-table thead { + position: sticky; + top: 0; + z-index: 1; +} +.log-table td { + font: 11px/1.5 var(--mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 9px 14px; +} +.log-table tr { + cursor: pointer; +} +.log-table tr:hover { + background: #1a281f; +} +.log-table tr.selected { + background: #25351e77; + box-shadow: inset 2px 0 var(--lime); +} +.log-table tr.selected:has(.level.error) { + background: #45241d66; + box-shadow: inset 2px 0 var(--red); +} +.log-table-panel > .table-scroll { + min-height: 350px; + max-height: 560px; +} +.log-select, +.related-log-select { + display: block; + padding: 0; + font: inherit; + text-align: left; + white-space: nowrap; +} +.level { + font: inherit; + font-weight: 600; +} +.level.info { + color: var(--blue); +} +.level.warn { + color: var(--amber); +} +.level.error { + color: var(--red); +} +.level.debug { + color: #9ca8be; +} +.inspector { + position: sticky; + top: 82px; + min-height: 440px; + max-height: calc(100svh - 105px); + display: flex; + flex-direction: column; + scroll-margin-top: 82px; + overflow: hidden; +} +.event-summary { + padding: 14px 15px; +} +.event-stamp { + display: flex; + gap: 10px; + flex-wrap: wrap; + font: 10px var(--mono); + margin-bottom: 14px; +} +.event-stamp time { + color: #a6b9ad; +} +.field-list { + display: grid; + grid-template-columns: 70px minmax(0, 1fr); + gap: 9px 10px; + font-size: 11px; + margin: 0; +} +.field-list dt { + color: #9fb1a5; +} +.field-list dd { + margin: 0; + overflow-wrap: anywhere; +} +.field-pair { + display: contents; +} +.event-message { + font: 10px/1.6 var(--mono); + background: #0a100d80; + padding: 8px; + border: 1px solid var(--line); + border-radius: 4px; +} +.inspector-tabs { + display: flex; + align-items: center; + border-bottom: 1px solid var(--line); + padding: 0 12px; +} +.inspector-tabs > button { + font-size: 11px; + padding: 9px 7px; + border-bottom: 2px solid transparent; +} +.inspector-tabs > [aria-selected='true'] { + border-bottom-color: var(--lime); +} +.copy-button { + margin-left: auto; +} +.inspector-content { + overflow: auto; + min-height: 140px; + flex: 1; +} +.json-view { + font: 10px/1.7 var(--mono); + margin: 0; + padding: 14px; + color: #c5d2c9; + white-space: pre-wrap; + overflow-wrap: anywhere; + tab-size: 2; +} +.json-key { + color: #9dbce3; +} +.json-string { + color: #dba987; +} +.json-number { + color: #b8d88e; +} +.parsed-fields { + font-size: 10px; + margin: 0; + padding: 12px; +} +.parsed-fields > div { + display: grid; + grid-template-columns: 95px minmax(0, 1fr); + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--line-soft); +} +.parsed-fields dt { + color: #9bb5a2; + overflow-wrap: anywhere; +} +.parsed-fields dd { + margin: 0; + overflow-wrap: anywhere; +} +.copy-status { + padding: 8px 12px; + font-size: 10px; + color: var(--lime); + border-top: 1px solid var(--line); +} +.alert-toolbar { + display: flex; + gap: 12px; + margin: 2px 0 14px; + align-items: stretch; + flex-wrap: wrap; +} +.alert-toolbar .search-input { + min-width: 210px; +} +.alert-toolbar select { + min-width: 130px; + flex: 0 1 150px; +} +.alerts-layout { + display: grid; + grid-template-columns: 1fr; + gap: 14px; + min-height: 500px; +} +.alerts-layout.has-selection { + grid-template-columns: minmax(240px, 0.82fr) minmax(340px, 1fr); + align-items: stretch; +} +.alert-queue { + display: flex; + flex-direction: column; + min-height: 0; + max-height: max(520px, calc(100svh - 184px)); + overflow: hidden; +} +.alert-queue > .panel-header, +.alert-detail > .panel-header, +.related-events > .panel-header { + min-height: 43px; + flex: none; +} +.alert-list { + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: #3a4b40 transparent; +} +.alert-card { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) 13px; + align-items: start; + gap: 10px; + text-align: left; + padding: 13px 12px; + color: var(--text); + width: 100%; + flex: none; + min-height: 90px; + position: relative; + border-bottom: 1px solid var(--line-soft); + transition: background 0.12s; +} +.alert-card:last-child { + border-bottom: 0; +} +.alert-card:hover { + background: #19251d; +} +.alert-card.selected { + background: linear-gradient(110deg, #23341eaa, #17241c66); + box-shadow: inset 2px 0 var(--lime); +} +.alert-card > svg { + align-self: center; + color: #9aae9f; +} +.alert-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.alert-card-top time { + color: #a6baac; + font: 10px var(--mono); + white-space: nowrap; +} +.alert-card h2 { + font-size: 13px; + font-weight: 500; + line-height: 1.45; + margin: 6px 0 4px; + overflow-wrap: anywhere; +} +.alert-card-meta { + display: flex; + flex-wrap: wrap; + gap: 3px 8px; + color: #a2bba8; + font-size: 11px; + overflow-wrap: anywhere; +} +.alert-card-meta > span + span:before { + content: '·'; + margin-right: 8px; + color: #657b6c; +} +.severity { + text-transform: uppercase; + font: 10px var(--mono); + letter-spacing: 0.5px; + padding: 3px 7px; + display: inline-block; + color: #bdd7b8; + border: 1px solid #4c6943; + border-radius: 3px; + background: #22351c; +} +.severity.high, +.severity.critical { + color: var(--red); + border-color: #a85343; + background: #3b201a; +} +.severity.medium { + color: var(--amber); + border-color: #80642e; + background: #322714; +} +.severity.low { + color: #a6d0b9; + border-color: #3d6b56; + background: #1d2f25; +} +.severity-dot { + display: block; + width: 9px; + height: 9px; + margin-top: 6px; + border-radius: 50%; + background: #a6d0b9; +} +.severity-dot.high, +.severity-dot.critical { + background: var(--red); +} +.severity-dot.medium { + background: var(--amber); +} +.ack-mark { + display: flex; + align-items: center; + gap: 4px; + color: #a5c28e; + font-size: 10px; + margin-top: 5px; +} +.alert-detail { + padding: 0; + min-height: 0; + scroll-margin-top: 82px; +} +.alert-detail-body { + padding: 18px 16px; +} +.alert-detail-header { + margin-bottom: 20px; +} +.alert-detail-status { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.alert-detail-header h2 { + font-size: 20px; + font-weight: 600; + line-height: 1.35; + margin: 12px 0 5px; + overflow-wrap: anywhere; +} +.alert-reason { + font-size: 12px; + line-height: 1.6; + color: #bdcbbf; +} +.alert-meta { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px 12px; + color: #a2bba8; + font-size: 10px; + margin-top: 16px; + overflow-wrap: anywhere; +} +.alert-meta > span:first-child { + display: flex; + gap: 7px; + align-items: center; + min-width: 0; +} +.alert-meta time { + margin-left: 4px; + color: #bfccc0; + font-family: var(--mono); +} +.acknowledged { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--lime); + font-size: 11px; +} +.observed-metric { + border-top: 1px solid var(--line); + padding: 20px 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 5px; +} +.observed-metric > span { + font-size: 12px; + color: #bdcbbf; +} +.observed-metric strong { + font-size: clamp(36px, 3.7vw, 58px); + line-height: 1.15; + font-weight: 600; + font-variant-numeric: tabular-nums; + overflow-wrap: anywhere; + max-width: 100%; +} +.evidence-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin: 0; + padding: 0; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} +.evidence-grid > div { + padding: 13px 8px; + text-align: center; + min-width: 0; + border-right: 1px solid var(--line); +} +.evidence-grid > div:nth-child(3n), +.evidence-grid > div:last-child { + border-right: 0; +} +.evidence-grid dt, +.evidence-grid > div > span { + color: #9fb5a3; + font-size: 10px; +} +.evidence-grid dd { + font-size: 18px; + font-weight: 600; + margin: 5px 0 0; + font-variant-numeric: tabular-nums; + overflow-wrap: anywhere; +} +.threshold-chart { + padding: 18px 0 16px; + margin: 0; + border-bottom: 1px solid var(--line); +} +.threshold-chart figcaption { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 20px; +} +.threshold-chart h3 { + font-size: 12px; + font-weight: 500; +} +.threshold-chart figcaption > span { + font: 10px var(--mono); + white-space: nowrap; +} +.comparison-exceeded { + color: #f5a894; +} +.comparison-row { + display: grid; + grid-template-columns: 60px minmax(20px, 1fr) minmax(35px, auto); + align-items: center; + gap: 10px; + margin: 15px 0; + font-size: 11px; +} +.comparison-row > span { + color: #acbfb0; +} +.comparison-row strong { + font: 11px var(--mono); + text-align: right; + color: #efb09e; +} +.comparison-track { + height: 10px; + background: #27352a; + border-radius: 2px; + overflow: hidden; +} +.comparison-track i { + display: block; + height: 100%; + background: #e58e78; + border-radius: 2px; +} +.threshold-row .comparison-track i { + background: #a0b2a2; +} +.threshold-row strong { + color: #c0d1c3; +} +.threshold-chart > p { + margin: 20px 0 0; + color: #92a797; + font-size: 10px; +} +.evidence-context { + color: #9fb69f; + font: 10px var(--mono); + margin: 15px 0; +} +.alert-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin: 17px 0; +} +.alert-actions .button { + font-size: 11px; + padding-left: 10px; + padding-right: 10px; +} +.detection-details { + border: 1px solid var(--line); + border-radius: 4px; + margin-top: 12px; +} +.detection-details summary, +.related-raw summary { + cursor: pointer; + padding: 11px 13px; + color: #b3c8b4; + font-size: 11px; +} +.original-detection { + font-size: 11px; + line-height: 1.6; + padding: 12px; + border-top: 1px solid var(--line-soft); + color: #a8bcad; + overflow-wrap: anywhere; +} +.detection-details .json-view, +.related-raw .json-view { + max-height: 240px; + overflow: auto; + border-top: 1px solid var(--line); + font-size: 10px; +} +.related-events { + grid-column: 1/-1; + display: flex; + flex-direction: column; + min-height: 0; + height: 620px; + overflow: hidden; +} +.related-log-scroll { + flex: 1; + min-height: 180px; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: #3a4b40 transparent; +} +.related-log-table { + table-layout: fixed; + min-width: 440px; + font-family: var(--mono); +} +.related-log-table thead { + position: sticky; + top: 0; + z-index: 1; + background: #111a17; +} +.related-log-table th { + padding: 11px 10px; + font-size: 9px; +} +.related-log-table th:first-child { + width: 107px; +} +.related-log-table th:nth-child(2) { + width: 78px; +} +.related-log-table th:nth-child(3) { + width: 62px; +} +.related-log-table td { + padding: 11px 10px; + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.related-log-table tbody tr { + cursor: pointer; +} +.related-log-table tbody tr:hover { + background: #1a271e; +} +.related-log-table tr.selected { + background: #23341d88; + box-shadow: inset 2px 0 var(--lime); +} +.related-log-select:hover { + color: var(--lime); +} +.related-error { + padding: 16px; + color: #e4ad9c; + font-size: 12px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} +.related-log-inspector { + padding: 16px; + border-top: 1px solid var(--line); + flex: none; + max-height: 48%; + overflow: auto; +} +.related-inspector-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 16px; +} +.related-inspector-heading h3 { + font-size: 12px; +} +.related-inspector-heading a { + display: inline-flex; + align-items: center; + font-size: 10px; + gap: 3px; +} +.related-log-fields { + display: grid; + grid-template-columns: 58px minmax(0, 1fr); + gap: 9px 12px; + font: 11px/1.6 var(--mono); + margin: 0; +} +.related-log-fields dt { + color: #9eb3a5; +} +.related-log-fields dd { + margin: 0; + overflow-wrap: anywhere; + white-space: pre-wrap; +} +.related-raw { + margin-top: 12px; + border: 1px solid var(--line); + border-radius: 4px; +} +.related-raw summary { + padding: 8px 10px; +} +.overview-page { + display: grid; + gap: 14px; +} +.metric-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; +} +.metric-card { + display: flex; + align-items: center; + gap: 15px; + padding: 18px; + min-height: 108px; +} +.metric-card > svg { + color: #b5c6b9; +} +.metric-card strong { + font-size: 24px; + display: block; + line-height: 1.2; +} +.metric-card span { + display: block; + font-size: 12px; + margin-top: 6px; +} +.metric-card small { + display: block; + color: var(--muted); + font-size: 9px; + margin-top: 3px; +} +.two-column { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} +.health-table { + min-width: 420px; +} +.health-table td { + font-size: 11px; +} +.health-table td:first-child { + white-space: nowrap; +} +.health-table td:first-child svg { + vertical-align: middle; + margin-right: 9px; +} +.health-table .status-dot { + margin-right: 7px; +} +.health-table td:last-child { + color: var(--muted); + font-size: 10px; +} +.source-shares { + display: grid; + gap: 14px; + padding: 18px 16px; +} +.source-shares > a { + display: grid; + grid-template-columns: 80px minmax(30px, 1fr) 45px 45px; + align-items: center; + gap: 10px; + font-size: 11px; +} +.source-shares > a > span:first-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.source-shares strong { + text-align: right; + font: 11px var(--mono); +} +.source-shares > a > span:nth-last-child(2) { + text-align: right; + font: 10px var(--mono); + color: var(--muted); +} +.share-track { + height: 5px; + border-radius: 3px; + background: #243329; + overflow: hidden; +} +.share-track i { + display: block; + height: 100%; + background: #9fc96f; + border-radius: 3px; +} +.activity-row { + padding: 16px; + display: flex; + align-items: center; + gap: 14px; + font-size: 12px; +} +.activity-row > span:nth-child(2) { + flex: 1; +} +.activity-row time { + font: 10px var(--mono); + color: var(--muted); + white-space: nowrap; +} +@media (min-width: 1280px) { + .alerts-layout.has-selection { + grid-template-columns: minmax(240px, 0.82fr) minmax(340px, 1fr) minmax(360px, 1.4fr); + height: max(660px, calc(100svh - 184px)); + } + .alerts-layout.has-selection .alert-queue { + max-height: none; + } + .alerts-layout.has-selection .alert-detail { + height: 100%; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #3a4b40 transparent; + } + .alerts-layout.has-selection .related-events { + grid-column: auto; + height: 100%; + } +} +@media (min-width: 1800px) { + .page-content { + padding-left: 30px; + padding-right: 30px; + } + .topbar { + padding-left: 30px; + padding-right: 30px; + } + .log-table td { + font-size: 12px; + } +} +@media (max-width: 1180px) { + .workspace { + grid-template-columns: 174px minmax(0, 1fr); + } + .page-content { + padding: 16px; + } + .topbar { + padding: 10px 16px; + } + .topbar-metrics { + gap: 16px; + } + .inline-metric + .inline-metric { + padding-left: 16px; + } + .explorer-layout.with-inspector { + grid-template-columns: minmax(0, 1fr) 310px; + } + .search-toolbar { + flex-wrap: wrap; + gap: 8px; + } + .search-toolbar .search-input { + flex-basis: 40%; + } + .metric-card { + padding: 14px; + gap: 10px; + } + .metric-card strong { + font-size: 20px; + } + .alert-toolbar .segmented { + order: 3; + } + .alert-toolbar .search-input { + flex-basis: 40%; + } +} +@media (max-width: 1000px) { + .explorer-layout.with-inspector { + grid-template-columns: minmax(0, 1fr); + } + .inspector { + position: static; + max-height: 800px; + } + .metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .two-column { + grid-template-columns: minmax(0, 1fr); + } + .topbar-controls { + gap: 7px; + } + .inline-metric strong { + font-size: 17px; + } +} +@media (max-width: 950px) { + .alerts-layout.has-selection { + grid-template-columns: minmax(0, 1fr); + } + .alert-queue { + max-height: none; + } + .alert-list { + max-height: 270px; + } + .alert-card { + padding: 13px 16px; + } + .alert-card h2 { + font-size: 14px; + } + .alert-detail { + min-height: 0; + position: static; + } + .pipeline-legend { + font-size: 9px; + } +} +@media (max-width: 700px) { + .workspace { + display: block; + } + .sidebar { + position: sticky; + top: 0; + z-index: 30; + width: 100%; + height: 56px; + padding: 6px 10px; + border-right: 0; + border-bottom: 1px solid var(--line); + background: #0d1510; + flex-direction: row; + justify-content: space-between; + } + .primary-nav { + display: flex; + gap: 5px; + } + .nav-item { + height: 42px; + padding: 0 10px; + font-size: 12px; + gap: 7px; + } + .nav-item svg { + width: 17px; + } + .nav-item.active:before { + top: auto; + bottom: -7px; + height: 2px; + width: auto; + right: 7px; + left: 7px; + } + .nav-count { + font-size: 9px; + line-height: 17px; + min-width: 17px; + padding: 0 4px; + } + .sources-heading, + .source-nav { + display: none; + } + .environment { + padding: 0; + margin: 0; + gap: 5px; + font-size: 10px; + } + .environment > span { + display: none; + } + .topbar { + top: 56px; + min-height: 62px; + gap: 10px; + padding: 10px 12px; + } + .topbar-metrics .inline-metric > svg { + display: none; + } + .inline-metric { + gap: 0; + } + .inline-metric strong { + font-size: 17px; + } + .inline-metric span { + font-size: 9px; + } + .inline-metric + .inline-metric { + padding-left: 12px; + } + .topbar-controls { + gap: 6px; } - - .dd-topbar, - .dd-query-bar, - .dd-stream-header-row, - .dd-stream-row { - grid-template-columns: 1fr; + .topbar-controls .button { + font-size: 11px; + padding: 6px 9px; + gap: 5px; + min-height: 33px; } - - .dd-topbar { - flex-direction: column; + .topbar-controls select { + font-size: 11px; + padding: 6px 18px 6px 9px; + max-width: 63px; + min-height: 33px; + } + .mode-pill { + font-size: 10px; + padding: 5px 7px; + } + .page-content { + padding: 14px 12px; + } + .pipeline-legend { + top: 2px; + } + .pipeline-section { + padding-top: 16px; + } + .chart { + padding: 13px 12px 10px; + } + .chart-readout strong { + font-size: 20px; + } + .chart-footer { + font-size: 9px; + } + .chart-footer > span:last-child { + display: none; + } + .search-toolbar .search-input { + flex-basis: 100%; + } + .search-toolbar .segmented { + flex: 1; + } + .segmented button { + flex: 1; + } + .search-toolbar > select { + max-width: 150px; + min-width: 105px; + font-size: 11px; + } + .log-table-panel > .table-scroll { + min-height: 330px; + max-height: 600px; + } + .results-footer { + align-items: flex-start; + flex-wrap: wrap; + gap: 12px; + } + .footer-actions { + gap: 8px; + flex-wrap: wrap; + } + .load-older { + min-width: 140px; + } + .inspector { + scroll-margin-top: 132px; + } + .metric-grid { + gap: 10px; + } + .metric-card { + min-height: 85px; + padding: 13px; + } + .metric-card strong { + font-size: 20px; + } + .metric-card span { + font-size: 10px; + } + .overview-page { + gap: 12px; + } + .alert-toolbar { + gap: 8px; + margin-bottom: 14px; + } + .alert-toolbar .search-input { + flex-basis: 100%; + } + .alert-toolbar > select { + flex: 1; + max-width: none; + min-width: 100px; + font-size: 11px; + } + .alert-toolbar .segmented { + width: 100%; + order: 0; + } + .alert-toolbar .segmented button { + padding: 7px 8px; + } + .alert-detail-body { + padding: 18px 15px; + } + .observed-metric strong { + font-size: 44px; + } + .alert-detail-header h2 { + font-size: 17px; + } + .alert-detail { + scroll-margin-top: 132px; + } + .evidence-grid dd { + font-size: 16px; + } + .activity-row { + font-size: 11px; + gap: 10px; + flex-wrap: wrap; + } + .activity-row time { + font-size: 9px; + } + .time-filters input { + max-width: 180px; + } + .source-shares > a { + grid-template-columns: 68px minmax(20px, 1fr) 40px 40px; + gap: 8px; + } +} +@media (max-width: 440px) { + .topbar-metrics .inline-metric:nth-child(2) { + display: none; + } + .environment { + display: none; + } + .nav-item { + padding: 0 9px; + } + .mode-pill { + font-size: 9px; + } + .topbar-controls { + gap: 5px; + } + .metric-card > svg { + display: none; + } + .metric-card strong { + font-size: 21px; + } + .segmented button { + font-size: 10px; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *:before, + *:after { + scroll-behavior: auto !important; + transition: none !important; + animation: none !important; } } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 3bb3bbc..da0d812 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,19 +1,22 @@ import type { Metadata } from 'next'; import './globals.css'; - +import { DashboardProvider } from '../components/DashboardProvider'; +import Shell from '../components/Shell'; export const metadata: Metadata = { - title: 'Log Aggregation System', - description: 'Distributed log aggregation dashboard', + title: 'Explore logs', + description: 'Search logs, monitor ingestion, and investigate detections.', + icons: { + icon: 'data:image/svg+xml,' + encodeURIComponent(''), + }, }; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function Layout({ children }: { children: React.ReactNode }) { return ( - - {children} + + + + {children} + + ); } diff --git a/frontend/src/app/overview/page.tsx b/frontend/src/app/overview/page.tsx new file mode 100644 index 0000000..47d71bf --- /dev/null +++ b/frontend/src/app/overview/page.tsx @@ -0,0 +1,2 @@ +import OverviewPage from '../../components/OverviewPage'; +export default OverviewPage; diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 9133904..b024e2e 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,9 +1,9 @@ -import LogDashboard from '@/components/LogDashboard'; - -export default function Home() { +import { Suspense } from 'react'; +import ExplorePage from '../components/ExplorePage'; +export default function Page() { return ( -
- -
+ Loading workspace…}> + + ); } diff --git a/frontend/src/components/AlertDetail.tsx b/frontend/src/components/AlertDetail.tsx new file mode 100644 index 0000000..f02ecaa --- /dev/null +++ b/frontend/src/components/AlertDetail.tsx @@ -0,0 +1,172 @@ +'use client'; +import Link from 'next/link'; +import type { AlertItem } from '../lib/model'; +import { + alertSource, + alertTitle, + clock, + count, + numeric, + relatedParams, + serverDate, +} from '../lib/events'; +import Icon from './Icon'; +import JsonView from './JsonView'; +export default function AlertDetail({ + alert, + saving, + acknowledged, + onAcknowledge, +}: { + alert: AlertItem; + saving: boolean; + acknowledged: boolean; + onAcknowledge: () => void; +}) { + const d = alert.details ?? {}, + current = numeric(d, 'errorsPerMinute', 'errors_per_minute'), + threshold = numeric(d, 'threshold'), + window = numeric(d, 'windowMinutes', 'window_minutes'), + errors = numeric(d, 'errorCount', 'error_count'), + score = numeric(d, 'anomaly_score'), + title = alertTitle(alert), + link = relatedParams(alert); + if (alert.logId && alert.logId !== 'system') link.set('logId', alert.logId); + const reason = + current != null && threshold != null + ? 'The observed error rate exceeded ' + + threshold + + ' errors per minute' + + (window != null ? ' over a ' + window + '-minute window.' : '.') + : alert.message; + const facts: [string, string, string?][] = []; + if (threshold != null) + facts.push(['Threshold', String(threshold), current != null ? 'errors/minute' : undefined]); + if (window != null) facts.push(['Detection window', window + ' min']); + if (errors != null) + facts.push([ + 'Error events', + count(errors), + window != null ? 'in ' + window + ' minutes' : undefined, + ]); + if (numeric(d, 'current_count') != null) + facts.push(['Current count', count(numeric(d, 'current_count'))]); + if (numeric(d, 'mean_count') != null) + facts.push(['Historical mean', count(numeric(d, 'mean_count'))]); + if (numeric(d, 'z_score') != null) facts.push(['Z-score', numeric(d, 'z_score')!.toFixed(2)]); + return ( + + ); +} diff --git a/frontend/src/components/AlertRelatedLogs.tsx b/frontend/src/components/AlertRelatedLogs.tsx new file mode 100644 index 0000000..c845171 --- /dev/null +++ b/frontend/src/components/AlertRelatedLogs.tsx @@ -0,0 +1,131 @@ +'use client'; +import Link from 'next/link'; +import { useState } from 'react'; +import { usePolling } from '../hooks/usePolling'; +import type { AlertItem, LogPage } from '../lib/model'; +import { clock, count, eventFields, rawEvent, relatedParams } from '../lib/events'; +import Icon from './Icon'; +import JsonView from './JsonView'; +export default function AlertRelatedLogs({ alert }: { alert: AlertItem }) { + const params = relatedParams(alert); + params.set('limit', '25'); + const resource = usePolling('/api/logs/page?' + params, 0, true), + [selectedId, setSelectedId] = useState(null), + logs = resource.data?.logs ?? [], + selected = logs.find((log) => log.id === selectedId) ?? logs[0], + fields = selected ? eventFields(selected) : {}, + latency = fields.latency_ms ?? fields.latency, + link = relatedParams(alert); + if (selected) link.set('logId', selected.id); + return ( +
+
+

Related logs

+ + {resource.data ? count(logs.length) + ' shown · ' : ''}Newest first + +
+
+ + + + + + + + + + + {logs.map((log) => ( + setSelectedId(log.id)} + > + + + + + + ))} + +
Time ↓SourceLevelMessage
+ + {log.source} + {log.level.toUpperCase()} + {log.message}
+ {!resource.data && resource.loading && ( +

+ Loading related events… +

+ )} + {resource.data && !logs.length && ( +

No retained events match this detection window.

+ )} + {resource.error && ( +
+

Related events are unavailable.

+ +
+ )} +
+ {selected && ( +
+
+

Selected log

+ + Open in Explore + +
+
+
Time
+
{clock(selected.timestamp, true)}
+
Source
+
{selected.source}
+
Level
+
+ {selected.level.toUpperCase()} +
+
Message
+
{selected.message}
+ {latency != null && ( + <> +
Latency
+
{typeof latency === 'number' ? latency + ' ms' : String(latency)}
+ + )} +
+
+ Raw event + +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/AlertsPage.tsx b/frontend/src/components/AlertsPage.tsx new file mode 100644 index 0000000..b492d8f --- /dev/null +++ b/frontend/src/components/AlertsPage.tsx @@ -0,0 +1,302 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { usePolling } from '../hooks/usePolling'; +import { useDashboard } from './DashboardProvider'; +import { requestJson } from '../lib/api'; +import type { AlertItem } from '../lib/model'; +import { alertSource, alertTitle, alertValue, clock, count, serverDate } from '../lib/events'; +import AlertDetail from './AlertDetail'; +import AlertRelatedLogs from './AlertRelatedLogs'; +import Icon from './Icon'; +export default function AlertsPage() { + const { paused, stats, alertStats, refreshAlerts } = useDashboard(); + const [tab, setTab] = useState('open'), + [source, setSource] = useState(''), + [severity, setSeverity] = useState(''), + [draft, setDraft] = useState(''), + [q, setQ] = useState(''), + [offset, setOffset] = useState(0), + [selectedId, setSelectedId] = useState(null), + [saving, setSaving] = useState(null), + [confirmed, setConfirmed] = useState(null), + [error, setError] = useState(null); + const params = new URLSearchParams({ limit: '25', offset: String(offset) }); + if (tab !== 'all') params.set('acknowledged', String(tab === 'acknowledged')); + if (q) params.set('q', q); + if (source) params.set('source', source); + if (severity) params.set('severity', severity); + const resource = usePolling<{ alerts: AlertItem[]; total: number }>( + '/api/alerts?' + params, + 5000, + paused, + ), + alerts = resource.data?.alerts ?? [], + total = resource.data?.total ?? 0, + selected = alerts.find((alert) => alert.id === selectedId) ?? alerts[0], + activeId = selected?.id; + useEffect(() => { + if (activeId && activeId !== selectedId) setSelectedId(activeId); + }, [activeId, selectedId]); + useEffect(() => { + if (resource.data && offset > 0 && offset >= resource.data.total) { + setOffset(Math.max(0, Math.floor((resource.data.total - 1) / 25) * 25)); + setSelectedId(null); + } + }, [resource.data, offset]); + const reset = () => { + setOffset(0); + setSelectedId(null); + setError(null); + }; + async function acknowledge() { + if (!selected || saving) return; + const target = selected; + setSaving(target.id); + setError(null); + try { + await requestJson( + '/api/alerts/' + encodeURIComponent(target.id) + '/acknowledge', + undefined, + { method: 'POST' }, + ); + setConfirmed(target.id); + resource.refresh(); + refreshAlerts(); + } catch (cause) { + setError( + 'Detection at ' + + clock(target.createdAt) + + ': ' + + (cause instanceof Error ? cause.message : 'Acknowledgment failed.'), + ); + } finally { + setSaving(null); + } + } + return ( +
+

Alerts

+
{ + event.preventDefault(); + setQ(draft); + reset(); + }} + > +
+ + setDraft(event.target.value)} + placeholder="Search alerts…" + aria-label="Search alerts" + /> + {draft && ( + + )} +
+
+ {[ + ['open', 'Open' + (alertStats ? ' (' + count(alertStats.unacknowledged) + ')' : '')], + [ + 'acknowledged', + 'Acknowledged' + + (alertStats + ? ' (' + count(alertStats.total - alertStats.unacknowledged) + ')' + : ''), + ], + ['all', 'All'], + ].map(([value, label]) => ( + + ))} +
+ + + +
+ {(error || resource.error) && ( +
+ {error ?? resource.error} + +
+ )} +
+
+
+

Alerts {resource.data && '(' + count(total) + ')'}

+ Newest first +
+
+ {alerts.map((alert) => ( +